diff --git a/app/src/App.tsx b/app/src/App.tsx index 8169f28..600a539 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -138,8 +138,8 @@ export function App() { const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null; effectiveFocusRef.current = effectiveFocus; - const termFont = config ? { family: config.font_family, size: config.font_size } : null; - const termPalette = config ? resolvePalette(config.theme, config.accent) : null; + const termPalette = useMemo(() => (config ? resolvePalette(config.theme, config.accent) : null), [config?.theme, config?.accent]); + const termFont = useMemo(() => (config ? { family: config.font_family, size: config.font_size } : null), [config?.font_family, config?.font_size]); function selectWorkspace(id: string) { setActiveId(id); @@ -149,7 +149,7 @@ export function App() { return (
- setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => setSettingsOpen(true)} /> + setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => { if (config) setSettingsOpen(true); }} />
{sidebarOpen && setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />}
diff --git a/app/src/Settings.tsx b/app/src/Settings.tsx index 85f9b10..4334fe5 100644 --- a/app/src/Settings.tsx +++ b/app/src/Settings.tsx @@ -1,17 +1,21 @@ import { useEffect, useRef, useState } from "react"; -import { COLORS, FONT } from "./theme"; +import { COLORS, FONT, ACCENTS } from "./theme"; import { setConfig, shutdownDaemon, restartDaemon } from "./socketBridge"; import type { ConfigView, DaemonHealth } from "./socketBridge"; const FONTS = ["JetBrains Mono", "Menlo", "Monaco", "SF Mono", "Fira Code", "Cascadia Code"]; -const ACCENTS: { id: string; hex: string }[] = [ - { id: "blue", hex: "#4C8DFF" }, { id: "teal", hex: "#34D3C2" }, { id: "purple", hex: "#9B7BFF" }, - { id: "green", hex: "#3FB950" }, { id: "orange", hex: "#F2934B" }, -]; export function Settings({ config, health, onClose }: { config: ConfigView; health: DaemonHealth | null; onClose: () => void }) { const ref = useRef(null); useEffect(() => { ref.current?.focus(); }, []); + + // Fix 2: local state for font-size slider — committed only on pointer release. + const [sizeLocal, setSizeLocal] = useState(config.font_size); + useEffect(() => { setSizeLocal(config.font_size); }, [config.font_size]); + + // Fix 3: controlled shell input — synced from config, committed on blur. + const [shellLocal, setShellLocal] = useState(config.default_shell); + useEffect(() => { setShellLocal(config.default_shell); }, [config.default_shell]); return (
e.stopPropagation()} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Escape") onClose(); }} @@ -24,8 +28,11 @@ export function Settings({ config, health, onClose }: { config: ConfigView; heal {FONTS.map((f) => )}
- Size {config.font_size} - void setConfig({ font_size: Number(e.target.value) })} style={{ flex: 1 }} /> + Size {sizeLocal} + setSizeLocal(Number(e.target.value))} + onPointerUp={() => void setConfig({ font_size: sizeLocal })} + style={{ flex: 1 }} />
Theme
@@ -39,15 +46,15 @@ export function Settings({ config, health, onClose }: { config: ConfigView; heal
Accent
- {ACCENTS.map((a) => ( -
Default shell (empty = auto)
- void setConfig({ default_shell: e.target.value })} + setShellLocal(e.target.value)} onBlur={() => void setConfig({ default_shell: shellLocal })} style={{ width: "100%", padding: 8, marginBottom: 18, background: COLORS.bgPanel, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 8 }} /> diff --git a/app/src/TerminalView.tsx b/app/src/TerminalView.tsx index 35c0361..cc5817a 100644 --- a/app/src/TerminalView.tsx +++ b/app/src/TerminalView.tsx @@ -103,10 +103,7 @@ export function TerminalView({ surfaceId, font, palette }: { surfaceId: string; }, [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; + // font and palette are memoized in App.tsx so stable identity = no spurious re-applies. useEffect(() => { const t = termRef.current; if (!t) return; @@ -116,7 +113,7 @@ export function TerminalView({ surfaceId, font, palette }: { surfaceId: string; } 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 + }, [font, palette]); // eslint-disable-line react-hooks/exhaustive-deps return
; } diff --git a/app/src/main.tsx b/app/src/main.tsx index 4340d68..09c0067 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { App } from "./App"; +import { applyTheme } from "./theme"; import "@fontsource/inter/400.css"; import "@fontsource/inter/500.css"; import "@fontsource/inter/600.css"; @@ -9,6 +10,10 @@ import "@fontsource-variable/jetbrains-mono"; import "@xterm/xterm/css/xterm.css"; import "./styles.css"; +// Apply default theme before React renders so CSS vars are never unset, +// even if the daemon is slow or offline. getConfig() overrides this later. +applyTheme("dark", "blue"); + ReactDOM.createRoot(document.getElementById("root")!).render(