From 4f9ecbd328cad461e507aec5215459c8b93ff05a Mon Sep 17 00:00:00 2001 From: oratis Date: Sat, 30 May 2026 00:39:43 +0800 Subject: [PATCH] style: prettier --write across repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run `pnpm format` (prettier --write) across the entire repo to fix the ~90 files that were never Prettier-formatted. This clears the long-standing `format:check` failure in the "Typecheck + Lint + Test" CI job. Purely cosmetic — deterministic Prettier output only (quote style, trailing commas, line-wrapping, JSX prop reflow, markdown table alignment). No logic changes (`git diff --ignore-all-space` confirms only style-level diffs). Done as a standalone PR on purpose: a repo-wide reformat churns git blame and shouldn't be bundled into feature/fix PRs. Also: - .prettierignore: exclude `release-artifacts/` (release-pipeline output). - .husky/pre-commit: add `pnpm format:check` step, consistent with the existing `pnpm lint` step, to prevent future drift. Co-Authored-By: Claude Opus 4.8 (1M context) --- .husky/pre-commit | 3 + .prettierignore | 3 + CHANGELOG.md | 15 +++- MORNING_REPORT.md | 27 +++--- README.md | 32 +++---- apps/cli/src/commands.test.ts | 14 ++- apps/cli/src/commands.ts | 16 ++-- apps/cli/src/parse-args.test.ts | 12 +-- apps/cli/src/repl.ts | 4 +- apps/desktop/README.md | 12 +-- apps/desktop/src-tauri/tauri.conf.json | 11 +-- apps/desktop/src/App.tsx | 17 +--- apps/desktop/src/components/Dropdown.tsx | 4 +- apps/desktop/src/components/ErrorBoundary.tsx | 17 +++- apps/desktop/src/components/InspectorRail.tsx | 4 +- apps/desktop/src/components/PlusMenu.tsx | 3 +- .../src/components/ProjectPickerOverlay.tsx | 14 ++- apps/desktop/src/components/Screen.tsx | 29 ++---- apps/desktop/src/components/Sidebar.tsx | 3 +- apps/desktop/src/components/ToolCard.tsx | 12 +-- apps/desktop/src/components/UpdateBanner.tsx | 5 +- apps/desktop/src/lib/mac-agent.ts | 21 +---- apps/desktop/src/lib/mac-tools.test.ts | 33 ++----- apps/desktop/src/lib/mac-tools.ts | 18 +--- apps/desktop/src/lib/repl-stream.test.ts | 10 ++- apps/desktop/src/lib/repl-stream.ts | 14 +-- apps/desktop/src/lib/tauri-api.ts | 5 +- apps/desktop/src/lib/window-shim.ts | 16 +--- apps/desktop/src/screens/About.tsx | 25 ++---- apps/desktop/src/screens/MCPManager.tsx | 24 ++--- apps/desktop/src/screens/Onboarding.tsx | 8 +- apps/desktop/src/screens/Permissions.tsx | 45 ++++------ apps/desktop/src/screens/Plugins.tsx | 23 ++--- apps/desktop/src/screens/Repl.tsx | 88 +++++++++++-------- apps/desktop/src/screens/Sessions.tsx | 21 ++--- apps/desktop/src/screens/Settings.tsx | 25 ++---- apps/desktop/src/screens/Skills.tsx | 14 ++- apps/desktop/src/types/global.d.ts | 5 +- apps/desktop/vite.config.ts | 4 +- apps/lsp/README.md | 17 ++-- apps/lsp/src/handler.test.ts | 8 +- apps/lsp/src/handler.ts | 8 +- apps/vscode/README.md | 30 +++---- apps/vscode/package.json | 47 ++++++++-- apps/vscode/src/extension.ts | 13 +-- docs/BEHAVIOR_PARITY.md | 30 +++---- docs/DEMO_SCRIPT.md | 7 ++ docs/HANDOFF.md | 64 +++++++++----- docs/MIGRATION_FROM_CLAUDE_CODE.md | 66 +++++++------- docs/RELEASING.md | 28 +++--- docs/SHIPPING_MAC.md | 17 ++-- docs/security-model.md | 36 ++++---- packages/core/skills/deepseek-api/SKILL.md | 14 +-- packages/core/skills/loop/SKILL.md | 16 ++-- packages/core/skills/pdf/SKILL.md | 14 +-- packages/core/skills/run/SKILL.md | 16 ++-- packages/core/skills/schedule/SKILL.md | 1 + packages/core/skills/skill-creator/SKILL.md | 12 +-- packages/core/skills/update-config/SKILL.md | 29 +++--- packages/core/skills/verify/SKILL.md | 18 ++-- packages/core/src/agent.ts | 12 ++- packages/core/src/auto-mode/index.ts | 4 +- packages/core/src/config/schema.ts | 4 +- packages/core/src/hooks/dispatcher.test.ts | 4 +- packages/core/src/hooks/dispatcher.ts | 3 +- packages/core/src/index.ts | 6 +- packages/core/src/ipc/protocol.ts | 30 +++++-- packages/core/src/keybindings/vim.ts | 7 +- packages/core/src/plugins/install.ts | 4 +- packages/core/src/plugins/marketplace.test.ts | 8 +- packages/core/src/plugins/marketplace.ts | 20 +++-- .../core/src/plugins/runtime/subprocess.ts | 5 +- packages/core/src/plugins/wireup.ts | 12 +-- packages/core/src/providers/deepseek.test.ts | 23 ++++- packages/core/src/reminders/index.ts | 9 +- packages/core/src/sandbox/attacks.test.ts | 8 +- packages/core/src/sandbox/dns-proxy.test.ts | 4 +- packages/core/src/sandbox/pipeline.test.ts | 4 +- packages/core/src/sandbox/profile.test.ts | 5 +- packages/core/src/sandbox/profile.ts | 3 +- packages/core/src/tools/ask-user.test.ts | 5 +- packages/core/src/tools/ask-user.ts | 5 +- packages/core/src/tools/ssrf.test.ts | 20 +++-- packages/core/src/tools/ssrf.ts | 5 +- packages/core/src/tools/tool-search.test.ts | 10 +-- packages/core/src/tools/tool-search.ts | 9 +- packages/core/src/tools/web-fetch.test.ts | 12 +-- packages/core/src/tools/web-fetch.ts | 5 +- packages/core/src/tools/web-search.ts | 5 +- packages/core/src/voice/index.ts | 8 +- packages/core/src/worktree/index.test.ts | 6 +- packages/core/src/worktree/index.ts | 3 +- 92 files changed, 692 insertions(+), 723 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 0987320..7ea00f3 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -4,6 +4,9 @@ set -e +echo "▎ pre-commit: format" +pnpm format:check + echo "▎ pre-commit: lint" pnpm lint diff --git a/.prettierignore b/.prettierignore index d433a25..c40dfa0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,6 @@ coverage pnpm-lock.yaml docs/VISUAL_DESIGN.html reference + +# Generated release artifacts (README/signing log/install script are produced by the release pipeline) +release-artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9368d49..d9f3d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.6] — 2026-05-28 ### 🐛 Critical fix — Bash tool calls were always reporting "error" + The Rust output structs (`ReadOk`, `EditOk`, `BashOk`) returned fields in snake_case (`exit_code`, `lines_total`, `diff_preview`) while the TS wrappers read them in camelCase. Result: `r.exitCode` was always @@ -21,6 +22,7 @@ output structs. Glob and Grep were already single-word fields, no change needed. ### Polish carry-over + - **Keyboard shortcuts**: ⌘N starts a new session, ⌘, opens Settings, ⌘/ opens About. New `src/lib/keyboard.ts` helper. - **Switching project now clears chat history** so the next message @@ -30,6 +32,7 @@ change needed. ## [0.1.5] — 2026-05-28 ### Polish + dead-code removal + - **Composer `+` menu wired**. Click `+` → popover with three actions: Attach file (opens native file picker, inserts `@` into the textarea), Slash command (prepends `/`), Memory note @@ -51,6 +54,7 @@ change needed. ## [0.1.4] — 2026-05-28 ### Robustness + polish + - **React error boundary** wraps the entire app. Uncaught render errors now show a recoverable error panel ("DeepCode crashed") with the stack trace + reload button, instead of leaving the user with a @@ -64,12 +68,13 @@ change needed. ## [0.1.3] — 2026-05-28 ### Visual redesign — phase 2 + - **All 7 utility screens** (Sessions / Plugins / Skills / Permissions / MCP / Settings / About) redesigned to match `docs/VISUAL_DESIGN.html`. New shared `Screen` + `Card` + `Row` primitives. - **About** is now a proper hero card with brand mark + gradient text - + status diagnostics + docs links (replacing the boxed table layout - the user shared as visually off-spec). + - status diagnostics + docs links (replacing the boxed table layout + the user shared as visually off-spec). - **Settings** has a GUI/JSON segmented toggle: GUI shows a quick reference + filterable flat table; JSON shows a live-validated textarea. Save persists to ~/.deepcode/settings.json (was @@ -81,6 +86,7 @@ change needed. - **MCP** uses status badges + tool count + inline error tail. ### Release pipeline (M9) + - `release.yml` rewritten for Tauri (was Electron-era). Tag → CI → npm publish + signed/notarized DMG + GitHub Release with notes. - `docs/RELEASING.md` explains the 6 secrets needed and step-by-step. @@ -88,6 +94,7 @@ change needed. ## [0.1.2] — 2026-05-28 ### Fixes — caught from user playtest of 0.1.1 + - **Tool input field-name fix.** `tool_write` (and read / edit / bash / glob / grep) were failing with `missing required key filePath` when DeepSeek emitted snake_case keys but the wrapper expected camelCase. @@ -109,6 +116,7 @@ change needed. Skills, MCP, About, Settings). Expand-chevron ‹ still deferred. ### UX improvements + - **Proper dropdowns** for mode / model / effort — click-popover with inline descriptions and meta annotations, replacing the brittle click-to-cycle pattern. @@ -120,6 +128,7 @@ change needed. ## [0.1.1] — 2026-05-28 ### Visual redesign — phase 1 + Major UI overhaul aligning the desktop client to `docs/VISUAL_DESIGN.html`. Phase 1 covers the three highest-traffic surfaces: Onboarding, Sessions sidebar, and the main Chat / REPL view. Other six screens land in 0.1.2. @@ -143,6 +152,7 @@ sidebar, and the main Chat / REPL view. Other six screens land in 0.1.2. text headline matching the design spec. ### Conversation flow + - Carries over the `dangerouslyAllowBrowser: true` fix from 0.1.0 so the OpenAI SDK's browser-environment guard doesn't trip in the Tauri webview - Surfaces full error stack traces in the chat stream when the agent @@ -152,6 +162,7 @@ sidebar, and the main Chat / REPL view. Other six screens land in 0.1.2. ## [0.1.0] — 2026-05-28 ### Mac client + CLI baseline + - **CLI:** agent loop, 30+ slash commands, MCP support, plugin system, sandbox, hooks, modes, skills, sub-agents, output styles, effort levels, headless `-p` mode diff --git a/MORNING_REPORT.md b/MORNING_REPORT.md index c49383d..ec3fc8c 100644 --- a/MORNING_REPORT.md +++ b/MORNING_REPORT.md @@ -9,11 +9,11 @@ 本轮 (v6 → v7) 又推了 3 个 feature PR + 这个汇报。重点是为 v1.1 开了头: VS Code 扩展 + LSP bridge,让 DeepCode 进入 IDE 生态。 -| # | 主题 | 主要内容 | -| --- | --- | --- | -| #55 | v1.1 入口 — VS Code + LSP | `apps/vscode` 扩展骨架(commands + Chat 视图 + 配置) · `apps/lsp` stdio LSP 服务器 + JSON-RPC handler + 3 个 custom commands + 8 个单元测试 + Neovim/Emacs/Sublime 配置示例 | -| #56 | schema + image + migration | `packages/core/schemas/settings.schema.json` (draft-07 全覆盖) + `validateSettingsShallow` + Vision 接口(Stub + OpenAICompat with 14 tests)+ `docs/MIGRATION_FROM_CLAUDE_CODE.md` 5 分钟切换指南 | -| 本 PR | README 完善 + 报告 | 重写 README.md:状态从 "M0 设计阶段" 改为生产级 progress bar / 文档地图 / 项目结构表 · 本汇报 v7 | +| # | 主题 | 主要内容 | +| ----- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #55 | v1.1 入口 — VS Code + LSP | `apps/vscode` 扩展骨架(commands + Chat 视图 + 配置) · `apps/lsp` stdio LSP 服务器 + JSON-RPC handler + 3 个 custom commands + 8 个单元测试 + Neovim/Emacs/Sublime 配置示例 | +| #56 | schema + image + migration | `packages/core/schemas/settings.schema.json` (draft-07 全覆盖) + `validateSettingsShallow` + Vision 接口(Stub + OpenAICompat with 14 tests)+ `docs/MIGRATION_FROM_CLAUDE_CODE.md` 5 分钟切换指南 | +| 本 PR | README 完善 + 报告 | 重写 README.md:状态从 "M0 设计阶段" 改为生产级 progress bar / 文档地图 / 项目结构表 · 本汇报 v7 | ## 状态对照 @@ -75,14 +75,14 @@ Marketplace 上线 ████████░░░░░ ## 6 个包的最终矩阵 -| 包 | 状态 | 测试 | 备注 | -| --- | --- | --- | --- | -| `@deepcode/core` | ✅ ship-ready | 478 | 内核,UI-agnostic,npm 可发 | -| `@deepcode/shared-ui` | ✅ ship-ready | 0 (types-only) | 共享类型 | -| `deepcode-cli` | ✅ ship-ready | 47 | npm 可发,npx 可跑 | -| `@deepcode/desktop` | 🟡 等装 Electron | 0 (TBD) | UI/IPC/build 配置全在 | -| `@deepcode/vscode` | 🟡 v1.1 骨架 | 0 (TBD) | manifest + extension.ts 骨架 | -| `@deepcode/lsp` | 🟡 v1.1 骨架 | 8 | stdio server + handler 完整 | +| 包 | 状态 | 测试 | 备注 | +| --------------------- | ---------------- | -------------- | ---------------------------- | +| `@deepcode/core` | ✅ ship-ready | 478 | 内核,UI-agnostic,npm 可发 | +| `@deepcode/shared-ui` | ✅ ship-ready | 0 (types-only) | 共享类型 | +| `deepcode-cli` | ✅ ship-ready | 47 | npm 可发,npx 可跑 | +| `@deepcode/desktop` | 🟡 等装 Electron | 0 (TBD) | UI/IPC/build 配置全在 | +| `@deepcode/vscode` | 🟡 v1.1 骨架 | 0 (TBD) | manifest + extension.ts 骨架 | +| `@deepcode/lsp` | 🟡 v1.1 骨架 | 8 | stdio server + handler 完整 | ## 总结 @@ -97,6 +97,7 @@ Claude session 能做的代码工作已经穷尽。 - 文档(28 个 .md)100% 剩下的全部需要用户层动作或外部资源: + - Apple Developer ID($99/yr + Xcode) - Electron binary 装包(~250MB CI 时间) - 真录 demo 视频(人 + mic + iMovie) diff --git a/README.md b/README.md index 547f4f0..5d0105d 100644 --- a/README.md +++ b/README.md @@ -67,25 +67,25 @@ v1.1 VS Code/JetBrains █████░░░░░░░░░░░░░ ### 用户文档 -| 文件 | 内容 | -| --- | --- | -| [docs/MIGRATION_FROM_CLAUDE_CODE.md](docs/MIGRATION_FROM_CLAUDE_CODE.md) | 从 Claude Code 5 分钟迁移指南 + 字段映射 | -| [docs/BEHAVIOR_PARITY.md](docs/BEHAVIOR_PARITY.md) | 与 Claude Code 的逐项行为对比 | -| [docs/SHIPPING_MAC.md](docs/SHIPPING_MAC.md) | 给 maintainer:Apple Dev ID + 签名 + 公证全流程 | -| [docs/VOICE_INPUT.md](docs/VOICE_INPUT.md) | 装 whisper.cpp 本地语音输入 | -| [docs/DEMO_SCRIPT.md](docs/DEMO_SCRIPT.md) | 5 分钟 launch 视频逐段录制脚本 | +| 文件 | 内容 | +| ------------------------------------------------------------------------ | ----------------------------------------------- | +| [docs/MIGRATION_FROM_CLAUDE_CODE.md](docs/MIGRATION_FROM_CLAUDE_CODE.md) | 从 Claude Code 5 分钟迁移指南 + 字段映射 | +| [docs/BEHAVIOR_PARITY.md](docs/BEHAVIOR_PARITY.md) | 与 Claude Code 的逐项行为对比 | +| [docs/SHIPPING_MAC.md](docs/SHIPPING_MAC.md) | 给 maintainer:Apple Dev ID + 签名 + 公证全流程 | +| [docs/VOICE_INPUT.md](docs/VOICE_INPUT.md) | 装 whisper.cpp 本地语音输入 | +| [docs/DEMO_SCRIPT.md](docs/DEMO_SCRIPT.md) | 5 分钟 launch 视频逐段录制脚本 | ### 设计文档 -| 文件 | 内容 | -| --- | --- | -| [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md) | 整体开发方案 v0.5(1500+ 行 / §3 模块 / §6 里程碑) | -| [docs/VISUAL_DESIGN.html](docs/VISUAL_DESIGN.html) | 视觉设计 v0.4(11 屏 mockup) | -| [docs/security-model.md](docs/security-model.md) | 威胁模型 + 防御层 + 攻击向量测试 + 已知缺口 | -| [docs/design/sandbox-plan-worktree.md](docs/design/sandbox-plan-worktree.md) | sandbox × plan mode × worktree 关系矩阵 | -| [docs/design/plugin-security.md](docs/design/plugin-security.md) | plugin 信任 ladder + sandbox 子进程 | -| [docs/design/effort-levels.md](docs/design/effort-levels.md) | 5 档 effort 到 DeepSeek API 参数映射 | -| [docs/m1-validation.md](docs/m1-validation.md) | M1 用真 DeepSeek API 验证记录 | +| 文件 | 内容 | +| ---------------------------------------------------------------------------- | --------------------------------------------------- | +| [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md) | 整体开发方案 v0.5(1500+ 行 / §3 模块 / §6 里程碑) | +| [docs/VISUAL_DESIGN.html](docs/VISUAL_DESIGN.html) | 视觉设计 v0.4(11 屏 mockup) | +| [docs/security-model.md](docs/security-model.md) | 威胁模型 + 防御层 + 攻击向量测试 + 已知缺口 | +| [docs/design/sandbox-plan-worktree.md](docs/design/sandbox-plan-worktree.md) | sandbox × plan mode × worktree 关系矩阵 | +| [docs/design/plugin-security.md](docs/design/plugin-security.md) | plugin 信任 ladder + sandbox 子进程 | +| [docs/design/effort-levels.md](docs/design/effort-levels.md) | 5 档 effort 到 DeepSeek API 参数映射 | +| [docs/m1-validation.md](docs/m1-validation.md) | M1 用真 DeepSeek API 验证记录 | ## 项目结构 diff --git a/apps/cli/src/commands.test.ts b/apps/cli/src/commands.test.ts index 0dd2113..155691a 100644 --- a/apps/cli/src/commands.test.ts +++ b/apps/cli/src/commands.test.ts @@ -266,10 +266,9 @@ describe('built-in command behavior', () => { // Modify the file after snapshot await fs.writeFile(file, 'changed'); const ctx = makeContext({ sessions: sm, sessionId: meta.id }); - const out = await reg.match(`/rewind ${snap!.seq} code`)!.cmd.run( - [String(snap!.seq), 'code'], - ctx, - ); + const out = await reg + .match(`/rewind ${snap!.seq} code`)! + .cmd.run([String(snap!.seq), 'code'], ctx); expect(out.join('\n')).toMatch(/Restored/); const after = await fs.readFile(file, 'utf8'); expect(after).toBe('original'); @@ -306,10 +305,9 @@ describe('built-in command behavior', () => { sessionId: meta.id, history: [before, after], }); - const out = await reg.match(`/rewind ${snap!.seq} conversation`)!.cmd.run( - [String(snap!.seq), 'conversation'], - ctx, - ); + const out = await reg + .match(`/rewind ${snap!.seq} conversation`)! + .cmd.run([String(snap!.seq), 'conversation'], ctx); expect(out.join('\n')).toMatch(/kept 1 of 2 messages/); expect(ctx.newHistory).toEqual([before]); }); diff --git a/apps/cli/src/commands.ts b/apps/cli/src/commands.ts index d8ad0c6..319a80a 100644 --- a/apps/cli/src/commands.ts +++ b/apps/cli/src/commands.ts @@ -163,7 +163,8 @@ const EFFORT_TIERS: Array<{ export const EffortCommand: SlashCommand = { name: '/effort', - description: 'Set effort tier (interactive picker if no arg): /effort [low|medium|high|xhigh|max]', + description: + 'Set effort tier (interactive picker if no arg): /effort [low|medium|high|xhigh|max]', run(args, ctx) { if (args.length === 0) { // Selector UI — show the table; user picks via `/effort ` next turn. @@ -183,9 +184,7 @@ export const EffortCommand: SlashCommand = { const next = args[0]!; const tier = EFFORT_TIERS.find((t) => t.name === next); if (!tier) { - return [ - `Unknown effort "${next}". Valid: ${EFFORT_TIERS.map((t) => t.name).join(' | ')}`, - ]; + return [`Unknown effort "${next}". Valid: ${EFFORT_TIERS.map((t) => t.name).join(' | ')}`]; } ctx.effort = next; return [ @@ -328,8 +327,7 @@ export const TodosCommand: SlashCommand = { if (todos.length === 0) return ['No active todos.']; const lines = [`Todos (${todos.length}):`]; for (const t of todos) { - const marker = - t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '●' : '○'; + const marker = t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '●' : '○'; const text = t.status === 'in_progress' ? t.activeForm : t.content; lines.push(` ${marker} ${text}`); } @@ -492,7 +490,8 @@ export const RewindCommand: SlashCommand = { ]; } case 'summarize-from': { - if (!ctx.provider) return ['(/rewind summarize-from requires a provider — none configured.)']; + if (!ctx.provider) + return ['(/rewind summarize-from requires a provider — none configured.)']; const kept = trimHistoryBefore(currentHistory, cutoffMs); const tail = currentHistory.slice(kept.length); if (tail.length === 0) { @@ -506,7 +505,8 @@ export const RewindCommand: SlashCommand = { ]; } case 'summarize-up-to': { - if (!ctx.provider) return ['(/rewind summarize-up-to requires a provider — none configured.)']; + if (!ctx.provider) + return ['(/rewind summarize-up-to requires a provider — none configured.)']; const head = trimHistoryBefore(currentHistory, cutoffMs); const tail = currentHistory.slice(head.length); if (head.length === 0) { diff --git a/apps/cli/src/parse-args.test.ts b/apps/cli/src/parse-args.test.ts index ee63c9c..b7baf64 100644 --- a/apps/cli/src/parse-args.test.ts +++ b/apps/cli/src/parse-args.test.ts @@ -132,15 +132,11 @@ describe('parseArgs', () => { describe('resolveEffort (precedence)', () => { it('cli flag wins over env and settings', () => { - expect( - resolveEffort({ cliFlag: 'high', envVar: 'low', settingsLevel: 'max' }), - ).toBe('high'); + expect(resolveEffort({ cliFlag: 'high', envVar: 'low', settingsLevel: 'max' })).toBe('high'); }); it('env var wins when no cli flag', () => { - expect(resolveEffort({ envVar: 'xhigh', settingsLevel: 'low' })).toBe( - 'xhigh', - ); + expect(resolveEffort({ envVar: 'xhigh', settingsLevel: 'low' })).toBe('xhigh'); }); it('settings wins when no cli flag and no env', () => { @@ -152,9 +148,7 @@ describe('resolveEffort (precedence)', () => { }); it('ignores invalid env var', () => { - expect(resolveEffort({ envVar: 'ultra', settingsLevel: 'low' })).toBe( - 'low', - ); + expect(resolveEffort({ envVar: 'ultra', settingsLevel: 'low' })).toBe('low'); }); it('trims whitespace in env var', () => { diff --git a/apps/cli/src/repl.ts b/apps/cli/src/repl.ts index 68887db..f202c70 100644 --- a/apps/cli/src/repl.ts +++ b/apps/cli/src/repl.ts @@ -313,9 +313,7 @@ export async function startRepl(opts: ReplOpts): Promise { sandboxConfig: settings.sandbox, approval: async (toolName, _input, verdict) => { output.write(`\n ⏸ Approve ${toolName}? Reason: ${verdict.reason}\n`); - const answer = ( - await rl.question(' [y]es / [n]o / [a]lways: ') - ).trim().toLowerCase(); + const answer = (await rl.question(' [y]es / [n]o / [a]lways: ')).trim().toLowerCase(); if (answer === 'a' || answer === 'always') { // Persist a bare-tool matcher to project-local settings so the next // run of this tool from this project skips the prompt. diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 7e2855c..95ade6b 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -38,12 +38,12 @@ mv apps/desktop/postcss.config.template.js apps/desktop/postcss.config.js 之后: -| 命令 | 作用 | -| ------------------ | --------------------------------------------------- | -| `pnpm dev` | Vite dev server + electron 自动重载 | -| `pnpm build:all` | 构建 renderer (dist/) + main process (dist-electron/) | -| `pnpm pack` | 打包未签名 .app(本地测试) | -| `pnpm dist` | 完整签名 + 公证 + .dmg(需要 Apple Developer ID) | +| 命令 | 作用 | +| ---------------- | ----------------------------------------------------- | +| `pnpm dev` | Vite dev server + electron 自动重载 | +| `pnpm build:all` | 构建 renderer (dist/) + main process (dist-electron/) | +| `pnpm pack` | 打包未签名 .app(本地测试) | +| `pnpm dist` | 完整签名 + 公证 + .dmg(需要 Apple Developer ID) | ## 还没做(M6-rest 余下任务) diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 39a3a7b..8aca01e 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -35,12 +35,7 @@ "shortDescription": "DeepSeek-powered coding agent", "longDescription": "DeepCode is a Claude-Code-parity coding agent powered by DeepSeek — chat, plan mode, tool use, sandboxed bash, MCP, plugins.", "copyright": "Copyright © 2026 DeepCode", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns" - ], + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns"], "macOS": { "minimumSystemVersion": "11.0", "frameworks": [], @@ -56,9 +51,7 @@ "plugins": { "updater": { "active": true, - "endpoints": [ - "https://github.com/oratis/deepcode/releases/latest/download/latest.json" - ], + "endpoints": ["https://github.com/oratis/deepcode/releases/latest/download/latest.json"], "dialog": false, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDk0MDMzRUQ0RkVFNUREODUKUldTRjNlWCsxRDREbEttVHEwUEplK1FKbnRCakpGb3dVWTYveFdxSmxwK2ZROWFZcW1kYzZMSGcK" } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index a1b3c46..0ddb6c1 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -120,15 +120,9 @@ export function App(): JSX.Element { }} />
- {renderScreen(screen, setScreen, projectPath, () => - setSessionEpoch((k) => k + 1), - )} + {renderScreen(screen, setScreen, projectPath, () => setSessionEpoch((k) => k + 1))}
- setScreen(s)} - contextFill={undefined} - /> + setScreen(s)} contextFill={undefined} /> ); } @@ -144,12 +138,7 @@ function renderScreen( // 'chat' folded into 'repl' — the new shell has only the REPL surface. return ; case 'sessions': - return ( - setScreen('repl')} - onNew={() => setScreen('repl')} - /> - ); + return setScreen('repl')} onNew={() => setScreen('repl')} />; case 'plugins': return ; case 'skills': diff --git a/apps/desktop/src/components/Dropdown.tsx b/apps/desktop/src/components/Dropdown.tsx index 0ceecda..0ecc72e 100644 --- a/apps/desktop/src/components/Dropdown.tsx +++ b/apps/desktop/src/components/Dropdown.tsx @@ -87,9 +87,7 @@ export function Dropdown({ {selected.meta && {selected.meta}} )} - - ⌄ - + {open && ( diff --git a/apps/desktop/src/components/ErrorBoundary.tsx b/apps/desktop/src/components/ErrorBoundary.tsx index 2427020..89399dd 100644 --- a/apps/desktop/src/components/ErrorBoundary.tsx +++ b/apps/desktop/src/components/ErrorBoundary.tsx @@ -51,7 +51,16 @@ export class ErrorBoundary extends Component { boxShadow: 'var(--shadow)', }} > -
+
DeepCode crashed

{ Something went wrong rendering this screen.

- This is a bug in DeepCode. The conversation, your settings, and - your project folder are intact — reload to recover. If it keeps - happening, please share the error below at github.com/oratis/deepcode/issues. + This is a bug in DeepCode. The conversation, your settings, and your project folder are + intact — reload to recover. If it keeps happening, please share the error below at + github.com/oratis/deepcode/issues.

 onChange('permissions')}
       >
         ▤
-        {planCount !== undefined && planCount > 0 && (
-          {planCount}
-        )}
+        {planCount !== undefined && planCount > 0 && {planCount}}
       
 
       
diff --git a/apps/desktop/src/components/Screen.tsx b/apps/desktop/src/components/Screen.tsx index d75af5f..f794d45 100644 --- a/apps/desktop/src/components/Screen.tsx +++ b/apps/desktop/src/components/Screen.tsx @@ -31,10 +31,7 @@ export function Screen({ title, subtitle, actions, children }: ScreenProps): JSX {actions &&
{actions}
} -
+
{children}
@@ -54,13 +51,7 @@ interface CardProps { flush?: boolean; } -export function Card({ - title, - actions, - children, - padding = 16, - flush, -}: CardProps): JSX.Element { +export function Card({ title, actions, children, padding = 16, flush }: CardProps): JSX.Element { return (
)} - {actions && ( -
- {actions} -
- )} + {actions &&
{actions}
}
)}
{children}
@@ -128,15 +115,9 @@ export function Row({ label, hint, children }: RowProps): JSX.Element { >
{label}
- {hint && ( -
- {hint} -
- )} -
-
- {children} + {hint &&
{hint}
}
+
{children}
); } diff --git a/apps/desktop/src/components/Sidebar.tsx b/apps/desktop/src/components/Sidebar.tsx index 482a71d..c38b1ae 100644 --- a/apps/desktop/src/components/Sidebar.tsx +++ b/apps/desktop/src/components/Sidebar.tsx @@ -173,8 +173,7 @@ export function Sidebar({ lineHeight: 1.5, }} > - No sessions yet — your conversations will appear here once you - start one. + No sessions yet — your conversations will appear here once you start one. )} diff --git a/apps/desktop/src/components/ToolCard.tsx b/apps/desktop/src/components/ToolCard.tsx index 567b31a..f011ed3 100644 --- a/apps/desktop/src/components/ToolCard.tsx +++ b/apps/desktop/src/components/ToolCard.tsx @@ -24,13 +24,7 @@ interface ToolCardProps { diff?: boolean; } -export function ToolCard({ - name, - target, - status, - body, - diff, -}: ToolCardProps): JSX.Element { +export function ToolCard({ name, target, status, body, diff }: ToolCardProps): JSX.Element { return (
@@ -38,9 +32,7 @@ export function ToolCard({ {target && {target}} {status && {status.label}}
- {body !== undefined && ( -
{body}
- )} + {body !== undefined &&
{body}
}
); } diff --git a/apps/desktop/src/components/UpdateBanner.tsx b/apps/desktop/src/components/UpdateBanner.tsx index abc7668..b29acb5 100644 --- a/apps/desktop/src/components/UpdateBanner.tsx +++ b/apps/desktop/src/components/UpdateBanner.tsx @@ -36,10 +36,7 @@ export function UpdateBanner({ info }: BannerProps): JSX.Element | null { > {relaunching ? 'Relaunching…' : 'Relaunch now'} - diff --git a/apps/desktop/src/lib/mac-agent.ts b/apps/desktop/src/lib/mac-agent.ts index ef936cc..7cf3c96 100644 --- a/apps/desktop/src/lib/mac-agent.ts +++ b/apps/desktop/src/lib/mac-agent.ts @@ -12,16 +12,8 @@ // avoid pulling BUILTIN_TOOLS / SessionManager / etc. at module-load time. // The renderer can't link against node:fs / node:child_process. import { runAgent } from '@deepcode/core/dist/agent.js'; -import { - DeepSeekProvider, - EFFORT_PARAMS, -} from '@deepcode/core/dist/providers/deepseek.js'; -import type { - AgentEvent, - Effort, - Mode, - ToolHandler, -} from '@deepcode/core/dist/types.js'; +import { DeepSeekProvider, EFFORT_PARAMS } from '@deepcode/core/dist/providers/deepseek.js'; +import type { AgentEvent, Effort, Mode, ToolHandler } from '@deepcode/core/dist/types.js'; import { MAC_TOOLS } from './mac-tools.js'; import { readCredentials, sessionAppend, sessionCreate } from './tauri-api.js'; @@ -106,10 +98,7 @@ export interface StartTurnArgs { * 'deny' — reject * 'always' — permit + persist a permissions.allow matcher */ - onApproval?: ( - toolName: string, - reason: string, - ) => Promise<'allow' | 'deny' | 'always'>; + onApproval?: (toolName: string, reason: string) => Promise<'allow' | 'deny' | 'always'>; } export interface StartTurnResult { @@ -188,9 +177,7 @@ export async function startAgentTurn(args: StartTurnArgs): Promise 0) { - const newestAssistant = [...history] - .reverse() - .find((m) => m.role === 'assistant'); + const newestAssistant = [...history].reverse().find((m) => m.role === 'assistant'); if (newestAssistant) { try { await sessionAppend(currentSessionId, { diff --git a/apps/desktop/src/lib/mac-tools.test.ts b/apps/desktop/src/lib/mac-tools.test.ts index 6ef1591..9175e26 100644 --- a/apps/desktop/src/lib/mac-tools.test.ts +++ b/apps/desktop/src/lib/mac-tools.test.ts @@ -15,30 +15,21 @@ import { describe, expect, it } from 'vitest'; // which can't load outside a Tauri webview. The helpers are pure so // duplicating them in the test is fine; if either ever changes, both // places must be updated. -function pickStr( - input: Record, - ...keys: string[] -): string | undefined { +function pickStr(input: Record, ...keys: string[]): string | undefined { for (const k of keys) { const v = input[k]; if (typeof v === 'string') return v; } return undefined; } -function pickNum( - input: Record, - ...keys: string[] -): number | undefined { +function pickNum(input: Record, ...keys: string[]): number | undefined { for (const k of keys) { const v = input[k]; if (typeof v === 'number') return v; } return undefined; } -function pickBool( - input: Record, - ...keys: string[] -): boolean | undefined { +function pickBool(input: Record, ...keys: string[]): boolean | undefined { for (const k of keys) { const v = input[k]; if (typeof v === 'boolean') return v; @@ -54,9 +45,9 @@ describe('mac-tools key pickers', () => { }); it('pickStr prefers earlier-listed keys (snake_case wins over camelCase)', () => { - expect( - pickStr({ file_path: '/snake', filePath: '/camel' }, 'file_path', 'filePath'), - ).toBe('/snake'); + expect(pickStr({ file_path: '/snake', filePath: '/camel' }, 'file_path', 'filePath')).toBe( + '/snake', + ); }); it('pickStr returns undefined when no key matches', () => { @@ -64,12 +55,8 @@ describe('mac-tools key pickers', () => { }); it('pickStr skips non-string values', () => { - expect( - pickStr({ file_path: 42, filePath: '/ok' }, 'file_path', 'filePath'), - ).toBe('/ok'); - expect( - pickStr({ file_path: null, filePath: '/ok' }, 'file_path', 'filePath'), - ).toBe('/ok'); + expect(pickStr({ file_path: 42, filePath: '/ok' }, 'file_path', 'filePath')).toBe('/ok'); + expect(pickStr({ file_path: null, filePath: '/ok' }, 'file_path', 'filePath')).toBe('/ok'); }); it('pickNum handles primitives correctly', () => { @@ -81,9 +68,7 @@ describe('mac-tools key pickers', () => { it('pickBool handles primitives correctly', () => { expect(pickBool({ replace_all: true }, 'replace_all', 'replaceAll')).toBe(true); expect(pickBool({ replaceAll: false }, 'replace_all', 'replaceAll')).toBe(false); - expect( - pickBool({ replace_all: 'true' as unknown as boolean }, 'replace_all'), - ).toBeUndefined(); + expect(pickBool({ replace_all: 'true' as unknown as boolean }, 'replace_all')).toBeUndefined(); }); it('empty input returns undefined for all pickers', () => { diff --git a/apps/desktop/src/lib/mac-tools.ts b/apps/desktop/src/lib/mac-tools.ts index e748c9c..716d31d 100644 --- a/apps/desktop/src/lib/mac-tools.ts +++ b/apps/desktop/src/lib/mac-tools.ts @@ -19,30 +19,21 @@ import type { ToolHandler, ToolResult } from '@deepcode/core/dist/types.js'; * exact name in the schema, the value is undefined and the Tauri call * fails with "missing required key …". This helper lets us accept both. */ -function pickStr( - input: Record, - ...keys: string[] -): string | undefined { +function pickStr(input: Record, ...keys: string[]): string | undefined { for (const k of keys) { const v = input[k]; if (typeof v === 'string') return v; } return undefined; } -function pickNum( - input: Record, - ...keys: string[] -): number | undefined { +function pickNum(input: Record, ...keys: string[]): number | undefined { for (const k of keys) { const v = input[k]; if (typeof v === 'number') return v; } return undefined; } -function pickBool( - input: Record, - ...keys: string[] -): boolean | undefined { +function pickBool(input: Record, ...keys: string[]): boolean | undefined { for (const k of keys) { const v = input[k]; if (typeof v === 'boolean') return v; @@ -218,8 +209,7 @@ export const MacBashTool: ToolHandler = { timeout_ms: pickNum(input, 'timeout_ms', 'timeoutMs', 'timeout'), }, })) as { stdout: string; stderr: string; exitCode: number; timedOut: boolean }; - const combined = - (r.stdout || '') + (r.stderr ? `\n[stderr]\n${r.stderr}` : ''); + const combined = (r.stdout || '') + (r.stderr ? `\n[stderr]\n${r.stderr}` : ''); return { content: combined || `(no output, exit ${r.exitCode})`, data: { exitCode: r.exitCode, timedOut: r.timedOut }, diff --git a/apps/desktop/src/lib/repl-stream.test.ts b/apps/desktop/src/lib/repl-stream.test.ts index 5570c0e..1bc8bce 100644 --- a/apps/desktop/src/lib/repl-stream.test.ts +++ b/apps/desktop/src/lib/repl-stream.test.ts @@ -33,9 +33,7 @@ describe('repl-stream mutators', () => { it('does NOT spawn a second cursor when a system note interleaves the stream', () => { // Reproduces the "two blinking cursors" bug: a breadcrumb pushed between // streaming deltas must not orphan the open turn or start a new one. - let m: Msg[] = [ - { role: 'user', text: 'write a game' }, - ]; + let m: Msg[] = [{ role: 'user', text: 'write a game' }]; m = appendTextDelta(m, 'Creating files'); m = appendToolUse(m, tool('w1', 'Write')); // User clicks "always allow" → a system note is pushed mid-turn. @@ -62,7 +60,11 @@ describe('repl-stream mutators', () => { const turn = m[0]!; if (turn.role !== 'assistant') throw new Error('expected assistant'); expect(turn.turn.tools[0]).toMatchObject({ toolId: 'a', status: 'running' }); - expect(turn.turn.tools[1]).toMatchObject({ toolId: 'b', status: 'ok', resultText: 'grep output' }); + expect(turn.turn.tools[1]).toMatchObject({ + toolId: 'b', + status: 'ok', + resultText: 'grep output', + }); }); it('finalizeStreaming clears the flag on ALL assistant turns', () => { diff --git a/apps/desktop/src/lib/repl-stream.ts b/apps/desktop/src/lib/repl-stream.ts index 3560725..87b3446 100644 --- a/apps/desktop/src/lib/repl-stream.ts +++ b/apps/desktop/src/lib/repl-stream.ts @@ -70,7 +70,10 @@ export function appendToolUse(msgs: Msg[], tool: ToolInvocation): Msg[] { const target = idx === -1 ? null : (msgs[idx] as AssistantMsg); if (target && target.turn.streaming) { const copy = [...msgs]; - copy[idx] = { role: 'assistant', turn: { ...target.turn, tools: [...target.turn.tools, tool] } }; + copy[idx] = { + role: 'assistant', + turn: { ...target.turn, tools: [...target.turn.tools, tool] }, + }; return copy; } return [...msgs, { role: 'assistant', turn: { text: '', tools: [tool], streaming: true } }]; @@ -106,10 +109,11 @@ export function attachToolResult( /** Clear the streaming flag on ALL assistant turns (not just the last one). */ export function finalizeStreaming(msgs: Msg[]): Msg[] { - return msgs.map((m): Msg => - m.role === 'assistant' && m.turn.streaming - ? { role: 'assistant', turn: { ...m.turn, streaming: false } } - : m, + return msgs.map( + (m): Msg => + m.role === 'assistant' && m.turn.streaming + ? { role: 'assistant', turn: { ...m.turn, streaming: false } } + : m, ); } diff --git a/apps/desktop/src/lib/tauri-api.ts b/apps/desktop/src/lib/tauri-api.ts index 43557f7..816ea9e 100644 --- a/apps/desktop/src/lib/tauri-api.ts +++ b/apps/desktop/src/lib/tauri-api.ts @@ -131,10 +131,7 @@ export async function sessionCreate(cwd: string): Promise { } /** Append one JSON message line to a session's JSONL file. */ -export async function sessionAppend( - id: string, - message: Record, -): Promise { +export async function sessionAppend(id: string, message: Record): Promise { await invoke('session_append', { id, message }); } diff --git a/apps/desktop/src/lib/window-shim.ts b/apps/desktop/src/lib/window-shim.ts index 76a11ca..b491db9 100644 --- a/apps/desktop/src/lib/window-shim.ts +++ b/apps/desktop/src/lib/window-shim.ts @@ -34,10 +34,7 @@ function emitEvent(e: unknown): void { // a `permission_request` event carrying a unique requestId and stash the // resolver here. The UI calls api.agent.approve({ requestId, decision }) // which pops the resolver and resolves the original promise. -const pendingApprovals = new Map< - string, - (decision: 'allow' | 'deny' | 'always') => void ->(); +const pendingApprovals = new Map void>(); function nextRequestId(): string { return `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`; @@ -111,15 +108,8 @@ export function installTauriShim(): void { model, mode: mode as Mode | undefined, cwd, - effort: effort as - | 'low' - | 'medium' - | 'high' - | 'xhigh' - | 'max' - | undefined, - onEvent: (e: AgentEvent) => - emitEvent({ kind: 'event', turnId: pendingTurnId, ...e }), + effort: effort as 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined, + onEvent: (e: AgentEvent) => emitEvent({ kind: 'event', turnId: pendingTurnId, ...e }), onDone: (reason) => emitEvent({ kind: 'turn_done', turnId: pendingTurnId, stopReason: reason }), onApproval: (toolName, reason) => { diff --git a/apps/desktop/src/screens/About.tsx b/apps/desktop/src/screens/About.tsx index fd9150a..20d5199 100644 --- a/apps/desktop/src/screens/About.tsx +++ b/apps/desktop/src/screens/About.tsx @@ -72,9 +72,7 @@ export function AboutScreen(): JSX.Element {

DeepSeek-powered AI coding agent · Claude Code parity

-

- v{diag.version} -

+

v{diag.version}

{/* Diagnostics */} @@ -102,24 +100,16 @@ export function AboutScreen(): JSX.Element { Paths - - ~/.deepcode/credentials.json - + ~/.deepcode/credentials.json - - ~/.deepcode/settings.json - + ~/.deepcode/settings.json - - ~/.deepcode/sessions/ - + ~/.deepcode/sessions/ - - ~/.deepcode/keybindings.json - + ~/.deepcode/keybindings.json @@ -136,10 +126,7 @@ export function AboutScreen(): JSX.Element { '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', - ], + ['CHANGELOG', 'https://github.com/oratis/deepcode/blob/main/CHANGELOG.md'], ].map(([label, href]) => ( = { +const STATUS_BADGE: Record = { connected: { kind: 'ok', label: '● connected' }, failed: { kind: 'err', label: '✕ failed' }, disabled: { kind: 'warn', label: '○ disabled' }, @@ -58,9 +55,7 @@ export function MCPManagerScreen(): JSX.Element {
@@ -76,8 +71,8 @@ export function MCPManagerScreen(): JSX.Element { > No MCP servers configured.
- Add the snippet below to ~/.deepcode/settings.json{' '} - and relaunch DeepCode. + Add the snippet below to ~/.deepcode/settings.json and relaunch + DeepCode.
) : ( @@ -90,9 +85,7 @@ export function MCPManagerScreen(): JSX.Element { style={{ padding: '14px 16px', borderBottom: - i === servers.length - 1 - ? 'none' - : '1px solid var(--line-soft)', + i === servers.length - 1 ? 'none' : '1px solid var(--line-soft)', }} >
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. + 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/Onboarding.tsx b/apps/desktop/src/screens/Onboarding.tsx index 3fdbd86..1ea69f8 100644 --- a/apps/desktop/src/screens/Onboarding.tsx +++ b/apps/desktop/src/screens/Onboarding.tsx @@ -45,9 +45,7 @@ export function OnboardingScreen({ onComplete }: OnboardingProps): JSX.Element {

DeepSeek-powered coding agent.

-

- From this key to your first edit in under 90 seconds. -

+

From this key to your first edit in under 90 seconds.

@@ -73,8 +71,8 @@ export function OnboardingScreen({ onComplete }: OnboardingProps): JSX.Element { > platform.deepseek.com - . Your key is stored locally in ~/.deepcode/credentials.json — it - never leaves your machine except to call api.deepseek.com. + . Your key is stored locally in ~/.deepcode/credentials.json — it never leaves your + machine except to call api.deepseek.com.
diff --git a/apps/desktop/src/screens/Permissions.tsx b/apps/desktop/src/screens/Permissions.tsx index 3f59fdf..9815eaa 100644 --- a/apps/desktop/src/screens/Permissions.tsx +++ b/apps/desktop/src/screens/Permissions.tsx @@ -71,9 +71,7 @@ export function PermissionsScreen(): JSX.Element { } function removeRule(type: RuleType, idx: number): void { - setPerm((p) => - p ? { ...p, [type]: p[type].filter((_, i) => i !== idx) } : p, - ); + setPerm((p) => (p ? { ...p, [type]: p[type].filter((_, i) => i !== idx) } : p)); setSaveMsg(null); } @@ -114,12 +112,7 @@ export function PermissionsScreen(): JSX.Element { title="Permissions" subtitle="deny > ask > allow" actions={ - @@ -134,10 +127,9 @@ export function PermissionsScreen(): JSX.Element { background: saveMsg.startsWith('✓') ? 'rgba(20, 228, 162, 0.12)' : 'rgba(255, 84, 112, 0.12)', - border: '1px solid ' - + (saveMsg.startsWith('✓') - ? 'rgba(20, 228, 162, 0.3)' - : 'rgba(255, 84, 112, 0.3)'), + border: + '1px solid ' + + (saveMsg.startsWith('✓') ? 'rgba(20, 228, 162, 0.3)' : 'rgba(255, 84, 112, 0.3)'), color: saveMsg.startsWith('✓') ? 'var(--accent)' : 'var(--error)', borderRadius: 'var(--radius-sm)', fontSize: 12, @@ -157,9 +149,7 @@ export function PermissionsScreen(): JSX.Element {
setNewRule({ ...newRule, pattern: e.target.value })} - placeholder='e.g. Bash(npm test:*) or Read(./src/**)' + placeholder="e.g. Bash(npm test:*) or Read(./src/**)" className="input" style={{ flex: 1 }} onKeyDown={(e) => { @@ -194,9 +184,9 @@ export function PermissionsScreen(): JSX.Element { lineHeight: 1.5, }} > - Pattern syntax: bare tool name (Bash) · subcommand - (Bash(git:*)) · prefix (Read(/etc/*)) · - domain (WebFetch(domain:github.com)). + Pattern syntax: bare tool name (Bash) · subcommand ( + Bash(git:*)) · prefix (Read(/etc/*)) · domain ( + WebFetch(domain:github.com)).
@@ -230,9 +220,7 @@ export function PermissionsScreen(): JSX.Element { style={{ padding: '8px 16px', borderBottom: - i === rules.length - 1 - ? 'none' - : '1px solid var(--line-soft)', + i === rules.length - 1 ? 'none' : '1px solid var(--line-soft)', display: 'flex', alignItems: 'center', gap: 10, @@ -267,7 +255,11 @@ export function PermissionsScreen(): JSX.Element { })} {perm.additionalDirectories.length > 0 && ( - +
    {perm.additionalDirectories.map((d, i) => (
  • 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. + 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 f9daa5d..2c70a7c 100644 --- a/apps/desktop/src/screens/Plugins.tsx +++ b/apps/desktop/src/screens/Plugins.tsx @@ -39,9 +39,7 @@ export function PluginsScreen(): JSX.Element { loadSettingsFile().catch(() => ({}) as Record), ]); const disabled = new Set( - Array.isArray(settings.disabledPlugins) - ? (settings.disabledPlugins as string[]) - : [], + Array.isArray(settings.disabledPlugins) ? (settings.disabledPlugins as string[]) : [], ); const merged = (rows as PluginRow[]).map((p) => ({ ...p, @@ -62,9 +60,7 @@ export function PluginsScreen(): JSX.Element { try { const current = (await loadSettingsFile()) as Record; const disabled = new Set( - Array.isArray(current.disabledPlugins) - ? (current.disabledPlugins as string[]) - : [], + Array.isArray(current.disabledPlugins) ? (current.disabledPlugins as string[]) : [], ); if (nextEnabled) disabled.delete(name); else disabled.add(name); @@ -78,11 +74,7 @@ export function PluginsScreen(): JSX.Element { } catch (err) { // Revert setPlugins((ps) => - ps - ? ps.map((p) => - p.name === name ? { ...p, enabled: !nextEnabled } : p, - ) - : ps, + ps ? ps.map((p) => (p.name === name ? { ...p, enabled: !nextEnabled } : p)) : ps, ); setFeedback(`✕ Toggle failed: ${(err as Error).message}`); } @@ -116,10 +108,7 @@ export function PluginsScreen(): JSX.Element { const enabled = plugins.filter((p) => p.enabled).length; return ( - +
@@ -183,9 +172,7 @@ export function PluginsScreen(): JSX.Element { style={{ padding: '14px 16px', borderBottom: - i === plugins.length - 1 - ? 'none' - : '1px solid var(--line-soft)', + i === plugins.length - 1 ? 'none' : '1px solid var(--line-soft)', }} >
[] = [ - { value: 'low', label: 'Low', meta: '4k', description: 'Cheap & quick — short answers, simple edits.' }, - { value: 'medium', label: 'Medium', meta: '8k', description: 'Balanced default. Good for most coding tasks.' }, - { value: 'high', label: 'High', meta: '16k', description: 'Longer context — multi-file refactors.' }, - { value: 'xhigh', label: 'Extra High', meta: '24k', description: 'Deep reasoning for architecture changes.' }, - { value: 'max', label: 'Max', meta: '32k', description: 'Max tokens. Slow & expensive — use sparingly.' }, + { + value: 'low', + label: 'Low', + meta: '4k', + description: 'Cheap & quick — short answers, simple edits.', + }, + { + value: 'medium', + label: 'Medium', + meta: '8k', + description: 'Balanced default. Good for most coding tasks.', + }, + { + value: 'high', + label: 'High', + meta: '16k', + description: 'Longer context — multi-file refactors.', + }, + { + value: 'xhigh', + label: 'Extra High', + meta: '24k', + description: 'Deep reasoning for architecture changes.', + }, + { + value: 'max', + label: 'Max', + meta: '32k', + description: 'Max tokens. Slow & expensive — use sparingly.', + }, ]; const MODEL_OPTIONS: DropdownOption<'deepseek-chat' | 'deepseek-reasoner'>[] = [ - { value: 'deepseek-chat', label: 'DeepSeek-Chat', meta: '128k', description: 'Faster, cheaper. Best default.' }, + { + value: 'deepseek-chat', + label: 'DeepSeek-Chat', + meta: '128k', + description: 'Faster, cheaper. Best default.', + }, { value: 'deepseek-reasoner', label: 'DeepSeek-Reasoner (R1)', @@ -78,7 +108,12 @@ const MODEL_OPTIONS: DropdownOption<'deepseek-chat' | 'deepseek-reasoner'>[] = [ const MODE_OPTIONS: DropdownOption< 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions' >[] = [ - { value: 'default', label: 'Default', meta: '●', description: 'Ask before every tool call that needs approval.' }, + { + value: 'default', + label: 'Default', + meta: '●', + description: 'Ask before every tool call that needs approval.', + }, { value: 'acceptEdits', label: 'Accept edits', @@ -135,10 +170,7 @@ interface PendingApproval { // ─── Component ──────────────────────────────────────────────────────── -export function ReplScreen({ - projectPath, - onTurnComplete, -}: ReplScreenProps): JSX.Element { +export function ReplScreen({ projectPath, onTurnComplete }: ReplScreenProps): JSX.Element { const [messages, setMessages] = useState([ { role: 'system', @@ -370,9 +402,7 @@ export function ReplScreen({ } // ── Approval ── - async function handleApproval( - decision: 'allow' | 'deny' | 'always', - ): Promise { + async function handleApproval(decision: 'allow' | 'deny' | 'always'): Promise { if (!pendingApproval) return; const req = pendingApproval; setPendingApproval(null); @@ -571,13 +601,13 @@ export function ReplScreen({ disabled={controlsLocked} triggerClass={ 'mode-badge ' + - (mode === 'bypassPermissions' - ? 'bypass' - : mode === 'plan' - ? 'plan' - : 'default') + (mode === 'bypassPermissions' ? 'bypass' : mode === 'plan' ? 'plan' : 'default') } - renderTrigger={(opt) => {opt.meta} {opt.label}} + renderTrigger={(opt) => ( + + {opt.meta} {opt.label} + + )} title="Mode controls how tool calls are approved" panelWidth={300} options={MODE_OPTIONS} @@ -587,11 +617,7 @@ export function ReplScreen({ @@ -701,10 +727,7 @@ function renderMessage( textAlign: 'center', padding: '6px 12px', fontFamily: m.level === 'error' ? 'JetBrains Mono, monospace' : 'inherit', - background: - m.level === 'error' - ? 'rgba(255, 84, 112, 0.06)' - : 'transparent', + background: m.level === 'error' ? 'rgba(255, 84, 112, 0.06)' : 'transparent', borderRadius: 'var(--radius-sm)', lineHeight: 1.5, }} @@ -730,11 +753,7 @@ function renderMessage( status={{ kind: t.status === 'running' ? 'info' : t.status === 'ok' ? 'ok' : 'err', label: - t.status === 'running' - ? '… running' - : t.status === 'ok' - ? '✓ done' - : '✕ error', + t.status === 'running' ? '… running' : t.status === 'ok' ? '✓ done' : '✕ error', }} body={t.resultText ? truncate(t.resultText, 1500) : undefined} /> @@ -787,4 +806,3 @@ function abbreviatePath(p: string): string { function truncate(s: string, n: number): string { return s.length > n ? s.slice(0, n) + '…\n[truncated]' : s; } - diff --git a/apps/desktop/src/screens/Sessions.tsx b/apps/desktop/src/screens/Sessions.tsx index f12a1da..8e6d1d4 100644 --- a/apps/desktop/src/screens/Sessions.tsx +++ b/apps/desktop/src/screens/Sessions.tsx @@ -28,8 +28,8 @@ export function SessionsScreen({ onPick, onNew }: SessionsProps): JSX.Element { ); } - const filtered = sessions.filter((s) => - !filter || s.id.toLowerCase().includes(filter.toLowerCase()), + const filtered = sessions.filter( + (s) => !filter || s.id.toLowerCase().includes(filter.toLowerCase()), ); return ( @@ -74,22 +74,15 @@ export function SessionsScreen({ onPick, onNew }: SessionsProps): JSX.Element { onClick={() => onPick(s.id)} style={{ padding: '12px 16px', - borderBottom: - i === filtered.length - 1 - ? 'none' - : '1px solid var(--line-soft)', + borderBottom: i === filtered.length - 1 ? 'none' : '1px solid var(--line-soft)', cursor: 'pointer', display: 'grid', gridTemplateColumns: '1fr auto', gap: 12, alignItems: 'baseline', }} - onMouseEnter={(e) => - (e.currentTarget.style.background = 'var(--bg-3)') - } - onMouseLeave={(e) => - (e.currentTarget.style.background = 'transparent') - } + onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-3)')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} >
- Sessions are stored as JSONL under ~/.deepcode/sessions/. Resume to - continue any previous conversation. + Sessions are stored as JSONL under ~/.deepcode/sessions/. Resume to continue any previous + conversation.

diff --git a/apps/desktop/src/screens/Settings.tsx b/apps/desktop/src/screens/Settings.tsx index 4466f12..eadb2a9 100644 --- a/apps/desktop/src/screens/Settings.tsx +++ b/apps/desktop/src/screens/Settings.tsx @@ -5,11 +5,7 @@ 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'; +import { getSettingsPath, loadSettingsFile, saveSettingsFile } from '../lib/tauri-api.js'; export function SettingsScreen(): JSX.Element { const [settings, setSettings] = useState | null>(null); @@ -159,10 +155,8 @@ export function SettingsScreen(): JSX.Element { ? 'rgba(20, 228, 162, 0.12)' : 'rgba(255, 84, 112, 0.12)', border: - '1px solid ' - + (feedback.startsWith('✓') - ? 'rgba(20, 228, 162, 0.3)' - : 'rgba(255, 84, 112, 0.3)'), + '1px solid ' + + (feedback.startsWith('✓') ? 'rgba(20, 228, 162, 0.3)' : 'rgba(255, 84, 112, 0.3)'), color: feedback.startsWith('✓') ? 'var(--accent)' : 'var(--error)', borderRadius: 'var(--radius-sm)', fontSize: 12, @@ -233,9 +227,7 @@ export function SettingsScreen(): JSX.Element { fontSize: 12, }} > - {flat.length === 0 - ? 'Settings file is empty.' - : 'No matching keys.'} + {flat.length === 0 ? 'Settings file is empty.' : 'No matching keys.'}
) : ( {visibleFlat.map((e) => ( - +
Tip
- Use the JSON view (toggle in the header) to edit nested keys - and arrays directly. Save validates JSON before writing. + Use the JSON view (toggle in the header) to edit nested keys and arrays directly. Save + validates JSON before writing.
)} diff --git a/apps/desktop/src/screens/Skills.tsx b/apps/desktop/src/screens/Skills.tsx index 1367662..50f623b 100644 --- a/apps/desktop/src/screens/Skills.tsx +++ b/apps/desktop/src/screens/Skills.tsx @@ -99,12 +99,9 @@ export function SkillsScreen(): JSX.Element { style={{ padding: '10px 14px', borderBottom: - i === filtered.length - 1 - ? 'none' - : '1px solid var(--line-soft)', + i === filtered.length - 1 ? 'none' : '1px solid var(--line-soft)', cursor: 'pointer', - background: - s.name === active ? 'var(--brand-tint)' : 'transparent', + background: s.name === active ? 'var(--brand-tint)' : 'transparent', }} >
{badge.label}
-
- {s.description} -
+
{s.description}
); })} @@ -173,7 +168,8 @@ export function SkillsScreen(): JSX.Element { lineHeight: 1.5, }} > - {current.body ?? '(SKILL.md body not loaded — the desktop IPC for fetching skill body lands in v0.2.)'} + {current.body ?? + '(SKILL.md body not loaded — the desktop IPC for fetching skill body lands in v0.2.)'} ) : ( diff --git a/apps/desktop/src/types/global.d.ts b/apps/desktop/src/types/global.d.ts index 07b264b..22269e1 100644 --- a/apps/desktop/src/types/global.d.ts +++ b/apps/desktop/src/types/global.d.ts @@ -78,10 +78,7 @@ export interface DeepCodeAPI { abort: (args: { turnId: string }) => Promise; /** Resolve an in-flight permission_request event. `decision === 'always'` * also persists a matcher to ~/.deepcode/settings.json. */ - approve: (args: { - requestId: string; - decision: 'allow' | 'deny' | 'always'; - }) => Promise; + approve: (args: { requestId: string; decision: 'allow' | 'deny' | 'always' }) => Promise; answer: (args: { turnId: string; questionId: string; answer: string }) => Promise; onEvent: (cb: (e: unknown) => void) => () => void; }; diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 31b021e..5f7ae9d 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -21,9 +21,7 @@ export default defineConfig({ port: 5173, strictPort: true, host: host ?? false, - hmr: host - ? { protocol: 'ws', host, port: 5174 } - : undefined, + hmr: host ? { protocol: 'ws', host, port: 5174 } : undefined, watch: { ignored: ['**/src-tauri/**'] }, }, envPrefix: ['VITE_', 'TAURI_'], diff --git a/apps/lsp/README.md b/apps/lsp/README.md index 1a49bb9..75f4a24 100644 --- a/apps/lsp/README.md +++ b/apps/lsp/README.md @@ -6,17 +6,20 @@ LSP plugin) can drive DeepCode via `workspace/executeCommand`. ## Custom commands -| Command | Args | Returns | -| ---------------------- | ------------------------ | -------------------------------------- | -| `deepcode.runAgent` | `{ prompt: string }` | `{ turnId: string }` + streams events | -| `deepcode.abort` | `{ turnId: string }` | `{ aborted: boolean }` | -| `deepcode.listSkills` | none | `{ skills: SkillRow[] }` | +| Command | Args | Returns | +| --------------------- | -------------------- | ------------------------------------- | +| `deepcode.runAgent` | `{ prompt: string }` | `{ turnId: string }` + streams events | +| `deepcode.abort` | `{ turnId: string }` | `{ aborted: boolean }` | +| `deepcode.listSkills` | none | `{ skills: SkillRow[] }` | Streamed events are sent as `deepcode/agentEvent` notifications: ```json -{ "jsonrpc": "2.0", "method": "deepcode/agentEvent", - "params": { "turnId": "lsp-...", "kind": "text_delta", "text": "..." } } +{ + "jsonrpc": "2.0", + "method": "deepcode/agentEvent", + "params": { "turnId": "lsp-...", "kind": "text_delta", "text": "..." } +} ``` The `kind` field mirrors the AgentStreamEvent union from diff --git a/apps/lsp/src/handler.test.ts b/apps/lsp/src/handler.test.ts index 10cdc61..f041698 100644 --- a/apps/lsp/src/handler.test.ts +++ b/apps/lsp/src/handler.test.ts @@ -48,8 +48,7 @@ describe('handleMessage — executeCommand', () => { for (let i = 0; i < 50; i++) { const done = out.find( (m) => - m.method === 'deepcode/agentEvent' && - (m.params as { kind: string }).kind === 'turn_done', + m.method === 'deepcode/agentEvent' && (m.params as { kind: string }).kind === 'turn_done', ); if (done) break; await new Promise((r) => setTimeout(r, 20)); @@ -108,10 +107,7 @@ describe('handleMessage — executeCommand', () => { describe('handleMessage — unknown method', () => { it('returns -32603 internal error', async () => { const out: LspMessage[] = []; - await handleMessage( - { jsonrpc: '2.0', id: 6, method: 'unknown/method' }, - (m) => out.push(m), - ); + await handleMessage({ jsonrpc: '2.0', id: 6, method: 'unknown/method' }, (m) => out.push(m)); expect(out[0]!.error).toBeDefined(); }); }); diff --git a/apps/lsp/src/handler.ts b/apps/lsp/src/handler.ts index fdea276..94dee15 100644 --- a/apps/lsp/src/handler.ts +++ b/apps/lsp/src/handler.ts @@ -97,10 +97,7 @@ interface ExecuteCommandParams { arguments?: unknown[]; } -async function handleExecuteCommand( - params: ExecuteCommandParams, - send: SendFn, -): Promise { +async function handleExecuteCommand(params: ExecuteCommandParams, send: SendFn): Promise { switch (params.command) { case 'deepcode.runAgent': return handleRunAgent((params.arguments?.[0] ?? {}) as { prompt?: string }, send); @@ -167,8 +164,7 @@ async function handleRunAgent( const result = await runAgent({ provider, tools: new ToolRegistry(BUILTIN_TOOLS), - systemPrompt: - 'You are DeepCode, an AI coding assistant powered by DeepSeek. Be concise.', + systemPrompt: 'You are DeepCode, an AI coding assistant powered by DeepSeek. Be concise.', userMessage: args.prompt!, model: args.model ?? 'deepseek-chat', cwd: state.rootUri ? new URL(state.rootUri).pathname : process.cwd(), diff --git a/apps/vscode/README.md b/apps/vscode/README.md index 514a75e..3235452 100644 --- a/apps/vscode/README.md +++ b/apps/vscode/README.md @@ -19,11 +19,11 @@ pnpm add -D --filter @deepcode/vscode @vscode/vsce @types/vscode Then: -| Command | Result | -| --------------------------------------------- | --------------------------------------------------- | -| `pnpm --filter @deepcode/vscode build` | Compile `src/extension.ts` → `dist/extension.cjs` | -| `pnpm --filter @deepcode/vscode package` | Produce a `.vsix` file (vsce) | -| Press F5 in VS Code with this folder open | Launch Extension Development Host | +| Command | Result | +| ----------------------------------------- | ------------------------------------------------- | +| `pnpm --filter @deepcode/vscode build` | Compile `src/extension.ts` → `dist/extension.cjs` | +| `pnpm --filter @deepcode/vscode package` | Produce a `.vsix` file (vsce) | +| Press F5 in VS Code with this folder open | Launch Extension Development Host | ## Architecture @@ -35,19 +35,19 @@ Then: ## Commands -| ID | Default keybinding | What it does | -| -------------------- | -------------------------- | ------------------------------------------- | -| `deepcode.openPanel` | `Cmd/Ctrl+Shift+D` | Reveal the DeepCode chat view | -| `deepcode.run` | (palette) | Run agent on the selected text | -| `deepcode.review` | (palette) | Run `code-review` skill on current diff | +| ID | Default keybinding | What it does | +| -------------------- | ------------------ | --------------------------------------- | +| `deepcode.openPanel` | `Cmd/Ctrl+Shift+D` | Reveal the DeepCode chat view | +| `deepcode.run` | (palette) | Run agent on the selected text | +| `deepcode.review` | (palette) | Run `code-review` skill on current diff | ## Settings -| Key | Type | Default | Notes | -| ------------------ | -------- | ---------------------- | ----------------------------------------- | -| `deepcode.apiKey` | string | `""` | Falls back to `~/.deepcode/credentials.json` | -| `deepcode.model` | enum | `"deepseek-chat"` | Standard alias + concrete model names | -| `deepcode.effort` | enum | `"medium"` | low / medium / high / xhigh / max | +| Key | Type | Default | Notes | +| ----------------- | ------ | ----------------- | -------------------------------------------- | +| `deepcode.apiKey` | string | `""` | Falls back to `~/.deepcode/credentials.json` | +| `deepcode.model` | enum | `"deepseek-chat"` | Standard alias + concrete model names | +| `deepcode.effort` | enum | `"medium"` | low / medium / high / xhigh / max | ## Roadmap diff --git a/apps/vscode/package.json b/apps/vscode/package.json index ceeb5c1..788aa90 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -23,9 +23,18 @@ ], "contributes": { "commands": [ - { "command": "deepcode.openPanel", "title": "DeepCode: Open Panel" }, - { "command": "deepcode.run", "title": "DeepCode: Run on selection" }, - { "command": "deepcode.review", "title": "DeepCode: Review current diff" } + { + "command": "deepcode.openPanel", + "title": "DeepCode: Open Panel" + }, + { + "command": "deepcode.run", + "title": "DeepCode: Run on selection" + }, + { + "command": "deepcode.review", + "title": "DeepCode: Review current diff" + } ], "configuration": { "title": "DeepCode", @@ -39,29 +48,51 @@ "deepcode.model": { "type": "string", "default": "deepseek-chat", - "enum": ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro"], + "enum": [ + "deepseek-chat", + "deepseek-reasoner", + "deepseek-v4-flash", + "deepseek-v4-pro" + ], "description": "DeepSeek model to use." }, "deepcode.effort": { "type": "string", "default": "medium", - "enum": ["low", "medium", "high", "xhigh", "max"], + "enum": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], "description": "Effort tier (affects maxTokens + temperature)." } } }, "viewsContainers": { "activitybar": [ - { "id": "deepcode", "title": "DeepCode", "icon": "media/icon.svg" } + { + "id": "deepcode", + "title": "DeepCode", + "icon": "media/icon.svg" + } ] }, "views": { "deepcode": [ - { "id": "deepcode.chat", "name": "Chat" } + { + "id": "deepcode.chat", + "name": "Chat" + } ] }, "keybindings": [ - { "command": "deepcode.openPanel", "key": "ctrl+shift+d", "mac": "cmd+shift+d" } + { + "command": "deepcode.openPanel", + "key": "ctrl+shift+d", + "mac": "cmd+shift+d" + } ] }, "scripts": { diff --git a/apps/vscode/src/extension.ts b/apps/vscode/src/extension.ts index caa7471..9fc5f16 100644 --- a/apps/vscode/src/extension.ts +++ b/apps/vscode/src/extension.ts @@ -92,8 +92,7 @@ async function runAgent( await core.runAgent({ provider, tools: new core.ToolRegistry(core.BUILTIN_TOOLS), - systemPrompt: - 'You are DeepCode, an AI coding assistant powered by DeepSeek. Be concise.', + systemPrompt: 'You are DeepCode, an AI coding assistant powered by DeepSeek. Be concise.', userMessage, model: 'deepseek-chat', cwd, @@ -101,9 +100,7 @@ async function runAgent( if (e.type === 'text_delta') out.append(e.text); else if (e.type === 'tool_use') out.appendLine(`\n[${e.name}] ${formatInput(e.input)}`); else if (e.type === 'tool_result') - out.appendLine( - ` ${e.result.isError ? '✕' : '✓'} ${truncate(e.result.content, 200)}`, - ); + out.appendLine(` ${e.result.isError ? '✕' : '✓'} ${truncate(e.result.content, 200)}`); else if (e.type === 'error') out.appendLine(`\n✕ ${e.error}`); }, }); @@ -161,8 +158,7 @@ class ChatViewProvider implements vscode.WebviewViewProvider { await core.runAgent({ provider, tools: new core.ToolRegistry(core.BUILTIN_TOOLS), - systemPrompt: - 'You are DeepCode, an AI coding assistant powered by DeepSeek. Be concise.', + systemPrompt: 'You are DeepCode, an AI coding assistant powered by DeepSeek. Be concise.', userMessage: msg.text, model: 'deepseek-chat', cwd: this.vscodeMod.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(), @@ -178,8 +174,7 @@ class ChatViewProvider implements vscode.WebviewViewProvider { } else if (e.type === 'tool_result') { view.webview.postMessage({ kind: 'tool', - text: - (e.result.isError ? '✕ ' : '✓ ') + truncate(e.result.content, 200), + text: (e.result.isError ? '✕ ' : '✓ ') + truncate(e.result.content, 200), }); } else if (e.type === 'error') { view.webview.postMessage({ kind: 'assistant', text: `✕ ${e.error}` }); diff --git a/docs/BEHAVIOR_PARITY.md b/docs/BEHAVIOR_PARITY.md index b0f3903..b91276c 100644 --- a/docs/BEHAVIOR_PARITY.md +++ b/docs/BEHAVIOR_PARITY.md @@ -24,8 +24,8 @@ Legend: `✅` matches · `🟡` matches with caveats · `🔄` deferred · `⚠ | `/init` | ✓ | ✓ (stub) | 🔄 — multi-phase interactive flow deferred to M3c-ext | | `/mcp` | ✓ | ✓ | ✅ | | `/add-dir` | ✓ | ✓ (records intent) | 🟡 — M3 will enforce | -| `/todos` | ✓ | ✓ | ✅ — reads `/todos.json` written by TodoWrite tool | -| `/plugins` | ✓ | ✓ | ✅ — lists wired plugins + contributed hook events + warnings (M5.2) | +| `/todos` | ✓ | ✓ | ✅ — reads `/todos.json` written by TodoWrite tool | +| `/plugins` | ✓ | ✓ | ✅ — lists wired plugins + contributed hook events + warnings (M5.2) | | `/compact` | ✓ | ✓ auto-trigger | 🟡 — manual `/compact` slash command not exposed yet (auto works via agent loop) | | `/btw` | ✓ | ✗ | 🔄 | | `/recap` | ✓ | ✗ | 🔄 | @@ -153,20 +153,20 @@ Specific deviations: ## CLI flags -| Flag | Status | -| ---------------------------------------------------------------------------- | ------------------------------- | -| `--help` / `--version` | ✅ | -| `--mode` / `--permission-mode` | ✅ | -| `--model` / `--effort` | ✅ | -| `--max-turns` | ✅ | -| `--system-prompt` / `--append-system-prompt[-file]` | ✅ | -| `--allowedTools` / `--disallowedTools` | ✅ | -| `--bare` | 🔄 (parsed, semantics deferred) | -| `--settings` / `--agents` / `--mcp-config` / `--plugin-dir` / `--plugin-url` | 🔄 (parsed only) | -| `--no-plugins` / `--strict` | 🔄 (parsed only) | -| `-p` headless | ✅ text/json/stream-json, 5 exit codes | +| Flag | Status | +| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `--help` / `--version` | ✅ | +| `--mode` / `--permission-mode` | ✅ | +| `--model` / `--effort` | ✅ | +| `--max-turns` | ✅ | +| `--system-prompt` / `--append-system-prompt[-file]` | ✅ | +| `--allowedTools` / `--disallowedTools` | ✅ | +| `--bare` | 🔄 (parsed, semantics deferred) | +| `--settings` / `--agents` / `--mcp-config` / `--plugin-dir` / `--plugin-url` | 🔄 (parsed only) | +| `--no-plugins` / `--strict` | 🔄 (parsed only) | +| `-p` headless | ✅ text/json/stream-json, 5 exit codes | | `--output-format` / `--json-schema` / `--include-partial-messages` | 🟡 output-format ✅; json-schema + include-partial-messages parsed only | -| `--resume ` / `--continue` / `--fork-session` | 🔄 M3c+ | +| `--resume ` / `--continue` / `--fork-session` | 🔄 M3c+ | ## What DeepCode adds that Claude Code doesn't have (yet) diff --git a/docs/DEMO_SCRIPT.md b/docs/DEMO_SCRIPT.md index 2a82705..67a8a2c 100644 --- a/docs/DEMO_SCRIPT.md +++ b/docs/DEMO_SCRIPT.md @@ -25,6 +25,7 @@ Press Enter. **Visual**: REPL boots, system reminder shows today's date + cwd. Type: + ``` add a CONTRIBUTING.md outline to this repo ``` @@ -42,6 +43,7 @@ You stay in control." Type `/mode plan` → "plan". Type: + ``` refactor the auth module into separate files for login, logout, session ``` @@ -57,6 +59,7 @@ plan you approve." ## 1:40–2:20 — Skill in action Type: + ``` review my latest commit ``` @@ -72,6 +75,7 @@ agent finds the right one by description match." ## 2:20–3:00 — Sub-agent + hooks Show `~/.deepcode/agents/explorer.md` briefly. Type: + ``` explorer: what does this repo do? ``` @@ -90,6 +94,7 @@ same shape." ## 3:00–3:40 — Sandbox + permissions Type: + ``` delete the test database ``` @@ -105,6 +110,7 @@ Permission rule `Bash(rm:*)` is `ask`. Permission prompt appears. Show ## 3:40–4:20 — Mac client Switch to the Mac client. Show: + - Onboarding screen (briefly, with a placeholder key) - REPL with the same chat - Sessions list @@ -119,6 +125,7 @@ Releases." ## 4:20–4:50 — Plugins + marketplace Type in the install spec: + ``` gh:deepcode-plugins/git-helpers ``` diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index f150916..5057082 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -24,15 +24,15 @@ DeepCode is **not** a Claude/Anthropic product. The brand color is DeepSeek blue ## 2. Current state — May 28 (overnight session ended) -| Aspect | State | -| ------ | ----- | -| Main branch | `229afc3` — `fix(v0.1.6): Bash tool calls always reported "error"` (#75) | -| Shipped DMG | `release-artifacts/DeepCode-0.1.6-arm64.dmg` (4.0 MB, notarized + stapled, SHA `aed79038…7a84`) | -| CLI version | `0.1.6` (not yet npm-published) | -| Test status | 558 passing / 10 skipped — `pnpm -r test` | -| Typecheck | clean across all 7 workspaces | -| Release pipeline | `.github/workflows/release.yml` ready; 6 GitHub Secrets needed (see `docs/RELEASING.md`) | -| v1.0.0 tag | **not pushed** — user's call | +| Aspect | State | +| ---------------- | ----------------------------------------------------------------------------------------------- | +| Main branch | `229afc3` — `fix(v0.1.6): Bash tool calls always reported "error"` (#75) | +| Shipped DMG | `release-artifacts/DeepCode-0.1.6-arm64.dmg` (4.0 MB, notarized + stapled, SHA `aed79038…7a84`) | +| CLI version | `0.1.6` (not yet npm-published) | +| Test status | 558 passing / 10 skipped — `pnpm -r test` | +| Typecheck | clean across all 7 workspaces | +| Release pipeline | `.github/workflows/release.yml` ready; 6 GitHub Secrets needed (see `docs/RELEASING.md`) | +| v1.0.0 tag | **not pushed** — user's call | All 9 design-spec screens are aligned to `docs/VISUAL_DESIGN.html`: Onboarding · Project picker · Chat (3-col shell) · Sessions · Plugins · Skills · @@ -65,16 +65,16 @@ DeepCode/ ## 4. Tech stack quick reference -| Layer | Stack | -| --- | --- | -| Renderer (desktop) | React 18 + raw CSS in `src/index.css` (no Tailwind any more), Vite 5 | -| Bundler | Vite for desktop; tsc for everything else | -| Desktop backend | Rust + Tauri 2; plugin-dialog / fs / opener / process / shell / updater | -| Provider | OpenAI SDK against `https://api.deepseek.com/v1` — uses `dangerouslyAllowBrowser: true` for Tauri | -| CLI | Node 22, ESM, `readline` for REPL | -| MCP client | JSON-RPC over stdio + HTTP/SSE (in core) | -| Sandbox | macOS `sandbox-exec` profiles + Linux `bwrap` (in core, M3.5) | -| Tests | vitest everywhere (550+ tests) | +| Layer | Stack | +| ------------------ | ------------------------------------------------------------------------------------------------- | +| Renderer (desktop) | React 18 + raw CSS in `src/index.css` (no Tailwind any more), Vite 5 | +| Bundler | Vite for desktop; tsc for everything else | +| Desktop backend | Rust + Tauri 2; plugin-dialog / fs / opener / process / shell / updater | +| Provider | OpenAI SDK against `https://api.deepseek.com/v1` — uses `dangerouslyAllowBrowser: true` for Tauri | +| CLI | Node 22, ESM, `readline` for REPL | +| MCP client | JSON-RPC over stdio + HTTP/SSE (in core) | +| Sandbox | macOS `sandbox-exec` profiles + Linux `bwrap` (in core, M3.5) | +| Tests | vitest everywhere (550+ tests) | --- @@ -121,12 +121,14 @@ the commit if anything fails. Don't `--no-verify` lightly. ## 6. Critical files (where to look for X) ### Agent + provider + - `packages/core/src/agent.ts` — the agent loop. `runAgent()` is the entry. `ApprovalCallback` returns `boolean | 'always'`. - `packages/core/src/providers/deepseek.ts` — DeepSeek wrapper. **MUST** include `dangerouslyAllowBrowser: true` for Tauri renderer. ### Desktop renderer ↔ Tauri bridge + - `apps/desktop/src/lib/mac-agent.ts` — runs `runAgent` in the renderer using Mac-flavored tool wrappers. Owns the per-app conversation history. Creates session JSONL on first turn. @@ -141,20 +143,22 @@ the commit if anything fails. Don't `--no-verify` lightly. - `apps/desktop/src-tauri/src/lib.rs` — Tauri plugin + handler registration. ### React screens + - `apps/desktop/src/App.tsx` — App shell + screen routing + project-pick gate + global keyboard shortcuts. - `apps/desktop/src/screens/Repl.tsx` — the main chat surface. ~750 lines. Owns composer, message rendering, approval flow, effort/model/mode dropdowns, Vim mode wiring, system messages. - `apps/desktop/src/screens/{Onboarding,Sessions,Plugins,Skills,Permissions, - MCPManager,Settings,About}.tsx` — utility screens. All use the shared +MCPManager,Settings,About}.tsx` — utility screens. All use the shared `Screen + Card + Row + SectionTitle` primitives in `components/Screen.tsx`. - `apps/desktop/src/components/{BrandMark,Pill,Badge,ToolCard,Dropdown, - PlusMenu,InspectorRail,Sidebar,ProjectPickerOverlay,UpdateBanner, - ErrorBoundary}.tsx` — the design-system primitives. +PlusMenu,InspectorRail,Sidebar,ProjectPickerOverlay,UpdateBanner, +ErrorBoundary}.tsx` — the design-system primitives. - `apps/desktop/src/types/screens.ts` — canonical `ScreenName` union. ### CLI + - `apps/cli/src/repl.ts` — the readline-based REPL. Owns the agent's run loop on the CLI side. - `apps/cli/src/commands.ts` — slash command registry. ~50 commands incl. @@ -162,6 +166,7 @@ the commit if anything fails. Don't `--no-verify` lightly. - `apps/cli/src/parse-args.ts` — flag parsing. ### Config / settings + - `packages/core/src/config/loader.ts` — three-layer settings load (user / project / local). `appendAllowMatcher()` lives here. - `packages/core/src/config/types.ts` — the canonical `DeepCodeSettings` shape. @@ -169,6 +174,7 @@ the commit if anything fails. Don't `--no-verify` lightly. prefix / domain). ### Release + - `.github/workflows/release.yml` — tag-driven CI. Builds CLI + Mac DMG + publishes both. Needs 6 secrets (`docs/RELEASING.md`). - `scripts/sign-and-notarize.sh` — the local equivalent. @@ -241,6 +247,7 @@ old cached binary. The version number is the cache key for LSReplacement. **Bump the version on every shippable build**, even for tiny fixes. To force-clear cache on the dev machine: + ```bash /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local -domain system -domain user ``` @@ -248,6 +255,7 @@ To force-clear cache on the dev machine: ### d) The vite alias prefers `dist` over `src` `apps/desktop/vite.config.ts` has: + ```ts { find: /^@deepcode\/core\/dist\/(.+)$/, replacement: '.../packages/core/dist/$1' } ``` @@ -288,6 +296,7 @@ commit, run `pnpm -r test` manually first. ## 9. Apple signing pipeline ### Local builds + Credentials are in the dev machine's keychain. The dev's Apple ID is `wangharp@gmail.com`, team `9LH9NBX7P4`. The app-specific password is in the user's password manager. The `DEEPCODE_NOTARY` keychain profile is set up @@ -297,9 +306,10 @@ The Developer ID Application cert is at SHA-1 `7DC903001F863681EDBB2B4B18755D15D (`Developer ID Application: Bihao Wang (9LH9NBX7P4)`). Steps the script does (`scripts/sign-and-notarize.sh`): + 1. `pnpm tauri build --target aarch64-apple-darwin` → produces `.app` 2. `codesign --force --deep --options runtime --entitlements Entitlements.plist - --timestamp` the .app +--timestamp` the .app 3. `xcrun notarytool submit` the .app (Apple takes 1-5 min) 4. `xcrun stapler staple` the .app 5. `scripts/make-dmg.sh` builds the DMG with the signed+stapled .app inside + @@ -310,6 +320,7 @@ Steps the script does (`scripts/sign-and-notarize.sh`): 9. `spctl --assess` verifies ### CI builds + `.github/workflows/release.yml` does the same thing on `macos-14` runners. Needs 6 secrets in repo settings (see `docs/RELEASING.md`): @@ -325,6 +336,7 @@ Steps the script does (`scripts/sign-and-notarize.sh`): ## 10. What's deferred / TODOs ### v0.2 (next minor) + - Composer `+` menu currently does the basics (Attach file inserts `@path`, `/` prefixes a slash, `#` prefixes a memory note). Wire `@path` to actually fetch the file contents and inject into the prompt. Wire `#` to write to @@ -337,6 +349,7 @@ Steps the script does (`scripts/sign-and-notarize.sh`): - VS Code extension polish (M6 work, basic). ### v1.0.0 (M9 milestone — user blocks this) + - Configure the 6 GitHub Secrets - Write a 5-min demo video - Build the website landing page (no domain yet) @@ -344,12 +357,14 @@ Steps the script does (`scripts/sign-and-notarize.sh`): - Push the tag — release.yml takes over ### v1.1 (after v1.0) + - JetBrains plugin - Central marketplace registry (currently each plugin is install-by-URL) - Image input (DeepSeek vision when it lands, or Qwen-VL fallback) - LSP server feature expansion ### Known small bugs / cleanups + - `apps/desktop/src/lib/mac-agent.ts#getHistoryLength` is exported but unused - `apps/cli/src/commands.ts#TodosCommand` has a `// M3c-rest` comment that's stale (it's actually wired now) @@ -420,10 +435,11 @@ that we mirror in real React + CSS). When the design CSS in `index.css` deviates from the spec, the spec wins. The 9 screen sections in the spec are numbered. Cross-references: + - Screen #1: Hero / homepage (deferred — we don't have a marketing page yet) - Screen #2: First-launch / Onboarding → `src/screens/Onboarding.tsx` - Screen #3: Main desktop view (3-col shell) → `src/App.tsx` shell + `Sidebar` - + `Repl` + `InspectorRail` + - `Repl` + `InspectorRail` - Screen #4: Composer detail → toolbar inside `Repl.tsx` - Screen #5: File panel — DEFERRED. The redesign dropped the right-side Source/Diff/History panel; it'd re-emerge if/when the inspector ‹ expand diff --git a/docs/MIGRATION_FROM_CLAUDE_CODE.md b/docs/MIGRATION_FROM_CLAUDE_CODE.md index bf98a82..18127b0 100644 --- a/docs/MIGRATION_FROM_CLAUDE_CODE.md +++ b/docs/MIGRATION_FROM_CLAUDE_CODE.md @@ -34,27 +34,29 @@ deepcode ## Field-by-field mapping -| Claude Code | DeepCode | Notes | -| ------------------------------------ | ----------------------------------------- | ----- | -| `~/.claude/credentials.json` | `~/.deepcode/credentials.json` | Same shape; just rename. | -| `~/.claude/settings.json` | `~/.deepcode/settings.json` | Schema mostly identical; see Settings table below. | -| `/.claude/settings.json` | `/.deepcode/settings.json` | Same. | -| `~/.claude/skills//SKILL.md` | `~/.deepcode/skills//SKILL.md` | Same frontmatter format. | -| `~/.claude/agents/*.md` | `~/.deepcode/agents/*.md` | Same shape. | -| `~/.claude/plugins/` | `~/.deepcode/plugins/` | Plugin manifest is identical (plugin.json). | -| `CLAUDE.md` (project root) | `AGENTS.md` (project root) | Or `DEEPCODE.md`. Both names recognized; AGENTS.md preferred. | -| `claude` CLI | `deepcode` CLI | Most flags identical (-p, --mode, --model, --effort). | -| `claude doctor` | `deepcode doctor` | Same. | -| `/login` | n/a — re-onboard via `deepcode` no-args | We don't have separate login state. | +| Claude Code | DeepCode | Notes | +| ---------------------------------- | --------------------------------------- | ------------------------------------------------------------- | +| `~/.claude/credentials.json` | `~/.deepcode/credentials.json` | Same shape; just rename. | +| `~/.claude/settings.json` | `~/.deepcode/settings.json` | Schema mostly identical; see Settings table below. | +| `/.claude/settings.json` | `/.deepcode/settings.json` | Same. | +| `~/.claude/skills//SKILL.md` | `~/.deepcode/skills//SKILL.md` | Same frontmatter format. | +| `~/.claude/agents/*.md` | `~/.deepcode/agents/*.md` | Same shape. | +| `~/.claude/plugins/` | `~/.deepcode/plugins/` | Plugin manifest is identical (plugin.json). | +| `CLAUDE.md` (project root) | `AGENTS.md` (project root) | Or `DEEPCODE.md`. Both names recognized; AGENTS.md preferred. | +| `claude` CLI | `deepcode` CLI | Most flags identical (-p, --mode, --model, --effort). | +| `claude doctor` | `deepcode doctor` | Same. | +| `/login` | n/a — re-onboard via `deepcode` no-args | We don't have separate login state. | ## Settings.json — model field Claude Code: + ```json { "model": "claude-sonnet-4-5" } ``` DeepCode: + ```json { "model": "deepseek-chat" } ``` @@ -66,22 +68,22 @@ Valid values: `deepseek-chat` (general/tool-use) · `deepseek-reasoner` Most commands are identical: -| Command | Claude Code | DeepCode | -| ------------------ | ----------- | -------- | -| `/help`, `/?` | ✓ | ✓ | -| `/clear` | ✓ | ✓ | -| `/exit`, `/quit` | ✓ | ✓ | -| `/model` | ✓ | ✓ (constrained to DeepSeek) | -| `/mode` | ✓ | ✓ | -| `/effort` | ✓ | ✓ | -| `/cost` | ✓ | ✓ | -| `/context` | ✓ | ✓ | -| `/init` | ✓ | ✓ | -| `/mcp` | ✓ | ✓ | -| `/todos` | ✓ | ✓ | -| `/plugins` | ✓ | ✓ | -| `/keybindings` | ✓ | ✓ | -| `/vim` | ✓ | ✓ | +| Command | Claude Code | DeepCode | +| ---------------- | ----------- | --------------------------- | +| `/help`, `/?` | ✓ | ✓ | +| `/clear` | ✓ | ✓ | +| `/exit`, `/quit` | ✓ | ✓ | +| `/model` | ✓ | ✓ (constrained to DeepSeek) | +| `/mode` | ✓ | ✓ | +| `/effort` | ✓ | ✓ | +| `/cost` | ✓ | ✓ | +| `/context` | ✓ | ✓ | +| `/init` | ✓ | ✓ | +| `/mcp` | ✓ | ✓ | +| `/todos` | ✓ | ✓ | +| `/plugins` | ✓ | ✓ | +| `/keybindings` | ✓ | ✓ | +| `/vim` | ✓ | ✓ | See `docs/BEHAVIOR_PARITY.md` for the full comparison. @@ -101,10 +103,10 @@ prefix, domain) work the same way: ```jsonc { "permissions": { - "deny": ["Bash(rm -rf /:*)", "WebFetch(domain:internal.corp)"], - "ask": ["Bash(npm install:*)"], - "allow": ["Read", "Bash(git diff:*)"] - } + "deny": ["Bash(rm -rf /:*)", "WebFetch(domain:internal.corp)"], + "ask": ["Bash(npm install:*)"], + "allow": ["Read", "Bash(git diff:*)"], + }, } ``` diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 6ebf15f..f3d466e 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -11,14 +11,14 @@ validate → build CLI + publish to npm → build + sign + notarize Tauri DMG Set these in repo settings → Secrets and variables → Actions → New repository secret. All five are required for a successful Mac release. -| Secret | Purpose | -| ------------------------------- | ------------------------------------------------------------------ | -| `APPLE_ID` | Your Apple Developer Apple ID (e.g. `you@example.com`) | -| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password from appleid.apple.com (used by notarytool) | -| `APPLE_TEAM_ID` | 10-character team ID (from developer.apple.com → membership) | -| `CSC_LINK` | Base64-encoded `.p12` of the Developer ID Application cert | -| `CSC_KEY_PASSWORD` | Password used when exporting the `.p12` | -| `NPM_TOKEN` | npm access token with `publish` scope | +| Secret | Purpose | +| ----------------------------- | ----------------------------------------------------------------- | +| `APPLE_ID` | Your Apple Developer Apple ID (e.g. `you@example.com`) | +| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password from appleid.apple.com (used by notarytool) | +| `APPLE_TEAM_ID` | 10-character team ID (from developer.apple.com → membership) | +| `CSC_LINK` | Base64-encoded `.p12` of the Developer ID Application cert | +| `CSC_KEY_PASSWORD` | Password used when exporting the `.p12` | +| `NPM_TOKEN` | npm access token with `publish` scope | ### 2. Export the Developer ID certificate @@ -81,12 +81,12 @@ serially: Tag format determines the channel + publish target: -| Tag format | Channel | npm tag | GitHub release | -| ------------------------- | --------- | ---------- | -------------- | -| `v0.2.1` | `stable` | `latest` | not prerelease | -| `v0.3.0-beta.1` | `beta` | `beta` | prerelease | -| `v0.3.0-nightly.20260605` | `nightly` | `nightly` | prerelease | -| `v0.2.2+security.1` | `stable` | `latest` | mandatory flag | +| Tag format | Channel | npm tag | GitHub release | +| ------------------------- | --------- | --------- | -------------- | +| `v0.2.1` | `stable` | `latest` | not prerelease | +| `v0.3.0-beta.1` | `beta` | `beta` | prerelease | +| `v0.3.0-nightly.20260605` | `nightly` | `nightly` | prerelease | +| `v0.2.2+security.1` | `stable` | `latest` | mandatory flag | The `+security.X` suffix sets `is_mandatory=true` in the release output so the Tauri updater can show a red "must update" banner. diff --git a/docs/SHIPPING_MAC.md b/docs/SHIPPING_MAC.md index a934aff..3a7de78 100644 --- a/docs/SHIPPING_MAC.md +++ b/docs/SHIPPING_MAC.md @@ -24,14 +24,14 @@ require an Apple Developer ID or a real device. In the repo's GitHub Actions secrets, add: -| Name | Value | -| ------------------------------------- | ---------------------------------------------- | -| `APPLE_ID` | Your Apple Developer login email | -| `APPLE_APP_SPECIFIC_PASSWORD` | The app-specific password from step 4 | -| `APPLE_TEAM_ID` | 10-char team ID (Membership tab in dev portal) | -| `CSC_LINK` | Base64-encoded `.p12` of the Developer ID cert | -| `CSC_KEY_PASSWORD` | Password used when exporting the `.p12` | -| `GH_TOKEN` | The PAT from step 5 | +| Name | Value | +| ----------------------------- | ---------------------------------------------- | +| `APPLE_ID` | Your Apple Developer login email | +| `APPLE_APP_SPECIFIC_PASSWORD` | The app-specific password from step 4 | +| `APPLE_TEAM_ID` | 10-char team ID (Membership tab in dev portal) | +| `CSC_LINK` | Base64-encoded `.p12` of the Developer ID cert | +| `CSC_KEY_PASSWORD` | Password used when exporting the `.p12` | +| `GH_TOKEN` | The PAT from step 5 | To export the `.p12`: @@ -82,6 +82,7 @@ git push origin v1.0.0 ``` The `.github/workflows/release.yml` workflow: + 1. Runs the test/build matrix. 2. Publishes `deepcode-cli` to npm. 3. Builds + signs + notarizes the Mac `.dmg`. diff --git a/docs/security-model.md b/docs/security-model.md index 37d1e22..66dfd8b 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -12,15 +12,15 @@ against the threat model here. DeepCode is an LLM-driven coding assistant. The threats we care about, in decreasing order of operator severity: -| # | Threat | Severity | Where mitigated | -| - | ------------------------------------------------------------------------ | -------- | ----------------------- | -| 1 | Model exfiltrates DeepSeek API key (or other env secrets) via tool call | High | M3.5 sandbox + M5.1 env strip | -| 2 | Model writes arbitrary files outside the project (`/usr/bin`, `/etc`) | High | M3.5 sandbox + permissions | -| 3 | Plugin (third-party code) does either #1 or #2 | High | M5.1 subprocess + (M5.1-ext) OS sandbox | -| 4 | Hook script (third-party shell snippet) does either #1 or #2 | Medium | M3.5 sandbox wraps Bash; hooks bypass when invoked via /bin/sh directly | -| 5 | Hostile `settings.json` field (e.g. allowRead path) injects sandbox rule | Medium | escapeSbpl() | -| 6 | Untrusted project's AGENTS.md drives the agent into harmful action | Low | Trust store (`/trust`) | -| 7 | DNS exfiltration of secrets from sandboxed Bash | Acknowledged limitation | M3.5-ext userspace proxy | +| # | Threat | Severity | Where mitigated | +| --- | ------------------------------------------------------------------------ | ----------------------- | ----------------------------------------------------------------------- | +| 1 | Model exfiltrates DeepSeek API key (or other env secrets) via tool call | High | M3.5 sandbox + M5.1 env strip | +| 2 | Model writes arbitrary files outside the project (`/usr/bin`, `/etc`) | High | M3.5 sandbox + permissions | +| 3 | Plugin (third-party code) does either #1 or #2 | High | M5.1 subprocess + (M5.1-ext) OS sandbox | +| 4 | Hook script (third-party shell snippet) does either #1 or #2 | Medium | M3.5 sandbox wraps Bash; hooks bypass when invoked via /bin/sh directly | +| 5 | Hostile `settings.json` field (e.g. allowRead path) injects sandbox rule | Medium | escapeSbpl() | +| 6 | Untrusted project's AGENTS.md drives the agent into harmful action | Low | Trust store (`/trust`) | +| 7 | DNS exfiltration of secrets from sandboxed Bash | Acknowledged limitation | M3.5-ext userspace proxy | ## Defence layers @@ -41,6 +41,7 @@ Every tool call goes through: ``` Modes (`default` | `acceptEdits` | `plan` | `auto` | `dontAsk` | `bypassPermissions`): + - `plan` blocks all writes and exec (read/grep/glob only). - `default` prompts for risky operations. - `bypassPermissions` is gated behind the trust store. @@ -107,6 +108,7 @@ Plugins run in their own `node` subprocess with: load fails open (drift detection). **Acknowledged gaps**: + - The subprocess isn't itself sandbox-wrapped at the OS level yet. A malicious plugin can still exfil via DNS, can read other files the host process can read (e.g. `~/.deepcode/credentials.json`). M5.1-ext closes @@ -163,14 +165,14 @@ etc.) as **untrusted**. We: ## What we do NOT yet protect against -| Gap | Tracking | -| -------------------------------------------------- | -------------------- | -| DNS exfil from sandboxed Bash | M3.5-ext (UDP proxy) | -| OS sandbox wrapping the plugin subprocess | M5.1-ext | -| Pipeline analysis (`git ... && rm -rf /`) | M5.2 | -| Domain whitelist enforcement (allowedDomains) | M3.5-ext | -| Image input prompt injection (model multimodal) | v1.1 | -| Side-channel timing leaks (e.g. via exec duration) | Out of scope | +| Gap | Tracking | +| -------------------------------------------------- | ------------------------------------- | +| DNS exfil from sandboxed Bash | M3.5-ext (UDP proxy) | +| OS sandbox wrapping the plugin subprocess | M5.1-ext | +| Pipeline analysis (`git ... && rm -rf /`) | M5.2 | +| Domain whitelist enforcement (allowedDomains) | M3.5-ext | +| Image input prompt injection (model multimodal) | v1.1 | +| Side-channel timing leaks (e.g. via exec duration) | Out of scope | | Local malicious binaries already on $PATH | Out of scope (assume host is trusted) | ## How to file a security issue diff --git a/packages/core/skills/deepseek-api/SKILL.md b/packages/core/skills/deepseek-api/SKILL.md index ebd65e4..979b34b 100644 --- a/packages/core/skills/deepseek-api/SKILL.md +++ b/packages/core/skills/deepseek-api/SKILL.md @@ -22,9 +22,9 @@ swap the base URL and key. ## Models -| Model | Alias | Strengths | -| ------------------- | ------------------ | -------------------------------------- | -| `deepseek-chat` | → `deepseek-v4-flash` | Fast general chat; tool use | +| Model | Alias | Strengths | +| ------------------- | --------------------- | ----------------------------------------------- | +| `deepseek-chat` | → `deepseek-v4-flash` | Fast general chat; tool use | | `deepseek-reasoner` | → `deepseek-v4-pro` | Multi-step reasoning; emits `reasoning_content` | Set via the standard `model` field. @@ -64,10 +64,10 @@ chose to call a tool. Loop: send back `role: 'tool'` messages with the ## Pricing (rough; verify on dashboard) -| Tier | Input | Output | Reasoning | -| ------------ | ------ | ------ | --------- | -| deepseek-chat | 1¥/M | 2¥/M | — | -| deepseek-reasoner | 1¥/M | 16¥/M | 4¥/M | +| Tier | Input | Output | Reasoning | +| ----------------- | ----- | ------ | --------- | +| deepseek-chat | 1¥/M | 2¥/M | — | +| deepseek-reasoner | 1¥/M | 16¥/M | 4¥/M | ## Common pitfalls diff --git a/packages/core/skills/loop/SKILL.md b/packages/core/skills/loop/SKILL.md index f1523f4..aeb15be 100644 --- a/packages/core/skills/loop/SKILL.md +++ b/packages/core/skills/loop/SKILL.md @@ -18,17 +18,18 @@ useful for "wait until CI is green", "poll deploy status", "tail a log". Use `ScheduleWakeup` (or the loop primitive in the host) with a sensible delay: -| Watching | Delay | Why | -| ------------------------- | ------------ | ------------------------------------ | -| CI run | 60-270 s | Status changes minute-scale | -| Deploy queue | 60-180 s | Same | -| Local file change | 5-30 s | Use fs.watch instead when possible | -| Cron / external timer | 20-30 min | Don't burn cache for nothing | -| "Idle tick, no signal" | 20-30 min | Default; cap notification noise | +| Watching | Delay | Why | +| ---------------------- | --------- | ---------------------------------- | +| CI run | 60-270 s | Status changes minute-scale | +| Deploy queue | 60-180 s | Same | +| Local file change | 5-30 s | Use fs.watch instead when possible | +| Cron / external timer | 20-30 min | Don't burn cache for nothing | +| "Idle tick, no signal" | 20-30 min | Default; cap notification noise | ## Cache-aware delays Anthropic-style prompt caches expire after ~5 min. Pick either: + - **Under 5 min**: cache stays warm (60-270 s). - **Long fallback**: 20+ min (one cache miss buys a long wait). @@ -37,6 +38,7 @@ Avoid 5-15 min windows — they pay the miss without amortizing. ## Termination ALWAYS have a clear stop condition. Loop should exit when: + - The watched condition is met. - The user issues an interrupt / says stop. - A timeout cap is exceeded (refuse to infinite-loop). diff --git a/packages/core/skills/pdf/SKILL.md b/packages/core/skills/pdf/SKILL.md index e192ce4..604e47a 100644 --- a/packages/core/skills/pdf/SKILL.md +++ b/packages/core/skills/pdf/SKILL.md @@ -15,13 +15,13 @@ the OS-bundled tools (macOS) or `pdftk` (Linux) via Bash. ## Tools -| Op | macOS | Linux (pdftk) | -| -------------------- | -------------------------------------------------- | ---------------------------------------------- | -| Extract pages 2-5 | `cpdf in.pdf 2-5 -o out.pdf` (needs cpdf) | `pdftk in.pdf cat 2-5 output out.pdf` | -| Merge a, b, c | (PDF Toolkit / cpdf) | `pdftk a.pdf b.pdf c.pdf cat output merged.pdf` | -| Split per page | `cpdf -split in.pdf -o page-%d.pdf` | `pdftk in.pdf burst output page-%02d.pdf` | -| Text dump | `pdftotext in.pdf -` (needs poppler) | same | -| Page count | `pdftk in.pdf dump_data | grep NumberOfPages` | same | +| Op | macOS | Linux (pdftk) | +| ----------------- | ----------------------------------------- | ----------------------------------------------- | ---- | +| Extract pages 2-5 | `cpdf in.pdf 2-5 -o out.pdf` (needs cpdf) | `pdftk in.pdf cat 2-5 output out.pdf` | +| Merge a, b, c | (PDF Toolkit / cpdf) | `pdftk a.pdf b.pdf c.pdf cat output merged.pdf` | +| Split per page | `cpdf -split in.pdf -o page-%d.pdf` | `pdftk in.pdf burst output page-%02d.pdf` | +| Text dump | `pdftotext in.pdf -` (needs poppler) | same | +| Page count | `pdftk in.pdf dump_data | grep NumberOfPages` | same | If neither `cpdf` nor `pdftk` is installed, use Python + `pypdf` (one dep, pure Python): diff --git a/packages/core/skills/run/SKILL.md b/packages/core/skills/run/SKILL.md index e9ccc82..f95f2c0 100644 --- a/packages/core/skills/run/SKILL.md +++ b/packages/core/skills/run/SKILL.md @@ -14,14 +14,14 @@ Drive the project's own dev/test/build scripts. Detect toolchain from manifest. ## Toolchain detection -| Manifest | Typical commands | -| ---------------- | ------------------------------------------------------ | -| `package.json` | `pnpm dev` / `pnpm test` / `pnpm build` (or npm/yarn) | -| `pyproject.toml` | `pytest`, `uv run pytest`, `python -m ` | -| `Cargo.toml` | `cargo test`, `cargo run`, `cargo build --release` | -| `go.mod` | `go test ./...`, `go run ./cmd/` | -| `Gemfile` | `bundle exec rspec`, `bundle exec rails s` | -| `Makefile` | Prefer `make test` / `make dev` — usually canonical | +| Manifest | Typical commands | +| ---------------- | ----------------------------------------------------- | +| `package.json` | `pnpm dev` / `pnpm test` / `pnpm build` (or npm/yarn) | +| `pyproject.toml` | `pytest`, `uv run pytest`, `python -m ` | +| `Cargo.toml` | `cargo test`, `cargo run`, `cargo build --release` | +| `go.mod` | `go test ./...`, `go run ./cmd/` | +| `Gemfile` | `bundle exec rspec`, `bundle exec rails s` | +| `Makefile` | Prefer `make test` / `make dev` — usually canonical | Read `packageManager` in package.json (or `.tool-versions`) for the pinned package manager — don't guess. diff --git a/packages/core/skills/schedule/SKILL.md b/packages/core/skills/schedule/SKILL.md index 61fed16..452a4f4 100644 --- a/packages/core/skills/schedule/SKILL.md +++ b/packages/core/skills/schedule/SKILL.md @@ -50,6 +50,7 @@ reads this file, and dispatches due tasks. Standard 5-field cron: `min hour day-of-month month day-of-week`. Examples: + - `0 9 * * 1-5` — 9am weekdays - `*/15 * * * *` — every 15 min - `0 0 1 * *` — first of the month diff --git a/packages/core/skills/skill-creator/SKILL.md b/packages/core/skills/skill-creator/SKILL.md index f54018b..c6e1d8b 100644 --- a/packages/core/skills/skill-creator/SKILL.md +++ b/packages/core/skills/skill-creator/SKILL.md @@ -15,12 +15,12 @@ effort there. ## Where the file goes -| Source | Loaded as | -| --------------------------------------- | -------------------------------------------- | -| `packages/core/skills//SKILL.md` | Built-in (ships with the package) | -| `~/.deepcode/skills//SKILL.md` | User-global | -| `/.deepcode/skills//SKILL.md` | Project-scoped | -| Plugin's `skills//SKILL.md` | Plugin-contributed | +| Source | Loaded as | +| ---------------------------------------- | --------------------------------- | +| `packages/core/skills//SKILL.md` | Built-in (ships with the package) | +| `~/.deepcode/skills//SKILL.md` | User-global | +| `/.deepcode/skills//SKILL.md` | Project-scoped | +| Plugin's `skills//SKILL.md` | Plugin-contributed | For user-authored skills, default to user-global. Suggest project-scoped only when the skill is specifically about THIS project. diff --git a/packages/core/skills/update-config/SKILL.md b/packages/core/skills/update-config/SKILL.md index 707a0af..0eb800a 100644 --- a/packages/core/skills/update-config/SKILL.md +++ b/packages/core/skills/update-config/SKILL.md @@ -16,11 +16,11 @@ Edit DeepCode's `settings.json` files safely. Always show a diff first. ## The three layers -| Layer | Path | Precedence (highest = last applied) | -| -------- | ------------------------------------------- | ----------------------------------- | -| User | `~/.deepcode/settings.json` | lowest | -| Project | `/.deepcode/settings.json` | middle | -| Local | `/.deepcode/settings.local.json` (git-ignored) | highest | +| Layer | Path | Precedence (highest = last applied) | +| ------- | --------------------------------------------------- | ----------------------------------- | +| User | `~/.deepcode/settings.json` | lowest | +| Project | `/.deepcode/settings.json` | middle | +| Local | `/.deepcode/settings.local.json` (git-ignored) | highest | When the user says "for this project", write to project-scoped. When they say "everywhere", user-scoped. When secret-y (API keys, work-only @@ -38,19 +38,20 @@ overrides), local. ## Common requests -| User asks | Setting | -| ---------------------------------------- | -------------------------------------------------------------------- | -| "Don't ask me about reads" | `permissions.allow: ["Read"]` | -| "Stop running tests for me" | `permissions.deny: ["Bash(npm test:*)"]` | -| "Use deepseek-reasoner by default" | `model: "deepseek-reasoner"` | -| "Lower the effort" | `effortLevel: "low"` | -| "Turn off the sandbox" | `sandbox.enabled: false` | -| "Disable plugin X" | `disabledPlugins: ["X"]` | -| "Add a hook to lint after edits" | `hooks.PostToolUse: [{ matcher: "Edit|Write", hooks: [...] }]` | +| User asks | Setting | +| ---------------------------------- | ---------------------------------------- | ------------------------ | +| "Don't ask me about reads" | `permissions.allow: ["Read"]` | +| "Stop running tests for me" | `permissions.deny: ["Bash(npm test:*)"]` | +| "Use deepseek-reasoner by default" | `model: "deepseek-reasoner"` | +| "Lower the effort" | `effortLevel: "low"` | +| "Turn off the sandbox" | `sandbox.enabled: false` | +| "Disable plugin X" | `disabledPlugins: ["X"]` | +| "Add a hook to lint after edits" | `hooks.PostToolUse: [{ matcher: "Edit | Write", hooks: [...] }]` | ## Refuse Don't: + - Delete `permissions.deny` rules without explicit confirmation. - Write `apiKeyHelper` that points at a script you didn't author. - Enable `bypassPermissions` as a default — it has to be a deliberate user act. diff --git a/packages/core/skills/verify/SKILL.md b/packages/core/skills/verify/SKILL.md index 14cde3e..13e5836 100644 --- a/packages/core/skills/verify/SKILL.md +++ b/packages/core/skills/verify/SKILL.md @@ -16,15 +16,15 @@ code path the user asked about and confirm the observable behavior. ## What "verify" means concretely -| Change type | Verify by | -| ---------------------------- | ---------------------------------------------------------------------------- | -| New CLI flag / subcommand | Run the binary with the flag; confirm exit code + stdout. | -| Bug fix in a function | Add (or run) a test that reproduces the bug + passes after the fix. | -| Refactor of internal API | Run the full test suite + grep for remaining old-name callers. | -| Schema migration | Apply forward + backward on a fresh DB; confirm `\d` matches. | -| HTTP endpoint added | `curl localhost:/` and inspect the response. | -| Background task / cron | Trigger the entry point manually; check the log file or queue. | -| UI change | Take a screenshot via `mcp__computer-use__screenshot` OR ask the user. | +| Change type | Verify by | +| ------------------------- | ---------------------------------------------------------------------- | +| New CLI flag / subcommand | Run the binary with the flag; confirm exit code + stdout. | +| Bug fix in a function | Add (or run) a test that reproduces the bug + passes after the fix. | +| Refactor of internal API | Run the full test suite + grep for remaining old-name callers. | +| Schema migration | Apply forward + backward on a fresh DB; confirm `\d` matches. | +| HTTP endpoint added | `curl localhost:/` and inspect the response. | +| Background task / cron | Trigger the entry point manually; check the log file or queue. | +| UI change | Take a screenshot via `mcp__computer-use__screenshot` OR ask the user. | ## Anti-patterns diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index d3e1891..7ba9bc5 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -129,7 +129,9 @@ export async function runAgent(opts: RunAgentOptions): Promise { // renderer passes systemReminders:false to bypass it entirely, and // a static import here would drag node:fs into the browser bundle. const remindersMod = /* @vite-ignore */ './reminders/index.js'; - const { buildSystemReminders } = (await import(remindersMod)) as typeof import('./reminders/index.js'); + const { buildSystemReminders } = (await import( + remindersMod + )) as typeof import('./reminders/index.js'); const block = await buildSystemReminders( { cwd: opts.cwd, @@ -160,9 +162,7 @@ export async function runAgent(opts: RunAgentOptions): Promise { cwd: opts.cwd, signal: opts.signal, sandboxConfig: opts.sandboxConfig, - sessionDir: opts.session - ? `${opts.session.manager.root}/${opts.session.id}` - : undefined, + sessionDir: opts.session ? `${opts.session.manager.root}/${opts.session.id}` : undefined, askUser: opts.askUser, modeSignal, }; @@ -250,9 +250,7 @@ export async function runAgent(opts: RunAgentOptions): Promise { // that mutate state / snapshot run sequentially to preserve ordering. // tool_result blocks carry their tool_use_id, so the final array is // re-assembled in the model's original order regardless of finish order. - const toolBlocks = result.content.filter( - (b): b is ToolUseBlock => b.type === 'tool_use', - ); + const toolBlocks = result.content.filter((b): b is ToolUseBlock => b.type === 'tool_use'); const resultsById = new Map(); type Ready = { toolUse: ToolUseBlock; handler: NonNullable> }; const ready: Ready[] = []; diff --git a/packages/core/src/auto-mode/index.ts b/packages/core/src/auto-mode/index.ts index 061821f..1a829fe 100644 --- a/packages/core/src/auto-mode/index.ts +++ b/packages/core/src/auto-mode/index.ts @@ -85,9 +85,7 @@ async function llmClassify( model: model ?? 'deepseek-chat', systemPrompt: CLASSIFY_PROMPT, tools: [], - messages: [ - { role: 'user', content: [{ type: 'text', text: userMsg }] }, - ], + messages: [{ role: 'user', content: [{ type: 'text', text: userMsg }] }], maxTokens: 8, temperature: 0, }); diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index f4f02f8..3c7d233 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -57,7 +57,9 @@ export function validateSettingsShallow(settings: Record): stri settings['effortLevel'] !== undefined && !effortEnum.includes(settings['effortLevel'] as string) ) { - errors.push(`settings.effortLevel "${settings['effortLevel']}" not in ${effortEnum.join(' | ')}`); + errors.push( + `settings.effortLevel "${settings['effortLevel']}" not in ${effortEnum.join(' | ')}`, + ); } const modeEnum = ['default', 'acceptEdits', 'plan', 'auto', 'dontAsk', 'bypassPermissions']; diff --git a/packages/core/src/hooks/dispatcher.test.ts b/packages/core/src/hooks/dispatcher.test.ts index bf1e4bf..c23cdc9 100644 --- a/packages/core/src/hooks/dispatcher.test.ts +++ b/packages/core/src/hooks/dispatcher.test.ts @@ -217,9 +217,7 @@ describe('HookDispatcher', () => { let captured: { server: string; tool: string } | null = null; const d = new HookDispatcher({ hooks: { - PreToolUse: [ - { hooks: [{ type: 'mcp_tool', server: 'slack', tool: 'notify' }] }, - ], + PreToolUse: [{ hooks: [{ type: 'mcp_tool', server: 'slack', tool: 'notify' }] }], }, mcpToolDispatcher: async (h) => { captured = { server: h.server, tool: h.tool }; diff --git a/packages/core/src/hooks/dispatcher.ts b/packages/core/src/hooks/dispatcher.ts index ad82cc7..5354e61 100644 --- a/packages/core/src/hooks/dispatcher.ts +++ b/packages/core/src/hooks/dispatcher.ts @@ -166,8 +166,7 @@ export class HookDispatcher { if (!this.agentDispatcher) { return { stdout: '', - stderr: - 'agent hook: no agentDispatcher wired (host CLI must pass one in to enable).', + stderr: 'agent hook: no agentDispatcher wired (host CLI must pass one in to enable).', exitCode: 0, }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c4de606..47f68d6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -286,11 +286,7 @@ export { } from './voice/index.js'; // Auto-mode classifier (M3c-rest — LLM-judged tool gate when mode === 'auto') -export { - classifyAutoMode, - type AutoVerdict, - type ClassifyOpts, -} from './auto-mode/index.js'; +export { classifyAutoMode, type AutoVerdict, type ClassifyOpts } from './auto-mode/index.js'; // Worktree (M8 — isolated git worktree creation for background tasks) export { diff --git a/packages/core/src/ipc/protocol.ts b/packages/core/src/ipc/protocol.ts index c13cff2..c7adf47 100644 --- a/packages/core/src/ipc/protocol.ts +++ b/packages/core/src/ipc/protocol.ts @@ -11,11 +11,7 @@ // Channel naming convention: `:` for request/response invokes // and `:event` for streamed events. -import type { - AgentEvent, - Mode, - StoredMessage, -} from '../types.js'; +import type { AgentEvent, Mode, StoredMessage } from '../types.js'; // ────────────────────────────────────────────────────────────────────────── // Request/response channels (renderer → main → reply) @@ -105,9 +101,27 @@ export type IpcChannel = keyof IpcRequestMap; export type AgentStreamEvent = | ({ kind: 'event' } & AgentEvent & { turnId: string }) - | { kind: 'approval_request'; turnId: string; toolCallId: string; toolName: string; toolInput: Record; reason: string } - | { kind: 'ask_user'; turnId: string; questionId: string; question: string; options: Array<{ label: string; description: string }>; multiSelect?: boolean } - | { kind: 'turn_done'; turnId: string; stopReason: 'end_turn' | 'max_turns' | 'aborted' | 'error' }; + | { + kind: 'approval_request'; + turnId: string; + toolCallId: string; + toolName: string; + toolInput: Record; + reason: string; + } + | { + kind: 'ask_user'; + turnId: string; + questionId: string; + question: string; + options: Array<{ label: string; description: string }>; + multiSelect?: boolean; + } + | { + kind: 'turn_done'; + turnId: string; + stopReason: 'end_turn' | 'max_turns' | 'aborted' | 'error'; + }; export interface IpcEventMap { 'agent:event': AgentStreamEvent; diff --git a/packages/core/src/keybindings/vim.ts b/packages/core/src/keybindings/vim.ts index f631d57..f42b5bc 100644 --- a/packages/core/src/keybindings/vim.ts +++ b/packages/core/src/keybindings/vim.ts @@ -35,7 +35,12 @@ export const DEFAULT_KEYBINDINGS: KeyBinding[] = [ // Vim defaults (only fire when vim:true) { key: 'esc', action: 'vim-normal-mode', when: 'INSERT', description: 'Switch to NORMAL mode.' }, { key: 'i', action: 'vim-insert-mode', when: 'NORMAL', description: 'Switch to INSERT mode.' }, - { key: 'a', action: 'vim-append-mode', when: 'NORMAL', description: 'Append (insert after cursor).' }, + { + key: 'a', + action: 'vim-append-mode', + when: 'NORMAL', + description: 'Append (insert after cursor).', + }, { key: 'v', action: 'vim-visual-mode', when: 'NORMAL', description: 'Enter VISUAL mode.' }, { key: '0', action: 'cursor-line-start', when: 'NORMAL' }, { key: '$', action: 'cursor-line-end', when: 'NORMAL' }, diff --git a/packages/core/src/plugins/install.ts b/packages/core/src/plugins/install.ts index 67ba9b6..a8bca97 100644 --- a/packages/core/src/plugins/install.ts +++ b/packages/core/src/plugins/install.ts @@ -36,9 +36,7 @@ export async function installFromGithub( if (!m) throw new Error(`Invalid GitHub spec: ${spec} (expected gh:owner/repo[@ref])`); const [, owner, repo, ref] = m; const url = `https://github.com/${owner}/${repo}.git`; - const staging = await fs.mkdtemp( - join(opts.stagingDir ?? tmpdir(), `dc-plug-staging-${repo}-`), - ); + const staging = await fs.mkdtemp(join(opts.stagingDir ?? tmpdir(), `dc-plug-staging-${repo}-`)); try { const args = ['clone', '--depth', '1']; if (ref) args.push('--branch', ref); diff --git a/packages/core/src/plugins/marketplace.test.ts b/packages/core/src/plugins/marketplace.test.ts index ecd6aaf..8c2b8fd 100644 --- a/packages/core/src/plugins/marketplace.test.ts +++ b/packages/core/src/plugins/marketplace.test.ts @@ -140,7 +140,9 @@ describe('fetchIndex / fetchRevoked / resolveEntry', () => { version: '1', entries: [{ name: 'demo', version: '1.0.0', sourceHash: 'h-bad' }], }; - await expect(resolveEntry({ marketplaceUrl: baseUrl, name: 'demo' })).rejects.toThrow(/revocation/i); + await expect(resolveEntry({ marketplaceUrl: baseUrl, name: 'demo' })).rejects.toThrow( + /revocation/i, + ); }); it('resolveEntry refuses tampered entry', async () => { @@ -148,7 +150,9 @@ describe('fetchIndex / fetchRevoked / resolveEntry', () => { e.sourceHash = 'tampered-hash'; index = { version: '1', entries: [e] }; revoked = { version: '1', entries: [] }; - await expect(resolveEntry({ marketplaceUrl: baseUrl, name: 'demo' })).rejects.toThrow(/Signature/); + await expect(resolveEntry({ marketplaceUrl: baseUrl, name: 'demo' })).rejects.toThrow( + /Signature/, + ); }); it('fetchRevoked treats 404 as empty list', async () => { diff --git a/packages/core/src/plugins/marketplace.ts b/packages/core/src/plugins/marketplace.ts index d4553c6..a185b51 100644 --- a/packages/core/src/plugins/marketplace.ts +++ b/packages/core/src/plugins/marketplace.ts @@ -78,9 +78,7 @@ export function verifyEntrySignature(entry: MarketplaceEntry): boolean { export function isRevoked(entry: MarketplaceEntry, revoked: RevokedList): boolean { return revoked.entries.some( (r) => - r.name === entry.name && - r.version === entry.version && - r.sourceHash === entry.sourceHash, + r.name === entry.name && r.version === entry.version && r.sourceHash === entry.sourceHash, ); } @@ -92,7 +90,8 @@ export async function fetchIndex(url: string): Promise { const res = await fetch(url, { method: 'GET' }); if (!res.ok) throw new Error(`marketplace index ${url}: HTTP ${res.status}`); const json = (await res.json()) as MarketplaceIndex; - if (json.version !== '1') throw new Error(`unsupported marketplace index version: ${json.version}`); + if (json.version !== '1') + throw new Error(`unsupported marketplace index version: ${json.version}`); return json; } @@ -130,12 +129,17 @@ export async function resolveEntry(args: { .filter((e) => e.name === args.name) .filter((e) => !args.version || e.version === args.version) .sort((a, b) => versionCompare(b.version, a.version))[0]; - if (!candidate) throw new Error(`No entry "${args.name}"${args.version ? `@${args.version}` : ''} in ${args.marketplaceUrl}`); + if (!candidate) + throw new Error( + `No entry "${args.name}"${args.version ? `@${args.version}` : ''} in ${args.marketplaceUrl}`, + ); if (!verifyEntrySignature(candidate)) throw new Error(`Signature verification failed for ${args.name}@${candidate.version}`); const revoked = await fetchRevoked(args.marketplaceUrl); if (isRevoked(candidate, revoked)) - throw new Error(`${args.name}@${candidate.version} is in the revocation list — refusing to install`); + throw new Error( + `${args.name}@${candidate.version} is in the revocation list — refusing to install`, + ); return candidate; } @@ -154,9 +158,7 @@ function versionCompare(a: string, b: string): number { * Load the user's marketplace registry (~/.deepcode/marketplaces.json). * Returns { marketplaces: {} } if missing. */ -export async function loadMarketplaceConfig( - home: string = homedir(), -): Promise { +export async function loadMarketplaceConfig(home: string = homedir()): Promise { try { const raw = await fs.readFile(marketplacesPath(home), 'utf8'); return JSON.parse(raw) as MarketplaceConfig; diff --git a/packages/core/src/plugins/runtime/subprocess.ts b/packages/core/src/plugins/runtime/subprocess.ts index 1ad7fa5..a732276 100644 --- a/packages/core/src/plugins/runtime/subprocess.ts +++ b/packages/core/src/plugins/runtime/subprocess.ts @@ -278,7 +278,10 @@ async function wrapNodeSpawn( }; if (platform === 'macos') { const profile = buildMacOsProfile(merged, pluginDir); - const profilePath = join(tmpdir(), `deepcode-plug-sb-${process.pid}-${Date.now().toString(36)}.sb`); + const profilePath = join( + tmpdir(), + `deepcode-plug-sb-${process.pid}-${Date.now().toString(36)}.sb`, + ); await fs.writeFile(profilePath, profile, 'utf8'); return { command: 'sandbox-exec', args: ['-f', profilePath, 'node', entry] }; } diff --git a/packages/core/src/plugins/wireup.ts b/packages/core/src/plugins/wireup.ts index 809a07e..007fe6f 100644 --- a/packages/core/src/plugins/wireup.ts +++ b/packages/core/src/plugins/wireup.ts @@ -21,16 +21,8 @@ import { homedir } from 'node:os'; import type { Hooks } from '../config/types.js'; import type { HookDispatcher } from '../hooks/dispatcher.js'; import type { ToolHandler } from '../types.js'; -import { - discoverPlugins, - type DiscoverOptions, - type InstalledPlugin, -} from './manifest.js'; -import { - PluginSubprocess, - shutdownAllPlugins, - spawnAllPlugins, -} from './runtime/subprocess.js'; +import { discoverPlugins, type DiscoverOptions, type InstalledPlugin } from './manifest.js'; +import { PluginSubprocess, shutdownAllPlugins, spawnAllPlugins } from './runtime/subprocess.js'; export interface PluginCapabilityBridge { fs_read: (path: string) => Promise; diff --git a/packages/core/src/providers/deepseek.test.ts b/packages/core/src/providers/deepseek.test.ts index 94603ae..ff9a9ac 100644 --- a/packages/core/src/providers/deepseek.test.ts +++ b/packages/core/src/providers/deepseek.test.ts @@ -150,7 +150,11 @@ describe('DeepSeekProvider', () => { { delta: { tool_calls: [ - { index: 0, id: 'call_w', function: { name: 'Write', arguments: '{"file_path":"a.js","content":"cons' } }, + { + index: 0, + id: 'call_w', + function: { name: 'Write', arguments: '{"file_path":"a.js","content":"cons' }, + }, ], }, }, @@ -183,14 +187,25 @@ describe('DeepSeekProvider', () => { { delta: { tool_calls: [ - { index: 0, id: 'ok1', function: { name: 'Read', arguments: '{"file_path":"a.ts"}' } }, - { index: 1, id: 'bad', function: { name: 'Write', arguments: '{"file_path":"b.ts","content":"x' } }, + { + index: 0, + id: 'ok1', + function: { name: 'Read', arguments: '{"file_path":"a.ts"}' }, + }, + { + index: 1, + id: 'bad', + function: { name: 'Write', arguments: '{"file_path":"b.ts","content":"x' }, + }, ], }, }, ], }, - { choices: [{ delta: {}, finish_reason: 'length' }], usage: { prompt_tokens: 5, completion_tokens: 8 } }, + { + choices: [{ delta: {}, finish_reason: 'length' }], + usage: { prompt_tokens: 5, completion_tokens: 8 }, + }, ]; const p = new DeepSeekProvider({ apiKey: 'sk-test', fetch: mockFetch(chunks) }); const result = await p.runTurn({ diff --git a/packages/core/src/reminders/index.ts b/packages/core/src/reminders/index.ts index 523c1e7..1979e28 100644 --- a/packages/core/src/reminders/index.ts +++ b/packages/core/src/reminders/index.ts @@ -154,9 +154,7 @@ export async function todosPendingReminder(ctx: ReminderContext): Promise { +export async function externalFileModifiedReminder(ctx: ReminderContext): Promise { if (!ctx.knownFiles || ctx.knownFiles.size === 0) return null; const drifted: Array<{ path: string; was: number; now: number }> = []; for (const [path, was] of ctx.knownFiles) { @@ -170,7 +168,10 @@ export async function externalFileModifiedReminder( } } if (drifted.length === 0) return null; - const list = drifted.slice(0, 5).map((d) => ` - ${d.path}`).join('\n'); + const list = drifted + .slice(0, 5) + .map((d) => ` - ${d.path}`) + .join('\n'); const more = drifted.length > 5 ? `\n ... and ${drifted.length - 5} more` : ''; return `Files modified externally since you last read them:\n${list}${more}\nRe-read them with the Read tool before editing.`; } diff --git a/packages/core/src/sandbox/attacks.test.ts b/packages/core/src/sandbox/attacks.test.ts index cdcd864..9d246c2 100644 --- a/packages/core/src/sandbox/attacks.test.ts +++ b/packages/core/src/sandbox/attacks.test.ts @@ -201,8 +201,7 @@ describe('wrapBashCommand: excluded-command spoofing', () => { // ────────────────────────────────────────────────────────────────────────── const isMac = process.platform === 'darwin'; -const hasSandboxExec = - isMac && spawnSync('which', ['sandbox-exec']).status === 0; +const hasSandboxExec = isMac && spawnSync('which', ['sandbox-exec']).status === 0; describe.runIf(hasSandboxExec)('sandbox-exec end-to-end (macOS)', () => { let workDir: string; @@ -312,7 +311,10 @@ describe.runIf(hasBwrap)('bwrap end-to-end (Linux)', () => { }); it('blocks writing outside the bound cwd', async () => { - const outsideTarget = join(tmpdir(), `outside-${Date.now()}-${Math.random().toString(36).slice(2)}`); + const outsideTarget = join( + tmpdir(), + `outside-${Date.now()}-${Math.random().toString(36).slice(2)}`, + ); const wrapped = await wrapBashCommand({ userCommand: `echo evil > "${outsideTarget}" 2>&1; echo "[exit=$?]"`, cwd: workDir, diff --git a/packages/core/src/sandbox/dns-proxy.test.ts b/packages/core/src/sandbox/dns-proxy.test.ts index 5c9b17b..7c7ed3b 100644 --- a/packages/core/src/sandbox/dns-proxy.test.ts +++ b/packages/core/src/sandbox/dns-proxy.test.ts @@ -4,7 +4,9 @@ import { buildNxDomain, parseQName, startDnsProxy, type DnsProxyHandle } from '. /** Build a minimal DNS query packet for a single domain. */ function buildQuery(domain: string, txnId = 0x1234): Buffer { - const labels = domain.split('.').map((l) => Buffer.concat([Buffer.from([l.length]), Buffer.from(l, 'utf8')])); + const labels = domain + .split('.') + .map((l) => Buffer.concat([Buffer.from([l.length]), Buffer.from(l, 'utf8')])); const qname = Buffer.concat([...labels, Buffer.from([0])]); // Header (12 bytes) + qname + qtype (2) + qclass (2) const header = Buffer.alloc(12); diff --git a/packages/core/src/sandbox/pipeline.test.ts b/packages/core/src/sandbox/pipeline.test.ts index 4abb02c..58e7753 100644 --- a/packages/core/src/sandbox/pipeline.test.ts +++ b/packages/core/src/sandbox/pipeline.test.ts @@ -38,9 +38,7 @@ describe('splitClauses', () => { }); it('strips empty clauses', () => { - expect(splitClauses(';;; ; a')).toEqual([ - expect.objectContaining({ command: 'a' }), - ]); + expect(splitClauses(';;; ; a')).toEqual([expect.objectContaining({ command: 'a' })]); }); }); diff --git a/packages/core/src/sandbox/profile.test.ts b/packages/core/src/sandbox/profile.test.ts index 1639584..7732138 100644 --- a/packages/core/src/sandbox/profile.test.ts +++ b/packages/core/src/sandbox/profile.test.ts @@ -121,7 +121,10 @@ describe('buildLinuxBwrapArgs', () => { const idx = args.indexOf('--ro-bind'); // Walk forward through args looking for the resolv.conf binding const has = args.some( - (a, i) => a === '--ro-bind' && args[i + 1] === '/tmp/dc-resolv.conf' && args[i + 2] === '/etc/resolv.conf', + (a, i) => + a === '--ro-bind' && + args[i + 1] === '/tmp/dc-resolv.conf' && + args[i + 2] === '/etc/resolv.conf', ); expect(has).toBe(true); void idx; diff --git a/packages/core/src/sandbox/profile.ts b/packages/core/src/sandbox/profile.ts index 12617bb..99c3351 100644 --- a/packages/core/src/sandbox/profile.ts +++ b/packages/core/src/sandbox/profile.ts @@ -175,8 +175,7 @@ export function buildLinuxBwrapArgs( // 3. allowedDomains: undefined → full network access (default) const explicitEmpty = (net.allowedDomains ?? []).length === 0 && (net.allowedDomains ?? null) !== null; - const whitelisted = - (net.allowedDomains ?? []).length > 0 && opts.dnsProxyPort !== undefined; + const whitelisted = (net.allowedDomains ?? []).length > 0 && opts.dnsProxyPort !== undefined; if (explicitEmpty) { args.push('--unshare-net'); } else if (whitelisted) { diff --git a/packages/core/src/tools/ask-user.test.ts b/packages/core/src/tools/ask-user.test.ts index 47c94d4..224a638 100644 --- a/packages/core/src/tools/ask-user.test.ts +++ b/packages/core/src/tools/ask-user.test.ts @@ -21,7 +21,10 @@ describe('AskUserQuestionTool', () => { const r = await AskUserQuestionTool.execute( { question: 'A or B?', - options: [{ label: 'A', description: 'first' }, { label: 'B', description: 'second' }], + options: [ + { label: 'A', description: 'first' }, + { label: 'B', description: 'second' }, + ], }, { cwd: '/x', diff --git a/packages/core/src/tools/ask-user.ts b/packages/core/src/tools/ask-user.ts index 4b6e585..7a1b882 100644 --- a/packages/core/src/tools/ask-user.ts +++ b/packages/core/src/tools/ask-user.ts @@ -38,7 +38,10 @@ export const AskUserQuestionTool: ToolHandler = { type: 'object', properties: { label: { type: 'string', description: 'Short option text (1-5 words).' }, - description: { type: 'string', description: 'Explanation of what this option means.' }, + description: { + type: 'string', + description: 'Explanation of what this option means.', + }, }, required: ['label'], }, diff --git a/packages/core/src/tools/ssrf.test.ts b/packages/core/src/tools/ssrf.test.ts index 7543c02..8c1c41f 100644 --- a/packages/core/src/tools/ssrf.test.ts +++ b/packages/core/src/tools/ssrf.test.ts @@ -48,8 +48,10 @@ describe('blockedIpReason', () => { }); describe('ssrfCheckUrl', () => { - const stub = (addrs: string[]): Resolver => async () => - addrs.map((address) => ({ address, family: address.includes(':') ? 6 : 4 })); + const stub = + (addrs: string[]): Resolver => + async () => + addrs.map((address) => ({ address, family: address.includes(':') ? 6 : 4 })); it('blocks literal metadata IP without resolving (default policy)', async () => { expect(await ssrfCheckUrl(new URL('http://169.254.169.254/latest/meta-data/'))).toMatch( @@ -67,12 +69,16 @@ describe('ssrfCheckUrl', () => { const throwing: Resolver = async () => { throw new Error('should not resolve'); }; - expect( - await ssrfCheckUrl(new URL('http://metadata.google.internal/'), {}, throwing), - ).toMatch(/internal/); + expect(await ssrfCheckUrl(new URL('http://metadata.google.internal/'), {}, throwing)).toMatch( + /internal/, + ); expect(await ssrfCheckUrl(new URL('http://localhost/'), {}, throwing)).toBeNull(); - expect(await ssrfCheckUrl(new URL('http://localhost/'), HARDENED, throwing)).toMatch(/internal/); - expect(await ssrfCheckUrl(new URL('http://foo.local/'), HARDENED, throwing)).toMatch(/internal/); + expect(await ssrfCheckUrl(new URL('http://localhost/'), HARDENED, throwing)).toMatch( + /internal/, + ); + expect(await ssrfCheckUrl(new URL('http://foo.local/'), HARDENED, throwing)).toMatch( + /internal/, + ); }); it('blocks hostnames resolving to the metadata IP even under default policy', async () => { diff --git a/packages/core/src/tools/ssrf.ts b/packages/core/src/tools/ssrf.ts index 503bc14..a4a9cd8 100644 --- a/packages/core/src/tools/ssrf.ts +++ b/packages/core/src/tools/ssrf.ts @@ -121,7 +121,10 @@ export async function ssrfCheckUrl( return `blocked internal hostname: ${host}`; } // localhost / *.local resolve to loopback — only block in hardened mode. - if (!allowPrivate && (lower === 'localhost' || lower.endsWith('.localhost') || lower.endsWith('.local'))) { + if ( + !allowPrivate && + (lower === 'localhost' || lower.endsWith('.localhost') || lower.endsWith('.local')) + ) { return `blocked internal hostname: ${host}`; } diff --git a/packages/core/src/tools/tool-search.test.ts b/packages/core/src/tools/tool-search.test.ts index 3ae4568..d385b3c 100644 --- a/packages/core/src/tools/tool-search.test.ts +++ b/packages/core/src/tools/tool-search.test.ts @@ -53,10 +53,7 @@ describe('ToolSearch keyword query', () => { for (let i = 0; i < 20; i++) entries.push(entry(`tool${i}`, 'common-word common')); const store = new RegistryDeferredStore(reg, entries); const search = makeToolSearchTool(store); - const r = await search.execute( - { query: 'common', max_results: 3 }, - { cwd: '/x' }, - ); + const r = await search.execute({ query: 'common', max_results: 3 }, { cwd: '/x' }); const data = r.data as { hits: unknown[] }; expect(data.hits).toHaveLength(3); }); @@ -65,10 +62,7 @@ describe('ToolSearch keyword query', () => { describe('ToolSearch select: query', () => { it('loads named tools into the registry', async () => { const reg = new ToolRegistry([]); - const store = new RegistryDeferredStore(reg, [ - entry('A', 'desc A'), - entry('B', 'desc B'), - ]); + const store = new RegistryDeferredStore(reg, [entry('A', 'desc A'), entry('B', 'desc B')]); const search = makeToolSearchTool(store); const r = await search.execute({ query: 'select:A,B' }, { cwd: '/x' }); expect(r.content).toMatch(/Loaded: A, B/); diff --git a/packages/core/src/tools/tool-search.ts b/packages/core/src/tools/tool-search.ts index 15e9c5c..2bb94e6 100644 --- a/packages/core/src/tools/tool-search.ts +++ b/packages/core/src/tools/tool-search.ts @@ -97,9 +97,7 @@ export function makeToolSearchTool(store: DeferredToolStore): ToolHandler { if (ranked.length === 0) { return { content: `No deferred tools matched "${input.query}".`, data: { hits: [] } }; } - const lines = ranked.map( - (r) => `${r.entry.name} — ${r.entry.description.slice(0, 120)}`, - ); + const lines = ranked.map((r) => `${r.entry.name} — ${r.entry.description.slice(0, 120)}`); lines.push(''); lines.push(`Use \`select:${ranked.map((r) => r.entry.name).join(',')}\` to load.`); return { @@ -129,7 +127,10 @@ function score(entry: DeferredToolEntry, tokens: string[]): number { export class RegistryDeferredStore implements DeferredToolStore { private readonly entries = new Map(); constructor( - private readonly registry: { register: (h: ToolHandler) => void; get: (name: string) => ToolHandler | undefined }, + private readonly registry: { + register: (h: ToolHandler) => void; + get: (name: string) => ToolHandler | undefined; + }, entries: DeferredToolEntry[], ) { for (const e of entries) this.entries.set(e.name, e); diff --git a/packages/core/src/tools/web-fetch.test.ts b/packages/core/src/tools/web-fetch.test.ts index a2f467f..1881158 100644 --- a/packages/core/src/tools/web-fetch.test.ts +++ b/packages/core/src/tools/web-fetch.test.ts @@ -3,7 +3,12 @@ import { AddressInfo } from 'node:net'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { WebFetchTool } from './web-fetch.js'; -function startServer(handler: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void): Promise<{ server: Server; url: string }> { +function startServer( + handler: ( + req: import('node:http').IncomingMessage, + res: import('node:http').ServerResponse, + ) => void, +): Promise<{ server: Server; url: string }> { return new Promise((res) => { const server = createServer(handler); server.listen(0, '127.0.0.1', () => { @@ -56,10 +61,7 @@ describe('WebFetchTool', () => { }); it('rejects invalid URL', async () => { - const result = await WebFetchTool.execute( - { url: 'not-a-url' }, - { cwd: process.cwd() }, - ); + const result = await WebFetchTool.execute({ url: 'not-a-url' }, { cwd: process.cwd() }); expect(result.isError).toBe(true); expect(result.content).toMatch(/invalid URL/i); }); diff --git a/packages/core/src/tools/web-fetch.ts b/packages/core/src/tools/web-fetch.ts index 837e915..82444cc 100644 --- a/packages/core/src/tools/web-fetch.ts +++ b/packages/core/src/tools/web-fetch.ts @@ -159,7 +159,10 @@ export const WebFetchTool: ToolHandler = { } catch (err) { const e = err as Error; if (e.name === 'AbortError') { - return { content: `Error: fetch aborted (timeout ${TIMEOUT_MS}ms or signal).`, isError: true }; + return { + content: `Error: fetch aborted (timeout ${TIMEOUT_MS}ms or signal).`, + isError: true, + }; } return { content: `Error fetching ${parsed.toString()}: ${e.message}`, isError: true }; } finally { diff --git a/packages/core/src/tools/web-search.ts b/packages/core/src/tools/web-search.ts index 8bd9c4c..56b9453 100644 --- a/packages/core/src/tools/web-search.ts +++ b/packages/core/src/tools/web-search.ts @@ -98,7 +98,10 @@ export const WebSearchTool: ToolHandler = { } catch (err) { const e = err as Error; if (e.name === 'AbortError') { - return { content: `Error: search aborted (timeout ${TIMEOUT_MS}ms or signal).`, isError: true }; + return { + content: `Error: search aborted (timeout ${TIMEOUT_MS}ms or signal).`, + isError: true, + }; } return { content: `Error: ${e.message}`, isError: true }; } finally { diff --git a/packages/core/src/voice/index.ts b/packages/core/src/voice/index.ts index 0789cd0..793a1d3 100644 --- a/packages/core/src/voice/index.ts +++ b/packages/core/src/voice/index.ts @@ -58,7 +58,13 @@ export class WhisperCppProvider implements VoiceProvider { if (opts.language) args.push('-l', opts.language); const bin = this.opts.binPath ?? 'whisper'; const spawnFn = this.opts.exec ?? spawn; - const { stdout, stderr, code } = await runCommand(spawnFn, bin, args, this.opts.cwd, opts.signal); + const { stdout, stderr, code } = await runCommand( + spawnFn, + bin, + args, + this.opts.cwd, + opts.signal, + ); const latency = Date.now() - t0; if (code !== 0) { throw new Error(`whisper.cpp exited ${code}: ${stderr.slice(0, 300)}`); diff --git a/packages/core/src/worktree/index.test.ts b/packages/core/src/worktree/index.test.ts index 618fc8a..c20edbf 100644 --- a/packages/core/src/worktree/index.test.ts +++ b/packages/core/src/worktree/index.test.ts @@ -113,9 +113,9 @@ describe('createWorktree / removeWorktree', () => { it('errors when source is not a git repo', async () => { const notARepo = await canonicalMkdtemp('dc-not-repo-'); try { - await expect( - createWorktree({ source: notARepo, parentDir: parent }), - ).rejects.toThrow(/not a git repository/); + await expect(createWorktree({ source: notARepo, parentDir: parent })).rejects.toThrow( + /not a git repository/, + ); } finally { await rm(notARepo, { recursive: true, force: true }); } diff --git a/packages/core/src/worktree/index.ts b/packages/core/src/worktree/index.ts index 3141426..8c4d73c 100644 --- a/packages/core/src/worktree/index.ts +++ b/packages/core/src/worktree/index.ts @@ -41,7 +41,8 @@ export async function createWorktree(opts: CreateWorktreeOpts): Promise