From d22a5748a0ee9852746aa23ea0402c6bb2ed9789 Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Wed, 20 May 2026 19:10:26 +0800 Subject: [PATCH 1/8] feat(ui): implement extensible theme system with dark/light modes Add VS Code-inspired CSS custom property theme architecture supporting Tokyo Night (dark) and VS Code Light themes. Replace hardcoded colors with semantic tokens. Redesign layout with a 32px header bar that bridges the native macOS title bar and terminal content, eliminating the 48px dark padding gap. Co-Authored-By: Claude Opus 4.7 --- sandbox-web/index.html | 2 +- sandbox-web/src/components/Terminal.tsx | 72 ++++++----- sandbox-web/src/index.css | 13 +- sandbox-web/src/main.tsx | 155 +++++++++++++++--------- sandbox-web/src/themes/ThemeContext.tsx | 81 +++++++++++++ sandbox-web/src/themes/registry.ts | 30 +++++ sandbox-web/src/themes/tokyo-night.ts | 47 +++++++ sandbox-web/src/themes/types.ts | 51 ++++++++ sandbox-web/src/themes/vscode-light.ts | 48 ++++++++ sandbox-web/tailwind.config.js | 30 +++-- 10 files changed, 427 insertions(+), 102 deletions(-) create mode 100644 sandbox-web/src/themes/ThemeContext.tsx create mode 100644 sandbox-web/src/themes/registry.ts create mode 100644 sandbox-web/src/themes/tokyo-night.ts create mode 100644 sandbox-web/src/themes/types.ts create mode 100644 sandbox-web/src/themes/vscode-light.ts diff --git a/sandbox-web/index.html b/sandbox-web/index.html index 7fcf91e..5752b8f 100644 --- a/sandbox-web/index.html +++ b/sandbox-web/index.html @@ -5,7 +5,7 @@ System Test Sandbox diff --git a/sandbox-web/src/main.tsx b/sandbox-web/src/main.tsx index f5690e8..0fb425c 100644 --- a/sandbox-web/src/main.tsx +++ b/sandbox-web/src/main.tsx @@ -78,9 +78,16 @@ function App() { return (
- {/* Header bar — bridges native title bar and terminal */} + {/* Window drag region — sits above header, full width for traffic lights */} +
+ + {/* Header bar — below traffic lights */}
Date: Wed, 20 May 2026 21:33:30 +0800 Subject: [PATCH 4/8] feat(ui): redesign macOS app with three-panel dashboard layout Restructure the sandbox window UI from single-panel to a professional three-panel layout matching the Atlas-style design: - Dark sidebar with logo, navigation, and instance list - Main dashboard with terminal card (resource stats), screenshot button, and instances list - Right detail panel with sandbox status, tools, network, and files info Extended theme system with 5 new tokens (sidebarBg/Fg/Border/Active, panelBg) for consistent theming across both dark and light modes. Co-Authored-By: Claude Opus 4.7 --- release/README.md | 2 +- sandbox-web/src/api.ts | 1 - sandbox-web/src/components/Dashboard.tsx | 212 ++++++++++++++ sandbox-web/src/components/DetailPanel.tsx | 168 ++++++++++++ sandbox-web/src/components/Sidebar.tsx | 304 +++++++++++++++++++++ sandbox-web/src/index.css | 49 ++++ sandbox-web/src/main.tsx | 151 ++++------ sandbox-web/src/themes/ThemeContext.tsx | 18 +- sandbox-web/src/themes/tokyo-night.ts | 5 + sandbox-web/src/themes/types.ts | 7 + sandbox-web/src/themes/vscode-light.ts | 75 ++--- sandbox-web/tailwind.config.js | 9 + 12 files changed, 859 insertions(+), 142 deletions(-) create mode 100644 sandbox-web/src/components/Dashboard.tsx create mode 100644 sandbox-web/src/components/DetailPanel.tsx create mode 100644 sandbox-web/src/components/Sidebar.tsx diff --git a/release/README.md b/release/README.md index 629f060..dc0ab50 100644 --- a/release/README.md +++ b/release/README.md @@ -135,4 +135,4 @@ A: 等待几秒让 Claude 启动,终端会自动连接 PTY 输出。 --- -**版本**: v${VERSION} | **构建时间**: __BUILD_DATE__ +**版本**: v${VERSION} | **构建时间**: 2026-05-20 21:24 diff --git a/sandbox-web/src/api.ts b/sandbox-web/src/api.ts index 9bb5658..46918d9 100644 --- a/sandbox-web/src/api.ts +++ b/sandbox-web/src/api.ts @@ -226,4 +226,3 @@ export async function listWindows(): Promise<[number, string][]> { const res = await fetch(`${BASE()}/windows`); return res.json(); } - diff --git a/sandbox-web/src/components/Dashboard.tsx b/sandbox-web/src/components/Dashboard.tsx new file mode 100644 index 0000000..6ca5714 --- /dev/null +++ b/sandbox-web/src/components/Dashboard.tsx @@ -0,0 +1,212 @@ +import { type ReactNode } from "react"; +import SandboxTerminal from "./Terminal"; +import type { ProcessInfo } from "../api"; + +interface DashboardProps { + sandboxName: string; + connected: boolean; + activePid: number | null; + onTerminalInput: (data: string) => void; + onScreenshot: () => void; + processes: ProcessInfo[]; + children?: ReactNode; +} + +export default function Dashboard({ + sandboxName, + connected, + activePid, + onTerminalInput, + onScreenshot, + processes, + children, +}: DashboardProps) { + return ( +
+ {/* Header */} +
+

+ Dashboard +

+
+ + +
+
+ + {/* Content */} +
+ {/* Sandbox card */} +
+ {/* Card header */} +
+
+ + {">_"} + + + {sandboxName} (Sandboxed) + +
+ +
+ + {/* Terminal */} +
+ +
+
+ + {/* Instances list */} +
+

+ Instances +

+
+ {processes.map((p) => ( + + ))} + {processes.length === 0 && ( +
+ No running instances +
+ )} +
+
+
+ + {/* Screenshot preview floating panel */} + {children} +
+ ); +} + +function ResourceStats({ connected }: { connected: boolean }) { + return ( +
+ + + +
+ ); +} + +function Stat({ label, value }: { label: string; value: string }) { + return ( +
+
+ {value} +
+
{label}
+
+ ); +} + +function InstanceRow({ process }: { process: ProcessInfo }) { + const isRunning = process.is_running; + return ( +
+
+ + {">_"} + + + {process.name} + +
+
+ + {isRunning ? "Running" : "Stopped"} + + {isRunning && ( + + )} +
+
+ ); +} diff --git a/sandbox-web/src/components/DetailPanel.tsx b/sandbox-web/src/components/DetailPanel.tsx new file mode 100644 index 0000000..71d7c12 --- /dev/null +++ b/sandbox-web/src/components/DetailPanel.tsx @@ -0,0 +1,168 @@ +import type { HealthResponse } from "../api"; + +interface DetailPanelProps { + health: HealthResponse | null; + connected: boolean; +} + +export default function DetailPanel({ health, connected }: DetailPanelProps) { + const uptime = health?.uptime_secs ? formatUptime(health.uptime_secs) : "--"; + + return ( + + ); +} + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

+ {title} +

+
{children}
+
+ ); +} + +function DetailLine({ label, value }: { label: string; value: string }) { + return ( +
+ {label} {value} +
+ ); +} + +function formatUptime(secs: number): string { + const h = Math.floor(secs / 3600); + const m = Math.floor((secs % 3600) / 60); + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +} diff --git a/sandbox-web/src/components/Sidebar.tsx b/sandbox-web/src/components/Sidebar.tsx new file mode 100644 index 0000000..a8e788f --- /dev/null +++ b/sandbox-web/src/components/Sidebar.tsx @@ -0,0 +1,304 @@ +import { useTheme } from "../themes/ThemeContext"; + +interface SidebarProps { + sandboxName: string; +} + +export default function Sidebar({ sandboxName }: SidebarProps) { + const { toggleTheme, theme } = useTheme(); + const isDark = theme.kind === "dark"; + + return ( + + ); +} + +function NavItem({ + icon, + label, + active, +}: { + icon: React.ReactNode; + label: string; + active?: boolean; +}) { + return ( +
{ + if (!active) + e.currentTarget.style.backgroundColor = + "var(--sandbox-sidebar-active)"; + }} + onMouseLeave={(e) => { + if (!active) e.currentTarget.style.backgroundColor = "transparent"; + }} + > + {icon} + {label} +
+ ); +} + +function InstanceItem({ + name, + icon, + selected, +}: { + name: string; + icon: React.ReactNode; + selected?: boolean; +}) { + return ( +
{ + if (!selected) + e.currentTarget.style.backgroundColor = + "var(--sandbox-sidebar-active)"; + }} + onMouseLeave={(e) => { + if (!selected) e.currentTarget.style.backgroundColor = "transparent"; + }} + > + {icon} + {name} +
+ ); +} + +// --- Icons --- + +function CircleIcon() { + return ( + + + + + ); +} + +function TemplateIcon() { + return ( + + + + ); +} + +function HistoryIcon() { + return ( + + + + ); +} + +function TerminalIcon() { + return ( + + + + ); +} + +function AppsIcon() { + return ( + + + + ); +} + +function LogsIcon() { + return ( + + + + ); +} diff --git a/sandbox-web/src/index.css b/sandbox-web/src/index.css index ef328cf..57a4f5b 100644 --- a/sandbox-web/src/index.css +++ b/sandbox-web/src/index.css @@ -47,7 +47,56 @@ html, body, #root { transition: opacity 0.1s; } +/* Sidebar scrollbar */ +.sidebar-scroll::-webkit-scrollbar { + width: 4px; +} + +.sidebar-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.sidebar-scroll::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.08); + border-radius: 2px; +} + +.sidebar-scroll::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.15); +} + +/* Panel scrollbar */ +.panel-scroll::-webkit-scrollbar { + width: 4px; +} + +.panel-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.panel-scroll::-webkit-scrollbar-thumb { + background: var(--sandbox-scrollbar-fg); + border-radius: 2px; +} + +/* Three-panel layout */ +.three-panel { + display: flex; + height: 100vh; + width: 100vw; + overflow: hidden; +} + @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } + +/* DEBUG: enable to visualize element boundaries */ +.debug-outlines * { + outline: 1px solid red !important; + outline-offset: -1px; +} +.debug-outlines html { outline: 2px solid magenta !important; } +.debug-outlines body { outline: 2px solid cyan !important; } +.debug-outlines #root { outline: 2px solid lime !important; } diff --git a/sandbox-web/src/main.tsx b/sandbox-web/src/main.tsx index 0fb425c..f2cd5e5 100644 --- a/sandbox-web/src/main.tsx +++ b/sandbox-web/src/main.tsx @@ -1,24 +1,28 @@ import { useState, useCallback, useEffect, useRef } from "react"; import ReactDOM from "react-dom/client"; -import SandboxTerminal from "./components/Terminal"; -import { ThemeProvider, useTheme } from "./themes/ThemeContext"; +import Sidebar from "./components/Sidebar"; +import Dashboard from "./components/Dashboard"; +import DetailPanel from "./components/DetailPanel"; +import { ThemeProvider } from "./themes/ThemeContext"; import * as api from "./api"; +import type { ProcessInfo, HealthResponse } from "./api"; import "./index.css"; function App() { const [activePid, setActivePid] = useState(null); const [connected, setConnected] = useState(false); + const [processes, setProcesses] = useState([]); + const [health, setHealth] = useState(null); const [screenshotUrl, setScreenshotUrl] = useState(null); - const [screenshotLoading, setScreenshotLoading] = useState(false); const [showPreview, setShowPreview] = useState(false); const hasConnectedRef = useRef(false); - const { theme, toggleTheme } = useTheme(); // Auto-connect to spawned processes useEffect(() => { const pollProcesses = async () => { try { const list = await api.listProcesses(); + setProcesses(list); if (list.length > 0) { setConnected(true); if (activePid === null && !hasConnectedRef.current) { @@ -41,6 +45,22 @@ function App() { return () => clearInterval(interval); }, [activePid]); + // Health poll + useEffect(() => { + const pollHealth = async () => { + try { + const h = await api.health(); + setHealth(h); + } catch { + // silent + } + }; + + pollHealth(); + const interval = setInterval(pollHealth, 5000); + return () => clearInterval(interval); + }, []); + // Terminal input -> PTY const handleTerminalInput = useCallback( (data: string) => { @@ -53,19 +73,15 @@ function App() { // Screenshot const handleScreenshot = useCallback(async () => { - setScreenshotLoading(true); try { const url = await api.takeScreenshot(); setScreenshotUrl(url); setShowPreview(true); } catch { // silent - } finally { - setScreenshotLoading(false); } }, []); - // Close preview const closePreview = useCallback(() => { setShowPreview(false); if (screenshotUrl) { @@ -74,110 +90,39 @@ function App() { } }, [screenshotUrl]); - const isDark = theme.kind === "dark"; + const sandboxName = health?.sandbox_id ?? "Sandbox"; return ( -
- {/* Window drag region — sits above header, full width for traffic lights */} -
- - {/* Header bar — below traffic lights */} -
+ + - {/* Left: sandbox info */} -
-
- - {connected ? "Connected" : "Waiting..."} -
-
- - {/* Right: actions */} -
- {/* Theme toggle */} - - - {/* Screenshot button */} - -
-
- - {/* Terminal — fills remaining space */} -
- - - {/* Screenshot preview — floating panel */} + {/* Screenshot preview floating panel */} {showPreview && screenshotUrl && ( -
-
+
- {/* Preview header */} -
- Screenshot + + Screenshot +
- {/* Preview image */} Screenshot
)} -
+ +
); } diff --git a/sandbox-web/src/themes/ThemeContext.tsx b/sandbox-web/src/themes/ThemeContext.tsx index 678cfda..9b676ad 100644 --- a/sandbox-web/src/themes/ThemeContext.tsx +++ b/sandbox-web/src/themes/ThemeContext.tsx @@ -1,4 +1,11 @@ -import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from "react"; +import { + createContext, + useContext, + useState, + useCallback, + useEffect, + type ReactNode, +} from "react"; import type { SandboxTheme, TerminalTheme } from "./types"; import { themeRegistry } from "./registry"; @@ -33,6 +40,11 @@ function applyThemeCSS(theme: SandboxTheme): void { root.style.setProperty("--sandbox-error", c.error); root.style.setProperty("--sandbox-titlebar-bg", c.titlebarBg); root.style.setProperty("--sandbox-titlebar-fg", c.titlebarFg); + root.style.setProperty("--sandbox-sidebar-bg", c.sidebarBg); + root.style.setProperty("--sandbox-sidebar-fg", c.sidebarFg); + root.style.setProperty("--sandbox-sidebar-border", c.sidebarBorder); + root.style.setProperty("--sandbox-sidebar-active", c.sidebarActive); + root.style.setProperty("--sandbox-panel-bg", c.panelBg); } export { ThemeContext, STORAGE_KEY }; @@ -68,7 +80,9 @@ export function ThemeProvider({ children }: { children: ReactNode }) { }, [theme.id, setTheme]); return ( - + {children} ); diff --git a/sandbox-web/src/themes/tokyo-night.ts b/sandbox-web/src/themes/tokyo-night.ts index 4da8d6c..3948f1e 100644 --- a/sandbox-web/src/themes/tokyo-night.ts +++ b/sandbox-web/src/themes/tokyo-night.ts @@ -19,6 +19,11 @@ export const tokyoNight: SandboxTheme = { error: "#f7768e", titlebarBg: "#1f2233", titlebarFg: "#a9b1d6", + sidebarBg: "#16161e", + sidebarFg: "#a9b1d6", + sidebarBorder: "#292e42", + sidebarActive: "rgba(122, 162, 247, 0.15)", + panelBg: "#1f2233", }, terminal: { background: "#1a1b26", diff --git a/sandbox-web/src/themes/types.ts b/sandbox-web/src/themes/types.ts index 3bfe516..1bc451f 100644 --- a/sandbox-web/src/themes/types.ts +++ b/sandbox-web/src/themes/types.ts @@ -14,6 +14,13 @@ export interface SandboxThemeColors { error: string; titlebarBg: string; titlebarFg: string; + /** Sidebar background (typically dark even in light themes) */ + sidebarBg: string; + sidebarFg: string; + sidebarBorder: string; + sidebarActive: string; + /** Right detail panel background */ + panelBg: string; } /** xterm.js terminal theme (subset of ITerminalOptions['theme']) */ diff --git a/sandbox-web/src/themes/vscode-light.ts b/sandbox-web/src/themes/vscode-light.ts index a03c7a5..0818072 100644 --- a/sandbox-web/src/themes/vscode-light.ts +++ b/sandbox-web/src/themes/vscode-light.ts @@ -6,43 +6,48 @@ export const vscodeLight: SandboxTheme = { name: "VS Code Light", kind: "light", colors: { - bgPrimary: "#ffffff", - bgSecondary: "#f3f3f3", - bgTertiary: "#e7e7e7", - fgPrimary: "#333333", - fgSecondary: "#717171", - fgTertiary: "#a0a0a0", - border: "#e7e7e7", - accent: "#0090f1", + bgPrimary: "#f5f5f7", + bgSecondary: "#ffffff", + bgTertiary: "#e8e8e8", + fgPrimary: "#1d1d1f", + fgSecondary: "#6e6e73", + fgTertiary: "#aeaeb2", + border: "#d2d2d7", + accent: "#0071e3", scrollbarBg: "transparent", - scrollbarFg: "rgba(0, 0, 0, 0.2)", - success: "#388a34", - error: "#a1260d", - titlebarBg: "#dddddd", - titlebarFg: "#333333", + scrollbarFg: "rgba(0, 0, 0, 0.15)", + success: "#34c759", + error: "#ff3b30", + titlebarBg: "#e8e8ed", + titlebarFg: "#1d1d1f", + sidebarBg: "#1c1c2e", + sidebarFg: "#c7c7cc", + sidebarBorder: "#2c2c3e", + sidebarActive: "rgba(0, 113, 227, 0.2)", + panelBg: "#ffffff", }, terminal: { - background: "#ffffff", - foreground: "#333333", - cursor: "#0090f1", - cursorAccent: "#ffffff", - selectionBackground: "rgba(0, 144, 241, 0.2)", - selectionForeground: "#333333", - black: "#000000", - red: "#cd3131", - green: "#00bc00", - yellow: "#949800", - blue: "#0451a5", - magenta: "#bc05bc", - cyan: "#0598bc", - white: "#555555", - brightBlack: "#666666", - brightRed: "#cd3131", - brightGreen: "#14ce14", - brightYellow: "#b5ba00", - brightBlue: "#0451a5", - brightMagenta: "#bc05bc", - brightCyan: "#0598bc", - brightWhite: "#a5a5a5", + background: "#1e1e2e", + foreground: "#cdd6f4", + cursor: "#f5e0dc", + cursorAccent: "#1e1e2e", + selectionBackground: "rgba(137, 180, 250, 0.3)", + selectionForeground: "#cdd6f4", + black: "#45475a", + red: "#f38ba8", + green: "#a6e3a1", + yellow: "#f9e2af", + blue: "#89b4fa", + magenta: "#f5c2e7", + cyan: "#94e2d5", + white: "#bac2de", + brightBlack: "#585b70", + brightRed: "#f38ba8", + brightGreen: "#a6e3a1", + brightYellow: "#f9e2af", + brightBlue: "#89b4fa", + brightMagenta: "#f5c2e7", + brightCyan: "#94e2d5", + brightWhite: "#a6adc8", }, }; diff --git a/sandbox-web/tailwind.config.js b/sandbox-web/tailwind.config.js index 8188559..5dfcd2a 100644 --- a/sandbox-web/tailwind.config.js +++ b/sandbox-web/tailwind.config.js @@ -27,6 +27,15 @@ export default { bg: "var(--sandbox-titlebar-bg)", fg: "var(--sandbox-titlebar-fg)", }, + sidebar: { + bg: "var(--sandbox-sidebar-bg)", + fg: "var(--sandbox-sidebar-fg)", + border: "var(--sandbox-sidebar-border)", + active: "var(--sandbox-sidebar-active)", + }, + panel: { + bg: "var(--sandbox-panel-bg)", + }, }, }, fontFamily: { From aff4ccf3b164ce584156cc44e17edae1e46bd8b1 Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Wed, 20 May 2026 21:47:52 +0800 Subject: [PATCH 5/8] feat(ui): terminal fills full middle area, show command in sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Terminal now fills the entire middle panel (removed fixed 320px height) - Removed Instances list from Dashboard (kept only in sidebar) - Sidebar shows actual command (e.g., "claude -p 你是谁?") instead of sandbox ID - Fetches command from Tauri IPC get_sandbox_config Co-Authored-By: Claude Opus 4.7 --- sandbox-web/src/components/Dashboard.tsx | 79 ++---------------------- sandbox-web/src/components/Sidebar.tsx | 6 +- sandbox-web/src/main.tsx | 28 +++++++-- 3 files changed, 31 insertions(+), 82 deletions(-) diff --git a/sandbox-web/src/components/Dashboard.tsx b/sandbox-web/src/components/Dashboard.tsx index 6ca5714..b228a71 100644 --- a/sandbox-web/src/components/Dashboard.tsx +++ b/sandbox-web/src/components/Dashboard.tsx @@ -1,6 +1,5 @@ import { type ReactNode } from "react"; import SandboxTerminal from "./Terminal"; -import type { ProcessInfo } from "../api"; interface DashboardProps { sandboxName: string; @@ -8,7 +7,6 @@ interface DashboardProps { activePid: number | null; onTerminalInput: (data: string) => void; onScreenshot: () => void; - processes: ProcessInfo[]; children?: ReactNode; } @@ -18,7 +16,6 @@ export default function Dashboard({ activePid, onTerminalInput, onScreenshot, - processes, children, }: DashboardProps) { return ( @@ -77,15 +74,15 @@ export default function Dashboard({
{/* Content */} -
+
{/* Sandbox card */}
{/* Card header */}
@@ -105,34 +102,11 @@ export default function Dashboard({
- {/* Terminal */} -
+ {/* Terminal — fills remaining space */} +
- - {/* Instances list */} -
-

- Instances -

-
- {processes.map((p) => ( - - ))} - {processes.length === 0 && ( -
- No running instances -
- )} -
-
{/* Screenshot preview floating panel */} @@ -167,46 +141,3 @@ function Stat({ label, value }: { label: string; value: string }) {
); } - -function InstanceRow({ process }: { process: ProcessInfo }) { - const isRunning = process.is_running; - return ( -
-
- - {">_"} - - - {process.name} - -
-
- - {isRunning ? "Running" : "Stopped"} - - {isRunning && ( - - )} -
-
- ); -} diff --git a/sandbox-web/src/components/Sidebar.tsx b/sandbox-web/src/components/Sidebar.tsx index a8e788f..dd80e09 100644 --- a/sandbox-web/src/components/Sidebar.tsx +++ b/sandbox-web/src/components/Sidebar.tsx @@ -1,10 +1,10 @@ import { useTheme } from "../themes/ThemeContext"; interface SidebarProps { - sandboxName: string; + command: string; } -export default function Sidebar({ sandboxName }: SidebarProps) { +export default function Sidebar({ command }: SidebarProps) { const { toggleTheme, theme } = useTheme(); const isDark = theme.kind === "dark"; @@ -60,7 +60,7 @@ export default function Sidebar({ sandboxName }: SidebarProps) { > Instances
- } selected /> + } selected />
{/* Spacer */} diff --git a/sandbox-web/src/main.tsx b/sandbox-web/src/main.tsx index f2cd5e5..d27d882 100644 --- a/sandbox-web/src/main.tsx +++ b/sandbox-web/src/main.tsx @@ -1,20 +1,21 @@ import { useState, useCallback, useEffect, useRef } from "react"; import ReactDOM from "react-dom/client"; +import { invoke } from "@tauri-apps/api/core"; import Sidebar from "./components/Sidebar"; import Dashboard from "./components/Dashboard"; import DetailPanel from "./components/DetailPanel"; import { ThemeProvider } from "./themes/ThemeContext"; import * as api from "./api"; -import type { ProcessInfo, HealthResponse } from "./api"; +import type { HealthResponse } from "./api"; import "./index.css"; function App() { const [activePid, setActivePid] = useState(null); const [connected, setConnected] = useState(false); - const [processes, setProcesses] = useState([]); const [health, setHealth] = useState(null); const [screenshotUrl, setScreenshotUrl] = useState(null); const [showPreview, setShowPreview] = useState(false); + const [command, setCommand] = useState("Sandbox"); const hasConnectedRef = useRef(false); // Auto-connect to spawned processes @@ -22,7 +23,6 @@ function App() { const pollProcesses = async () => { try { const list = await api.listProcesses(); - setProcesses(list); if (list.length > 0) { setConnected(true); if (activePid === null && !hasConnectedRef.current) { @@ -61,6 +61,25 @@ function App() { return () => clearInterval(interval); }, []); + // Fetch command from Tauri sandbox config + useEffect(() => { + invoke<{ + command?: string; + mode?: string; + args?: string[]; + }>("get_sandbox_config") + .then((config) => { + if (config.command) { + const args = + config.args && config.args.length > 0 + ? " " + config.args.join(" ") + : ""; + setCommand(config.command + args); + } + }) + .catch(() => {}); + }, []); + // Terminal input -> PTY const handleTerminalInput = useCallback( (data: string) => { @@ -94,14 +113,13 @@ function App() { return (
- + {/* Screenshot preview floating panel */} {showPreview && screenshotUrl && ( From 12c024e8c407c9ff4baec439f50f4db73c6fb614 Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Wed, 20 May 2026 22:21:05 +0800 Subject: [PATCH 6/8] feat(ui): simplify layout, add native macOS controls, add arg parsing tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove right detail panel, terminal fills full width - Simplify sidebar to instances list + dark/light mode toggle only - Use titleBarStyle "Overlay" for native macOS traffic light buttons - Add min window size (800x500) and center on launch - Add logging to CLI start and Tauri arg parsing for debugging - Add 17 unit tests covering arg parsing, config construction, kind derivation, and CLI→Tauri round-trip scenarios Co-Authored-By: Claude Opus 4.7 --- crates/sandbox-cli/src/main.rs | 9 +- release/README.md | 2 +- sandbox-web/src/components/DetailPanel.tsx | 168 ------------ sandbox-web/src/components/Sidebar.tsx | 230 ++-------------- sandbox-web/src/index.css | 63 +---- sandbox-web/src/main.tsx | 16 +- src-tauri/src/main.rs | 292 ++++++++++++++++++++- src-tauri/tauri.conf.json | 6 +- 8 files changed, 349 insertions(+), 437 deletions(-) delete mode 100644 sandbox-web/src/components/DetailPanel.tsx diff --git a/crates/sandbox-cli/src/main.rs b/crates/sandbox-cli/src/main.rs index 9588fe8..b2f33ff 100644 --- a/crates/sandbox-cli/src/main.rs +++ b/crates/sandbox-cli/src/main.rs @@ -90,12 +90,19 @@ fn cmd_start(command: &str, args: &[String]) -> anyhow::Result<()> { tauri_args.extend(args.iter().cloned()); } + tracing::info!("[start] bundle_path: {}", bundle_path.display()); + tracing::info!("[start] app_binary: {}", app_binary.display()); + tracing::info!("[start] tauri_args: {:?}", tauri_args); + tracing::info!("[start] app_binary exists: {}", app_binary.exists()); + // Run the binary directly (not via open -a) so arguments are passed correctly - Command::new(&app_binary) + let child = Command::new(&app_binary) .args(&tauri_args) .spawn() .context("Failed to launch Tauri sandbox app")?; + tracing::info!("[start] child pid: {:?}", child.id()); + let full_cmd = if args.is_empty() { command.to_string() } else { diff --git a/release/README.md b/release/README.md index dc0ab50..867255c 100644 --- a/release/README.md +++ b/release/README.md @@ -135,4 +135,4 @@ A: 等待几秒让 Claude 启动,终端会自动连接 PTY 输出。 --- -**版本**: v${VERSION} | **构建时间**: 2026-05-20 21:24 +**版本**: v${VERSION} | **构建时间**: 2026-05-20 22:19 diff --git a/sandbox-web/src/components/DetailPanel.tsx b/sandbox-web/src/components/DetailPanel.tsx deleted file mode 100644 index 71d7c12..0000000 --- a/sandbox-web/src/components/DetailPanel.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import type { HealthResponse } from "../api"; - -interface DetailPanelProps { - health: HealthResponse | null; - connected: boolean; -} - -export default function DetailPanel({ health, connected }: DetailPanelProps) { - const uptime = health?.uptime_secs ? formatUptime(health.uptime_secs) : "--"; - - return ( - - ); -} - -function Section({ - title, - children, -}: { - title: string; - children: React.ReactNode; -}) { - return ( -
-

- {title} -

-
{children}
-
- ); -} - -function DetailLine({ label, value }: { label: string; value: string }) { - return ( -
- {label} {value} -
- ); -} - -function formatUptime(secs: number): string { - const h = Math.floor(secs / 3600); - const m = Math.floor((secs % 3600) / 60); - if (h > 0) return `${h}h ${m}m`; - return `${m}m`; -} diff --git a/sandbox-web/src/components/Sidebar.tsx b/sandbox-web/src/components/Sidebar.tsx index dd80e09..de91aa9 100644 --- a/sandbox-web/src/components/Sidebar.tsx +++ b/sandbox-web/src/components/Sidebar.tsx @@ -10,7 +10,7 @@ export default function Sidebar({ command }: SidebarProps) { return ( ); } - -function NavItem({ - icon, - label, - active, -}: { - icon: React.ReactNode; - label: string; - active?: boolean; -}) { - return ( -
{ - if (!active) - e.currentTarget.style.backgroundColor = - "var(--sandbox-sidebar-active)"; - }} - onMouseLeave={(e) => { - if (!active) e.currentTarget.style.backgroundColor = "transparent"; - }} - > - {icon} - {label} -
- ); -} - -function InstanceItem({ - name, - icon, - selected, -}: { - name: string; - icon: React.ReactNode; - selected?: boolean; -}) { - return ( -
{ - if (!selected) - e.currentTarget.style.backgroundColor = - "var(--sandbox-sidebar-active)"; - }} - onMouseLeave={(e) => { - if (!selected) e.currentTarget.style.backgroundColor = "transparent"; - }} - > - {icon} - {name} -
- ); -} - -// --- Icons --- - -function CircleIcon() { - return ( - - - - - ); -} - -function TemplateIcon() { - return ( - - - - ); -} - -function HistoryIcon() { - return ( - - - - ); -} - -function TerminalIcon() { - return ( - - - - ); -} - -function AppsIcon() { - return ( - - - - ); -} - -function LogsIcon() { - return ( - - - - ); -} diff --git a/sandbox-web/src/index.css b/sandbox-web/src/index.css index 57a4f5b..a304d6b 100644 --- a/sandbox-web/src/index.css +++ b/sandbox-web/src/index.css @@ -18,7 +18,18 @@ html, body, #root { -moz-osx-font-smoothing: grayscale; } -/* xterm.js — edge-to-edge terminal with minimal padding */ +/* Tauri drag region */ +[data-tauri-drag-region] { + -webkit-app-region: drag; +} + +[data-tauri-drag-region] button, +[data-tauri-drag-region] a, +[data-tauri-drag-region] input { + -webkit-app-region: no-drag; +} + +/* xterm.js */ .xterm { padding: 8px 16px 12px 16px; height: 100%; @@ -42,61 +53,11 @@ html, body, #root { opacity: 0.8; } -/* Smooth cursor */ .xterm-cursor-layer { transition: opacity 0.1s; } -/* Sidebar scrollbar */ -.sidebar-scroll::-webkit-scrollbar { - width: 4px; -} - -.sidebar-scroll::-webkit-scrollbar-track { - background: transparent; -} - -.sidebar-scroll::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.08); - border-radius: 2px; -} - -.sidebar-scroll::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.15); -} - -/* Panel scrollbar */ -.panel-scroll::-webkit-scrollbar { - width: 4px; -} - -.panel-scroll::-webkit-scrollbar-track { - background: transparent; -} - -.panel-scroll::-webkit-scrollbar-thumb { - background: var(--sandbox-scrollbar-fg); - border-radius: 2px; -} - -/* Three-panel layout */ -.three-panel { - display: flex; - height: 100vh; - width: 100vw; - overflow: hidden; -} - @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } - -/* DEBUG: enable to visualize element boundaries */ -.debug-outlines * { - outline: 1px solid red !important; - outline-offset: -1px; -} -.debug-outlines html { outline: 2px solid magenta !important; } -.debug-outlines body { outline: 2px solid cyan !important; } -.debug-outlines #root { outline: 2px solid lime !important; } diff --git a/sandbox-web/src/main.tsx b/sandbox-web/src/main.tsx index d27d882..304665d 100644 --- a/sandbox-web/src/main.tsx +++ b/sandbox-web/src/main.tsx @@ -3,12 +3,14 @@ import ReactDOM from "react-dom/client"; import { invoke } from "@tauri-apps/api/core"; import Sidebar from "./components/Sidebar"; import Dashboard from "./components/Dashboard"; -import DetailPanel from "./components/DetailPanel"; import { ThemeProvider } from "./themes/ThemeContext"; import * as api from "./api"; import type { HealthResponse } from "./api"; import "./index.css"; +const isMac = + typeof navigator !== "undefined" && navigator.platform.startsWith("Mac"); + function App() { const [activePid, setActivePid] = useState(null); const [connected, setConnected] = useState(false); @@ -112,7 +114,16 @@ function App() { const sandboxName = health?.sandbox_id ?? "Sandbox"; return ( -
+
+ {/* macOS drag region — reserves space for traffic light buttons */} + {isMac && ( +
+ )} + )} -
); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c7a7ad5..5e0a2fd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -55,8 +55,7 @@ struct SandboxLaunchArgs { args: Vec, } -fn parse_sandbox_args() -> SandboxLaunchArgs { - let args: Vec = std::env::args().collect(); +fn parse_sandbox_args_from_slice(args: &[String]) -> SandboxLaunchArgs { let mut result = SandboxLaunchArgs::default(); let mut i = 1; while i < args.len() { @@ -82,7 +81,6 @@ fn parse_sandbox_args() -> SandboxLaunchArgs { i += 1; result.cmd = Some(args[i].clone()); } else if arg == "--" { - // Everything after -- is passed as trailing args to the CLI command result.args = args[(i + 1)..].to_vec(); break; } @@ -91,6 +89,21 @@ fn parse_sandbox_args() -> SandboxLaunchArgs { result } +fn parse_sandbox_args() -> SandboxLaunchArgs { + let args: Vec = std::env::args().collect(); + tracing::info!("[args] raw args: {:?}", args); + let result = parse_sandbox_args_from_slice(&args); + tracing::info!( + "[args] parsed: mode={:?}, cmd={:?}, args={:?}, sandbox_id={:?}, port={:?}", + result.mode, + result.cmd, + result.args, + result.sandbox_id, + result.sandbox_port, + ); + result +} + fn main() { let launch_args = parse_sandbox_args(); @@ -151,9 +164,14 @@ fn main() { init_sandbox, ]) .setup(move |app_handle| { + tracing::info!("[setup] sandbox_id={:?}, port={:?}, kind={:?}", sandbox_id, sandbox_port, kind); + // Set window title if let Some(window) = app_handle.get_webview_window("main") { + tracing::info!("[setup] setting window title: {}", title); let _ = window.set_title(&title); + } else { + tracing::warn!("[setup] main window not found!"); } // Start embedded HTTP server if in managed mode @@ -205,32 +223,45 @@ fn main() { if let Some(InstanceKind::Cli { command, args }) = &kind { let cmd = command.clone(); let cmd_args = args.clone(); + tracing::info!("[setup] auto-spawn CLI: cmd={:?}, args={:?}", cmd, cmd_args); tauri::async_runtime::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(500)).await; + tracing::info!("[setup] spawning CLI now: {} {:?}", cmd, cmd_args); match ProcessManager::spawn_cli(&cmd, &cmd_args) { Ok(info) => { - tracing::info!("Auto-spawned CLI: {} (pid={})", cmd, info.pid); + tracing::info!("[setup] auto-spawned CLI: {} (pid={})", cmd, info.pid); } Err(e) => { - tracing::error!("Failed to auto-spawn CLI '{cmd}': {e}"); + tracing::error!("[setup] failed to auto-spawn CLI '{}': {}", cmd, e); } } }); + } else { + tracing::info!("[setup] not CLI mode, skipping auto-spawn. kind={:?}", kind); } // Auto-discover the Tauri window's SCWindow ID for screenshot support. // The window needs time to render before ScreenCaptureKit can find it. tauri::async_runtime::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(2)).await; + tracing::info!("[setup] discovering window by title 'System Test Sandbox'"); match sandbox_core::capture::ScreenCapture::find_window_by_title( "System Test Sandbox", ) { Ok(id) => { - tracing::info!("Discovered sandbox window: SCWindow ID={id}"); + tracing::info!("[setup] discovered sandbox window: SCWindow ID={id}"); state_for_window.lock().await.window_id = Some(id); } Err(e) => { - tracing::warn!("Failed to discover sandbox window: {e}"); + tracing::warn!("[setup] failed to discover sandbox window: {e}"); + // List all windows for debugging + if let Ok(windows) = sandbox_core::capture::ScreenCapture::list_windows() { + for (wid, wtitle) in &windows { + if !wtitle.is_empty() { + tracing::info!("[setup] window {}: '{}'", wid, wtitle); + } + } + } } } }); @@ -257,3 +288,250 @@ fn main() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +#[cfg(test)] +mod tests { + use super::*; + + fn args(s: &[&str]) -> Vec { + s.iter().map(|s| s.to_string()).collect() + } + + // ── parse_sandbox_args_from_slice ────────────────────── + + #[test] + fn parse_mode_and_cmd_eq() { + let r = parse_sandbox_args_from_slice(&args(&["bin", "--mode=cli", "--cmd=claude"])); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("claude")); + assert!(r.args.is_empty()); + } + + #[test] + fn parse_mode_and_cmd_space() { + let r = parse_sandbox_args_from_slice(&args(&["bin", "--mode", "cli", "--cmd", "claude"])); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("claude")); + } + + #[test] + fn parse_trailing_args_after_separator() { + let r = parse_sandbox_args_from_slice(&args(&[ + "bin", "--mode=cli", "--cmd=claude", "--", "-p", "你是谁?", + ])); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("claude")); + assert_eq!(r.args, vec!["-p", "你是谁?"]); + } + + #[test] + fn parse_no_trailing_args() { + let r = parse_sandbox_args_from_slice(&args(&["bin", "--mode=cli", "--cmd=zsh"])); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("zsh")); + assert!(r.args.is_empty()); + } + + #[test] + fn parse_sandbox_id_and_port() { + let r = parse_sandbox_args_from_slice(&args(&[ + "bin", + "--sandbox-id=abc123", + "--sandbox-port=15801", + "--mode=cli", + "--cmd=echo", + ])); + assert_eq!(r.sandbox_id.as_deref(), Some("abc123")); + assert_eq!(r.sandbox_port, Some(15801)); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("echo")); + } + + #[test] + fn parse_empty_args() { + let r = parse_sandbox_args_from_slice(&args(&["bin"])); + assert!(r.mode.is_none()); + assert!(r.cmd.is_none()); + assert!(r.args.is_empty()); + } + + #[test] + fn parse_only_cmd_no_mode() { + let r = parse_sandbox_args_from_slice(&args(&["bin", "--cmd=claude"])); + assert!(r.mode.is_none()); + assert_eq!(r.cmd.as_deref(), Some("claude")); + } + + #[test] + fn parse_mixed_eq_and_space() { + let r = parse_sandbox_args_from_slice(&args(&[ + "bin", + "--mode=cli", + "--cmd", + "claude", + "--", + "-p", + "hello", + ])); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("claude")); + assert_eq!(r.args, vec!["-p", "hello"]); + } + + // ── SandboxConfig construction from parsed args ─────── + + #[test] + fn config_from_parsed_cli_args() { + let r = parse_sandbox_args_from_slice(&args(&[ + "bin", + "--sandbox-id=test1", + "--sandbox-port=5801", + "--mode=cli", + "--cmd=claude", + "--", + "-p", + "你是谁?", + ])); + + let config = SandboxConfig { + id: r.sandbox_id.clone(), + port: r.sandbox_port, + mode: r.mode.clone(), + command: r.cmd.clone(), + args: r.args.clone(), + ..SandboxConfig::default() + }; + + assert_eq!(config.id.as_deref(), Some("test1")); + assert_eq!(config.port, Some(5801)); + assert_eq!(config.mode.as_deref(), Some("cli")); + assert_eq!(config.command.as_deref(), Some("claude")); + assert_eq!(config.args, vec!["-p", "你是谁?"]); + } + + #[test] + fn kind_from_config_cli() { + let config = SandboxConfig { + mode: Some("cli".into()), + command: Some("claude".into()), + args: vec!["-p".into(), "你是谁?".into()], + ..SandboxConfig::default() + }; + let sandbox = Sandbox::new(config); + let kind = sandbox.kind().unwrap(); + match kind { + InstanceKind::Cli { command, args } => { + assert_eq!(command, "claude"); + assert_eq!(args, vec!["-p", "你是谁?"]); + } + _ => panic!("Expected CLI kind"), + } + } + + #[test] + fn kind_from_config_app() { + let config = SandboxConfig { + mode: Some("app".into()), + command: Some("/Applications/Safari.app".into()), + ..SandboxConfig::default() + }; + let sandbox = Sandbox::new(config); + let kind = sandbox.kind().unwrap(); + assert!(matches!(kind, InstanceKind::App { .. })); + } + + #[test] + fn kind_none_without_mode() { + let config = SandboxConfig::default(); + let sandbox = Sandbox::new(config); + assert!(sandbox.kind().is_none()); + } + + // ── CLI args construction (mirrors cmd_start logic) ─── + + #[test] + fn cli_builds_tauri_args_simple() { + let command = "claude"; + let cli_args: Vec = vec![]; + + let mut tauri_args = vec!["--mode=cli".to_string(), format!("--cmd={}", command)]; + if !cli_args.is_empty() { + tauri_args.push("--".to_string()); + tauri_args.extend(cli_args.iter().cloned()); + } + + assert_eq!(tauri_args, vec!["--mode=cli", "--cmd=claude"]); + } + + #[test] + fn cli_builds_tauri_args_with_trailing() { + let command = "claude"; + let cli_args: Vec = vec!["-p".into(), "你是谁?".into()]; + + let mut tauri_args = vec!["--mode=cli".to_string(), format!("--cmd={}", command)]; + if !cli_args.is_empty() { + tauri_args.push("--".to_string()); + tauri_args.extend(cli_args.iter().cloned()); + } + + assert_eq!( + tauri_args, + vec!["--mode=cli", "--cmd=claude", "--", "-p", "你是谁?"] + ); + } + + #[test] + fn cli_builds_tauri_args_zsh() { + let command = "zsh"; + let cli_args: Vec = vec![]; + + let mut tauri_args = vec!["--mode=cli".to_string(), format!("--cmd={}", command)]; + if !cli_args.is_empty() { + tauri_args.push("--".to_string()); + tauri_args.extend(cli_args.iter().cloned()); + } + + assert_eq!(tauri_args, vec!["--mode=cli", "--cmd=zsh"]); + } + + // ── Round-trip: CLI builds args → Tauri parses them ─── + + #[test] + fn roundtrip_claude_with_args() { + // CLI constructs these args + let command = "claude"; + let cli_args = vec!["-p".to_string(), "你是谁?".to_string()]; + let mut tauri_args = vec!["--mode=cli".to_string(), format!("--cmd={}", command)]; + tauri_args.push("--".to_string()); + tauri_args.extend(cli_args); + + // Prepend program name (as std::env::args would) + let mut full_args = vec!["system-test-sandbox".to_string()]; + full_args.extend(tauri_args); + + // Tauri parses them + let r = parse_sandbox_args_from_slice(&full_args); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("claude")); + assert_eq!(r.args, vec!["-p", "你是谁?"]); + } + + #[test] + fn roundtrip_simple_command() { + let command = "zsh"; + let cli_args: Vec = vec![]; + let mut tauri_args = vec!["--mode=cli".to_string(), format!("--cmd={}", command)]; + if !cli_args.is_empty() { + tauri_args.push("--".to_string()); + tauri_args.extend(cli_args); + } + + let mut full_args = vec!["system-test-sandbox".to_string()]; + full_args.extend(tauri_args); + + let r = parse_sandbox_args_from_slice(&full_args); + assert_eq!(r.mode.as_deref(), Some("cli")); + assert_eq!(r.cmd.as_deref(), Some("zsh")); + assert!(r.args.is_empty()); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1f7b4bf..ad3ec0d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,9 +16,11 @@ "title": "System Test Sandbox", "width": 1280, "height": 800, + "minWidth": 800, + "minHeight": 500, "resizable": true, - "transparent": true, - "decorations": false + "titleBarStyle": "Overlay", + "center": true } ], "security": { From 67523e792831bab5a7669405f38293790ff952aa Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Wed, 20 May 2026 22:26:29 +0800 Subject: [PATCH 7/8] fix(ci): apply cargo fmt to pass Rust formatting check Co-Authored-By: Claude Opus 4.7 --- src-tauri/src/main.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5e0a2fd..2667b85 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -164,7 +164,12 @@ fn main() { init_sandbox, ]) .setup(move |app_handle| { - tracing::info!("[setup] sandbox_id={:?}, port={:?}, kind={:?}", sandbox_id, sandbox_port, kind); + tracing::info!( + "[setup] sandbox_id={:?}, port={:?}, kind={:?}", + sandbox_id, + sandbox_port, + kind + ); // Set window title if let Some(window) = app_handle.get_webview_window("main") { @@ -229,10 +234,18 @@ fn main() { tracing::info!("[setup] spawning CLI now: {} {:?}", cmd, cmd_args); match ProcessManager::spawn_cli(&cmd, &cmd_args) { Ok(info) => { - tracing::info!("[setup] auto-spawned CLI: {} (pid={})", cmd, info.pid); + tracing::info!( + "[setup] auto-spawned CLI: {} (pid={})", + cmd, + info.pid + ); } Err(e) => { - tracing::error!("[setup] failed to auto-spawn CLI '{}': {}", cmd, e); + tracing::error!( + "[setup] failed to auto-spawn CLI '{}': {}", + cmd, + e + ); } } }); @@ -255,7 +268,9 @@ fn main() { Err(e) => { tracing::warn!("[setup] failed to discover sandbox window: {e}"); // List all windows for debugging - if let Ok(windows) = sandbox_core::capture::ScreenCapture::list_windows() { + if let Ok(windows) = + sandbox_core::capture::ScreenCapture::list_windows() + { for (wid, wtitle) in &windows { if !wtitle.is_empty() { tracing::info!("[setup] window {}: '{}'", wid, wtitle); @@ -317,7 +332,12 @@ mod tests { #[test] fn parse_trailing_args_after_separator() { let r = parse_sandbox_args_from_slice(&args(&[ - "bin", "--mode=cli", "--cmd=claude", "--", "-p", "你是谁?", + "bin", + "--mode=cli", + "--cmd=claude", + "--", + "-p", + "你是谁?", ])); assert_eq!(r.mode.as_deref(), Some("cli")); assert_eq!(r.cmd.as_deref(), Some("claude")); From 08ee382cd13fec1b9bb7cad8224da0507cf764fe Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Wed, 20 May 2026 22:37:57 +0800 Subject: [PATCH 8/8] fix(ui): remove window title text and show command in dashboard header - Set Tauri window title to empty string to remove "System Test Sandbox" text next to traffic light buttons - Replace sandbox ID with actual command name in Dashboard card header - Remove unused health state and HealthResponse import Co-Authored-By: Claude Opus 4.7 --- sandbox-web/src/components/Dashboard.tsx | 6 +++--- sandbox-web/src/main.tsx | 22 +--------------------- src-tauri/tauri.conf.json | 2 +- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/sandbox-web/src/components/Dashboard.tsx b/sandbox-web/src/components/Dashboard.tsx index b228a71..6815e8f 100644 --- a/sandbox-web/src/components/Dashboard.tsx +++ b/sandbox-web/src/components/Dashboard.tsx @@ -2,7 +2,7 @@ import { type ReactNode } from "react"; import SandboxTerminal from "./Terminal"; interface DashboardProps { - sandboxName: string; + command: string; connected: boolean; activePid: number | null; onTerminalInput: (data: string) => void; @@ -11,7 +11,7 @@ interface DashboardProps { } export default function Dashboard({ - sandboxName, + command, connected, activePid, onTerminalInput, @@ -96,7 +96,7 @@ export default function Dashboard({ className="text-sm font-medium" style={{ color: "var(--sandbox-fg-primary)" }} > - {sandboxName} (Sandboxed) + {command} (Sandboxed)
diff --git a/sandbox-web/src/main.tsx b/sandbox-web/src/main.tsx index 304665d..04ad903 100644 --- a/sandbox-web/src/main.tsx +++ b/sandbox-web/src/main.tsx @@ -5,7 +5,6 @@ import Sidebar from "./components/Sidebar"; import Dashboard from "./components/Dashboard"; import { ThemeProvider } from "./themes/ThemeContext"; import * as api from "./api"; -import type { HealthResponse } from "./api"; import "./index.css"; const isMac = @@ -14,7 +13,6 @@ const isMac = function App() { const [activePid, setActivePid] = useState(null); const [connected, setConnected] = useState(false); - const [health, setHealth] = useState(null); const [screenshotUrl, setScreenshotUrl] = useState(null); const [showPreview, setShowPreview] = useState(false); const [command, setCommand] = useState("Sandbox"); @@ -47,22 +45,6 @@ function App() { return () => clearInterval(interval); }, [activePid]); - // Health poll - useEffect(() => { - const pollHealth = async () => { - try { - const h = await api.health(); - setHealth(h); - } catch { - // silent - } - }; - - pollHealth(); - const interval = setInterval(pollHealth, 5000); - return () => clearInterval(interval); - }, []); - // Fetch command from Tauri sandbox config useEffect(() => { invoke<{ @@ -111,8 +93,6 @@ function App() { } }, [screenshotUrl]); - const sandboxName = health?.sandbox_id ?? "Sandbox"; - return (
{/* macOS drag region — reserves space for traffic light buttons */} @@ -126,7 +106,7 @@ function App() {