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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user