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
22 changes: 20 additions & 2 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
// Top-level React component for desktop client.
// Spec: docs/VISUAL_DESIGN.html
// Milestone: M6 skeleton — onboarding + REPL placeholder + update banner
// Milestone: M6-rest — Onboarding gate + Nav + 5 screens

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

export function App(): JSX.Element {
const [version, setVersion] = useState<string>('');
const [hasKey, setHasKey] = useState<boolean | null>(null);
const [update, setUpdate] = useState<UpdateInfo | null>(null);
const [screen, setScreen] = useState<ScreenName>('repl');

useEffect(() => {
void window.deepcode.version().then(setVersion);
Expand All @@ -35,9 +41,21 @@ export function App(): JSX.Element {
<span className="font-semibold">DeepCode</span>
<span className="text-muted">v{version}</span>
</header>
{hasKey && <Nav active={screen} onChange={setScreen} />}
<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 />
)}
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop/src/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Top nav — switches between the main screens.
// Spec: docs/VISUAL_DESIGN.html screen #1 header
// Milestone: M6-rest

export type ScreenName = 'repl' | 'chat' | 'sessions' | 'settings' | 'mcp';

interface NavProps {
active: ScreenName;
onChange: (next: ScreenName) => void;
}

const ITEMS: Array<{ name: ScreenName; label: string }> = [
{ name: 'repl', label: 'REPL' },
{ name: 'chat', label: 'Chat' },
{ name: 'sessions', label: 'Sessions' },
{ name: 'mcp', label: 'MCP' },
{ name: 'settings', label: 'Settings' },
];

export function Nav({ active, onChange }: NavProps): JSX.Element {
return (
<nav className="flex gap-1 border-b border-border bg-bg-elevated px-2 text-sm">
{ITEMS.map((item) => (
<button
key={item.name}
onClick={() => onChange(item.name)}
className={
'px-3 py-2 ' +
(active === item.name
? 'border-b-2 border-accent text-fg'
: 'text-muted hover:text-fg')
}
>
{item.label}
</button>
))}
</nav>
);
}
24 changes: 24 additions & 0 deletions apps/desktop/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,27 @@ code {
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 0.95em;
}

/* Additional utility classes used by the M6-rest screens. */
.border-l { border-left-width: 1px; border-left-style: solid; }
.border-b-2 { border-bottom-width: 2px; border-bottom-style: solid; }
.border-accent { border-color: var(--accent); }
.cursor-pointer { cursor: pointer; }
.hidden { display: none; }
@media (min-width: 1024px) { .lg\:block { display: block; } }
.w-1\/3 { width: 33.333%; }
.max-w-xl { max-width: 36rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.ml-2 { margin-left: 0.5rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.p-2 { padding: 0.5rem; }
.gap-1 { gap: 0.25rem; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.font-mono { font-family: ui-monospace, SFMono-Regular, monospace; }
.hover\:border-accent:hover { border-color: var(--accent); }
.hover\:text-fg:hover { color: var(--fg); }
table { border-collapse: collapse; width: 100%; }
27 changes: 22 additions & 5 deletions apps/desktop/src/screens/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
// Screen: Chat
// Milestone: M6
// Spec: docs/VISUAL_DESIGN.html
// Status: placeholder
// Chat screen — same shape as REPL but with split file panel.
// Spec: docs/VISUAL_DESIGN.html screen #2 + #8
// Milestone: M6-rest (file panel itself is M7)

export {};
import { ReplScreen } from './Repl.js';

export function ChatScreen(): JSX.Element {
return (
<div className="flex h-full">
<div className="flex-1">
<ReplScreen />
</div>
<div className="hidden w-1/3 border-l border-border lg:block">
<div className="p-4 text-center text-muted">
<p>File panel</p>
<p className="mt-2 text-xs">
Monaco-based file viewer · Source / Diff / History tabs — M7
</p>
</div>
</div>
</div>
);
}
90 changes: 85 additions & 5 deletions apps/desktop/src/screens/MCPManager.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,86 @@
// Screen: MCPManager
// Milestone: M6
// Spec: docs/VISUAL_DESIGN.html
// Status: placeholder
// MCP server manager screen — list / connect / test MCP servers.
// Spec: docs/VISUAL_DESIGN.html screen #7
// Milestone: M6-rest

export {};
import { useEffect, useState } from 'react';

interface McpServerStatus {
name: string;
status: 'connected' | 'failed' | 'disabled';
toolCount?: number;
error?: string;
}

export function MCPManagerScreen(): JSX.Element {
const [servers, setServers] = useState<McpServerStatus[] | null>(null);

useEffect(() => {
// Real impl: window.deepcode.mcp.list() — wired in M6-rest IPC PR.
setServers([]);
}, []);

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

return (
<div className="flex h-full flex-col">
<header className="border-b border-border p-3">
<h2 className="font-semibold">MCP Servers</h2>
<p className="mt-1 text-xs text-muted">
Connected via settings.json &gt; <code>mcpServers</code>.{' '}
{servers.length === 0
? 'None configured.'
: `${servers.filter((s) => s.status === 'connected').length} of ${servers.length} connected.`}
</p>
</header>
<div className="flex-1 overflow-y-auto p-3">
{servers.length === 0 ? (
<div className="p-8 text-center text-muted">
<p>No MCP servers configured.</p>
<pre className="mx-auto mt-4 max-w-xl rounded bg-bg-elevated p-3 text-left text-xs">
{`{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}`}
</pre>
<p className="mt-3 text-xs">
Add the snippet above to your settings.json then relaunch.
</p>
</div>
) : (
<ul className="space-y-2">
{servers.map((s) => (
<li key={s.name} className="rounded border border-border p-3">
<div className="flex items-center justify-between">
<span className="font-medium">{s.name}</span>
<span
className={
s.status === 'connected'
? 'text-accent'
: s.status === 'failed'
? 'text-error'
: 'text-muted'
}
>
{s.status}
{s.toolCount !== undefined && s.status === 'connected'
? ` · ${s.toolCount} tools`
: ''}
</span>
</div>
{s.error && (
<div className="mt-1 text-xs text-error">{s.error}</div>
)}
</li>
))}
</ul>
)}
</div>
</div>
);
}
91 changes: 86 additions & 5 deletions apps/desktop/src/screens/Sessions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,87 @@
// Screen: Sessions
// Milestone: M6
// Spec: docs/VISUAL_DESIGN.html
// Status: placeholder
// Sessions list screen — resume / inspect past conversations.
// Spec: docs/VISUAL_DESIGN.html screen #5
// Milestone: M6-rest

export {};
import { useEffect, useState } from 'react';

interface SessionMeta {
id: string;
title?: string;
cwd: string;
createdAt: string;
updatedAt: string;
model?: string;
}

interface SessionsProps {
onPick: (sessionId: string) => void;
onNew: () => void;
}

export function SessionsScreen({ onPick, onNew }: SessionsProps): JSX.Element {
const [sessions, setSessions] = useState<SessionMeta[] | null>(null);
const [filter, setFilter] = useState('');

useEffect(() => {
// Real impl wires through window.deepcode.sessions.list — added in
// M6-rest IPC PR. For now, render an empty state.
setSessions([]);
}, []);

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

const visible = sessions.filter(
(s) =>
!filter ||
(s.title ?? '').toLowerCase().includes(filter.toLowerCase()) ||
s.cwd.toLowerCase().includes(filter.toLowerCase()),
);

return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-border p-3">
<input
type="search"
placeholder="Filter sessions…"
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="flex-1 rounded border border-border bg-bg px-3 py-2 text-fg outline-none focus:border-accent"
/>
<button
onClick={onNew}
className="ml-2 rounded bg-accent px-4 py-2 font-medium text-bg"
>
+ New session
</button>
</div>
<div className="flex-1 overflow-y-auto p-3">
{visible.length === 0 ? (
<div className="p-8 text-center text-muted">
<p>No previous sessions yet.</p>
<p className="mt-2 text-xs">Start one with the New session button above.</p>
</div>
) : (
<ul className="space-y-2">
{visible.map((s) => (
<li
key={s.id}
onClick={() => onPick(s.id)}
className="cursor-pointer rounded border border-border p-3 hover:border-accent"
>
<div className="flex items-center justify-between">
<span className="font-medium">{s.title ?? s.id.slice(0, 8)}</span>
<span className="text-xs text-muted">
{new Date(s.updatedAt).toLocaleString()}
</span>
</div>
<div className="mt-1 text-xs text-muted">{s.cwd}</div>
</li>
))}
</ul>
)}
</div>
</div>
);
}
Loading
Loading