diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 846b117..232ea1c 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -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 { @@ -45,21 +49,39 @@ export function App(): JSX.Element {
{!hasKey ? ( setHasKey(true)} /> - ) : screen === 'chat' ? ( - - ) : screen === 'sessions' ? ( - setScreen('repl')} - onNew={() => setScreen('repl')} - /> - ) : screen === 'settings' ? ( - - ) : screen === 'mcp' ? ( - ) : ( - + renderScreen(screen, setScreen) )}
); } + +function renderScreen( + screen: ScreenName, + setScreen: (s: ScreenName) => void, +): JSX.Element { + switch (screen) { + case 'chat': + return ; + case 'sessions': + return ( + setScreen('repl')} onNew={() => setScreen('repl')} /> + ); + case 'plugins': + return ; + case 'skills': + return ; + case 'permissions': + return ; + case 'mcp': + return ; + case 'settings': + return ; + case 'about': + return ; + case 'repl': + default: + return ; + } +} diff --git a/apps/desktop/src/components/Nav.tsx b/apps/desktop/src/components/Nav.tsx index 872fc7b..3eee6af 100644 --- a/apps/desktop/src/components/Nav.tsx +++ b/apps/desktop/src/components/Nav.tsx @@ -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; @@ -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 { diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index f32cf40..cc80a80 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -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; } diff --git a/apps/desktop/src/screens/About.tsx b/apps/desktop/src/screens/About.tsx new file mode 100644 index 0000000..49076b0 --- /dev/null +++ b/apps/desktop/src/screens/About.tsx @@ -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(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
Loading…
; + + return ( +
+
+
+

DeepCode

+

+ DeepSeek-powered AI coding agent · Claude Code parity +

+
+ +
+ + + + + + +
+ + +
+
+ ); +} + +function Row({ label, value }: { label: string; value: string }): JSX.Element { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/apps/desktop/src/screens/FilePanel.tsx b/apps/desktop/src/screens/FilePanel.tsx new file mode 100644 index 0000000..fe377a8 --- /dev/null +++ b/apps/desktop/src/screens/FilePanel.tsx @@ -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([]); + const [active, setActive] = useState(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 ( +
+
File panel · M7
+
+
+

No file open.

+

+ Files referenced in the chat will open here automatically. +

+ +
+
+
+ ); + } + + const current = files[active]!; + return ( +
+ {/* Tab bar */} +
+ {files.map((f, i) => ( +
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') + } + > + {f.path.split('/').pop()} + +
+ ))} +
+ {/* View switcher */} +
+ {(['source', 'diff', 'history'] as const).map((v) => ( + + ))} +
{current.path}
+
+ {/* Body — Monaco lives here in M7. Stub shows the placeholder. */} +
+
+ Monaco editor mounts here in M7. {current.view} view of {current.path}. +
+
+
+ ); +} diff --git a/apps/desktop/src/screens/Permissions.tsx b/apps/desktop/src/screens/Permissions.tsx new file mode 100644 index 0000000..783d90a --- /dev/null +++ b/apps/desktop/src/screens/Permissions.tsx @@ -0,0 +1,125 @@ +// Permissions screen — view + edit settings.permissions rules. +// Spec: docs/VISUAL_DESIGN.html (permissions tab) · M3 permission rules +// Milestone: M6-rest + +import { useEffect, useState } from 'react'; + +type RuleType = 'allow' | 'ask' | 'deny'; + +interface PermissionsView { + defaultMode: string; + allow: string[]; + ask: string[]; + deny: string[]; + additionalDirectories: string[]; +} + +export function PermissionsScreen(): JSX.Element { + const [perm, setPerm] = useState(null); + const [newRule, setNewRule] = useState({ type: 'allow' as RuleType, pattern: '' }); + + 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 ?? [], + }); + }); + }, []); + + 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, + ); + setNewRule({ type: 'allow', pattern: '' }); + } + + return ( +
+
+

Permissions

+

+ Default mode: {perm.defaultMode}. Precedence: deny > ask > allow. +

+
+ +
+
+ + 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" + /> + +
+

+ Save isn't wired yet — view-only preview for M6-rest. +

+
+ +
+ {(['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} +
  • + ))} +
+
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/screens/Plugins.tsx b/apps/desktop/src/screens/Plugins.tsx new file mode 100644 index 0000000..da80212 --- /dev/null +++ b/apps/desktop/src/screens/Plugins.tsx @@ -0,0 +1,129 @@ +// 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 + +import { useEffect, useState } from 'react'; + +interface PluginRow { + name: string; + version: string; + enabled: boolean; + contributedHookEvents: string[]; + sourceHash: string; + trustedBy: 'user' | 'marketplace' | 'official'; + warning?: string; +} + +export function PluginsScreen(): JSX.Element { + const [plugins, setPlugins] = useState(null); + const [installSpec, setInstallSpec] = useState(''); + const [installing, setInstalling] = useState(false); + + useEffect(() => { + // Real impl: window.deepcode.plugins.list() — wired in IPC PR. + setPlugins([]); + }, []); + + async function handleInstall(): Promise { + if (!installSpec.trim()) return; + setInstalling(true); + try { + // window.deepcode.plugins.install(installSpec) — TODO wire in IPC PR + // For now no-op. + await new Promise((r) => setTimeout(r, 400)); + } finally { + setInstalling(false); + setInstallSpec(''); + } + } + + if (plugins === null) { + return
Loading plugins…
; + } + + return ( +
+
+

Plugins

+

+ {plugins.length} installed ·{' '} + {plugins.filter((p) => p.enabled).length} enabled ·{' '} + ~/.deepcode/plugins/ +

+
+ +
+
+ setInstallSpec(e.target.value)} + placeholder="gh:user/repo · @npm · /local/path" + className="flex-1 rounded border border-border bg-bg px-3 py-2 text-fg outline-none focus:border-accent" + /> + +
+
+ +
+ {plugins.length === 0 ? ( +
+

No plugins installed.

+

+ Try gh:owner/repo in the box above. +

+
+ ) : ( +
    + {plugins.map((p) => ( +
  • +
    +
    + {p.name} + v{p.version} + + {p.trustedBy} + +
    +
    + { + /* wire to window.deepcode.plugins.setEnabled */ + }} + /> +
    +
    + {p.contributedHookEvents.length > 0 && ( +
    + Hooks: {p.contributedHookEvents.join(', ')} +
    + )} +
    + hash: {p.sourceHash.slice(0, 12)} +
    + {p.warning &&
    ⚠ {p.warning}
    } +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/screens/Skills.tsx b/apps/desktop/src/screens/Skills.tsx new file mode 100644 index 0000000..42c2500 --- /dev/null +++ b/apps/desktop/src/screens/Skills.tsx @@ -0,0 +1,90 @@ +// 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 + +import { useEffect, useState } from 'react'; + +interface SkillRow { + name: string; + description: string; + source: 'builtin' | 'user' | 'project' | 'plugin'; + path: string; + body?: string; +} + +export function SkillsScreen(): JSX.Element { + const [skills, setSkills] = useState(null); + const [active, setActive] = useState(null); + const [filter, setFilter] = useState(''); + + useEffect(() => { + // Real impl: window.deepcode.skills.list() — wired in IPC PR. + setSkills([]); + }, []); + + if (skills === null) { + return
Loading skills…
; + } + + const visible = skills.filter( + (s) => + !filter || + s.name.toLowerCase().includes(filter.toLowerCase()) || + s.description.toLowerCase().includes(filter.toLowerCase()), + ); + const current = skills.find((s) => s.name === active); + + return ( +
+ +
+ {current ? ( +
+

{current.name}

+
+ {current.source} · {current.path} +
+
+              {current.body ?? '(SKILL.md body not loaded — wire IPC fetch in M6-rest.)'}
+            
+
+ ) : ( +
Select a skill to view its SKILL.md.
+ )} +
+
+ ); +}