diff --git a/README.md b/README.md index da24d381..98d360e1 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ 简体中文
+Desktop v1.3
+ --- DeepAgent Code is an AI coding agent built on persistent document memory. It keeps [opencode](https://github.com/sst/opencode)'s runtime foundations and adds a control plane so the agent behaves less like a one-shot chat and more like a teammate that remembers your project, sharpens vague asks, and goes deep on hard problems. @@ -45,6 +47,24 @@ The features below start from a real need — something a plain coding agent, op **What DeepAgent does:** Two scenario modes on the composer. **Direct** sends your prompt as-is — you own the wording. **Intelligence** refines a rough ask into a sharper prompt, surfaces a draft plan and decision suggestions, and waits for your confirmation before it automates anything. You decide how much the agent shapes the request. +### Choose how much the agent drives — and who writes the plan + +**The need:** Sometimes you want the agent to just take a request and run; sometimes you want to steer it with a plan you control; sometimes you've already written the plan and just want it executed faithfully. + +**What DeepAgent does:** Three collaboration modes on the composer, picked from a single selector. **Auto** — the agent sets the objective, designs and plans as needed, and executes to completion. **Loop** — you describe the goal, the agent writes a `goal+plan.md` you can edit, then a supervised loop drives it to completion (plan → execute → verify per tick, with hard budget/step ceilings and objective completion checks). **Design** — you author `goal+plan.md` yourself and the agent executes your plan faithfully without redefining the goal. Orthogonal to mode, a permission control offers three presets — **Read-only**, **Request approval** (default), **Full access** — so autonomy and approval are separate, explicit choices. + +### Get a second opinion before high-risk decisions + +**The need:** Some decisions — a breaking migration, a security-sensitive change, an architecture call — deserve more than one confident pass agreeing with itself. + +**What DeepAgent does:** Convene an **Expert Panel** from the composer. Differentiated expert lenses (correctness, security, performance, architecture, repro) review the same frozen question independently, debate anonymously, and a deterministic (non-LLM) arbiter aggregates a verdict — with minority opinions preserved and a fail-closed bias toward escalating to you when the panel can't safely agree. + +### Read and govern what the agent knows + +**The need:** Persistent memory is only trustworthy if you can see it and correct it. + +**What DeepAgent does:** A **Repo & Wiki** view projects the four graphs into human-readable pages — browse and full-text-search the agent's knowledge, follow docs↔code cross-links, and edit governable Knowledge/Memory pages through the same evidence-gate the agent uses (Documents and Code stay read-only). A separate governance view lists learned facts grouped by project and global scope, so you approve what becomes durable. + ### Go deep on genuinely hard problems **The need:** Complex work — an architecture decision, a tricky migration, a subtle bug — needs more than a single confident pass. It needs research, a second opinion, and someone actively trying to poke holes. diff --git a/README.zh.md b/README.zh.md index 9d1618ab..0a17532a 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,6 +13,8 @@ 简体中文 +桌面版 v1.3
+ --- DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体。它保留了 [opencode](https://github.com/sst/opencode) 的运行时基座,并在其上叠加了一层控制平面——让智能体不再像一次性的对话工具,而更像一位记得住你项目、会替你打磨模糊需求、能对硬骨头深挖到底的队友。 @@ -45,6 +47,24 @@ DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体 **DeepAgent 的做法:** 输入框上有两种情景模式。**直接**模式原样发送你的提示——措辞由你做主。**智能**模式会把粗糙的想法打磨成更精准的提示,给出草拟的方案和决策建议,并在自动执行任何操作前等你确认。智能体替你塑形到什么程度,由你决定。 +### 选择智能体驱动的方式——以及由谁来写计划 + +**需求:** 有时你希望智能体拿到需求就自己跑;有时你想用一份由自己掌控的计划来引导它;有时你已经把计划写好了,只需要它忠实执行。 + +**DeepAgent 的做法:** 输入框上的协作模式选择器提供三档。**自动** ——智能体自行定目标、做计划、执行到完成。**目标** ——你说明需求,智能体生成一份你可以编辑的 `goal+plan.md`,再由监督循环驱动执行(计划→执行→验证逐步推进,有硬性预算/步数上限和客观完成判据)。**设计** ——你自己写好 `goal+plan.md`,智能体读取并忠实执行你的方案,不会重新定义目标。与协作模式正交,权限控制提供三个预设——**只读**、**请求批准**(默认)、**完全访问**——自主程度与审批方式是两个独立的、明确的选项。 + +### 在高风险决策前听一次会诊 + +**需求:** 有些决策——一次破坏性迁移、一个安全敏感的改动、一个架构抉择——值得不止一次自信的作答自我认同。 + +**DeepAgent 的做法:** 在输入框旁召集**专家团**。差异化的专家视角(正确性、安全、性能、架构、可复现)对同一个冻结问题各自独立审阅,匿名辩论,再由一个确定性(非 LLM)的仲裁者聚合裁定——保留少数派意见,并在专家团无法安全达成一致时偏向将决策权升级给你。 + +### 读懂并治理智能体所知道的 + +**需求:** 持久记忆只有在你能看见、能纠错的情况下才值得信任。 + +**DeepAgent 的做法:** **仓库与百科**视图把四张图投影成人类可读的页面——浏览并全文检索智能体的知识,跟随文档↔代码的交叉链接,并通过与智能体相同的证据门编辑可治理的知识/记忆页面(文档与代码页面只读)。单独的知识治理视图按项目和全局分组列出已学事实,由你审批哪些成为永久知识。 + ### 对真正的难题深挖到底 **需求:** 复杂的工作——一个架构决策、一次棘手的迁移、一个隐蔽的 bug——需要的不止一次自信的单程作答。它需要调研、需要第二意见、需要有人主动来挑刺。 diff --git a/flake.nix b/flake.nix index a8f46676..1930ef22 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,17 @@ }; }); + apps = forEachSystem (pkgs: { + update-hashes = { + type = "app"; + program = let + script = pkgs.writeShellScriptBin "update-hashes" '' + exec ${pkgs.python3}/bin/python3 ${./nix/scripts/update-hashes.py} "$@" + ''; + in "${script}/bin/update-hashes"; + }; + }); + overlays = { default = final: _prev: @@ -63,7 +74,8 @@ deepagent-code-desktop = pkgs.callPackage ./nix/desktop.nix { inherit deepagent-code; }; - # Updater derivation with fakeHash - build fails and reveals correct hash + # Build with fakeHash to reveal the correct hash. + # Used by: nix run .#update-hashes node_modules_updater = node_modules.override { hash = pkgs.lib.fakeHash; }; diff --git a/nix/hashes.json b/nix/hashes.json index 17fbfc4d..18bc59a9 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-vB4imi5LtCxnDuOaJFQh+asK51RUDCnIVCkIzBQN12o=", + "x86_64-linux": "sha256-2cnaDO1erE8kXD6iZtzAqd0hoLLl3IV1hwqXI9yoUhw=", "aarch64-linux": "sha256-VsmW8tTiT3VkTxp5oiYc/maJLSsBMxK6VVUSVshWVT8=", "aarch64-darwin": "sha256-/MaQBX2zoJMtLA8jHt8+uKpIFve/eec3RfcKT3FvXC8=", "x86_64-darwin": "sha256-3ggap9rgR2TvVbYgnXsgkg6Rj1TqNZu32QgTVRdN0f0=" diff --git a/nix/node_modules.nix b/nix/node_modules.nix index 4d1c1516..15c37c72 100644 --- a/nix/node_modules.nix +++ b/nix/node_modules.nix @@ -3,6 +3,8 @@ stdenvNoCC, bun, rev ? "dirty", + # Auto-managed by nix run .#update-hashes. Do not edit manually. + # CI regenerates when bun.lock changes. hash ? (lib.pipe ./hashes.json [ builtins.readFile diff --git a/nix/scripts/update-hashes.py b/nix/scripts/update-hashes.py new file mode 100644 index 00000000..e3a6c6a9 --- /dev/null +++ b/nix/scripts/update-hashes.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Auto-update hashes.json by building with fakeHash and extracting the correct hash. + +Usage: + nix run .#update-hashes # update current platform + nix run .#update-hashes -- --check # CI: verify hashes are current + nix/scripts/update-hashes.py --root /path # run directly with explicit root +""" + +import json +import os +import re +import subprocess +import sys +from pathlib import Path + + +def get_current_platform(): + machine = os.uname().machine + system = os.uname().sysname.lower() + arch_map = {"x86_64": "x86_64", "aarch64": "aarch64"} + os_map = {"linux": "linux", "darwin": "darwin"} + arch = arch_map.get(machine, machine) + os_name = os_map.get(system, system) + return f"{arch}-{os_name}" + + +def extract_hash(stderr: str) -> str | None: + m = re.search(r"got:\s+(sha256-[A-Za-z0-9+/=]+)", stderr) + return m.group(1) if m else None + + +def parse_args(): + root = None + check = False + args = sys.argv[1:] + i = 0 + while i < len(args): + if args[i] == "--root" and i + 1 < len(args): + root = Path(args[i + 1]) + i += 2 + elif args[i] == "--check": + check = True + i += 1 + else: + print(f"Unknown argument: {args[i]}", file=sys.stderr) + sys.exit(1) + return root, check + + +def ensure_repo_root(root: Path, hashes_path: Path): + if not hashes_path.exists(): + print(f"ERROR: {hashes_path} not found", file=sys.stderr) + print(f"Is {root} the deepagent-code repo root?", file=sys.stderr) + print("Use --root PATH to specify the repo root", file=sys.stderr) + sys.exit(1) + + +def build_and_extract(root: Path): + result = subprocess.run( + ["nix", "build", ".#node_modules_updater", "--no-link", "--print-build-logs"], + capture_output=True, text=True, cwd=root, + ) + combined = (result.stdout or "") + (result.stderr or "") + return extract_hash(combined) + + +def main(): + arg_root, check = parse_args() + root = arg_root or Path(os.getcwd()) + hashes_path = root / "nix" / "hashes.json" + ensure_repo_root(root, hashes_path) + + platform = get_current_platform() + + if check: + data = json.loads(hashes_path.read_text()) + stored = data["nodeModules"].get(platform) + if not stored: + print(f"ERROR: no hash stored for platform {platform}", file=sys.stderr) + sys.exit(1) + actual = build_and_extract(root) + if not actual: + print("ERROR: could not extract hash from build", file=sys.stderr) + sys.exit(2) + if stored != actual: + print(f"HASH MISMATCH: stored={stored[:30]}... actual={actual[:30]}...", file=sys.stderr) + print("Run: nix run .#update-hashes", file=sys.stderr) + sys.exit(1) + print(f"OK: hash matches for {platform}") + return + + print(f"Platform: {platform}") + print("Building node_modules_updater to compute correct hash ...") + sys.stdout.flush() + + correct = build_and_extract(root) + if not correct: + print("ERROR: could not extract hash from build output", file=sys.stderr) + sys.exit(1) + + print(f"Correct hash: {correct}") + + data = json.loads(hashes_path.read_text()) + old = data["nodeModules"].get(platform, "(missing)") + data["nodeModules"][platform] = correct + hashes_path.write_text(json.dumps(data, indent=2) + "\n") + + print(f"Updated {platform}: {old} → {correct}") + print() + print("Done. You can now run: nix build .#deepagent-code") + + +if __name__ == "__main__": + main() diff --git a/packages/app/src/components/deepagent-settings-ux.test.ts b/packages/app/src/components/deepagent-settings-ux.test.ts index 8c2d60b5..f2e218fc 100644 --- a/packages/app/src/components/deepagent-settings-ux.test.ts +++ b/packages/app/src/components/deepagent-settings-ux.test.ts @@ -35,9 +35,11 @@ describe("DeepAgent settings UX", () => { // …it is a composer control next to the agent selector, backed by directory-level auto-accept. expect(composer).toContain("ApprovalControl") expect(control).toContain('"data-action": "prompt-approval"') - expect(control).toContain("toggleAutoAcceptDirectory") + // Tri-state Read-Only / Request / Full-Access selector backed by the permission context. + expect(control).toContain("setDirectoryApprovalMode") expect(control).toContain("composer.approval.request") - expect(control).toContain("composer.approval.auto") + expect(control).toContain("composer.approval.readOnly") + expect(control).toContain("composer.approval.fullAccess") }) test("routes the legacy settings dialog import to the unified settings page", async () => { diff --git a/packages/app/src/components/deepagent/approval-control.tsx b/packages/app/src/components/deepagent/approval-control.tsx index 5654f8ba..cabd2b57 100644 --- a/packages/app/src/components/deepagent/approval-control.tsx +++ b/packages/app/src/components/deepagent/approval-control.tsx @@ -1,39 +1,49 @@ import { createMemo, type JSX } from "solid-js" import { Select } from "@deepagent-code/ui/select" import { useLanguage } from "@/context/language" -import { usePermission } from "@/context/permission" +import { usePermission, type DirectoryApprovalMode } from "@/context/permission" /** * Approval-mode control for the composer toolbar, next to the agent (build/plan) selector. * - * Mirrors Codex's approval selector UX, simplified to two options: the button shows the CURRENT mode - * ("Request approval" by default, "Auto-approve" when armed); clicking opens a small picker to switch. - * The mode is DIRECTORY-scoped (persists across sessions in the same workspace), backed by the existing - * permission context (isAutoAcceptingDirectory / toggleAutoAcceptDirectory) — the same state the old - * settings toggle drove, now surfaced where the user acts. + * Mirrors Codex's approval selector UX: it collapses the approval×sandbox space into three named + * presets. The button shows the CURRENT mode; clicking opens a small picker to switch: + * - "Read-Only" — the agent may read/search but write/edit/bash requests are auto-rejected. + * - "Request" (default) — normal permission flow; the agent asks before write/execute. + * - "Full-Access" — auto-approve everything (the old directory-level auto-accept). + * + * The mode is DIRECTORY-scoped (persists across sessions in the same workspace), backed by the + * permission context tri-state (directoryApprovalMode / setDirectoryApprovalMode). Full-Access maps + * onto the existing isAutoAcceptingDirectory / toggleAutoAcceptDirectory state the old settings + * toggle drove, now surfaced where the user acts. */ -type ApprovalMode = "request" | "auto" +type ApprovalMode = DirectoryApprovalMode export function ApprovalControl(props: { directory: string; triggerStyle?: JSX.CSSProperties; onAfter?: () => void }) { const language = useLanguage() const permission = usePermission() - const auto = createMemo(() => (props.directory ? permission.isAutoAcceptingDirectory(props.directory) : false)) - const current = createMemo