From 3773cabb624edec90a0cc734564a215adaff2a0c Mon Sep 17 00:00:00 2001 From: oratis Date: Mon, 1 Jun 2026 13:27:51 +0800 Subject: [PATCH] feat(desktop): align inspector rail to spec #3 + Settings shell nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collapsed inspector rail now matches design spec screen #3 exactly: ‹ expand · ▤ Plan (pending badge) · ◐ Context · 📁 Recent files · ⓘ Session info · ⚙ Settings. The four middle icons are inspector hints — clicking one expands the 320px panel and scrolls to that section (new `focusSection` prop on InspectorPanel + `data-section` anchors). The rail previously doubled as the *only* navigation to Permissions / MCP / Plugins / Skills / About. Per spec screen #9 those live behind the ⚙ cog in a shared Settings shell, so this adds (a `.set-nav` left column + pane) and folds the settings-family screens into it — nothing is stranded. Sessions stay in the left sidebar where the spec puts them. - InspectorRail: trimmed to the 6 spec elements; onExpand(section) / onSettings / settingsActive replace the old screen-routing props. - SettingsLayout + SETTINGS_FAMILY: unified nav for settings-family screens. - App: expandInspector(section), settings-shell wrapping in renderScreen. - index.css: `.settings-shell` / `.set-nav` / `.set-pane` per spec lines 754-768; sticky `.inspector-head` so focused sections land below it. Verified in a browser harness: rail = 48px with exactly ‹/▤(badge)/◐/📁/ⓘ/⚙, panel = 320px, set-nav switches active screen, ⌘\ unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/src/App.tsx | 55 +++++++++--- .../desktop/src/components/InspectorPanel.tsx | 28 ++++-- apps/desktop/src/components/InspectorRail.tsx | 86 +++++++------------ .../desktop/src/components/SettingsLayout.tsx | 67 +++++++++++++++ apps/desktop/src/index.css | 80 +++++++++++++++++ apps/desktop/src/types/inspector.ts | 6 ++ 6 files changed, 251 insertions(+), 71 deletions(-) create mode 100644 apps/desktop/src/components/SettingsLayout.tsx diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index dd1583d..891995b 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -7,6 +7,7 @@ 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 { SETTINGS_FAMILY, SettingsLayout } from './components/SettingsLayout.js'; import { Sidebar } from './components/Sidebar.js'; import { UpdateBanner } from './components/UpdateBanner.js'; import { registerShortcut } from './lib/keyboard.js'; @@ -25,7 +26,11 @@ 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'; +import { + emptyInspectorData, + type InspectorData, + type InspectorSection, +} from './types/inspector.js'; export function App(): JSX.Element { const [hasKey, setHasKey] = useState(null); @@ -39,8 +44,17 @@ export function App(): JSX.Element { const [resumedMessages, setResumedMessages] = useState(undefined); // Right inspector: 48 px rail by default, 320 px panel when expanded. const [inspectorExpanded, setInspectorExpanded] = useState(false); + // Which section to scroll to when expanding via a rail hint icon (null = top). + const [inspectorFocus, setInspectorFocus] = useState(null); const [inspector, setInspector] = useState(() => emptyInspectorData()); + // Expand the inspector, optionally scrolling to a section. Bumps focus to a + // fresh value each time so re-clicking the same icon re-scrolls. + const expandInspector = useCallback((section?: InspectorSection) => { + setInspectorExpanded(true); + setInspectorFocus(section ?? null); + }, []); + // 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) => { @@ -169,13 +183,14 @@ export function App(): JSX.Element { setInspectorExpanded(false)} /> ) : ( setScreen(s)} - onExpand={() => setInspectorExpanded(true)} + onExpand={expandInspector} + onSettings={() => setScreen('settings')} + settingsActive={SETTINGS_FAMILY.includes(screen)} planCount={planCount} contextFill={contextFill} /> @@ -205,18 +220,19 @@ function renderScreen( ); case 'sessions': return setScreen('repl')} onNew={() => setScreen('repl')} />; + // Settings-family screens share the Settings shell's left nav so they're + // mutually reachable now that the inspector rail no longer routes to them. case 'plugins': - return ; case 'skills': - return ; case 'permissions': - return ; case 'mcp': - return ; case 'settings': - return ; case 'about': - return ; + return ( + + {renderSettingsPane(screen)} + + ); case 'repl': default: return ( @@ -229,3 +245,22 @@ function renderScreen( ); } } + +/** The right-hand pane for a settings-family screen (hosted by SettingsLayout). */ +function renderSettingsPane(screen: ScreenName): JSX.Element { + switch (screen) { + case 'plugins': + return ; + case 'skills': + return ; + case 'permissions': + return ; + case 'mcp': + return ; + case 'about': + return ; + case 'settings': + default: + return ; + } +} diff --git a/apps/desktop/src/components/InspectorPanel.tsx b/apps/desktop/src/components/InspectorPanel.tsx index f21d5e2..1fa745d 100644 --- a/apps/desktop/src/components/InspectorPanel.tsx +++ b/apps/desktop/src/components/InspectorPanel.tsx @@ -10,15 +10,21 @@ // component is purely presentational. Sections with no data show an honest // empty state rather than a placeholder — per HANDOFF: no fake sections. +import { useEffect, useRef } from 'react'; import { contextWindowFor } from '@deepcode/core/dist/providers/deepseek.js'; import { projectName } from '../lib/project.js'; -import type { InspectorData } from '../types/inspector.js'; +import type { InspectorData, InspectorSection } from '../types/inspector.js'; interface InspectorPanelProps { projectPath: string; data: InspectorData; /** Collapse back to the 48 px rail (the › button / ⌘\). */ onCollapse: () => void; + /** + * Section to scroll into view when the panel opens — set when the user + * clicked one of the rail's hint icons (▤/◐/📁/ⓘ) rather than the chevron. + */ + focusSection?: InspectorSection | null; } const MODE_LABELS: Record = { @@ -33,6 +39,7 @@ export function InspectorPanel({ projectPath, data, onCollapse, + focusSection, }: InspectorPanelProps): JSX.Element { const { usage, costYuan, model, mode, recentFiles, todos } = data; @@ -42,8 +49,17 @@ export function InspectorPanel({ const pending = todos.filter((t) => t.status !== 'completed').length; + // Scroll the requested section to the top when the panel opens via a rail + // hint icon. The header is sticky so the heading lands just below it. + const rootRef = useRef(null); + useEffect(() => { + if (!focusSection) return; + const el = rootRef.current?.querySelector(`[data-section="${focusSection}"]`); + el?.scrollIntoView({ block: 'start', behavior: 'smooth' }); + }, [focusSection]); + return ( -