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
48 changes: 35 additions & 13 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// Top-level React component for desktop client.
// Spec: docs/VISUAL_DESIGN.html
// Milestone: M6-rest — Onboarding gate + Nav + 5 screens
// Milestone: M6-rest — Onboarding gate + Nav + 9 screens

import { useEffect, useState } from 'react';
import { Nav, type ScreenName } from './components/Nav.js';
import { UpdateBanner } from './components/UpdateBanner.js';
import { AboutScreen } from './screens/About.js';
import { ChatScreen } from './screens/Chat.js';
import { MCPManagerScreen } from './screens/MCPManager.js';
import { OnboardingScreen } from './screens/Onboarding.js';
import { PermissionsScreen } from './screens/Permissions.js';
import { PluginsScreen } from './screens/Plugins.js';
import { ReplScreen } from './screens/Repl.js';
import { SessionsScreen } from './screens/Sessions.js';
import { SettingsScreen } from './screens/Settings.js';
import { SkillsScreen } from './screens/Skills.js';
import type { UpdateInfo } from './types/global.js';

export function App(): JSX.Element {
Expand Down Expand Up @@ -45,21 +49,39 @@ export function App(): JSX.Element {
<main className="flex-1 overflow-hidden">
{!hasKey ? (
<OnboardingScreen onComplete={() => setHasKey(true)} />
) : screen === 'chat' ? (
<ChatScreen />
) : screen === 'sessions' ? (
<SessionsScreen
onPick={() => setScreen('repl')}
onNew={() => setScreen('repl')}
/>
) : screen === 'settings' ? (
<SettingsScreen />
) : screen === 'mcp' ? (
<MCPManagerScreen />
) : (
<ReplScreen />
renderScreen(screen, setScreen)
)}
</main>
</div>
);
}

function renderScreen(
screen: ScreenName,
setScreen: (s: ScreenName) => void,
): JSX.Element {
switch (screen) {
case 'chat':
return <ChatScreen />;
case 'sessions':
return (
<SessionsScreen onPick={() => setScreen('repl')} onNew={() => setScreen('repl')} />
);
case 'plugins':
return <PluginsScreen />;
case 'skills':
return <SkillsScreen />;
case 'permissions':
return <PermissionsScreen />;
case 'mcp':
return <MCPManagerScreen />;
case 'settings':
return <SettingsScreen />;
case 'about':
return <AboutScreen />;
case 'repl':
default:
return <ReplScreen />;
}
}
15 changes: 14 additions & 1 deletion apps/desktop/src/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
// Spec: docs/VISUAL_DESIGN.html screen #1 header
// Milestone: M6-rest

export type ScreenName = 'repl' | 'chat' | 'sessions' | 'settings' | 'mcp';
export type ScreenName =
| 'repl'
| 'chat'
| 'sessions'
| 'plugins'
| 'skills'
| 'permissions'
| 'mcp'
| 'settings'
| 'about';

interface NavProps {
active: ScreenName;
Expand All @@ -13,8 +22,12 @@ const ITEMS: Array<{ name: ScreenName; label: string }> = [
{ name: 'repl', label: 'REPL' },
{ name: 'chat', label: 'Chat' },
{ name: 'sessions', label: 'Sessions' },
{ name: 'plugins', label: 'Plugins' },
{ name: 'skills', label: 'Skills' },
{ name: 'permissions', label: 'Permissions' },
{ name: 'mcp', label: 'MCP' },
{ name: 'settings', label: 'Settings' },
{ name: 'about', label: 'About' },
];

export function Nav({ active, onChange }: NavProps): JSX.Element {
Expand Down
27 changes: 27 additions & 0 deletions apps/desktop/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,30 @@ code {
.hover\:border-accent:hover { border-color: var(--accent); }
.hover\:text-fg:hover { color: var(--fg); }
table { border-collapse: collapse; width: 100%; }

/* Additional classes used by the last 5 screens (FilePanel / Plugins / Skills /
* Permissions / About). */
.flex-row { flex-direction: row; }
.items-baseline { align-items: baseline; }
.ml-1 { margin-left: 0.25rem; }
.ml-auto { margin-left: auto; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-4 { margin-bottom: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mr-12 { margin-right: 3rem; }
.pr-2 { padding-right: 0.5rem; }
.overflow-auto { overflow: auto; }
.text-error { color: var(--error); }
.hover\:text-error:hover { color: var(--error); }
.hover\:bg-bg-elevated:hover { background: var(--bg-elevated); }
.hover\:underline:hover { text-decoration: underline; }
.cursor-default { cursor: default; }
select {
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
}
select:focus { outline: none; border-color: var(--accent); }
.block { display: block; }
75 changes: 75 additions & 0 deletions apps/desktop/src/screens/About.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// About screen — version + diagnostics + links.
// Spec: docs/VISUAL_DESIGN.html (about / doctor)
// Milestone: M6-rest

import { useEffect, useState } from 'react';

interface Diag {
version: string;
hasCreds: boolean;
baseURL?: string;
}

export function AboutScreen(): JSX.Element {
const [diag, setDiag] = useState<Diag | null>(null);

useEffect(() => {
void Promise.all([window.deepcode.version(), window.deepcode.creds.load()]).then(
([version, c]) => setDiag({ version, hasCreds: c.hasKey, baseURL: c.baseURL }),
);
}, []);

if (diag === null) return <div className="p-8 text-muted">Loading…</div>;

return (
<div className="flex h-full items-center justify-center">
<div className="w-full max-w-md rounded-lg border border-border bg-bg-elevated p-6">
<div className="text-center">
<h1 className="text-xl font-semibold">DeepCode</h1>
<p className="mt-1 text-sm text-muted">
DeepSeek-powered AI coding agent · Claude Code parity
</p>
</div>

<dl className="mt-6 space-y-2 text-sm">
<Row label="Version" value={`v${diag.version}`} />
<Row label="DeepSeek API" value={diag.hasCreds ? '✓ configured' : '✗ not configured'} />
<Row label="Base URL" value={diag.baseURL ?? 'https://api.deepseek.com/v1'} />
<Row label="Credentials" value="~/.deepcode/credentials.json (chmod 600)" />
<Row label="Settings" value="~/.deepcode/settings.json" />
<Row label="Sessions" value="~/.deepcode/sessions/" />
</dl>

<div className="mt-6 space-y-2 text-center text-xs">
<a
className="block text-accent hover:underline"
href="https://github.com/oratis/deepcode"
>
github.com/oratis/deepcode
</a>
<a
className="block text-accent hover:underline"
href="https://github.com/oratis/deepcode/blob/main/docs/security-model.md"
>
Security model
</a>
<a
className="block text-accent hover:underline"
href="https://github.com/oratis/deepcode/blob/main/docs/BEHAVIOR_PARITY.md"
>
Behavior parity vs Claude Code
</a>
</div>
</div>
</div>
);
}

function Row({ label, value }: { label: string; value: string }): JSX.Element {
return (
<div className="flex items-baseline justify-between">
<dt className="text-muted">{label}</dt>
<dd className="font-mono text-xs">{value}</dd>
</div>
);
}
98 changes: 98 additions & 0 deletions apps/desktop/src/screens/FilePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// File panel — right-side Monaco-based viewer with Source / Diff / History tabs.
// Spec: docs/VISUAL_DESIGN.html screens #8 and #11 (M7)
// Milestone: M6-rest skeleton; Monaco wiring lands when the binary dep is installed.

import { useState } from 'react';

interface OpenFile {
path: string;
view: 'source' | 'diff' | 'history';
}

export function FilePanel(): JSX.Element {
const [files, setFiles] = useState<OpenFile[]>([]);
const [active, setActive] = useState<number>(0);

function closeTab(idx: number): void {
setFiles((fs) => fs.filter((_, i) => i !== idx));
setActive((a) => Math.max(0, Math.min(a, files.length - 2)));
}

function switchView(view: OpenFile['view']): void {
setFiles((fs) => fs.map((f, i) => (i === active ? { ...f, view } : f)));
}

if (files.length === 0) {
return (
<div className="flex h-full flex-col">
<div className="border-b border-border p-3 text-xs text-muted">File panel · M7</div>
<div className="flex flex-1 items-center justify-center p-8 text-center text-muted">
<div>
<p>No file open.</p>
<p className="mt-2 text-xs">
Files referenced in the chat will open here automatically.
</p>
<button
className="mt-4 rounded bg-accent px-3 py-1 text-xs font-medium text-bg"
onClick={() => setFiles([{ path: 'demo/README.md', view: 'source' }])}
>
Open a demo tab
</button>
</div>
</div>
</div>
);
}

const current = files[active]!;
return (
<div className="flex h-full flex-col">
{/* Tab bar */}
<div className="flex items-center border-b border-border bg-bg-elevated text-xs">
{files.map((f, i) => (
<div
key={f.path}
onClick={() => setActive(i)}
className={
'flex cursor-pointer items-center gap-1 border-r border-border px-3 py-2 ' +
(i === active ? 'bg-bg' : 'text-muted hover:text-fg')
}
>
<span>{f.path.split('/').pop()}</span>
<button
className="ml-1 text-muted hover:text-error"
onClick={(e) => {
e.stopPropagation();
closeTab(i);
}}
>
×
</button>
</div>
))}
</div>
{/* View switcher */}
<div className="flex gap-1 border-b border-border px-2 py-1 text-xs">
{(['source', 'diff', 'history'] as const).map((v) => (
<button
key={v}
onClick={() => switchView(v)}
className={
'px-2 py-1 ' +
(current.view === v ? 'text-accent' : 'text-muted hover:text-fg')
}
>
{v}
</button>
))}
<div className="ml-auto pr-2 text-muted">{current.path}</div>
</div>
{/* Body — Monaco lives here in M7. Stub shows the placeholder. */}
<div className="flex-1 overflow-auto p-3 font-mono text-xs">
<div className="text-muted">
Monaco editor mounts here in M7. {current.view} view of <code>{current.path}</code>.
</div>
</div>
</div>
);
}
Loading
Loading