From ae0e2fa834858a47a706557cfd5ef606e17e3bdd Mon Sep 17 00:00:00 2001 From: oratis Date: Sat, 30 May 2026 11:52:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20inspector=20expand=20panel=20(?= =?UTF-8?q?320px)=20=E2=80=94=20Plan/Context/Recent/Session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the deferred inspector panel (design spec screen #3, HANDOFF §10). The right column is a 48px rail by default; the ‹ button or ⌘\ expands it to a 320px panel that squeezes the chat stream (via the .inspector-open grid modifier), not overlays it. Four sections: ▤ Plan — the agent's TodoWrite list + pending count ◐ Context — token usage bar (contextWindowFor(model) denominator) 📁 Recent files — files touched by Write/Edit/MultiEdit this conversation ⓘ Session info — project / path / model / mode / cumulative cost Empty sections show honest empty states (no fake placeholders). Wiring: ReplScreen lifts its inspector slice (usage/model/mode/recent files/ todos) to App via a single onInspector callback; App merges it into one InspectorData that feeds both the collapsed rail's badges and the expanded panel. The rail's ◐ context dot + ▤ plan badge are now driven by real data (previously hardcoded undefined). Taken over from the epic-neumann worktree and rebased onto main. Builds clean (vite 222 modules), typecheck + 24 desktop tests pass, format + 0 new lint warnings. NOTE: this is UI that still needs a visual pass in `pnpm --filter @deepcode/desktop tauri:dev` — confirm the 48↔320px toggle, ⌘\, and that all four sections render against the design spec. Logic/build are verified; pixels are not. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/App.tsx | 46 ++++- .../desktop/src/components/InspectorPanel.tsx | 153 +++++++++++++++ apps/desktop/src/components/InspectorRail.tsx | 13 +- apps/desktop/src/index.css | 182 ++++++++++++++++-- apps/desktop/src/screens/Repl.tsx | 45 ++++- apps/desktop/src/types/inspector.ts | 43 +++++ 6 files changed, 451 insertions(+), 31 deletions(-) create mode 100644 apps/desktop/src/components/InspectorPanel.tsx create mode 100644 apps/desktop/src/types/inspector.ts diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index bab4ddf..dd1583d 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -2,7 +2,9 @@ // Spec: docs/VISUAL_DESIGN.html // Milestone: 0.1.2 — adds project-folder flow + inspector wiring + session refresh. -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { contextWindowFor } from '@deepcode/core/dist/providers/deepseek.js'; +import { InspectorPanel } from './components/InspectorPanel.js'; import { InspectorRail } from './components/InspectorRail.js'; import { ProjectPickerOverlay } from './components/ProjectPickerOverlay.js'; import { Sidebar } from './components/Sidebar.js'; @@ -23,6 +25,7 @@ import { SettingsScreen } from './screens/Settings.js'; import { SkillsScreen } from './screens/Skills.js'; import type { ScreenName } from './types/screens.js'; import type { UpdateInfo } from './types/global.js'; +import { emptyInspectorData, type InspectorData } from './types/inspector.js'; export function App(): JSX.Element { const [hasKey, setHasKey] = useState(null); @@ -34,6 +37,15 @@ export function App(): JSX.Element { // Reconstructed messages for a resumed session; seeded into ReplScreen on its // next remount. Cleared when starting a fresh session. const [resumedMessages, setResumedMessages] = useState(undefined); + // Right inspector: 48 px rail by default, 320 px panel when expanded. + const [inspectorExpanded, setInspectorExpanded] = useState(false); + const [inspector, setInspector] = useState(() => emptyInspectorData()); + + // Merge the slice ReplScreen lifts up (usage / model / mode / files / todos). + // Stable identity so ReplScreen's sync effect doesn't refire every render. + const handleInspector = useCallback((patch: Partial) => { + setInspector((prev) => ({ ...prev, ...patch })); + }, []); useEffect(() => { void window.deepcode.creds.load().then((c) => setHasKey(c.hasKey)); @@ -52,6 +64,7 @@ export function App(): JSX.Element { }); const offComma = registerShortcut('meta+,', () => setScreen('settings')); const offSlash = registerShortcut('meta+/', () => setScreen('about')); + const offBackslash = registerShortcut('meta+\\', () => setInspectorExpanded((v) => !v)); return () => { offShim(); @@ -59,6 +72,7 @@ export function App(): JSX.Element { offN(); offComma(); offSlash(); + offBackslash(); }; }, []); @@ -95,9 +109,15 @@ export function App(): JSX.Element { return ; } - // Main shell: 3-column grid. + // Main shell: 3-column grid. The right column is a 48 px rail by default and + // a 320 px panel when expanded — the `inspector-open` modifier widens the + // grid track so the panel squeezes the chat stream rather than overlaying it. + const planCount = inspector.todos.filter((t) => t.status !== 'completed').length; + const usedTokens = inspector.usage.inputTokens + inspector.usage.outputTokens; + const contextFill = usedTokens > 0 ? usedTokens / contextWindowFor(inspector.model) : undefined; + return ( -
+
{update && } setSessionEpoch((k) => k + 1), + handleInspector, resumedMessages, )} - setScreen(s)} contextFill={undefined} /> + {inspectorExpanded ? ( + setInspectorExpanded(false)} + /> + ) : ( + setScreen(s)} + onExpand={() => setInspectorExpanded(true)} + planCount={planCount} + contextFill={contextFill} + /> + )}
); } @@ -154,6 +189,7 @@ function renderScreen( setScreen: (s: ScreenName) => void, projectPath: string, onTurnComplete: () => void, + onInspector: (patch: Partial) => void, initialMessages?: Msg[], ): JSX.Element { switch (screen) { @@ -164,6 +200,7 @@ function renderScreen( projectPath={projectPath} onTurnComplete={onTurnComplete} initialMessages={initialMessages} + onInspector={onInspector} /> ); case 'sessions': @@ -187,6 +224,7 @@ function renderScreen( projectPath={projectPath} onTurnComplete={onTurnComplete} initialMessages={initialMessages} + onInspector={onInspector} /> ); } diff --git a/apps/desktop/src/components/InspectorPanel.tsx b/apps/desktop/src/components/InspectorPanel.tsx new file mode 100644 index 0000000..f21d5e2 --- /dev/null +++ b/apps/desktop/src/components/InspectorPanel.tsx @@ -0,0 +1,153 @@ +// Right-column expanded inspector (320 px). +// Design spec screen #3 — the panel that the 48 px rail expands into when the +// user clicks ‹ or presses ⌘\. Four sections, top to bottom: +// ▤ Plan — the agent's TodoWrite list, with a pending count +// ◐ Context — token usage, same bar as the composer's .ctx-bar +// 📁 Recent files — files touched by Write/Edit this conversation +// ⓘ Session info — project / path / model / mode / cost +// +// All data comes from the InspectorData the parent (App) maintains; this +// component is purely presentational. Sections with no data show an honest +// empty state rather than a placeholder — per HANDOFF: no fake sections. + +import { contextWindowFor } from '@deepcode/core/dist/providers/deepseek.js'; +import { projectName } from '../lib/project.js'; +import type { InspectorData } from '../types/inspector.js'; + +interface InspectorPanelProps { + projectPath: string; + data: InspectorData; + /** Collapse back to the 48 px rail (the › button / ⌘\). */ + onCollapse: () => void; +} + +const MODE_LABELS: Record = { + default: 'Default · ask', + acceptEdits: 'Accept edits', + plan: 'Plan mode', + dontAsk: "Don't ask", + bypassPermissions: 'Bypass', +}; + +export function InspectorPanel({ + projectPath, + data, + onCollapse, +}: InspectorPanelProps): JSX.Element { + const { usage, costYuan, model, mode, recentFiles, todos } = data; + + const contextWindow = contextWindowFor(model); + const usedTokens = usage.inputTokens + usage.outputTokens; + const fillPct = Math.min(100, (usedTokens / contextWindow) * 100); + + const pending = todos.filter((t) => t.status !== 'completed').length; + + return ( + + ); +} + +// ─── path helpers ───────────────────────────────────────────────────── + +function basename(p: string): string { + const parts = p.split('/').filter(Boolean); + return parts[parts.length - 1] ?? p; +} + +function dirname(p: string): string { + const idx = p.lastIndexOf('/'); + if (idx <= 0) return ''; + return abbreviatePath(p.slice(0, idx)); +} + +/** Abbreviate a long path by replacing the $HOME prefix with "~". */ +function abbreviatePath(p: string): string { + const m = p.match(/^\/Users\/[^/]+/); + if (m) return '~' + p.slice(m[0].length); + return p; +} diff --git a/apps/desktop/src/components/InspectorRail.tsx b/apps/desktop/src/components/InspectorRail.tsx index 7b7815b..69a482c 100644 --- a/apps/desktop/src/components/InspectorRail.tsx +++ b/apps/desktop/src/components/InspectorRail.tsx @@ -3,8 +3,8 @@ // // Each rail button routes to a screen so users can reach Plan / Files / // Info / Settings without scrolling for a hidden menu. The ‹ expand -// chevron is still deferred (the full-width inspector panel lands in -// the next phase) — we leave it disabled with a tooltip. +// chevron opens the full-width inspector panel (InspectorPanel) — App +// owns the collapsed↔expanded state and passes it down via onExpand. import type { ScreenName } from '../types/screens.js'; @@ -17,6 +17,8 @@ interface InspectorRailProps { activeScreen: ScreenName; /** Switch screen. */ onChange: (screen: ScreenName) => void; + /** Expand the rail into the 320 px inspector panel (‹ / ⌘\). */ + onExpand: () => void; } export function InspectorRail({ @@ -24,6 +26,7 @@ export function InspectorRail({ contextFill, activeScreen, onChange, + onExpand, }: InspectorRailProps): JSX.Element { const ctxColor = contextFill === undefined @@ -38,9 +41,9 @@ export function InspectorRail({