Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<a href="README.zh.md">简体中文</a>
</p>

<p align="center"><sub>Desktop v1.3</sub></p>

---

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.
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<a href="README.zh.md">简体中文</a>
</p>

<p align="center"><sub>桌面版 v1.3</sub></p>

---

DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体。它保留了 [opencode](https://github.com/sst/opencode) 的运行时基座,并在其上叠加了一层控制平面——让智能体不再像一次性的对话工具,而更像一位记得住你项目、会替你打磨模糊需求、能对硬骨头深挖到底的队友。
Expand Down Expand Up @@ -45,6 +47,24 @@ DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体

**DeepAgent 的做法:** 输入框上有两种情景模式。**直接**模式原样发送你的提示——措辞由你做主。**智能**模式会把粗糙的想法打磨成更精准的提示,给出草拟的方案和决策建议,并在自动执行任何操作前等你确认。智能体替你塑形到什么程度,由你决定。

### 选择智能体驱动的方式——以及由谁来写计划

**需求:** 有时你希望智能体拿到需求就自己跑;有时你想用一份由自己掌控的计划来引导它;有时你已经把计划写好了,只需要它忠实执行。

**DeepAgent 的做法:** 输入框上的协作模式选择器提供三档。**自动** ——智能体自行定目标、做计划、执行到完成。**目标** ——你说明需求,智能体生成一份你可以编辑的 `goal+plan.md`,再由监督循环驱动执行(计划→执行→验证逐步推进,有硬性预算/步数上限和客观完成判据)。**设计** ——你自己写好 `goal+plan.md`,智能体读取并忠实执行你的方案,不会重新定义目标。与协作模式正交,权限控制提供三个预设——**只读**、**请求批准**(默认)、**完全访问**——自主程度与审批方式是两个独立的、明确的选项。

### 在高风险决策前听一次会诊

**需求:** 有些决策——一次破坏性迁移、一个安全敏感的改动、一个架构抉择——值得不止一次自信的作答自我认同。

**DeepAgent 的做法:** 在输入框旁召集**专家团**。差异化的专家视角(正确性、安全、性能、架构、可复现)对同一个冻结问题各自独立审阅,匿名辩论,再由一个确定性(非 LLM)的仲裁者聚合裁定——保留少数派意见,并在专家团无法安全达成一致时偏向将决策权升级给你。

### 读懂并治理智能体所知道的

**需求:** 持久记忆只有在你能看见、能纠错的情况下才值得信任。

**DeepAgent 的做法:** **仓库与百科**视图把四张图投影成人类可读的页面——浏览并全文检索智能体的知识,跟随文档↔代码的交叉链接,并通过与智能体相同的证据门编辑可治理的知识/记忆页面(文档与代码页面只读)。单独的知识治理视图按项目和全局分组列出已学事实,由你审批哪些成为永久知识。

### 对真正的难题深挖到底

**需求:** 复杂的工作——一个架构决策、一次棘手的迁移、一个隐蔽的 bug——需要的不止一次自信的单程作答。它需要调研、需要第二意见、需要有人主动来挑刺。
Expand Down
14 changes: 13 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
};
Expand Down
2 changes: 1 addition & 1 deletion nix/hashes.json
Original file line number Diff line number Diff line change
@@ -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="
Expand Down
2 changes: 2 additions & 0 deletions nix/node_modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions nix/scripts/update-hashes.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 4 additions & 2 deletions packages/app/src/components/deepagent-settings-ux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
48 changes: 29 additions & 19 deletions packages/app/src/components/deepagent/approval-control.tsx
Original file line number Diff line number Diff line change
@@ -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<ApprovalMode>(() => (auto() ? "auto" : "request"))
const current = createMemo<ApprovalMode>(() =>
props.directory ? permission.directoryApprovalMode(props.directory) : "request",
)

const options: ApprovalMode[] = ["request", "auto"]
const label = (mode: ApprovalMode) =>
mode === "auto"
? language.t("composer.approval.auto")
: language.t("composer.approval.request")
const options: ApprovalMode[] = ["read-only", "request", "full-access"]
const label = (mode: ApprovalMode) => {
switch (mode) {
case "read-only":
return language.t("composer.approval.readOnly")
case "full-access":
return language.t("composer.approval.fullAccess")
default:
return language.t("composer.approval.request")
}
}

const onSelect = (mode: ApprovalMode | undefined) => {
if (!mode || !props.directory) return
const isAuto = mode === "auto"
if (isAuto === auto()) return
// toggleAutoAcceptDirectory flips the directory-level state; only call it when the target differs.
permission.toggleAutoAcceptDirectory(props.directory)
if (mode === current()) return
permission.setDirectoryApprovalMode(props.directory, mode)
props.onAfter?.()
}

Expand All @@ -46,7 +56,7 @@ export function ApprovalControl(props: { directory: string; triggerStyle?: JSX.C
value={(o) => o}
label={label}
onSelect={onSelect}
class="capitalize max-w-[160px] text-text-base"
class="max-w-[180px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={props.triggerStyle}
triggerProps={{ "data-action": "prompt-approval" }}
Expand Down
Loading
Loading