-
-
-
DeepCode
-
+
+
+ {/* Brand block */}
+
+
+
+
+
+ DeepCode
+
+
DeepSeek-powered AI coding agent · Claude Code parity
+
+ v{diag.version}
+
-
-
-
-
-
-
-
-
+ {/* Diagnostics */}
+
+ Status
+
+ {diag.hasCreds ? (
+ ✓ configured
+ ) : (
+ ✗ not configured
+ )}
+
+
+ {diag.projectPath ? (
+ {diag.projectPath}
+ ) : (
+ none picked
+ )}
+
+
+
+ {diag.baseURL ?? 'https://api.deepseek.com/v1'}
+
+
-
-
-
- );
-}
+
Paths
+
+
+ ~/.deepcode/credentials.json
+
+
+
+
+ ~/.deepcode/settings.json
+
+
+
+
+ ~/.deepcode/sessions/
+
+
+
+
+ ~/.deepcode/keybindings.json
+
+
+
-function Row({ label, value }: { label: string; value: string }): JSX.Element {
- return (
-
-
{label}
- {value}
-
+ {/* Links */}
+
+
+ {[
+ ['github.com/oratis/deepcode', 'https://github.com/oratis/deepcode'],
+ [
+ 'Security model',
+ 'https://github.com/oratis/deepcode/blob/main/docs/security-model.md',
+ ],
+ [
+ 'Behavior parity vs Claude Code',
+ 'https://github.com/oratis/deepcode/blob/main/docs/BEHAVIOR_PARITY.md',
+ ],
+ [
+ 'CHANGELOG',
+ 'https://github.com/oratis/deepcode/blob/main/CHANGELOG.md',
+ ],
+ ].map(([label, href]) => (
+
{
+ e.preventDefault();
+ void openUrl(href!);
+ }}
+ style={{
+ color: '#b4c2ff',
+ fontSize: 13,
+ padding: '6px 0',
+ }}
+ >
+ {label} →
+
+ ))}
+
+
+
+
);
}
diff --git a/apps/desktop/src/screens/MCPManager.tsx b/apps/desktop/src/screens/MCPManager.tsx
index 4cb8fdf..21bcf26 100644
--- a/apps/desktop/src/screens/MCPManager.tsx
+++ b/apps/desktop/src/screens/MCPManager.tsx
@@ -1,8 +1,9 @@
-// MCP server manager screen — list / connect / test MCP servers.
-// Spec: docs/VISUAL_DESIGN.html screen #7
-// Milestone: M6-rest
+// MCP server manager — design-aligned per spec screen #15.
+// List / show status of MCP servers wired in settings.json#mcpServers.
import { useEffect, useState } from 'react';
+import { Badge, type BadgeKind } from '../components/Badge.js';
+import { Card, Screen, SectionTitle } from '../components/Screen.js';
interface McpServerStatus {
name: string;
@@ -11,6 +12,24 @@ interface McpServerStatus {
error?: string;
}
+const STATUS_BADGE: Record<
+ McpServerStatus['status'],
+ { kind: BadgeKind; label: string }
+> = {
+ connected: { kind: 'ok', label: '● connected' },
+ failed: { kind: 'err', label: '✕ failed' },
+ disabled: { kind: 'warn', label: '○ disabled' },
+};
+
+const EXAMPLE_JSON = `{
+ "mcpServers": {
+ "filesystem": {
+ "command": "npx",
+ "args": ["@modelcontextprotocol/server-filesystem", "/tmp"]
+ }
+ }
+}`;
+
export function MCPManagerScreen(): JSX.Element {
const [servers, setServers] = useState
(null);
@@ -26,67 +45,130 @@ export function MCPManagerScreen(): JSX.Element {
}, []);
if (servers === null) {
- return Loading MCP servers…
;
+ return (
+
+ Loading…
+
+ );
}
+ const connected = servers.filter((s) => s.status === 'connected').length;
+
return (
-
-
-
- {servers.length === 0 ? (
-
-
No MCP servers configured.
-
- {`{
- "mcpServers": {
- "filesystem": {
- "command": "npx",
- "args": ["@modelcontextprotocol/server-filesystem", "/tmp"]
- }
- }
-}`}
-
-
- Add the snippet above to your settings.json then relaunch.
-
-
- ) : (
-
- {servers.map((s) => (
-
-
-
{s.name}
-
+
+
+ {servers.length === 0 ? (
+
+ No MCP servers configured.
+
+ Add the snippet below to ~/.deepcode/settings.json{' '}
+ and relaunch DeepCode.
+
+
+ ) : (
+
+ {servers.map((s, i) => {
+ const badge = STATUS_BADGE[s.status];
+ return (
+
- {s.status}
- {s.toolCount !== undefined && s.status === 'connected'
- ? ` · ${s.toolCount} tools`
- : ''}
-
-
- {s.error && (
- {s.error}
- )}
-
- ))}
-
- )}
+
+
+ {s.name}
+
+ {badge.label}
+ {s.toolCount !== undefined && s.status === 'connected' && (
+
+ {s.toolCount} tools
+
+ )}
+
+ {s.error && (
+
+ {s.error}
+
+ )}
+
+ );
+ })}
+
+ )}
+
+
+
+
+ {EXAMPLE_JSON}
+
+
+
+ About MCP
+
+ Model Context Protocol servers expose tools, resources, and prompts
+ that DeepCode can route into via JSON-RPC. Failures show their
+ stderr inline — most issues are a missing binary on $PATH or an
+ arg typo.
+
-
+
);
}
diff --git a/apps/desktop/src/screens/Permissions.tsx b/apps/desktop/src/screens/Permissions.tsx
index 783d90a..3f59fdf 100644
--- a/apps/desktop/src/screens/Permissions.tsx
+++ b/apps/desktop/src/screens/Permissions.tsx
@@ -1,8 +1,11 @@
-// Permissions screen — view + edit settings.permissions rules.
-// Spec: docs/VISUAL_DESIGN.html (permissions tab) · M3 permission rules
-// Milestone: M6-rest
+// Permissions screen — design-aligned. Per spec screen #10.
+// View + edit settings.permissions rules. Save now actually persists
+// to ~/.deepcode/settings.json via saveSettingsFile.
import { useEffect, useState } from 'react';
+import { Badge, type BadgeKind } from '../components/Badge.js';
+import { Card, Row, Screen, SectionTitle } from '../components/Screen.js';
+import { loadSettingsFile, saveSettingsFile } from '../lib/tauri-api.js';
type RuleType = 'allow' | 'ask' | 'deny';
@@ -14,112 +17,286 @@ interface PermissionsView {
additionalDirectories: string[];
}
+const RULE_BADGE: Record
= {
+ allow: { kind: 'ok', label: 'allow' },
+ ask: { kind: 'warn', label: 'ask' },
+ deny: { kind: 'err', label: 'deny' },
+};
+
export function PermissionsScreen(): JSX.Element {
const [perm, setPerm] = useState(null);
- const [newRule, setNewRule] = useState({ type: 'allow' as RuleType, pattern: '' });
+ const [newRule, setNewRule] = useState({
+ type: 'allow' as RuleType,
+ pattern: '',
+ });
+ const [saving, setSaving] = useState(false);
+ const [saveMsg, setSaveMsg] = useState(null);
useEffect(() => {
- void window.deepcode.settings.load().then((settings) => {
- const p = (settings.permissions as PermissionsView | undefined) ?? {
- defaultMode: 'default',
- allow: [],
- ask: [],
- deny: [],
- additionalDirectories: [],
- };
- setPerm({
- defaultMode: p.defaultMode ?? 'default',
- allow: p.allow ?? [],
- ask: p.ask ?? [],
- deny: p.deny ?? [],
- additionalDirectories: p.additionalDirectories ?? [],
- });
- });
+ void (async () => {
+ try {
+ const settings = (await loadSettingsFile()) as Record;
+ const p = (settings.permissions as Partial | undefined) ?? {};
+ setPerm({
+ defaultMode: p.defaultMode ?? 'default',
+ allow: p.allow ?? [],
+ ask: p.ask ?? [],
+ deny: p.deny ?? [],
+ additionalDirectories: p.additionalDirectories ?? [],
+ });
+ } catch {
+ setPerm({
+ defaultMode: 'default',
+ allow: [],
+ ask: [],
+ deny: [],
+ additionalDirectories: [],
+ });
+ }
+ })();
}, []);
- if (perm === null) return Loading permissions…
;
-
function addRule(): void {
if (!newRule.pattern.trim()) return;
setPerm((p) =>
- p ? { ...p, [newRule.type]: [...p[newRule.type], newRule.pattern.trim()] } : p,
+ p
+ ? {
+ ...p,
+ [newRule.type]: [...p[newRule.type], newRule.pattern.trim()],
+ }
+ : p,
);
setNewRule({ type: 'allow', pattern: '' });
+ setSaveMsg(null);
+ }
+
+ function removeRule(type: RuleType, idx: number): void {
+ setPerm((p) =>
+ p ? { ...p, [type]: p[type].filter((_, i) => i !== idx) } : p,
+ );
+ setSaveMsg(null);
+ }
+
+ async function handleSave(): Promise {
+ if (!perm) return;
+ setSaving(true);
+ setSaveMsg(null);
+ try {
+ const current = (await loadSettingsFile()) as Record;
+ await saveSettingsFile({
+ ...current,
+ permissions: {
+ defaultMode: perm.defaultMode,
+ allow: perm.allow,
+ ask: perm.ask,
+ deny: perm.deny,
+ additionalDirectories: perm.additionalDirectories,
+ },
+ });
+ setSaveMsg('✓ Saved to ~/.deepcode/settings.json');
+ } catch (err) {
+ setSaveMsg(`✕ Failed to save: ${(err as Error).message}`);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ if (perm === null) {
+ return (
+
+ Loading…
+
+ );
}
return (
-
-
-
-
-
-
setNewRule({ ...newRule, type: e.target.value as RuleType })}
- className="rounded border border-border bg-bg px-3 py-2"
+
+ {saving && }
+ {saving ? 'Saving…' : 'Save'}
+
+ }
+ >
+
+ {saveMsg && (
+
-
allow
-
ask
-
deny
-
-
setNewRule({ ...newRule, pattern: e.target.value })}
- placeholder='e.g. Bash(npm test:*) or Read(./src/**)'
- className="flex-1 rounded border border-border bg-bg px-3 py-2 outline-none focus:border-accent"
- />
-
+ )}
+
+
+
+ {perm.defaultMode}
+
+
+
+
+
+
+ setNewRule({ ...newRule, type: e.target.value as RuleType })
+ }
+ className="input"
+ style={{ width: 110, fontFamily: 'inherit' }}
+ >
+ allow
+ ask
+ deny
+
+ setNewRule({ ...newRule, pattern: e.target.value })}
+ placeholder='e.g. Bash(npm test:*) or Read(./src/**)'
+ className="input"
+ style={{ flex: 1 }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') addRule();
+ }}
+ />
+
+ Add
+
+
+
- Add
-
-
-
- Save isn't wired yet — view-only preview for M6-rest.
-
-
+ Pattern syntax: bare tool name (
Bash) · subcommand
+ (
Bash(git:*)) · prefix (
Read(/etc/*)) ·
+ domain (
WebFetch(domain:github.com)).
+
+
+
+ {(['deny', 'ask', 'allow'] as const).map((kind) => {
+ const badge = RULE_BADGE[kind];
+ const rules = perm[kind];
+ return (
+ {badge.label}}
+ flush
+ padding={0}
+ >
+ {rules.length === 0 ? (
+
+ no rules
+
+ ) : (
+
+ {rules.map((p, i) => (
+
+ {p}
+ removeRule(kind, i)}
+ style={{
+ marginLeft: 'auto',
+ color: 'var(--text-3)',
+ fontSize: 14,
+ background: 'transparent',
+ border: 0,
+ cursor: 'pointer',
+ padding: '2px 6px',
+ borderRadius: 4,
+ }}
+ title="Remove"
+ >
+ ✕
+
+
+ ))}
+
+ )}
+
+ );
+ })}
-
- {(['deny', 'ask', 'allow'] as const).map((kind) => (
-
-
- {kind.toUpperCase()} · {perm[kind].length}
-
- {perm[kind].length === 0 ? (
- (none)
- ) : (
-
- {perm[kind].map((p, i) => (
-
- {p}
-
- ))}
-
- )}
-
- ))}
{perm.additionalDirectories.length > 0 && (
-
-
- Additional Directories
-
-
+
+
{perm.additionalDirectories.map((d, i) => (
-
+
{d}
))}
-
+
)}
+
+
Notes
+
+ Inline "Always allow" buttons in the chat (over a pending tool
+ card) also write to this list. Removing a rule here disables it
+ immediately for the next tool call.
+
-
+
);
}
diff --git a/apps/desktop/src/screens/Plugins.tsx b/apps/desktop/src/screens/Plugins.tsx
index 8a34b77..92dfeac 100644
--- a/apps/desktop/src/screens/Plugins.tsx
+++ b/apps/desktop/src/screens/Plugins.tsx
@@ -1,9 +1,11 @@
-// Plugins screen — list installed plugins, view trust state + contributed
-// hooks, enable/disable, install new ones.
-// Spec: docs/VISUAL_DESIGN.html screen #9
-// Milestone: M6-rest
+// Plugins screen — design-aligned. Per spec screen #12.
+// List installed plugins, their trust state + contributed hooks,
+// enable/disable, install new ones. Install IPC still stubbed (P3
+// will wire installFromSpec).
import { useEffect, useState } from 'react';
+import { Badge, type BadgeKind } from '../components/Badge.js';
+import { Card, Screen } from '../components/Screen.js';
interface PluginRow {
name: string;
@@ -15,10 +17,17 @@ interface PluginRow {
warning?: string;
}
+const TRUST_BADGE: Record
= {
+ official: { kind: 'ok', label: 'official' },
+ marketplace: { kind: 'info', label: 'marketplace' },
+ user: { kind: 'warn', label: 'user-installed' },
+};
+
export function PluginsScreen(): JSX.Element {
const [plugins, setPlugins] = useState(null);
const [installSpec, setInstallSpec] = useState('');
const [installing, setInstalling] = useState(false);
+ const [feedback, setFeedback] = useState(null);
useEffect(() => {
if (window.deepcode?.plugins?.list) {
@@ -34,102 +43,219 @@ export function PluginsScreen(): JSX.Element {
async function handleInstall(): Promise {
if (!installSpec.trim()) return;
setInstalling(true);
+ setFeedback(null);
try {
- // window.deepcode.plugins.install(installSpec) — TODO wire in IPC PR
- // For now no-op.
+ // Plugin install via Tauri is still not wired — surface that clearly.
await new Promise((r) => setTimeout(r, 400));
+ setFeedback(
+ 'Plugin install from the desktop UI is coming in v0.2. For now, install via the CLI: `deepcode plugin install ' +
+ installSpec.trim() +
+ '`',
+ );
} finally {
setInstalling(false);
- setInstallSpec('');
}
}
if (plugins === null) {
- return Loading plugins…
;
+ return (
+
+ Loading…
+
+ );
}
- return (
-
-
-
-
+ const enabled = plugins.filter((p) => p.enabled).length;
-
- {plugins.length === 0 ? (
-
-
No plugins installed.
-
- Try gh:owner/repo in the box above.
-
+ return (
+
+
+
+
- ) : (
-
- {plugins.map((p) => (
-
-
-
- {p.name}
- v{p.version}
-
+ {feedback}
+
+ )}
+
+
+
+ {plugins.length === 0 ? (
+
+ No plugins installed yet.
+
+ Try gh:owner/repo in the box above (or via CLI).
+
+
+ ) : (
+
+ {plugins.map((p, i) => {
+ const trust = TRUST_BADGE[p.trustedBy];
+ return (
+
+
- {p.trustedBy}
-
-
-
- {
- /* wire to window.deepcode.plugins.setEnabled */
+
+ {p.name}
+
+
+ v{p.version}
+
+ {trust.label}
+
+ {
+ /* TODO setEnabled via core */
+ }}
+ />
+
+
+ {p.contributedHookEvents.length > 0 && (
+
+ Hooks:{' '}
+
+ {p.contributedHookEvents.join(', ')}
+
+
+ )}
+
-
-
- {p.contributedHookEvents.length > 0 && (
-
- Hooks: {p.contributedHookEvents.join(', ')}
-
- )}
-
- hash: {p.sourceHash.slice(0, 12)}
-
- {p.warning && ⚠ {p.warning}
}
-
- ))}
-
- )}
+ >
+ hash {p.sourceHash.slice(0, 12)}
+
+ {p.warning && (
+
+ ⚠ {p.warning}
+
+ )}
+
+ );
+ })}
+
+ )}
+
-
+
+ );
+}
+
+interface ToggleProps {
+ checked: boolean;
+ onChange: () => void;
+}
+
+function Toggle({ checked, onChange }: ToggleProps): JSX.Element {
+ return (
+
+
+
);
}
diff --git a/apps/desktop/src/screens/Sessions.tsx b/apps/desktop/src/screens/Sessions.tsx
index d2f5a3d..f12a1da 100644
--- a/apps/desktop/src/screens/Sessions.tsx
+++ b/apps/desktop/src/screens/Sessions.tsx
@@ -1,17 +1,9 @@
-// Sessions list screen — resume / inspect past conversations.
-// Spec: docs/VISUAL_DESIGN.html screen #5
-// Milestone: M6-rest
+// Sessions list — design-aligned. Browse + filter + resume past
+// conversations. Per spec screen #5.
import { useEffect, useState } from 'react';
-
-interface SessionMeta {
- id: string;
- title?: string;
- cwd: string;
- createdAt: string;
- updatedAt: string;
- model?: string;
-}
+import { Card, Screen } from '../components/Screen.js';
+import { listSessions, type SessionMeta } from '../lib/tauri-api.js';
interface SessionsProps {
onPick: (sessionId: string) => void;
@@ -23,71 +15,132 @@ export function SessionsScreen({ onPick, onNew }: SessionsProps): JSX.Element {
const [filter, setFilter] = useState('');
useEffect(() => {
- // IPC call; fall back to empty list when main hasn't implemented yet.
- if (window.deepcode?.sessions?.list) {
- void window.deepcode.sessions
- .list()
- .then((rows) => setSessions(rows as SessionMeta[]))
- .catch(() => setSessions([]));
- } else {
- setSessions([]);
- }
+ void listSessions()
+ .then(setSessions)
+ .catch(() => setSessions([]));
}, []);
if (sessions === null) {
- return Loading sessions…
;
+ return (
+
+ Loading…
+
+ );
}
- const visible = sessions.filter(
- (s) =>
- !filter ||
- (s.title ?? '').toLowerCase().includes(filter.toLowerCase()) ||
- s.cwd.toLowerCase().includes(filter.toLowerCase()),
+ const filtered = sessions.filter((s) =>
+ !filter || s.id.toLowerCase().includes(filter.toLowerCase()),
);
return (
-
-
+
+ + New
+
+ }
+ >
+
setFilter(e.target.value)}
- className="flex-1 rounded border border-border bg-bg px-3 py-2 text-fg outline-none focus:border-accent"
+ style={{ marginBottom: 14, fontFamily: 'inherit' }}
/>
-
+ {filtered.length === 0 ? (
+
+ {sessions.length === 0
+ ? 'No sessions yet — your conversations are saved automatically.'
+ : 'No matches for that filter.'}
+
+ ) : (
+
+ )}
+
+
+
- + New session
-
-
-
- {visible.length === 0 ? (
-
-
No previous sessions yet.
-
Start one with the New session button above.
-
- ) : (
-
- )}
+ Sessions are stored as JSONL under ~/.deepcode/sessions/. Resume to
+ continue any previous conversation.
+
-
+
);
}
+
+function relativeTime(secs: number): string {
+ const diff = Math.floor(Date.now() / 1000) - secs;
+ if (diff < 60) return `${diff}s ago`;
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ return `${Math.floor(diff / 86400)}d ago`;
+}
diff --git a/apps/desktop/src/screens/Settings.tsx b/apps/desktop/src/screens/Settings.tsx
index 9c0725e..4466f12 100644
--- a/apps/desktop/src/screens/Settings.tsx
+++ b/apps/desktop/src/screens/Settings.tsx
@@ -1,82 +1,358 @@
-// Settings screen — inspect + edit ~/.deepcode/settings.json
-// Spec: docs/VISUAL_DESIGN.html screen #6
-// Milestone: M6-rest
+// Settings screen — design-aligned. Per spec screen #14.
+// Top: JSON view (Monaco-lite). Bottom: flat key/value table. Both
+// reflect the live settings file. Save button writes back.
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { Card, Row, Screen, SectionTitle } from '../components/Screen.js';
+import { loadProjectPath } from '../lib/project.js';
+import {
+ getSettingsPath,
+ loadSettingsFile,
+ saveSettingsFile,
+} from '../lib/tauri-api.js';
export function SettingsScreen(): JSX.Element {
const [settings, setSettings] = useState
| null>(null);
+ const [settingsPath, setSettingsPath] = useState(null);
+ const [projectPath, setProjectPath] = useState();
+ const [rawJson, setRawJson] = useState('');
+ const [parseError, setParseError] = useState(null);
const [search, setSearch] = useState('');
+ const [view, setView] = useState<'gui' | 'json'>('gui');
+ const [saving, setSaving] = useState(false);
+ const [feedback, setFeedback] = useState(null);
+ const textareaRef = useRef(null);
useEffect(() => {
- void window.deepcode.settings.load().then((s) => setSettings(s));
+ void (async () => {
+ const [s, path, project] = await Promise.all([
+ loadSettingsFile(),
+ getSettingsPath(),
+ loadProjectPath(),
+ ]);
+ setSettings(s);
+ setSettingsPath(path);
+ setProjectPath(project);
+ setRawJson(JSON.stringify(s, null, 2));
+ })();
}, []);
- if (settings === null) {
- return Loading settings…
;
+ function handleJsonChange(text: string): void {
+ setRawJson(text);
+ try {
+ const parsed = JSON.parse(text);
+ if (typeof parsed !== 'object' || parsed === null) {
+ setParseError('Top-level must be an object.');
+ return;
+ }
+ setSettings(parsed as Record);
+ setParseError(null);
+ setFeedback(null);
+ } catch (err) {
+ setParseError((err as Error).message);
+ }
}
- // Flat-key view: convert nested settings to dot.notation entries for display
- const flat: Array<{ key: string; value: string }> = [];
- function walk(prefix: string, obj: unknown): void {
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
- for (const [k, v] of Object.entries(obj as Record)) {
- walk(prefix ? `${prefix}.${k}` : k, v);
- }
- } else {
- flat.push({ key: prefix, value: JSON.stringify(obj) });
+ async function handleSave(): Promise {
+ if (!settings || parseError) return;
+ setSaving(true);
+ setFeedback(null);
+ try {
+ await saveSettingsFile(settings);
+ setFeedback('✓ Saved to ' + (settingsPath ?? '~/.deepcode/settings.json'));
+ } catch (err) {
+ setFeedback(`✕ Save failed: ${(err as Error).message}`);
+ } finally {
+ setSaving(false);
}
}
- walk('', settings);
- const visible = flat.filter(
+ // Flat key/value view
+ const flat = useMemo(() => {
+ const out: Array<{ key: string; value: string }> = [];
+ function walk(prefix: string, obj: unknown): void {
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
+ for (const [k, v] of Object.entries(obj as Record)) {
+ walk(prefix ? `${prefix}.${k}` : k, v);
+ }
+ } else {
+ out.push({ key: prefix, value: JSON.stringify(obj) });
+ }
+ }
+ if (settings) walk('', settings);
+ return out;
+ }, [settings]);
+
+ const visibleFlat = flat.filter(
(e) =>
!search ||
e.key.toLowerCase().includes(search.toLowerCase()) ||
e.value.toLowerCase().includes(search.toLowerCase()),
);
+ if (settings === null) {
+ return (
+
+ Loading…
+
+ );
+ }
+
return (
-
-
- setSearch(e.target.value)}
- className="w-full rounded border border-border bg-bg px-3 py-2 text-fg outline-none focus:border-accent"
- />
-
-
- {visible.length === 0 ? (
-
-
No matching settings.
-
- Default config: ~/.deepcode/settings.json · project: .deepcode/settings.json
-
+
+
+ {(['gui', 'json'] as const).map((v) => (
+ setView(v)}
+ style={{
+ padding: '4px 12px',
+ fontSize: 11,
+ fontWeight: 600,
+ borderRadius: 4,
+ background: view === v ? 'var(--brand)' : 'transparent',
+ color: view === v ? '#fff' : 'var(--text-2)',
+ border: 0,
+ cursor: 'pointer',
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ }}
+ >
+ {v}
+
+ ))}
+
+
+ {saving && }
+ {saving ? 'Saving…' : 'Save'}
+
+ >
+ }
+ >
+
+ {feedback && (
+
+ {feedback}
- ) : (
-
-
-
- Key
- Value
-
-
-
- {visible.map((e) => (
-
- {e.key}
- {e.value}
-
- ))}
-
-
+ )}
+
+ {view === 'gui' && (
+ <>
+
+
+ {projectPath ? (
+ {projectPath}
+ ) : (
+ not picked
+ )}
+
+
+
+ {settingsPath ?? '~/.deepcode/settings.json'}
+
+
+
+
+ {String(settings.model ?? 'deepseek-chat')}
+
+
+
+
+ {String(settings.effortLevel ?? 'medium')}
+
+
+
+
+ {String(settings.baseURL ?? 'https://api.deepseek.com/v1')}
+
+
+
+
+
setSearch(e.target.value)}
+ style={{
+ width: 200,
+ fontSize: 12,
+ padding: '4px 8px',
+ fontFamily: 'inherit',
+ }}
+ />
+ }
+ flush
+ padding={0}
+ >
+ {visibleFlat.length === 0 ? (
+
+ {flat.length === 0
+ ? 'Settings file is empty.'
+ : 'No matching keys.'}
+
+ ) : (
+
+
+
+
+ Key
+
+
+ Value
+
+
+
+
+ {visibleFlat.map((e) => (
+
+
+ {e.key}
+
+
+ {e.value}
+
+
+ ))}
+
+
+ )}
+
+
+
Tip
+
+ Use the JSON view (toggle in the header) to edit nested keys
+ and arrays directly. Save validates JSON before writing.
+
+ >
+ )}
+
+ {view === 'json' && (
+
+
)}
-
- Edit by opening ~/.deepcode/settings.json in your editor (visual editor lands in M7).
-
-
+
);
}
diff --git a/apps/desktop/src/screens/Skills.tsx b/apps/desktop/src/screens/Skills.tsx
index d6449b2..1367662 100644
--- a/apps/desktop/src/screens/Skills.tsx
+++ b/apps/desktop/src/screens/Skills.tsx
@@ -1,9 +1,10 @@
-// Skills screen — list available skills (built-in + user + project + plugin)
-// + open the SKILL.md body for inspection.
-// Spec: docs/VISUAL_DESIGN.html (skills tab)
-// Milestone: M6-rest
+// Skills screen — design-aligned.
+// List available skills (built-in + user + project + plugin) + show the
+// SKILL.md body for inspection. Per spec screen #11.
import { useEffect, useState } from 'react';
+import { Badge, type BadgeKind } from '../components/Badge.js';
+import { Card, Screen } from '../components/Screen.js';
interface SkillRow {
name: string;
@@ -13,6 +14,13 @@ interface SkillRow {
body?: string;
}
+const SOURCE_BADGE: Record
= {
+ builtin: { kind: 'info', label: 'built-in' },
+ user: { kind: 'warn', label: 'user' },
+ project: { kind: 'ok', label: 'project' },
+ plugin: { kind: 'info', label: 'plugin' },
+};
+
export function SkillsScreen(): JSX.Element {
const [skills, setSkills] = useState(null);
const [active, setActive] = useState(null);
@@ -30,10 +38,14 @@ export function SkillsScreen(): JSX.Element {
}, []);
if (skills === null) {
- return Loading skills…
;
+ return (
+
+ Loading…
+
+ );
}
- const visible = skills.filter(
+ const filtered = skills.filter(
(s) =>
!filter ||
s.name.toLowerCase().includes(filter.toLowerCase()) ||
@@ -42,55 +54,144 @@ export function SkillsScreen(): JSX.Element {
const current = skills.find((s) => s.name === active);
return (
-
-
-
+
+
+ {/* Left: filter + list */}
+
-
+
+
);
}