(null);
@@ -78,7 +80,7 @@ export function LayoutEngine({ workspaceId, layout, running, states, surfaces, f
if (!layout) {
return Empty workspace — apply a preset to add panels.
;
}
- const shared = { workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag: startPanelDrag, searchSurfaceId, searchNonce, onCloseSearch };
+ const shared = { workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag: startPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette };
if (zoomed) {
return (
@@ -103,6 +105,8 @@ interface NodeProps {
searchSurfaceId: string | null;
searchNonce: number;
onCloseSearch: () => void;
+ font: { family: string; size: number } | null;
+ palette: Record | null;
}
function Node({ node, path, ...rest }: NodeProps) {
@@ -112,7 +116,7 @@ function Node({ node, path, ...rest }: NodeProps) {
return ;
}
-function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch }: Omit & { id: string }) {
+function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Omit & { id: string }) {
const focused = focusedId === id;
const dropEdge = drop && drop.id === id ? drop.edge : null;
@@ -181,7 +185,7 @@ function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus,
onMouseDown={(e) => { e.stopPropagation(); onFocus(id); void setZoom(workspaceId, id); }} />}
-
+
{searchSurfaceId === id && (
diff --git a/app/src/TerminalView.tsx b/app/src/TerminalView.tsx
index f16ebb6..35c0361 100644
--- a/app/src/TerminalView.tsx
+++ b/app/src/TerminalView.tsx
@@ -9,15 +9,35 @@ import { registerSearch, unregisterSearch } from "./searchRegistry";
const decoder = new TextDecoder();
const encoder = new TextEncoder();
-export function TerminalView({ surfaceId }: { surfaceId: string }) {
+function xtermTheme(p: Record) {
+ return {
+ background: 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 | null }) {
const ref = useRef(null);
+ const termRef = useRef(null);
+ const fitRef = useRef(null);
useEffect(() => {
if (!ref.current) return;
// allowProposedApi is required by the search addon: its match decorations
// call registerMarker/registerDecoration (proposed API). Without it findNext
// throws and the scrollback search counter never updates.
- const term = new Terminal({ fontFamily: "'JetBrains Mono Variable', 'JetBrains Mono', monospace", fontSize: 13, convertEol: false, scrollback: 10000, allowProposedApi: true });
+ const term = new Terminal({
+ fontFamily: font ? `'${font.family}', monospace` : "'JetBrains Mono Variable', 'JetBrains Mono', monospace",
+ fontSize: font?.size ?? 13,
+ convertEol: false,
+ scrollback: 10000,
+ allowProposedApi: true,
+ theme: palette ? xtermTheme(palette) : undefined,
+ });
+ termRef.current = term;
+
try {
term.loadAddon(new WebglAddon());
} catch {
@@ -31,6 +51,7 @@ export function TerminalView({ surfaceId }: { surfaceId: string }) {
const fit = new FitAddon();
term.loadAddon(fit);
+ fitRef.current = fit;
// Fit the grid to the container and tell the daemon the new size. Coalesced
// through rAF so a burst of resize callbacks yields one resize per frame.
@@ -76,8 +97,26 @@ export function TerminalView({ surfaceId }: { surfaceId: string }) {
void detachSurface(surfaceId);
unregisterSearch(surfaceId);
term.dispose();
+ termRef.current = null;
+ fitRef.current = null;
};
- }, [surfaceId]);
+ }, [surfaceId]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Live re-apply font and theme when config changes without remounting.
+ // palette is a new object each render so we depend on a stable key instead.
+ const paletteKey = palette
+ ? `${palette["bg-panel"]}|${palette["text-primary"]}|${palette["search-match"]}`
+ : null;
+ useEffect(() => {
+ const t = termRef.current;
+ if (!t) return;
+ if (font) {
+ t.options.fontFamily = `'${font.family}', monospace`;
+ t.options.fontSize = font.size;
+ }
+ if (palette) t.options.theme = xtermTheme(palette);
+ requestAnimationFrame(() => { try { fitRef.current?.fit(); } catch { /* ignore */ } });
+ }, [font?.family, font?.size, paletteKey]); // eslint-disable-line react-hooks/exhaustive-deps
return ;
}