Add full disk access checks and settings

Add background themes and custom images

Add shell command logging toggle

Add UTF-8 locale guarantee for PTY

Add Claude hook settings injection

Add hotkey system for GUI

Add glass panel styling

Add search disabled state for agent panels

Add zoom toggle command

Add device report filtering

Add entitlements for notarization

Update version to 0.1.27
This commit is contained in:
2026-06-15 22:26:06 +07:00
parent 2ee2aaaffb
commit ee845e15b3
30 changed files with 859 additions and 123 deletions
+41 -3
View File
@@ -9,6 +9,27 @@ import { registerSearch, unregisterSearch } from "./searchRegistry";
const decoder = new TextDecoder();
const encoder = new TextEncoder();
// xterm.js auto-answers device queries (Device Attributes, cursor/status reports,
// OSC color queries, DECRPM mode reports) by emitting the reply through onData.
// But the daemon's alacritty grid is the authoritative emulator and already
// answers these on the PTY (see spacesh-core grid.rs `take_replies` →
// `write_input`). Forwarding xterm's duplicate — which arrives a full IPC
// roundtrip late — lands in the shell's input buffer after it stopped reading
// the reply, so the shell echoes it as literal escape gibberish and the prompt
// shifts. Drop these standalone reports; never the user's keystrokes/paste/mouse
// (mouse reports end in M/m and are real user input the program asked for).
function isDeviceReport(data: string): boolean {
if (data.charCodeAt(0) !== 0x1b) return false;
return (
/^\x1b\[[?>=]?[0-9;]*[cntR]$/.test(data) || // DA1/DA2 (c), DSR status (n), text-area/cell-size (t), cursor position (R)
/^\x1b\[\?[0-9;]*u$/.test(data) || // kitty keyboard QUERY reply (\x1b[?flags u) — NOT key input \x1b[<code>u
/^\x1b\[\?[0-9;]*\$[py]$/.test(data) || // DECRPM mode report
/^\x1b\][0-9]+;[^\x07\x1b]*(?:\x07|\x1b\\)$/.test(data) || // OSC color / query reply (BEL- or ST-terminated)
/^\x1bP[\s\S]*\x1b\\$/.test(data) || // any DCS report (XTVERSION / DECRQSS / status string)
/^\x1b_[\s\S]*\x1b\\$/.test(data) // any APC report (kitty graphics)
);
}
// Appended after the user font so Nerd Font icon glyphs (Private Use Area) render
// via fallback instead of blank boxes, without changing the base monospace font.
const NERD_FALLBACK = "'Symbols Nerd Font Mono'";
@@ -18,18 +39,25 @@ const fontStack = (family: string | null) =>
function xtermTheme(p: Record<string, string>) {
return {
background: p["bg-panel"],
background: p["term-bg"] ?? p["bg-panel"],
foreground: p["text-primary"],
cursor: p["text-primary"],
selectionBackground: p["search-match"],
};
}
export function TerminalView({ surfaceId, font, palette }: { surfaceId: string; font: { family: string; size: number } | null; palette: Record<string, string> | null }) {
export function TerminalView({ surfaceId, font, palette, focused }: { surfaceId: string; font: { family: string; size: number } | null; palette: Record<string, string> | null; focused?: boolean }) {
const ref = useRef<HTMLDivElement>(null);
const termRef = useRef<Terminal | null>(null);
const fitRef = useRef<FitAddon | null>(null);
const webglRef = useRef<WebglAddon | null>(null);
// A background theme makes term-bg fully transparent so the panel's glass fill
// shows through. allowTransparency is construction-time only, so it's part of the
// effect key to force a remount when it flips. WebGL stays on in both modes — the
// glass/blur lives on a sibling layer (see LayoutEngine), not an ancestor, so the
// WebGL canvas composites its transparent background without the WKWebView
// clipping/smearing artifacts that backdrop-filter ancestors cause.
const transparent = palette?.["term-bg"] === "rgba(0,0,0,0)";
useEffect(() => {
if (!ref.current) return;
@@ -42,6 +70,7 @@ export function TerminalView({ surfaceId, font, palette }: { surfaceId: string;
convertEol: false,
scrollback: 10000,
allowProposedApi: true,
allowTransparency: transparent,
theme: palette ? xtermTheme(palette) : undefined,
});
termRef.current = term;
@@ -83,6 +112,7 @@ export function TerminalView({ surfaceId, font, palette }: { surfaceId: string;
// Input → daemon.
const inputDisposable = term.onData((data) => {
if (isDeviceReport(data)) return; // daemon answers the PTY authoritatively; xterm's dup arrives late and echoes
void sendInput(surfaceId, encoder.encode(data));
});
@@ -117,7 +147,15 @@ export function TerminalView({ surfaceId, font, palette }: { surfaceId: string;
fitRef.current = null;
webglRef.current = null;
};
}, [surfaceId]); // eslint-disable-line react-hooks/exhaustive-deps
}, [surfaceId, transparent]); // eslint-disable-line react-hooks/exhaustive-deps
// Keyboard focus cycling (cmd+]/[) only changes the focusedId state — it never
// touches the DOM, so the new panel's xterm textarea stays unfocused and keys
// keep flowing to the old terminal. Mouse clicks don't hit this because the
// click lands on the textarea directly. Drive xterm focus from the prop.
useEffect(() => {
if (focused) termRef.current?.focus();
}, [focused]);
// Live re-apply font and theme when config changes without remounting.
// font and palette are memoized in App.tsx so stable identity = no spurious re-applies.