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
46 changes: 42 additions & 4 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<boolean | null>(null);
Expand All @@ -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<Msg[] | undefined>(undefined);
// Right inspector: 48 px rail by default, 320 px panel when expanded.
const [inspectorExpanded, setInspectorExpanded] = useState(false);
const [inspector, setInspector] = useState<InspectorData>(() => 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<InspectorData>) => {
setInspector((prev) => ({ ...prev, ...patch }));
}, []);

useEffect(() => {
void window.deepcode.creds.load().then((c) => setHasKey(c.hasKey));
Expand All @@ -52,13 +64,15 @@ 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();
offReal();
offN();
offComma();
offSlash();
offBackslash();
};
}, []);

Expand Down Expand Up @@ -95,9 +109,15 @@ export function App(): JSX.Element {
return <ProjectPickerOverlay onPicked={handlePickProject} />;
}

// 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 (
<div className="app-shell">
<div className={'app-shell' + (inspectorExpanded ? ' inspector-open' : '')}>
{update && <UpdateBanner info={update} />}
<Sidebar
key={`sb-${sessionEpoch}`}
Expand Down Expand Up @@ -141,10 +161,25 @@ export function App(): JSX.Element {
setScreen,
projectPath,
() => setSessionEpoch((k) => k + 1),
handleInspector,
resumedMessages,
)}
</main>
<InspectorRail activeScreen={screen} onChange={(s) => setScreen(s)} contextFill={undefined} />
{inspectorExpanded ? (
<InspectorPanel
projectPath={projectPath}
data={inspector}
onCollapse={() => setInspectorExpanded(false)}
/>
) : (
<InspectorRail
activeScreen={screen}
onChange={(s) => setScreen(s)}
onExpand={() => setInspectorExpanded(true)}
planCount={planCount}
contextFill={contextFill}
/>
)}
</div>
);
}
Expand All @@ -154,6 +189,7 @@ function renderScreen(
setScreen: (s: ScreenName) => void,
projectPath: string,
onTurnComplete: () => void,
onInspector: (patch: Partial<InspectorData>) => void,
initialMessages?: Msg[],
): JSX.Element {
switch (screen) {
Expand All @@ -164,6 +200,7 @@ function renderScreen(
projectPath={projectPath}
onTurnComplete={onTurnComplete}
initialMessages={initialMessages}
onInspector={onInspector}
/>
);
case 'sessions':
Expand All @@ -187,6 +224,7 @@ function renderScreen(
projectPath={projectPath}
onTurnComplete={onTurnComplete}
initialMessages={initialMessages}
onInspector={onInspector}
/>
);
}
Expand Down
153 changes: 153 additions & 0 deletions apps/desktop/src/components/InspectorPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
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 (
<aside className="inspector">
<div className="inspector-head">
<span className="inspector-title">Inspector</span>
<button
type="button"
className="rail-btn"
title="Collapse inspector (⌘\\)"
onClick={onCollapse}
>
</button>
</div>

{/* ── ▤ Plan ─────────────────────────────────────────────── */}
<h5>▤ 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>
) : (
<div className="todo-list">
{todos.map((t, i) => (
<div
key={i}
className={
'todo-item' +
(t.status === 'completed' ? ' done' : t.status === 'in_progress' ? ' active' : '')
}
>
<span className="check" />
<span className="label">{t.status === 'in_progress' ? t.activeForm : t.content}</span>
</div>
))}
</div>
)}

{/* ── ◐ Context ──────────────────────────────────────────── */}
<h5>◐ Context</h5>
<div className="ctx-bar">
<span>
{usedTokens.toLocaleString()} / {contextWindow.toLocaleString()}
</span>
<div className="progress">
<div className="fill" style={{ width: `${fillPct}%` }} />
</div>
<span>{fillPct.toFixed(1)}%</span>
</div>

{/* ── 📁 Recent files ────────────────────────────────────── */}
<h5>📁 Recent files</h5>
{recentFiles.length === 0 ? (
<p className="insp-empty">No files written or edited yet.</p>
) : (
<div className="recent-files">
{recentFiles.map((f) => (
<div className="recent-file" key={f} title={f}>
<span className="name">{basename(f)}</span>
<span className="dir">{dirname(f)}</span>
</div>
))}
</div>
)}

{/* ── ⓘ Session info ─────────────────────────────────────── */}
<h5>ⓘ Session info</h5>
<div className="insp-row">
<span className="k">Project</span>
<span className="v">{projectName(projectPath)}</span>
</div>
<div className="insp-row">
<span className="k">Path</span>
<span className="v" title={projectPath}>
{abbreviatePath(projectPath)}
</span>
</div>
<div className="insp-row">
<span className="k">Model</span>
<span className="v">{model}</span>
</div>
<div className="insp-row">
<span className="k">Mode</span>
<span className="v">{MODE_LABELS[mode] ?? mode}</span>
</div>
<div className="insp-row">
<span className="k">Spend</span>
<span className="v">¥ {costYuan.toFixed(4)}</span>
</div>
</aside>
);
}

// ─── 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;
}
13 changes: 8 additions & 5 deletions apps/desktop/src/components/InspectorRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -17,13 +17,16 @@ interface InspectorRailProps {
activeScreen: ScreenName;
/** Switch screen. */
onChange: (screen: ScreenName) => void;
/** Expand the rail into the 320 px inspector panel (‹ / ⌘\). */
onExpand: () => void;
}

export function InspectorRail({
planCount,
contextFill,
activeScreen,
onChange,
onExpand,
}: InspectorRailProps): JSX.Element {
const ctxColor =
contextFill === undefined
Expand All @@ -38,9 +41,9 @@ export function InspectorRail({
<aside className="inspector-rail">
<button
type="button"
className="rail-btn"
title="Expand inspector (⌘\\) — coming in next phase"
disabled
className="rail-btn expand"
title="Expand inspector (⌘\\)"
onClick={onExpand}
>
</button>
Expand Down
Loading
Loading