Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<boolean | null>(null);
Expand All @@ -39,8 +44,17 @@ export function App(): JSX.Element {
const [resumedMessages, setResumedMessages] = useState<Msg[] | undefined>(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<InspectorSection | null>(null);
const [inspector, setInspector] = useState<InspectorData>(() => 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<InspectorData>) => {
Expand Down Expand Up @@ -169,13 +183,14 @@ export function App(): JSX.Element {
<InspectorPanel
projectPath={projectPath}
data={inspector}
focusSection={inspectorFocus}
onCollapse={() => setInspectorExpanded(false)}
/>
) : (
<InspectorRail
activeScreen={screen}
onChange={(s) => setScreen(s)}
onExpand={() => setInspectorExpanded(true)}
onExpand={expandInspector}
onSettings={() => setScreen('settings')}
settingsActive={SETTINGS_FAMILY.includes(screen)}
planCount={planCount}
contextFill={contextFill}
/>
Expand Down Expand Up @@ -205,18 +220,19 @@ function renderScreen(
);
case 'sessions':
return <SessionsScreen onPick={() => 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 <PluginsScreen />;
case 'skills':
return <SkillsScreen />;
case 'permissions':
return <PermissionsScreen />;
case 'mcp':
return <MCPManagerScreen />;
case 'settings':
return <SettingsScreen />;
case 'about':
return <AboutScreen />;
return (
<SettingsLayout active={screen} onChange={setScreen}>
{renderSettingsPane(screen)}
</SettingsLayout>
);
case 'repl':
default:
return (
Expand All @@ -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 <PluginsScreen />;
case 'skills':
return <SkillsScreen />;
case 'permissions':
return <PermissionsScreen />;
case 'mcp':
return <MCPManagerScreen />;
case 'about':
return <AboutScreen />;
case 'settings':
default:
return <SettingsScreen />;
}
}
28 changes: 22 additions & 6 deletions apps/desktop/src/components/InspectorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
Expand All @@ -33,6 +39,7 @@ export function InspectorPanel({
projectPath,
data,
onCollapse,
focusSection,
}: InspectorPanelProps): JSX.Element {
const { usage, costYuan, model, mode, recentFiles, todos } = data;

Expand All @@ -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<HTMLElement>(null);
useEffect(() => {
if (!focusSection) return;
const el = rootRef.current?.querySelector(`[data-section="${focusSection}"]`);
el?.scrollIntoView({ block: 'start', behavior: 'smooth' });
}, [focusSection]);

return (
<aside className="inspector">
<aside className="inspector" ref={rootRef}>
<div className="inspector-head">
<span className="inspector-title">Inspector</span>
<button
Expand All @@ -57,7 +73,7 @@ export function InspectorPanel({
</div>

{/* ── ▤ Plan ─────────────────────────────────────────────── */}
<h5>▤ Plan{pending > 0 ? ` · ${pending} pending` : ''}</h5>
<h5 data-section="plan">▤ Plan{pending > 0 ? ` · ${pending} pending` : ''}</h5>
{todos.length === 0 ? (
<p className="insp-empty">No plan yet — the agent hasn’t written a todo list.</p>
) : (
Expand All @@ -78,7 +94,7 @@ export function InspectorPanel({
)}

{/* ── ◐ Context ──────────────────────────────────────────── */}
<h5>◐ Context</h5>
<h5 data-section="context">◐ Context</h5>
<div className="ctx-bar">
<span>
{usedTokens.toLocaleString()} / {contextWindow.toLocaleString()}
Expand All @@ -90,7 +106,7 @@ export function InspectorPanel({
</div>

{/* ── 📁 Recent files ────────────────────────────────────── */}
<h5>📁 Recent files</h5>
<h5 data-section="files">📁 Recent files</h5>
{recentFiles.length === 0 ? (
<p className="insp-empty">No files written or edited yet.</p>
) : (
Expand All @@ -105,7 +121,7 @@ export function InspectorPanel({
)}

{/* ── ⓘ Session info ─────────────────────────────────────── */}
<h5>ⓘ Session info</h5>
<h5 data-section="session">ⓘ Session info</h5>
<div className="insp-row">
<span className="k">Project</span>
<span className="v">{projectName(projectPath)}</span>
Expand Down
86 changes: 31 additions & 55 deletions apps/desktop/src/components/InspectorRail.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
// Right-column collapsed inspector rail (48 px).
// Design spec screen #3.
// Design spec screen #3 (line ~1220).
//
// Each rail button routes to a screen so users can reach Plan / Files /
// Info / Settings without scrolling for a hidden menu. The ‹ expand
// chevron opens the full-width inspector panel (InspectorPanel) — App
// owns the collapsed↔expanded state and passes it down via onExpand.
// Per the spec the rail is intentionally minimal: it hints at the inspector's
// contents with four small icons (▤ Plan · ◐ Context · 📁 Recent files ·
// ⓘ Session info) and nothing else but the ‹ expand chevron and a ⚙ Settings
// shortcut. Clicking ‹ — or any of the four hint icons — expands the 320 px
// panel (the icon picks which section to scroll to). The settings cog is the
// rail's one piece of navigation; everything else (Permissions / MCP / Plugins
// / Skills / About) lives inside the Settings shell's left nav.

import type { ScreenName } from '../types/screens.js';
import type { InspectorSection } from '../types/inspector.js';

interface InspectorRailProps {
/** Plan items pending — shown as a badge on ▤. */
planCount?: number;
/** Context fill 0..1 — drives the ◐ color (mint if < 0.6, warn ≥ 0.8). */
contextFill?: number;
/** Active screen so settings cog highlights when on settings. */
activeScreen: ScreenName;
/** Switch screen. */
onChange: (screen: ScreenName) => void;
/** Expand the rail into the 320 px inspector panel (‹ / ⌘\). */
onExpand: () => void;
/** Expand the rail into the 320 px panel, optionally focusing a section. */
onExpand: (section?: InspectorSection) => void;
/** Open the Settings shell. */
onSettings: () => void;
/** Highlight the cog when the user is on any settings-family screen. */
settingsActive: boolean;
}

export function InspectorRail({
planCount,
contextFill,
activeScreen,
onChange,
onExpand,
onSettings,
settingsActive,
}: InspectorRailProps): JSX.Element {
const ctxColor =
contextFill === undefined
Expand All @@ -43,17 +46,17 @@ export function InspectorRail({
type="button"
className="rail-btn expand"
title="Expand inspector (⌘\\)"
onClick={onExpand}
onClick={() => onExpand()}
>
</button>
<div className="rail-divider" />

<button
type="button"
className={'rail-btn' + (activeScreen === 'permissions' ? ' active' : '')}
title={planCount ? `Plan & permissions · ${planCount} pending` : 'Plan & permissions'}
onClick={() => onChange('permissions')}
className="rail-btn"
title={planCount ? `Plan · ${planCount} pending` : 'Plan'}
onClick={() => onExpand('plan')}
>
{planCount !== undefined && planCount > 0 && <span className="dot-badge">{planCount}</span>}
Expand All @@ -68,62 +71,35 @@ export function InspectorRail({
: `Context: ${Math.round(contextFill * 100)}% used`
}
style={{ color: ctxColor, borderColor: 'rgba(20,228,162,.18)' }}
onClick={() => onChange('repl')}
onClick={() => onExpand('context')}
>
</button>

<button
type="button"
className={'rail-btn' + (activeScreen === 'sessions' ? ' active' : '')}
title="Sessions"
onClick={() => onChange('sessions')}
>
</button>

<button
type="button"
className={'rail-btn' + (activeScreen === 'plugins' ? ' active' : '')}
title="Plugins"
onClick={() => onChange('plugins')}
>
</button>

<button
type="button"
className={'rail-btn' + (activeScreen === 'skills' ? ' active' : '')}
title="Skills"
onClick={() => onChange('skills')}
>
</button>

<button
type="button"
className={'rail-btn' + (activeScreen === 'mcp' ? ' active' : '')}
title="MCP servers"
onClick={() => onChange('mcp')}
className="rail-btn"
title="Recent files"
onClick={() => onExpand('files')}
>
📁
</button>

<button
type="button"
className={'rail-btn' + (activeScreen === 'about' ? ' active' : '')}
title="About / Info"
onClick={() => onChange('about')}
className="rail-btn"
title="Session info"
onClick={() => onExpand('session')}
>
</button>

<span className="rail-spacer" />
<button
type="button"
className={'rail-btn' + (activeScreen === 'settings' ? ' active' : '')}
className={'rail-btn' + (settingsActive ? ' active' : '')}
title="Settings"
onClick={() => onChange('settings')}
onClick={onSettings}
>
</button>
Expand Down
Loading
Loading