From abb6a7120968c91a6ce1f76e7cc32a1da31230a1 Mon Sep 17 00:00:00 2001
From: thomas
Date: Sat, 18 Jul 2026 01:06:41 +0800
Subject: [PATCH 1/3] fix(core): complete deepagent pipeline restructure
downstream sync (#73)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Completes the downstream sync left over from the deepagent context
pipeline
restructure (f578e4f / eeeac7e), and fixes two regressions found while
verifying the Wave 2–5 CLI commands against a live daemon.
- Drop orphaned `oversight`/`webhook` HTTP API modules whose service
layers
were removed by the restructure; the orphaned files broke typecheck and
the
CLI `oversight` command targeted endpoints that no longer exist.
- Fix `dacode run` hanging after completion: the session event stream is
now
aborted on `session.status idle`, cancelling the underlying SSE fetch
connection (the generated SSE client never cancels the fetch in its
generator `finally`, leaking the socket and pinning the event loop).
- Default missing `apiKey` in the provider discovery cache fetch
wrapper.
- Restore 6 V4 feature flags (`v4PanelAutoConvene`,
`v4AgentPushEnabled`,
`v4EventDrivenIm`, `v4MultiAgentRuntime`, `v4EventDrivenArchive`,
`v4Steering`) still consumed by app capabilities and test RuntimeFlags
overrides.
- Complete the goal-driver API sync started in eeeac7e: inline the
single-tick
logic in `goal-tick-port` (pause/stop check + `loop.tick` + status
publish),
remove dead steer-relay wiring and dead `maxFilesChanged` /
`maxTokensPerHour` gates, and delete/prune tests that targeted removed
APIs (`goal-steer`, `overflow`, `plan-status-cache`, `httpapi-webhook`,
plus pruned blocks in `steer`, `multi-agent-runtime`, `model-discovery`,
`goal-tick-cold-recovery`).
## Test plan
- `bun typecheck` across the workspace: deepagent-code package 75 → 0
errors,
turbo typecheck 15/15 green.
- Manual CLI verification (Wave 2–5 commands): `serve --register` starts
and
registers cleanly; `run` exits normally (~36s, previously hung past
150s);
`debug agents`, `agent list`, `models`, `session list`, `packs list`,
`service status` all pass.
- Affected tests (`steer`, `goal-tick-cold-recovery`,
`multi-agent-runtime`,
`model-discovery`): 41 pass / 4 fail — the 4 failures are pre-existing
runtime-behavior leftovers from the restructure (baseline was 8 failures
on
the same files), unrelated to typecheck errors and noted for follow-up.
---
.gitignore | 4 -
README.md | 211 +-
README.zh.md | 206 +-
bun.lock | 9 +-
design/README.md | 310 +--
.../components/deepagent-settings-ux.test.ts | 20 +-
.../components/deepagent/approval-control.tsx | 66 -
.../deepagent/goal-plan-edit-dialog.tsx | 164 --
.../deepagent/goal-start-button.tsx | 123 --
.../components/deepagent/goal-status-bar.tsx | 63 +-
.../src/components/deepagent/panel-button.tsx | 144 +-
.../deepagent/panel-goal-contract.test.ts | 62 +-
.../components/deepagent/panel-goal.api.ts | 101 +-
.../deepagent/panel-verdict-dialog.tsx | 25 +-
.../components/dialog-connect-provider.tsx | 7 +-
.../components/dialog-custom-provider-form.ts | 265 +--
.../components/dialog-custom-provider.test.ts | 312 +--
.../src/components/dialog-custom-provider.tsx | 312 +--
.../src/components/im/group-chat-panel.tsx | 58 +-
.../src/components/im/message-composer.tsx | 43 +-
.../app/src/components/im/message-item.tsx | 12 -
.../app/src/components/im/message-list.tsx | 3 -
.../app/src/components/im/message-search.tsx | 98 -
.../app/src/components/im/thread-view.tsx | 113 --
packages/app/src/components/im/types.ts | 16 -
packages/app/src/components/prompt-input.tsx | 147 +-
.../prompt-input/scenario-toggle.tsx | 2 +-
.../components/prompt-input/submit.test.ts | 93 +-
.../app/src/components/prompt-input/submit.ts | 79 +-
.../components/review/dialog-review.api.ts | 3 -
.../src/components/review/dialog-review.tsx | 37 +-
.../src/components/session/session-header.tsx | 6 +-
.../src/components/settings-v2/general.tsx | 38 +
.../components/settings-v2/import-history.tsx | 30 +-
.../src/components/settings-v2/providers.tsx | 30 +-
.../src/components/settings-v2/servers.tsx | 24 +-
.../components/settings-v2/settings-v2.css | 13 -
.../src/components/status-popover-body.tsx | 119 +-
.../app/src/components/status-popover.tsx | 76 +-
packages/app/src/components/terminal.tsx | 14 +-
packages/app/src/context/directory-sync.ts | 2 +-
.../app/src/context/global-sync/bootstrap.ts | 14 +-
.../app/src/context/global-sync/queue.test.ts | 20 -
packages/app/src/context/global-sync/queue.ts | 2 -
packages/app/src/context/layout.tsx | 41 +-
packages/app/src/context/notification.tsx | 30 +-
.../context/permission-auto-respond.test.ts | 35 +-
.../src/context/permission-auto-respond.ts | 16 -
packages/app/src/context/permission.tsx | 138 +-
packages/app/src/context/server-sync.tsx | 26 +-
packages/app/src/context/server.test.ts | 17 -
packages/app/src/context/server.tsx | 36 +-
packages/app/src/i18n/ar.ts | 6 -
packages/app/src/i18n/br.ts | 6 -
packages/app/src/i18n/bs.ts | 6 -
packages/app/src/i18n/da.ts | 6 -
packages/app/src/i18n/de.ts | 6 -
packages/app/src/i18n/en.ts | 165 +-
packages/app/src/i18n/es.ts | 6 -
packages/app/src/i18n/fr.ts | 6 -
packages/app/src/i18n/ja.ts | 6 -
packages/app/src/i18n/ko.ts | 6 -
packages/app/src/i18n/no.ts | 6 -
packages/app/src/i18n/pl.ts | 6 -
packages/app/src/i18n/ru.ts | 6 -
packages/app/src/i18n/th.ts | 6 -
packages/app/src/i18n/tr.ts | 6 -
packages/app/src/i18n/uk.ts | 6 -
packages/app/src/i18n/zh.ts | 161 +-
packages/app/src/i18n/zht.ts | 154 --
packages/app/src/pages/directory-layout.tsx | 56 +-
packages/app/src/pages/error.tsx | 27 +-
packages/app/src/pages/layout.tsx | 37 +-
.../app/src/pages/layout/sidebar-shell.tsx | 24 +-
packages/app/src/pages/session.tsx | 14 +-
.../composer/session-composer-region.tsx | 16 -
.../pages/session/im-panel-helpers.test.ts | 4 +-
.../app/src/pages/session/im-panel-helpers.ts | 29 +-
.../src/pages/session/session-side-panel.tsx | 559 +++---
.../app/src/pages/session/side-panel-im.tsx | 112 +-
.../src/pages/session/side-panel-plugins.tsx | 6 +-
.../src/pages/session/side-panel-profile.tsx | 2 +-
.../pages/session/use-session-commands.tsx | 3 +-
packages/app/src/utils/agent.ts | 4 -
packages/app/src/utils/im-client.ts | 76 +-
packages/cli/package.json | 7 +-
packages/cli/src/ambient.d.ts | 8 +
packages/cli/src/commands/commands.ts | 221 ++-
packages/cli/src/commands/handlers/acp.ts | 55 +
.../cli/src/commands/handlers/agent/list.ts | 25 +
packages/cli/src/commands/handlers/attach.ts | 48 +
.../cli/src/commands/handlers/auth/list.ts | 45 +
.../cli/src/commands/handlers/auth/login.ts | 82 +
.../cli/src/commands/handlers/auth/logout.ts | 30 +
packages/cli/src/commands/handlers/db.ts | 33 +
packages/cli/src/commands/handlers/db/path.ts | 10 +
.../commands/handlers/deepagent/env-facts.ts | 49 +
.../src/commands/handlers/deepagent/goal.ts | 40 +
.../src/commands/handlers/deepagent/packs.ts | 35 +
.../src/commands/handlers/deepagent/panel.ts | 25 +
.../src/commands/handlers/deepagent/review.ts | 40 +
.../src/commands/handlers/deepagent/util.ts | 47 +
.../src/commands/handlers/deepagent/wiki.ts | 56 +
packages/cli/src/commands/handlers/default.ts | 42 +-
packages/cli/src/commands/handlers/export.ts | 103 +
packages/cli/src/commands/handlers/github.ts | 19 +
packages/cli/src/commands/handlers/import.ts | 83 +
packages/cli/src/commands/handlers/mcp/add.ts | 52 +
.../cli/src/commands/handlers/mcp/list.ts | 63 +
packages/cli/src/commands/handlers/models.ts | 37 +
packages/cli/src/commands/handlers/pr.ts | 60 +
packages/cli/src/commands/handlers/run.ts | 216 ++
packages/cli/src/commands/handlers/serve.ts | 60 +-
.../src/commands/handlers/session/delete.ts | 14 +
.../cli/src/commands/handlers/session/list.ts | 78 +
packages/cli/src/commands/handlers/stats.ts | 57 +
.../cli/src/commands/handlers/uninstall.ts | 93 +
packages/cli/src/commands/handlers/upgrade.ts | 43 +
packages/cli/src/commands/handlers/web.ts | 37 +
packages/cli/src/index.ts | 38 +
packages/cli/src/services/daemon.ts | 2 +-
packages/cli/src/services/session-client.ts | 50 +
packages/cli/src/tui.ts | 97 +-
packages/cli/tsconfig.json | 5 +-
packages/core/src/agent-gateway.ts | 23 +-
packages/core/src/agent.ts | 10 +-
.../core/src/control-plane/move-session.ts | 42 +-
.../core/src/deepagent/background-learning.ts | 16 +-
packages/core/src/deepagent/code-indexer.ts | 19 -
.../core/src/deepagent/context-admission.ts | 5 +-
packages/core/src/deepagent/context/config.ts | 8 +
.../core/src/deepagent/context/curator.ts | 122 ++
packages/core/src/deepagent/context/index.ts | 12 +-
packages/core/src/deepagent/context/ingest.ts | 249 +++
.../core/src/deepagent/context/token-meter.ts | 67 +
.../core/src/deepagent/context/working-set.ts | 139 ++
packages/core/src/deepagent/document-store.ts | 34 +-
packages/core/src/deepagent/graph-query.ts | 6 -
.../core/src/deepagent/knowledge-source.ts | 16 -
packages/core/src/deepagent/orchestration.ts | 34 +-
packages/core/src/deepagent/prompt-policy.ts | 149 +-
packages/core/src/global.ts | 40 +-
packages/core/src/im/agent-executor.ts | 8 +-
packages/core/src/im/agent-list-provider.ts | 10 +-
packages/core/src/im/agent-orchestrator.ts | 27 +-
packages/core/src/im/builtin-agents.ts | 4 +-
packages/core/src/im/id.ts | 10 -
packages/core/src/im/mention-parser.ts | 12 -
packages/core/src/im/repository.ts | 533 +----
packages/core/src/im/sql.ts | 50 +-
packages/core/src/im/websocket.ts | 4 +-
packages/core/src/plugin/skill.ts | 2 +-
.../plugin/skill/customize-deepagent-code.md | 6 +-
packages/core/src/public/deepagent-code.ts | 130 ++
packages/core/src/public/index.ts | 1 +
packages/core/src/session/compaction.ts | 40 +-
packages/core/src/session/runner/llm.ts | 7 -
packages/core/src/session/sql.ts | 9 -
packages/core/src/util/identifier.ts | 33 +-
packages/core/src/v1/config/config.ts | 18 +
packages/core/src/v1/config/migrate.ts | 2 +-
packages/core/src/v1/config/provider.ts | 9 +-
packages/core/test/agent.test.ts | 2 +-
packages/core/test/database-migration.test.ts | 90 -
.../deepagent/background-learning.test.ts | 47 -
.../core/test/deepagent/code-indexer.test.ts | 42 +-
packages/core/test/deepagent/context.test.ts | 278 ++-
.../deepagent/domain-pack-registry.test.ts | 15 -
.../core/test/deepagent/graph-query.test.ts | 25 -
.../core/test/deepagent/orchestration.test.ts | 54 +-
.../core/test/deepagent/prompt-policy.test.ts | 189 --
.../session-state-panel-goal.test.ts | 28 -
.../core/test/deepagent/world-state.test.ts | 40 -
packages/core/test/global.test.ts | 8 -
.../core/test/im-agent-reply-sink.test.ts | 2 +-
packages/core/test/im-b3.test.ts | 385 ----
packages/core/test/im-e2e.test.ts | 19 +-
packages/core/test/im-integration.test.ts | 2 -
packages/core/test/im-orchestrator.test.ts | 158 +-
.../test/im-unified-context-graph.test.ts | 6 +-
packages/core/test/move-session.test.ts | 62 -
.../core/test/public-deepagent-code.test.ts | 177 ++
packages/core/test/security-resolvers.test.ts | 6 -
packages/core/test/session-compaction.test.ts | 19 -
.../core/test/session-runner-recorded.test.ts | 8 +-
packages/core/test/session-runner.test.ts | 14 +-
packages/deepagent-code/package.json | 5 -
packages/deepagent-code/script/build-node.ts | 16 +-
packages/deepagent-code/specs/v2/api.ts | 67 +
packages/deepagent-code/src/acp/service.ts | 2 +-
packages/deepagent-code/src/agent/agent.ts | 72 +-
.../src/cli/cmd/run/runtime.lifecycle.ts | 2 +-
.../deepagent-code/src/cli/cmd/uninstall.ts | 21 +-
packages/deepagent-code/src/config/config.ts | 38 +-
.../deepagent-code/src/config/tui-migrate.ts | 3 -
.../src/effect/runtime-flags.ts | 140 +-
.../src/im/agent-executor-server.ts | 93 +-
.../src/im/agent-reply-sink-server.ts | 13 +-
.../deepagent-code/src/import/map/events.ts | 2 +-
packages/deepagent-code/src/panel/arbiter.ts | 4 +-
.../deepagent-code/src/plugin/openai/codex.ts | 15 +-
.../src/provider/discovery-cache.ts | 3 +-
.../src/provider/model-discovery.ts | 68 +-
.../deepagent-code/src/provider/provider.ts | 177 +-
.../deepagent-code/src/provider/transform.ts | 10 +-
.../src/server/routes/instance/httpapi/api.ts | 4 -
.../instance/httpapi/groups/deepagent.ts | 214 +-
.../routes/instance/httpapi/groups/global.ts | 9 -
.../routes/instance/httpapi/groups/im.ts | 185 +-
.../instance/httpapi/groups/oversight.ts | 212 --
.../instance/httpapi/groups/provider.ts | 17 -
.../routes/instance/httpapi/groups/session.ts | 16 +-
.../routes/instance/httpapi/groups/webhook.ts | 178 --
.../instance/httpapi/handlers/deepagent.ts | 197 +-
.../instance/httpapi/handlers/global.ts | 8 -
.../routes/instance/httpapi/handlers/im.ts | 406 +---
.../instance/httpapi/handlers/oversight.ts | 159 --
.../instance/httpapi/handlers/provider.ts | 28 +-
.../instance/httpapi/handlers/session.ts | 88 +-
.../instance/httpapi/handlers/webhook.ts | 234 ---
.../server/routes/instance/httpapi/server.ts | 66 -
.../src/session/code-index-trigger.ts | 47 +-
.../deepagent-code/src/session/compaction.ts | 7 +-
.../deepagent-code/src/session/llm/request.ts | 143 +-
.../src/session/multi-agent-runtime.ts | 57 -
.../deepagent-code/src/session/overflow.ts | 199 +-
.../deepagent-code/src/session/processor.ts | 7 -
packages/deepagent-code/src/session/prompt.ts | 605 +-----
.../deepagent-code/src/session/reminders.ts | 45 +-
.../deepagent-code/src/session/run-state.ts | 12 +-
packages/deepagent-code/src/session/tools.ts | 139 +-
packages/deepagent-code/src/settings/store.ts | 26 +-
packages/deepagent-code/src/skill/index.ts | 2 +-
packages/deepagent-code/src/tool/plan.ts | 2 +-
.../deepagent-code/src/tool/shell/prompt.ts | 4 +-
.../src/wiki/execution-archiver.ts | 160 ++
.../deepagent-code/src/wiki/search-index.ts | 51 +-
.../src/wiki/session-archive.ts | 88 +-
.../deepagent-code/test/agent/agent.test.ts | 99 +-
.../test/agent/goal-mode-agent.test.ts | 25 +-
.../test/cli/cmd/tui/attention.test.ts | 6 +-
.../deepagent-code/test/cli/error.test.ts | 2 +-
.../test/cli/github-remote.test.ts | 21 +-
.../__snapshots__/help-snapshots.test.ts.snap | 2 -
.../test/cli/serve/prompt-prepare.test.ts | 50 +-
.../deepagent-code/test/config/config.test.ts | 12 +-
.../test/deepagent/debug-tool-d3-d4.test.ts | 6 +-
.../test/deepagent/plan-status-cache.test.ts | 293 ---
.../test/deepagent/request-prep.test.ts | 83 +-
.../test/effect/runtime-flags.test.ts | 64 -
.../test/im/agent-executor-server.test.ts | 5 -
.../im/agent-list-provider-server.test.ts | 221 +--
.../deepagent-code/test/plugin/codex.test.ts | 20 -
packages/deepagent-code/test/preload.ts | 9 -
.../test/provider/model-discovery.test.ts | 54 +-
.../test/provider/provider.test.ts | 195 +-
.../test/provider/transform.test.ts | 29 -
.../test/server/httpapi-im-agent.test.ts | 8 +-
.../test/server/httpapi-instance.test.ts | 114 --
.../test/server/httpapi-listen.test.ts | 7 +-
.../test/server/httpapi-sdk.test.ts | 31 +-
.../test/server/httpapi-ui.test.ts | 18 +-
.../test/server/httpapi-webhook.test.ts | 324 ---
.../test/server/session-actions.test.ts | 9 +-
.../test/session/code-index-trigger.test.ts | 26 +-
.../test/session/context-ledger.test.ts | 21 -
.../test/session/goal-driver.test.ts | 89 +-
.../session/goal-loop-bcd-integration.test.ts | 24 +-
.../test/session/goal-steer.test.ts | 335 ----
.../session/goal-tick-cold-recovery.test.ts | 5 +-
.../test/session/llm-native-recorded.test.ts | 20 -
.../deepagent-code/test/session/llm.test.ts | 678 +++----
.../test/session/multi-agent-runtime.test.ts | 72 -
.../test/session/overflow.test.ts | 298 ---
.../test/session/processor-effect.test.ts | 4 +-
.../test/session/prompt.test.ts | 49 +-
.../test/session/snapshot-tool-race.test.ts | 2 -
.../deepagent-code/test/session/steer.test.ts | 240 ---
.../test/session/v4-integration.test.ts | 2 -
.../test/settings/store.test.ts | 27 -
.../deepagent-code/test/tool/skill.test.ts | 7 -
.../test/wiki/execution-archiver.test.ts | 145 ++
.../test/wiki/search-index.test.ts | 15 -
.../test/wiki/session-archive-wiring.test.ts | 109 -
packages/desktop/electron-builder.config.ts | 11 +-
packages/desktop/package.json | 2 +-
packages/desktop/scripts/prebuild.ts | 8 -
.../domain-packs/code/architecture/README.md | 2 +-
.../architecture-astronomy.json | 2 +-
.../failure_dossiers/shared-everything.json | 2 +-
.../documents/knowledge/adr-not-spec.json | 2 +-
.../architecture-is-change-cost.json | 2 +-
.../knowledge/shared-kernel-risk.json | 2 +-
.../methodologies/adr-materialization.json | 2 +-
.../methodologies/architecture-scan.json | 2 +-
.../methodologies/cycle-risk-review.json | 2 +-
.../skills/produce-architecture-map.json | 2 +-
.../strategies/boundary-ownership.json | 2 +-
.../documents/strategies/decision-record.json | 2 +-
.../strategies/dependency-direction.json | 2 +-
.../strategies/small-surface-integration.json | 2 +-
.../domain-packs/code/benchmarking/README.md | 2 +-
.../changed-workload-comparison.json | 2 +-
.../failure_dossiers/single-run-claim.json | 2 +-
.../benchmark-needs-correctness.json | 2 +-
.../environment-is-part-of-result.json | 2 +-
.../knowledge/thresholds-prevent-noise.json | 2 +-
.../bench-harness-validation.json | 2 +-
.../benchmark-regression-protocol.json | 2 +-
.../methodologies/outlier-review.json | 2 +-
.../skills/run-benchmark-protocol.json | 2 +-
.../documents/strategies/baseline-lock.json | 2 +-
.../strategies/regression-threshold.json | 2 +-
.../strategies/repeat-and-variance.json | 2 +-
.../documents/strategies/warmup-control.json | 2 +-
packages/domain-packs/code/c/README.md | 2 +-
.../knowledge/manual-memory-contract.json | 2 +-
.../domain-packs/code/ci-triage/README.md | 2 +-
.../failure_dossiers/last-error-only.json | 2 +-
.../rerun-without-evidence.json | 2 +-
.../knowledge/ci-is-evidence-source.json | 2 +-
.../knowledge/skipped-checks-are-risk.json | 2 +-
.../knowledge/workflow-changes-are-code.json | 2 +-
.../methodologies/ci-evidence-adapter.json | 2 +-
.../methodologies/matrix-failure-slice.json | 2 +-
.../regression-classification.json | 2 +-
.../documents/skills/classify-ci-failure.json | 2 +-
.../documents/skills/parse-ci-log.json | 2 +-
.../strategies/ci-diff-correlation.json | 2 +-
.../strategies/first-failing-command.json | 2 +-
.../strategies/provider-neutral-status.json | 2 +-
.../strategies/retry-once-for-flake.json | 2 +-
packages/domain-packs/code/cpp/README.md | 2 +-
.../knowledge/abi-and-ownership.json | 2 +-
.../strategy-before-diagnosis.json | 2 +-
.../diagnosis-result-normalization.json | 2 +-
.../evidence-first-diagnosis.json | 2 +-
.../skills/collect-diagnostic-evidence.json | 2 +-
.../domain-packs/code/environment/README.md | 2 +-
.../failure_dossiers/fix-without-check.json | 2 +-
.../failure_dossiers/hung-health-check.json | 2 +-
.../knowledge/auto-fix-needs-retest.json | 2 +-
.../knowledge/environment-evidence-local.json | 2 +-
.../documents/knowledge/skip-not-fail.json | 2 +-
.../environment-drift-check.json | 2 +-
.../methodologies/restart-health-check.json | 2 +-
.../methodologies/smoke-test-loop.json | 2 +-
.../documents/skills/run-smoke-check.json | 2 +-
.../strategies/doctor-before-fix.json | 2 +-
.../strategies/idempotent-bootstrap.json | 2 +-
.../strategies/test-before-auto-fix.json | 2 +-
.../strategies/timeout-external-checks.json | 2 +-
.../code/frontend.accessibility/README.md | 2 +-
.../failure_dossiers/aria-coverup.json | 2 +-
.../failure_dossiers/focus-lost-on-close.json | 2 +-
.../knowledge/accessible-name-source.json | 2 +-
.../knowledge/aria-does-not-add-behavior.json | 2 +-
.../knowledge/focus-visible-required.json | 2 +-
.../accessibility-runtime-pass.json | 2 +-
.../contrast-and-motion-check.json | 2 +-
.../methodologies/focus-management-check.json | 2 +-
.../documents/skills/run-a11y-smoke.json | 2 +-
.../a11y-regression-in-browser.json | 2 +-
.../strategies/keyboard-complete.json | 2 +-
.../documents/strategies/name-role-state.json | 2 +-
.../strategies/prefer-native-semantics.json | 2 +-
.../code/frontend.performance/README.md | 2 +-
.../failure_dossiers/bundle-only-claim.json | 2 +-
.../unbounded-third-party.json | 2 +-
.../documents/knowledge/hydration-cost.json | 2 +-
.../knowledge/layout-shift-user-impact.json | 2 +-
.../knowledge/third-party-script-risk.json | 2 +-
.../methodologies/bundle-delta-review.json | 2 +-
.../methodologies/frontend-profile-pass.json | 2 +-
.../methodologies/web-vitals-regression.json | 2 +-
.../skills/capture-browser-performance.json | 2 +-
.../documents/strategies/asset-budget.json | 2 +-
.../strategies/avoid-render-churn.json | 2 +-
.../strategies/measure-user-path.json | 2 +-
.../strategies/profile-long-tasks.json | 2 +-
packages/domain-packs/code/go/README.md | 2 +-
.../knowledge/context-cancellation.json | 2 +-
packages/domain-packs/code/java-jvm/README.md | 2 +-
.../documents/knowledge/build-tool-scope.json | 2 +-
packages/domain-packs/code/mcp/README.md | 2 +-
.../tool-with-broad-shell.json | 2 +-
.../unstructured-tool-result.json | 2 +-
.../knowledge/external-tool-supply-chain.json | 2 +-
.../tool-output-needs-structure.json | 2 +-
.../knowledge/tool-schema-is-contract.json | 2 +-
.../methodologies/external-tool-review.json | 2 +-
.../methodologies/mcp-server-validation.json | 2 +-
.../methodologies/tool-audit-flow.json | 2 +-
.../documents/skills/validate-mcp-tool.json | 2 +-
.../strategies/artifact-backed-tools.json | 2 +-
.../strategies/lazy-load-external-tools.json | 2 +-
.../strategies/permission-minimal-tools.json | 2 +-
.../strategies/tool-contract-first.json | 2 +-
.../domain-packs/code/migration/README.md | 2 +-
.../failure_dossiers/big-bang-migration.json | 2 +-
.../codemod-without-review.json | 2 +-
.../knowledge/codemods-overmatch.json | 2 +-
.../knowledge/migration-is-stateful.json | 2 +-
.../version-upgrade-breaks-transitives.json | 2 +-
.../methodologies/codemod-safety-loop.json | 2 +-
.../methodologies/compat-cleanup-gate.json | 2 +-
.../migration-precheck-plan-execute.json | 2 +-
.../skills/run-migration-inventory.json | 2 +-
.../documents/strategies/compat-first.json | 2 +-
.../strategies/deprecation-window.json | 2 +-
.../strategies/inventory-before-codemod.json | 2 +-
.../documents/strategies/phase-upgrade.json | 2 +-
packages/domain-packs/code/mobile/README.md | 2 +-
.../failure_dossiers/online-only-mobile.json | 2 +-
.../permission-happy-path-only.json | 2 +-
.../app-lifecycle-breaks-assumptions.json | 2 +-
.../knowledge/device-data-privacy.json | 2 +-
.../mobile-permissions-are-state.json | 2 +-
.../methodologies/mobile-smoke-matrix.json | 2 +-
.../methodologies/offline-sync-check.json | 2 +-
.../methodologies/permission-flow-check.json | 2 +-
.../skills/run-mobile-flow-check.json | 2 +-
.../strategies/lifecycle-aware-state.json | 2 +-
.../strategies/offline-and-retry.json | 2 +-
.../strategies/permission-before-use.json | 2 +-
.../strategies/privacy-by-device-data.json | 2 +-
.../domain-packs/code/observability/README.md | 2 +-
.../failure_dossiers/log-everything.json | 2 +-
.../metric-without-action.json | 2 +-
.../knowledge/high-cardinality-cost.json | 2 +-
.../documents/knowledge/logs-can-leak.json | 2 +-
.../documents/knowledge/traces-show-path.json | 2 +-
.../methodologies/incident-evidence-pack.json | 2 +-
.../methodologies/instrumentation-review.json | 2 +-
.../methodologies/trace-search-flow.json | 2 +-
.../skills/build-diagnostic-timeline.json | 2 +-
.../strategies/correlation-first.json | 2 +-
.../strategies/metric-with-owner.json | 2 +-
.../documents/strategies/safe-telemetry.json | 2 +-
.../documents/strategies/structured-logs.json | 2 +-
.../domain-packs/code/performance/README.md | 2 +-
.../average-only-performance.json | 2 +-
.../optimize-without-baseline.json | 2 +-
.../cache-invalidates-complexity.json | 2 +-
.../knowledge/io-bound-vs-cpu-bound.json | 2 +-
.../knowledge/microbench-not-product.json | 2 +-
.../allocation-regression-check.json | 2 +-
.../methodologies/latency-budget-review.json | 2 +-
.../profile-baseline-patch-compare.json | 2 +-
.../skills/compare-performance-result.json | 2 +-
.../documents/skills/run-profile-pass.json | 2 +-
.../strategies/hot-path-minimality.json | 2 +-
.../strategies/measure-before-optimizing.json | 2 +-
.../strategies/tail-latency-awareness.json | 2 +-
.../strategies/tradeoff-explicitness.json | 2 +-
packages/domain-packs/code/python/README.md | 2 +-
.../documents/knowledge/venv-is-boundary.json | 2 +-
packages/domain-packs/code/refactor/README.md | 2 +-
.../behavior-change-called-refactor.json | 2 +-
.../failure_dossiers/extract-everything.json | 2 +-
.../documents/knowledge/abstraction-cost.json | 2 +-
.../refactor-can-change-behavior.json | 2 +-
.../tests-define-refactor-safety.json | 2 +-
.../methodologies/caller-impact-scan.json | 2 +-
.../methodologies/refactor-safety-loop.json | 2 +-
.../methodologies/rename-move-check.json | 2 +-
.../skills/scan-refactor-callers.json | 2 +-
.../behavior-preserving-contract.json | 2 +-
.../strategies/call-graph-before-move.json | 2 +-
.../strategies/name-real-concept.json | 2 +-
.../strategies/small-batch-refactor.json | 2 +-
packages/domain-packs/code/release/README.md | 2 +-
.../failure_dossiers/no-rollback-plan.json | 2 +-
.../publish-before-artifact-check.json | 2 +-
.../knowledge/changelog-user-impact.json | 2 +-
.../knowledge/green-tests-not-release.json | 2 +-
.../published-artifacts-persist.json | 2 +-
.../methodologies/artifact-validation.json | 2 +-
.../methodologies/post-release-monitor.json | 2 +-
.../methodologies/pre-release-gate.json | 2 +-
.../skills/build-release-evidence.json | 2 +-
.../strategies/release-evidence-bundle.json | 2 +-
.../strategies/rollback-before-publish.json | 2 +-
.../strategies/supply-chain-release-gate.json | 2 +-
.../strategies/version-contract.json | 2 +-
.../review-without-evidence-audit.json | 2 +-
.../independent-review-worker-profile.json | 2 +-
.../skills/produce-blocker-summary.json | 2 +-
.../evidence-completeness-review.json | 2 +-
packages/domain-packs/code/rust/README.md | 2 +-
.../documents/knowledge/ownership-is-api.json | 2 +-
packages/domain-packs/code/sdk/README.md | 2 +-
.../breaking-change-as-fix.json | 2 +-
.../hand-edit-generated-sdk.json | 2 +-
.../knowledge/defaults-are-contract.json | 2 +-
.../knowledge/generated-do-not-hand-edit.json | 2 +-
.../knowledge/sdk-errors-are-api.json | 2 +-
.../sdk-breaking-change-review.json | 2 +-
.../methodologies/sdk-contract-test.json | 2 +-
.../methodologies/sdk-doc-example-pass.json | 2 +-
.../skills/validate-sdk-contract.json | 2 +-
.../compat-shim-before-removal.json | 2 +-
.../documents/strategies/example-as-test.json | 2 +-
.../strategies/generated-client-contract.json | 2 +-
.../documents/strategies/semver-surface.json | 2 +-
packages/domain-packs/code/shell/README.md | 2 +-
.../knowledge/quoting-is-correctness.json | 2 +-
packages/domain-packs/code/sql/README.md | 2 +-
.../knowledge/query-plan-matters.json | 2 +-
packages/domain-packs/code/swift/README.md | 2 +-
.../knowledge/main-actor-boundary.json | 2 +-
.../validation-without-artifacts.json | 2 +-
.../acceptance-criteria-gate.json | 2 +-
.../executable-validation-contract.json | 2 +-
.../replayable-scenario-task.json | 2 +-
.../skills/normalize-validation-result.json | 2 +-
.../old-effect-api-memory.json | 2 +-
.../knowledge/effect-source-of-truth.json | 2 +-
.../effect-service-validation-loop.json | 2 +-
.../documents/skills/run-effect-test.json | 2 +-
.../strategies/effect-schema-boundaries.json | 2 +-
.../domain-packs/platform/cloud/README.md | 2 +-
.../failure_dossiers/broad-iam-fix.json | 2 +-
.../failure_dossiers/deploy-to-debug.json | 2 +-
.../knowledge/cloud-errors-often-policy.json | 2 +-
.../knowledge/cloud-logs-sensitive.json | 2 +-
.../resource-names-are-contract.json | 2 +-
.../methodologies/cloud-failure-triage.json | 2 +-
.../methodologies/cloud-preflight.json | 2 +-
.../documents/methodologies/iac-review.json | 2 +-
.../skills/cloud-preflight-report.json | 2 +-
.../strategies/cost-and-quota-awareness.json | 2 +-
.../strategies/diagnostic-query-record.json | 2 +-
.../strategies/iam-least-privilege.json | 2 +-
.../strategies/preflight-before-deploy.json | 2 +-
.../domain-packs/platform/docker/README.md | 2 +-
.../failure_dossiers/copy-dot-root.json | 2 +-
.../failure_dossiers/secret-in-arg.json | 2 +-
.../knowledge/dockerignore-security.json | 2 +-
.../knowledge/layers-retain-secrets.json | 2 +-
.../knowledge/root-container-risk.json | 2 +-
.../methodologies/compose-dev-check.json | 2 +-
.../methodologies/dockerfile-review.json | 2 +-
.../methodologies/image-smoke-test.json | 2 +-
.../skills/validate-docker-build.json | 2 +-
.../strategies/healthcheck-meaningful.json | 2 +-
.../strategies/multi-stage-runtime.json | 2 +-
.../strategies/no-secrets-in-image.json | 2 +-
.../strategies/small-build-context.json | 2 +-
.../domain-packs/platform/local-dev/README.md | 2 +-
.../production-secret-local.json | 2 +-
.../failure_dossiers/server-left-running.json | 2 +-
.../env-defaults-help-onboarding.json | 2 +-
.../generated-files-need-command.json | 2 +-
.../knowledge/localhost-not-production.json | 2 +-
.../methodologies/dev-server-startup.json | 2 +-
.../methodologies/local-bootstrap-check.json | 2 +-
.../methodologies/port-conflict-triage.json | 2 +-
.../documents/skills/start-dev-smoke.json | 2 +-
.../strategies/env-file-boundary.json | 2 +-
.../strategies/port-and-process-check.json | 2 +-
.../documents/strategies/watcher-scope.json | 2 +-
.../domain-packs/risk/supply-chain/README.md | 2 +-
.../failure_dossiers/blind-install.json | 2 +-
.../failure_dossiers/license-ignored.json | 2 +-
.../script-hidden-in-lockfile.json | 2 +-
.../knowledge/generated-low-provenance.json | 2 +-
.../knowledge/license-missing-is-risk.json | 2 +-
.../knowledge/scripts-are-execution.json | 2 +-
.../dependency-incident-check.json | 2 +-
.../external-skill-promotion.json | 2 +-
.../supply-chain-rereview-gate.json | 2 +-
.../skills/review-dependency-delta.json | 2 +-
.../strategies/lockfile-delta-review.json | 2 +-
.../strategies/rereview-external-change.json | 2 +-
.../strategies/script-permission-gate.json | 2 +-
.../strategies/source-trust-tier.json | 2 +-
packages/sdk/js/script/build.ts | 4 +-
packages/sdk/js/src/gen/client/client.gen.ts | 260 +--
packages/sdk/js/src/gen/client/types.gen.ts | 16 +-
packages/sdk/js/src/gen/client/utils.gen.ts | 20 +-
.../sdk/js/src/gen/core/bodySerializer.gen.ts | 12 +-
packages/sdk/js/src/gen/core/params.gen.ts | 10 +-
.../js/src/gen/core/serverSentEvents.gen.ts | 7 +-
packages/sdk/js/src/gen/core/types.gen.ts | 2 +-
packages/sdk/js/src/gen/core/utils.gen.ts | 2 +-
packages/sdk/js/src/gen/sdk.gen.ts | 1683 ++--------------
packages/sdk/js/src/gen/types.gen.ts | 1756 ++---------------
.../sdk/js/src/v2/gen/client/client.gen.ts | 260 +--
.../sdk/js/src/v2/gen/client/types.gen.ts | 14 +-
.../sdk/js/src/v2/gen/client/utils.gen.ts | 20 +-
.../js/src/v2/gen/core/bodySerializer.gen.ts | 12 +-
packages/sdk/js/src/v2/gen/core/params.gen.ts | 10 +-
.../src/v2/gen/core/serverSentEvents.gen.ts | 7 +-
packages/sdk/js/src/v2/gen/core/types.gen.ts | 2 +-
packages/sdk/js/src/v2/gen/core/utils.gen.ts | 2 +-
packages/sdk/js/src/v2/gen/sdk.gen.ts | 1683 ++--------------
packages/sdk/js/src/v2/gen/types.gen.ts | 1756 ++---------------
.../src/feature-plugins/home/tips-view.tsx | 2 +-
.../src/feature-plugins/sidebar/footer.tsx | 5 +-
packages/tui/src/routes/session/index.tsx | 2 +-
packages/ui/src/components/icon.tsx | 22 +-
packages/ui/src/components/session-retry.tsx | 6 -
packages/ui/src/i18n/ar.ts | 2 -
packages/ui/src/i18n/br.ts | 2 -
packages/ui/src/i18n/bs.ts | 2 -
packages/ui/src/i18n/da.ts | 2 -
packages/ui/src/i18n/de.ts | 2 -
packages/ui/src/i18n/en.ts | 2 -
packages/ui/src/i18n/es.ts | 2 -
packages/ui/src/i18n/fr.ts | 2 -
packages/ui/src/i18n/ja.ts | 2 -
packages/ui/src/i18n/ko.ts | 2 -
packages/ui/src/i18n/no.ts | 2 -
packages/ui/src/i18n/pl.ts | 2 -
packages/ui/src/i18n/ru.ts | 2 -
packages/ui/src/i18n/th.ts | 2 -
packages/ui/src/i18n/tr.ts | 2 -
packages/ui/src/i18n/uk.ts | 2 -
packages/ui/src/i18n/zh.ts | 1 -
packages/ui/src/i18n/zht.ts | 1 -
packages/ui/src/v2/components/icon.tsx | 66 +-
622 files changed, 6928 insertions(+), 20650 deletions(-)
delete mode 100644 packages/app/src/components/deepagent/approval-control.tsx
delete mode 100644 packages/app/src/components/deepagent/goal-plan-edit-dialog.tsx
delete mode 100644 packages/app/src/components/deepagent/goal-start-button.tsx
delete mode 100644 packages/app/src/components/im/message-search.tsx
delete mode 100644 packages/app/src/components/im/thread-view.tsx
create mode 100644 packages/cli/src/ambient.d.ts
create mode 100644 packages/cli/src/commands/handlers/acp.ts
create mode 100644 packages/cli/src/commands/handlers/agent/list.ts
create mode 100644 packages/cli/src/commands/handlers/attach.ts
create mode 100644 packages/cli/src/commands/handlers/auth/list.ts
create mode 100644 packages/cli/src/commands/handlers/auth/login.ts
create mode 100644 packages/cli/src/commands/handlers/auth/logout.ts
create mode 100644 packages/cli/src/commands/handlers/db.ts
create mode 100644 packages/cli/src/commands/handlers/db/path.ts
create mode 100644 packages/cli/src/commands/handlers/deepagent/env-facts.ts
create mode 100644 packages/cli/src/commands/handlers/deepagent/goal.ts
create mode 100644 packages/cli/src/commands/handlers/deepagent/packs.ts
create mode 100644 packages/cli/src/commands/handlers/deepagent/panel.ts
create mode 100644 packages/cli/src/commands/handlers/deepagent/review.ts
create mode 100644 packages/cli/src/commands/handlers/deepagent/util.ts
create mode 100644 packages/cli/src/commands/handlers/deepagent/wiki.ts
create mode 100644 packages/cli/src/commands/handlers/export.ts
create mode 100644 packages/cli/src/commands/handlers/github.ts
create mode 100644 packages/cli/src/commands/handlers/import.ts
create mode 100644 packages/cli/src/commands/handlers/mcp/add.ts
create mode 100644 packages/cli/src/commands/handlers/mcp/list.ts
create mode 100644 packages/cli/src/commands/handlers/models.ts
create mode 100644 packages/cli/src/commands/handlers/pr.ts
create mode 100644 packages/cli/src/commands/handlers/run.ts
create mode 100644 packages/cli/src/commands/handlers/session/delete.ts
create mode 100644 packages/cli/src/commands/handlers/session/list.ts
create mode 100644 packages/cli/src/commands/handlers/stats.ts
create mode 100644 packages/cli/src/commands/handlers/uninstall.ts
create mode 100644 packages/cli/src/commands/handlers/upgrade.ts
create mode 100644 packages/cli/src/commands/handlers/web.ts
create mode 100644 packages/cli/src/services/session-client.ts
create mode 100644 packages/core/src/deepagent/context/curator.ts
create mode 100644 packages/core/src/deepagent/context/ingest.ts
create mode 100644 packages/core/src/deepagent/context/token-meter.ts
create mode 100644 packages/core/src/deepagent/context/working-set.ts
create mode 100644 packages/core/src/public/deepagent-code.ts
delete mode 100644 packages/core/test/deepagent/prompt-policy.test.ts
delete mode 100644 packages/core/test/im-b3.test.ts
create mode 100644 packages/core/test/public-deepagent-code.test.ts
create mode 100644 packages/deepagent-code/specs/v2/api.ts
delete mode 100644 packages/deepagent-code/src/server/routes/instance/httpapi/groups/oversight.ts
delete mode 100644 packages/deepagent-code/src/server/routes/instance/httpapi/groups/webhook.ts
delete mode 100644 packages/deepagent-code/src/server/routes/instance/httpapi/handlers/oversight.ts
delete mode 100644 packages/deepagent-code/src/server/routes/instance/httpapi/handlers/webhook.ts
create mode 100644 packages/deepagent-code/src/wiki/execution-archiver.ts
delete mode 100644 packages/deepagent-code/test/deepagent/plan-status-cache.test.ts
delete mode 100644 packages/deepagent-code/test/server/httpapi-webhook.test.ts
delete mode 100644 packages/deepagent-code/test/session/goal-steer.test.ts
delete mode 100644 packages/deepagent-code/test/session/overflow.test.ts
create mode 100644 packages/deepagent-code/test/wiki/execution-archiver.test.ts
delete mode 100644 packages/deepagent-code/test/wiki/session-archive-wiring.test.ts
diff --git a/.gitignore b/.gitignore
index b3452621..2a77479a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,10 +13,6 @@ tmp
dist
ts-dist
.turbo
-# Web/edge build artifacts (nitro .output, cloudflare .wrangler) — e.g. packages/console, packages/stats
-.output
-.wrangler
-packages/console/app/public/sitemap.xml
**/.serena
.serena/
**/.omo
diff --git a/README.md b/README.md
index 3e8d672f..da24d381 100644
--- a/README.md
+++ b/README.md
@@ -6,102 +6,68 @@
-The AI coding agent that remembers, plans, collaborates, and finishes
+AI coding agent with persistent memory and control plane
English |
- 简体中文 |
- Enterprise
+ 简体中文
-Desktop 1.4.1 · DeepAgent Core V4.1
-
---
-DeepAgent Code is an AI coding workspace for work that lasts longer than one prompt. It combines a production coding-agent runtime with durable sessions, connected project memory, live planning, code intelligence, multi-agent collaboration, and human oversight.
-
-You can ask for a small edit, guide a running task without interrupting it, hand over a migration with objective completion criteria, or bring several specialist agents into a decision. DeepAgent keeps the work coherent across turns, restarts, tools, people, and projects.
-
-## One Workspace, Three Ways to Work
-
-Choose the collaboration style that fits the task:
-
-| Mode | You provide | DeepAgent does |
-|---|---|---|
-| **Auto** | A request | Defines the objective, designs and plans as needed, then executes end to end |
-| **Loop** | A goal | Writes an editable `goal+plan.md` and advances it through plan, execute, verify, and iterate ticks |
-| **Design** | Your `goal+plan.md` | Executes your design faithfully without redefining its objective or completion criteria |
-
-Autonomy and permission are independent. Use **Read-only**, **Request approval**, or **Full access** without changing the collaboration mode.
+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.
-## Stay in Control While It Works
+The features below start from a real need — something a plain coding agent, opencode included, leaves on the table — and work down to the architecture we built to serve it.
-DeepAgent is built for active collaboration, not fire-and-forget automation.
+## What You Can Do
-- **Live steering:** send new guidance while a model turn or tool is running. The message is durably admitted and absorbed at the next safe provider-turn boundary without aborting in-flight work.
-- **Goal steering:** guidance sent to an active goal is folded into the next tick, preserving the current tool and plan state.
-- **Hot plan editing:** edit a running or paused goal. Stable step IDs, evidence, completed work, and the new plan version carry into the next tick.
-- **Explicit queueing:** queue a future activity when the instruction should begin after the current activity instead of changing it.
-- **Pause, resume, take over, or roll back:** every long-running workflow has a human control path and a durable audit trail.
+### Bring your history over — switch tools without starting from zero
-## Memory You Can Inspect and Govern
+**The need:** You've built up months of context in another agent — Codex or Claude Code — and switching tools normally means abandoning all of it: the conversations, the accumulated memory, the skills you tuned. That cost alone keeps people on tools they've outgrown.
-DeepAgent does not hide memory in an opaque prompt. Project state lives in typed, versioned documents with provenance, confidence, scope, status, and links.
+**What DeepAgent does:** One-click import of your existing history. Point it at a Codex or Claude Code installation and it hot-imports your chat sessions, memory, and skills straight into DeepAgent — reading each tool's on-disk format, normalizing it, and replaying it into the document graph so imported conversations behave like native ones. Secrets are redacted on the way in, imported projects stay isolated so nothing collides with your active work, and re-running an import converges instead of duplicating. Available from the Settings "Import history" panel, a History view in the sidebar, or the `import-history` CLI command. Migration is a few minutes, not a fresh start.
-- Session-private working context stays with the current conversation.
-- Project-shared facts and decisions follow the repository.
-- User-global preferences can travel across projects.
-- Built-in skills and domain packs remain versioned system knowledge.
-- Sealed evaluator material stays audit-only and never enters model context.
+### Keep one conversation going indefinitely
-Learning follows a governed lifecycle: evidence creates a candidate, isolated review or a human decision changes its status, and regression/ablation gates publish a reproducible knowledge snapshot. Rejection reasons remain durable so discarded patterns are not silently relearned.
+**The need:** Long tasks overflow the context window. Most agents respond by truncating history or summarizing everything at a threshold — so mid-task the agent forgets a decision you made an hour ago, or the window fills with stale tool output and quality falls off a cliff.
-The **Repo & Wiki** view makes this system readable. Browse knowledge and execution archives, search across the repository, follow docs-to-code links, inspect lineage, and promote useful run evidence into governed knowledge.
+**What DeepAgent does:** Your conversation is treated as a continuously maintained work state, not a growing chat log. Before every turn, the agent rebuilds a working set — the task anchor, the most recent exchanges verbatim, active file references, and only the older facts that are relevant right now — while the full history is archived durably and stays queryable. The working set is held to a hard fraction of the model window, so there's always room for the model to actually think and respond. You just keep talking; the agent keeps focus.
-## Connected Context, Not a Larger Prompt
+### Switch windows, fork freely, never lose memory
-DeepAgent connects four views of the project:
+**The need:** You want to try two approaches, or hand the work to a fresh conversation, without starting from amnesia and without polluting the original thread.
-1. **Code graph:** files, symbols, imports, calls, diagnostics, and references.
-2. **Knowledge graph:** strategies, methodologies, facts, skills, and failure dossiers.
-3. **Project memory:** decisions, constraints, environment facts, and learned conventions.
-4. **Document graph:** plans, designs, worklogs, evaluations, run context, and evidence.
+**What DeepAgent does:** Fork any conversation from a chosen message. The fork opens carrying the parent's memory up to that point, shows a full-width "derived from" marker at the top of its transcript, and nests folder-style under its origin in the session tree (subagents and forks alike, up to three levels deep). Knowledge flows up a scope hierarchy — what one session learns can be promoted to the whole project, and cross-project preferences live at the user-global layer — so switching windows is a clean handoff, not a reset.
-The Session V2 runner assembles context from explicit sources under a durable Context Epoch. It selects linked evidence within budget, records why each reference was admitted or rejected, and preserves the current goal, constraints, decisions, open questions, next steps, and relevant files during compaction.
+### Ask roughly, and let the agent sharpen it
-Prompt caching remains effective across long runs: stable system instructions stay byte-stable, while plans, steering, budgets, round results, and other volatile state are appended in a dedicated tail block.
+**The need:** A half-formed prompt gets a half-useful answer, and you don't always know how to phrase what you want.
-## Built for Difficult Work
+**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.
-### AI IDE
+### Go deep on genuinely hard problems
-Query code by symbol and intent instead of guessing file locations. DeepAgent combines LSP definitions, references, call chains, type information, diagnostics, rename previews, and cross-file evidence. Unsaved editor buffers participate in LSP updates, so analysis follows the code you are actually editing.
+**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.
-### Domain packs
+**What DeepAgent does:** At higher work strengths the primary agent decomposes the task, fans it out to focused subagents that research modules in parallel, synthesizes their findings, and then runs independent reviewers whose job is to *break* the plan rather than agree with it. Fan-out is bounded by a configurable concurrency ceiling, and live subagents surface in a session side panel and inline in the transcript so you can watch and jump into any of them.
-Composable domain packs add language, framework, platform, hardware, business, and risk expertise without hardcoding it into the core. Packs activate from the problem profile, resolve conflicts with stricter-policy-wins semantics, and are snapshot-locked for reproducible runs.
+### Chat with your team and your agents in one place
-### Specialist agents and Expert Panel
+**The need:** Coordinating with teammates and driving agents usually happens in two different tools.
-DeepAgent can partition independent work across bounded, isolated workers. Write-capable subagents receive dedicated worktrees, return compact summaries and artifact references, and leave their full transcripts available for inspection.
+**What DeepAgent does:** A per-project group chat lives in the session side panel. @mention an agent as a chat member and it runs the full agent loop — query code, generate, fix — pulling project knowledge and recent messages for context, then replies inline with live progress streaming.
-For high-risk decisions, convene an **Expert Panel**. Correctness, security, performance, architecture, and reproducibility lenses review the same frozen question, debate anonymously for up to three rounds, and feed a deterministic arbiter that preserves minority opinions and fails closed to human review.
+## How It Works
-### Team and agent messaging
+Each capability above is served by a control-plane primitive underneath. These are the parts a plain runtime doesn't have.
-Project IM brings people and agents into the same thread. Mention an agent to start a scoped run with project context, stream its progress, inspect its artifacts, and keep the answer attached to the conversation that requested it.
+**Four-graph unification** — Code, knowledge, project memory, and the document graph are unified into one typed, bidirectionally-linked store. When the agent pulls context, a change to a symbol surfaces the design decisions, past diagnoses, and knowledge actually linked to it — connected context, not four disconnected keyword searches.
-## DeepAgent Core V4.1
+**Domain packs** — 140+ composable knowledge packages spanning languages, frameworks, platforms (cloud, Kubernetes, CI), hardware, and business/risk domains (security, privacy, compliance). Each pack bundles typed documents (strategies, methodologies, knowledge, skills, failure dossiers) with detectors that auto-activate the right packs for your task; conflicts resolve stricter-policy-wins, and the active set is version-locked so a run is reproducible. Core stays domain-neutral — expertise is data on disk, not hardcoded.
-V4.1 brings the complete DeepAgent control plane together:
+**Tiered knowledge invocation** — A monotonic strength ladder (`general → high → xhigh → max → ultra`) gates how much control-plane machinery engages. `general` stays close to the plain runtime — fast and cheap. Higher rungs progressively unlock durable knowledge, project handoff summaries, heavier strategy/methodology tiers, and multi-agent orchestration. You pay for depth only when you dial it up.
-- **Durable Session V2:** prompt admission is persisted before execution; exact retries do not duplicate user intent; same-session wakes coalesce safely.
-- **One provider-turn contract:** native and AI SDK providers share the same budget, permission, artifact, audit, learning, and close lifecycle.
-- **Single durable truth:** DocumentStore owns documents, plans, learning candidates, governance state, and version conflicts through atomic, recoverable writes.
-- **Event-driven Agent OS:** durable events, priority routing, backpressure, worker claims, leases, handoffs, retries, dead-letter recovery, and distributed placement coordinate autonomous work.
-- **Consumer-driven goals:** `goal.tick.requested` claims and executes one idempotent tick, records facts, and schedules the next tick only when the durable goal remains eligible.
-- **Human oversight:** approval queues, trace correlation, takeover, rollback, Wiki archives, notifications, and organization/workspace isolation remain part of the execution path.
-- **Secure integrations:** MCP credentials use environment references or native OS secret storage; catalog risk, runtime permissions, trusted sources, and tool capability checks fail closed.
+**Self-learning** — After work lands, the agent proposes candidate knowledge, facts, and methodologies. Promotion is evidence-gated (a test passed, a diagnostic cleared, a validation confirmed) and user-controllable — durable knowledge is carried over deliberately, not silently guessed. Session-stable conclusions consolidate into project memory over time, so the next session starts smarter about *your* codebase.
## Installation
@@ -117,67 +83,6 @@ deepagent-code
deepagent
```
-## Adding a Provider
-
-Before you can run a task, DeepAgent Code needs at least one model provider. It
-supports 75+ providers through the [AI SDK](https://ai-sdk.dev/) and
-[models.dev](https://models.dev), plus any OpenAI- or Anthropic-compatible
-endpoint. Pick whichever path fits how you work.
-
-### Desktop app (recommended)
-
-Open **Settings → Providers**:
-
-- **Official providers** (OpenAI, Anthropic, DeepSeek, Google, xAI, ZhipuAI/GLM):
- click **Connect**, paste your API key.
-- **Any other provider or gateway**: click **Connect** on *Custom provider*, paste
- the **Base URL** and **API key**. DeepAgent Code auto-detects the protocol
- (OpenAI-compatible or Anthropic) and discovers the available models from the
- endpoint's `/models` list — you don't have to fill anything else.
-
-Model specs (context window, reasoning) are auto-filled by matching each model
-against the models.dev catalog. You can reopen a custom provider to override a
-model's context/reasoning/temperature; those overrides are best-effort and not
-guaranteed to keep the model working.
-
-### Terminal
-
-```bash
-# Log in to a provider (official providers, or a plugin auth flow)
-deepagent auth login
-
-# See what's connected
-deepagent auth list
-```
-
-### Config file
-
-Providers also live in `~/.deepagent/code/config.jsonc`. A custom
-OpenAI-compatible endpoint looks like this — set `discovery: true` to have models
-refreshed from the endpoint at runtime, or list them explicitly under `models`:
-
-```jsonc
-{
- "$schema": "https://deepagent-code.ai/config.json",
- "provider": {
- "myprovider": {
- "name": "My Provider",
- "npm": "@ai-sdk/openai-compatible",
- "discovery": true,
- "options": {
- "baseURL": "https://api.myprovider.com/v1",
- "apiKey": "sk-..."
- }
- }
- }
-}
-```
-
-Official-provider keys added via the app/CLI are stored separately in
-`~/.deepagent/code/auth.json`, not in the config file. See the
-[providers guide](https://deepagent-code.ai/docs/providers/) for the full
-reference (base URL overrides, headers, per-model config, gateways).
-
## Quick Example
Start the agent and give it a task:
@@ -210,7 +115,7 @@ On your next session, when you ask to add rate limiting elsewhere, the agent alr
## Architecture
-```text
+```
┌─────────────────────────────────────────────────────────────┐
│ Control Plane (DeepAgent additions) │
│ • Four-graph unified store (code + knowledge + memory + doc)│
@@ -218,7 +123,6 @@ On your next session, when you ask to add rate limiting elsewhere, the agent alr
│ • Domain pack system (composable, auto-activating knowledge)│
│ • Context assembly & admission gates │
│ • Multi-agent orchestration & adversarial review │
-│ • Supervised goal loop & expert panel (event-driven) │
│ • Evidence-gated learning & work-strength ladder │
└─────────────────────────────────────────────────────────────┘
│
@@ -239,57 +143,24 @@ On your next session, when you ask to add rate limiting elsewhere, the agent alr
└─────────────────────────────────────────────────────────────┘
```
-The full architecture and its invariants are documented in [Architecture & Design](design/README.md).
-
-## Build From Source
-
-DeepAgent Code uses Bun 1.3.14.
-
-```bash
-git clone https://github.com/deepagent-ltd/deepagent-code.git
-cd deepagent-code
-bun install
-```
-
-Start the Desktop app:
-
-```bash
-bun run dev:desktop
-```
-
-Start the terminal experience:
-
-```bash
-bun run dev
-```
-
-Run a one-shot task:
-
-```bash
-bun run --cwd packages/deepagent-code dev run "add rate limiting to /api/users"
-```
-
-Import existing Codex or Claude Code history:
-
-```bash
-bun run --cwd packages/deepagent-code dev import-history --from codex --dry-run
-```
+DeepAgent's control plane operates at provider-turn boundaries: it selects context before each model call and writes evidence back into the document graph afterward. It does not replace opencode's runtime — it layers on top.
## Documentation
-- [Providers & Models](https://deepagent-code.ai/docs/providers/)
-- [Architecture & Design](design/README.md)
-- [Security Policy](SECURITY.md)
-- [Privacy Policy](PRIVACY.md)
-- [Contributing](CONTRIBUTING.md)
-- [Changelog](CHANGELOG.md)
+- [Architecture & Design](design/README.md) — Control plane, code intelligence, MCP security model
+- [Security Policy](SECURITY.md) — Vulnerability reporting, known limitations
+- [Privacy Policy](PRIVACY.md) — Data handling and storage
+- [Contributing](CONTRIBUTING.md) — Development setup and guidelines
+- [Changelog](CHANGELOG.md) — Release history
## License & Attribution
-DeepAgent Code is licensed under **AGPL-3.0-or-later**. If you modify and run it as a network service, you must make the corresponding source available to its users.
+DeepAgent Code is licensed under **AGPL-3.0-or-later**. If you modify and run it as a network service, you must make your source code available to users.
-DeepAgent Code is derived from [opencode](https://github.com/sst/opencode) under the MIT License. See [NOTICE](NOTICE) for upstream attribution. No endorsement by opencode or its contributors is implied.
+This project is derived from [opencode](https://github.com/sst/opencode) (MIT License). See [NOTICE](NOTICE) for the upstream license and attribution. No endorsement by opencode or its contributors is implied.
---
-Built by DeepAgent
+
+ Built by DeepAgent
+
diff --git a/README.zh.md b/README.zh.md
index 15a1714d..9d1618ab 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -6,102 +6,68 @@
-会记忆、会规划、会协作,也能把工作真正做完的 AI 编程智能体
+具备持久记忆与控制平面的 AI 编程智能体
English |
- 简体中文 |
- Enterprise 版本
+ 简体中文
-桌面版 1.4.1 · DeepAgent Core V4.1
-
---
-DeepAgent Code 是一套面向长期工作的 AI 编程工作区。它把生产级编程智能体运行时与持久会话、相互连通的项目记忆、实时计划、代码智能、多智能体协作和人类监督组合在一起。
-
-你可以让它完成一次小改动,在任务运行中继续补充指令,把一场迁移交给带客观完成判据的目标回路,或者召集多位专家共同审阅一项决策。DeepAgent 会让工作在多轮对话、进程重启、工具调用、团队成员和不同项目之间保持连贯。
-
-## 一个工作区,三种协作方式
-
-按任务选择最合适的协作方式:
-
-| 模式 | 你提供 | DeepAgent 负责 |
-|---|---|---|
-| **自动(Auto)** | 一项需求 | 自行明确目标,按需设计和规划,再端到端执行 |
-| **循环(Loop)** | 一个目标 | 生成可编辑的 `goal+plan.md`,按计划、执行、校验、迭代逐 tick 推进 |
-| **设计(Design)** | 你编写的 `goal+plan.md` | 忠实执行你的设计,不重新定义目标或完成判据 |
-
-自主程度和权限相互独立。你可以在不改变协作模式的情况下选择**只读**、**请求批准**或**完全访问**。
+DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体。它保留了 [opencode](https://github.com/sst/opencode) 的运行时基座,并在其上叠加了一层控制平面——让智能体不再像一次性的对话工具,而更像一位记得住你项目、会替你打磨模糊需求、能对硬骨头深挖到底的队友。
-## 它在工作,你仍然掌控全局
+下面的每一项特性,都从一个真实的需求出发——一个普通编程智能体(包括 opencode)尚未满足的需求——再向下讲到我们为满足它而构建的底层架构。
-DeepAgent 为持续协作而设计,不是一个发出指令后只能等待的黑盒。
+## 你可以做什么
-- **实时 Steering:** 模型或工具仍在运行时继续发送指导。消息会先持久化,再在下一个安全的供应商轮次边界被吸收,不会中断在飞工作。
-- **Goal Steering:** 发给活跃目标的指导会进入下一个 tick,同时保留当前工具状态和计划状态。
-- **运行中计划热编辑:** 编辑正在运行或已暂停的目标。稳定的步骤 ID、证据、已完成工作和新计划版本会一起进入下一 tick。
-- **显式排队:** 当一条指令应该在当前 activity 结束后独立开始时,把它放入未来队列,而不是改变当前工作。
-- **暂停、恢复、接管或回滚:** 每个长跑流程都有清晰的人类控制路径和持久审计记录。
+### 把历史一起带过来——换工具不必从零开始
-## 看得见、管得住的记忆
+**需求:** 你已经在另一个智能体里——Codex 或 Claude Code——积累了几个月的上下文,而换工具通常意味着把这一切统统丢下:那些对话、沉淀下来的记忆、你调好的技能。单是这份代价,就足以把人留在早该告别的工具上。
-DeepAgent 不会把记忆藏在不可见的提示词里。项目状态保存在带类型、版本、来源、置信度、作用域、状态和链接的文档中。
+**DeepAgent 的做法:** 一键导入你已有的历史。把它指向一个 Codex 或 Claude Code 的安装目录,它就会把你的会话、记忆和技能热导入 DeepAgent——读取各工具的本地格式,归一化后重放进文档图,让导入的对话表现得和原生对话一样。导入过程中会对密钥做脱敏,导入的项目彼此隔离、不会和你正在进行的工作冲突,重复导入会收敛而不是产生重复。入口有三处:设置里的"导入历史"面板、侧栏的历史视图,以及 `import-history` 命令行。迁移只需几分钟,而不是推倒重来。
-- 会话私有工作上下文只属于当前对话。
-- 项目共享事实与决策跟随代码仓库。
-- 用户全局偏好可以跨项目使用。
-- 内置技能与领域包保持系统级版本管理。
-- 封存的评测材料仅用于审计,永不进入模型上下文。
+### 一直对话下去,不必换新
-学习遵循可治理的生命周期:证据生成候选,隔离审阅或人工决策改变候选状态,回归与消融门发布可复现的知识快照。拒绝理由会持久保存,因此被淘汰的模式不会在后台被悄悄重新学习。
+**需求:** 长任务会撑爆上下文窗口。多数智能体的应对方式是截断历史,或在阈值处一刀切地整体摘要——于是任务进行到一半,智能体忘了你一小时前拍的板,或者窗口被陈旧的工具输出塞满,质量断崖式下跌。
-**仓库与百科(Repo & Wiki)** 让这套系统对人可读。你可以浏览知识与执行档案、搜索整个仓库、沿文档到代码的链接探索上下文、检查来源链,并把有价值的运行证据升格为受治理知识。
+**DeepAgent 的做法:** 你的对话被当作一份持续维护的工作状态,而不是不断变长的聊天记录。每一轮开始前,智能体都会重建一份工作面——任务锚点、最近若干轮的原文、活跃的文件引用,以及此刻真正相关的旧事实——同时完整历史被持久归档、随时可查。工作面被严格控制在模型窗口的一个固定比例以内,永远给模型留足思考和作答的余地。你只管一直说,智能体替你保持专注。
-## 相互连接的上下文,而不是更长的提示词
+### 随手分叉、切换窗口,记忆不丢
-DeepAgent 把项目的四个视图连接在一起:
+**需求:** 你想同时试两条思路,或把工作交给一个全新的对话,却不想从头失忆重来,也不想把原来的线程搅乱。
-1. **代码图:** 文件、符号、导入、调用、诊断与引用。
-2. **知识图:** 策略、方法论、事实、技能与故障档案。
-3. **项目记忆:** 决策、约束、环境事实与已学习的项目约定。
-4. **文档图:** 计划、设计、工作日志、评测、运行上下文与证据。
+**DeepAgent 的做法:** 从任意一条消息分叉当前对话。分叉出的新对话会继承父对话到该点为止的记忆,在时间线顶部显示一条贯穿窗口的"从对话派生"分割线,并像文件夹一样嵌套挂在来源对话之下(子 agent 与分叉同理,最多三层深)。知识沿作用域层级向上流动——单个会话学到的东西可提升到整个项目,跨项目的偏好则沉淀在用户全局层——所以换窗口是一次干净的交接,而非一次清零重来。
-Session V2 运行器在持久 Context Epoch 下从明确的 Context Source 装配上下文。它在预算内选择相互关联的证据,记录每条引用为什么被准入或拒绝,并在压缩时保留当前目标、约束、决策、开放问题、后续步骤与相关文件。
+### 想到哪问到哪,让智能体替你打磨
-长跑任务也能持续命中提示词缓存:稳定 system 指令保持字节级稳定,计划、Steering、预算、轮次结果等易变状态只追加到独立的尾部区块。
+**需求:** 半成品的提问只能换来半有用的回答,而你未必总知道该怎么把想要的东西说清楚。
-## 为困难工作而生
+**DeepAgent 的做法:** 输入框上有两种情景模式。**直接**模式原样发送你的提示——措辞由你做主。**智能**模式会把粗糙的想法打磨成更精准的提示,给出草拟的方案和决策建议,并在自动执行任何操作前等你确认。智能体替你塑形到什么程度,由你决定。
-### AI IDE
+### 对真正的难题深挖到底
-按符号和意图查询代码,不再猜文件位置。DeepAgent 组合 LSP 定义、引用、调用链、类型信息、诊断、重命名预览与跨文件证据。未保存的编辑器 buffer 也会实时进入 LSP,因此分析看到的是你正在编辑的代码。
+**需求:** 复杂的工作——一个架构决策、一次棘手的迁移、一个隐蔽的 bug——需要的不止一次自信的单程作答。它需要调研、需要第二意见、需要有人主动来挑刺。
-### 领域包
+**DeepAgent 的做法:** 在更高的工作强度下,主智能体会拆解任务,扇出给专注的子 agent 并行调研各个模块,综合它们的发现,再运行独立的审阅者——审阅者的职责是"击破"方案,而不是附和。扇出受可配置的并发上限约束;运行中的子 agent 会出现在会话侧栏面板和时间线内联卡片里,你可以旁观并随时跳进任意一个。
-可组合的领域包提供语言、框架、平台、硬件、业务与风险知识,而不把专业逻辑硬编码进内核。领域包根据问题画像自动激活,以“更严格策略优先”解决冲突,并锁定快照以保证运行可复现。
+### 团队与智能体,在同一处协作
-### 专业子智能体与 Expert Panel
+**需求:** 和队友协调、驱动智能体,通常发生在两个不同的工具里。
-DeepAgent 可以把独立工作拆分给数量有界、相互隔离的 Worker。具备写权限的子智能体获得独立 worktree,只向父会话返回紧凑摘要和工件引用,完整执行记录仍可随时查看。
+**DeepAgent 的做法:** 每个项目的群聊就在会话侧栏里。把某个智能体 @ 进来当作聊天成员,它便会跑完整的智能体回路——查代码、生成、修复——拉取项目知识与最近消息作为上下文,然后带着实时进度在群里内联回复。
-高风险决策可以召集 **Expert Panel**。正确性、安全、性能、架构与可复现性等专家视角审阅同一个冻结问题,进行最多三轮匿名辩论,再由确定性仲裁器生成裁定。少数派意见会被保留,无法安全达成一致时会失败关闭并交给人类。
+## 它是怎么做到的
-### 团队与智能体消息
+上面每一项能力,底层都由一个控制平面原语来支撑。这些正是普通运行时所没有的部分。
-项目 IM 把团队成员和智能体放进同一条讨论。@ 某个智能体即可启动有明确作用域的运行,使用项目上下文、流式展示进度、关联执行工件,并把答案留在发起任务的对话里。
+**四图合一** — 代码图、知识图、项目记忆、文档图被统一进同一个带类型、双向链接的存储。当智能体拉取上下文时,对某个符号的改动会连带浮现出与它真正相连的设计决策、过往诊断和知识——是连通的上下文,而不是四次互不相干的关键词检索。
-## DeepAgent Core V4.1
+**领域包** — 140+ 个可组合的知识包,覆盖编程语言、框架、平台(云、Kubernetes、CI)、硬件,以及业务/风险领域(安全、隐私、合规)。每个包捆绑了带类型的文档(策略、方法论、知识、技能、故障档案)与探测器,能为你的任务自动激活相应的包;冲突按"更严策略优先"消解,激活的集合会被版本锁定,从而让一次运行可复现。内核保持领域中立——专业能力是磁盘上的数据,而非写死的代码。
-V4.1 把完整的 DeepAgent 控制平面汇聚在一起:
+**知识分级调用** — 一条单调递增的强度阶梯(`general → high → xhigh → max → ultra`)决定控制平面机器启动到什么程度。`general` 贴近原生运行时——又快又省。越往上,逐级解锁持久知识、项目交接摘要、更重的策略/方法论层,以及多智能体编排。只有当你上调档位时,才为深度付费。
-- **持久 Session V2:** prompt 先持久准入、再调度执行;精确重试不会复制用户意图;同一 Session 的唤醒会安全合并。
-- **统一供应商轮次合同:** native 与 AI SDK provider 共享预算、权限、工件、审计、学习和关闭生命周期。
-- **单一持久真相:** DocumentStore 通过原子、可恢复写入统一管理文档、计划、学习候选、治理状态和版本冲突。
-- **事件驱动 Agent OS:** 持久事件、优先级路由、回压、Worker claim、租约、handoff、重试、死信恢复与分布式 placement 协调自主工作。
-- **消费者驱动 Goal:** `goal.tick.requested` 每次认领并执行一个幂等 tick,记录事实,并只在持久目标仍满足条件时调度下一 tick。
-- **人类监督:** 审批队列、全链路 trace、接管、回滚、Wiki 档案、通知,以及组织和 workspace 隔离始终位于执行路径上。
-- **安全集成:** MCP 凭据使用环境变量引用或原生操作系统 secret storage;目录风险、运行时权限、可信来源和工具 capability 逐层失败关闭。
+**自学习** — 工作落地后,智能体会提出候选的知识、事实与方法论。晋升是证据门控的(一项测试通过、一条诊断清零、一次校验确认)且由用户掌控——持久知识是被有意结转的,而非后台悄悄猜出来的。会话中稳定的结论会随时间巩固进项目记忆,于是下一次会话对*你的*代码库上手更聪明。
## 安装
@@ -117,62 +83,6 @@ deepagent-code
deepagent
```
-## 添加供应商
-
-在运行任务之前,DeepAgent Code 至少需要一个模型供应商。它通过
-[AI SDK](https://ai-sdk.dev/) 和 [models.dev](https://models.dev) 支持 75+ 家供应商,
-以及任意 OpenAI 或 Anthropic 兼容的接口。按你习惯的方式选一种即可。
-
-### 桌面应用(推荐)
-
-打开 **设置 → 供应商(Settings → Providers)**:
-
-- **官方供应商**(OpenAI、Anthropic、DeepSeek、Google、xAI、智谱/GLM):点击
- **连接**,粘贴你的 API Key。
-- **其他供应商或网关**:在「自定义供应商」上点击 **连接**,填入 **Base URL** 和
- **API Key**。DeepAgent Code 会自动探测协议(OpenAI 兼容或 Anthropic),并从接口的
- `/models` 列表自动发现可用模型——其余字段无需填写。
-
-模型规格(上下文窗口、推理能力)会通过与 models.dev 目录按模型 id 匹配来自动补全。
-你可以再次打开自定义供应商,覆盖某个模型的上下文/推理/温度;这些覆盖为尽力而为的默认值,
-修改后不保证模型仍能正常使用。
-
-### 终端
-
-```bash
-# 登录供应商(官方供应商,或插件鉴权流程)
-deepagent auth login
-
-# 查看已连接的供应商
-deepagent auth list
-```
-
-### 配置文件
-
-供应商也保存在 `~/.deepagent/code/config.jsonc` 中。一个自定义 OpenAI 兼容接口如下——
-设 `discovery: true` 让模型在运行时从接口刷新,或在 `models` 下显式列出:
-
-```jsonc
-{
- "$schema": "https://deepagent-code.ai/config.json",
- "provider": {
- "myprovider": {
- "name": "My Provider",
- "npm": "@ai-sdk/openai-compatible",
- "discovery": true,
- "options": {
- "baseURL": "https://api.myprovider.com/v1",
- "apiKey": "sk-..."
- }
- }
- }
-}
-```
-
-通过应用/CLI 添加的官方供应商密钥单独存放在 `~/.deepagent/code/auth.json`,不在配置文件里。
-完整参考(Base URL 覆盖、请求头、逐模型配置、网关)见
-[供应商文档](https://deepagent-code.ai/docs/providers/)。
-
## 快速示例
启动智能体并交给它一个任务:
@@ -205,7 +115,7 @@ deepagent-code "为 /api/users 端点添加限流"
## 架构
-```text
+```
┌─────────────────────────────────────────────────────────────┐
│ 控制平面(DeepAgent 新增) │
│ • 四图合一存储(代码 + 知识 + 记忆 + 文档) │
@@ -213,7 +123,6 @@ deepagent-code "为 /api/users 端点添加限流"
│ • 领域包系统(可组合、自动激活的知识) │
│ • 上下文装配与准入门 │
│ • 多智能体编排与对抗式审阅 │
-│ • 受监督的目标回路与专家团(事件驱动) │
│ • 证据门控的学习 + 工作强度阶梯 │
└─────────────────────────────────────────────────────────────┘
│
@@ -234,57 +143,24 @@ deepagent-code "为 /api/users 端点添加限流"
└─────────────────────────────────────────────────────────────┘
```
-完整架构与不变量见 [架构与设计](design/README.md)。
-
-## 从源码运行
-
-DeepAgent Code 使用 Bun 1.3.14。
-
-```bash
-git clone https://github.com/deepagent-ltd/deepagent-code.git
-cd deepagent-code
-bun install
-```
-
-启动桌面应用:
-
-```bash
-bun run dev:desktop
-```
-
-启动终端界面:
-
-```bash
-bun run dev
-```
-
-执行一次性任务:
-
-```bash
-bun run --cwd packages/deepagent-code dev run "为 /api/users 添加限流"
-```
-
-导入已有 Codex 或 Claude Code 历史:
-
-```bash
-bun run --cwd packages/deepagent-code dev import-history --from codex --dry-run
-```
+DeepAgent 的控制平面在供应商轮次的边界上运作:在每次模型调用前挑选上下文,调用后把证据写回文档图。它不替换 opencode 的运行时——只是叠加在其之上。
## 文档
-- [供应商与模型](https://deepagent-code.ai/docs/providers/)
-- [架构与设计](design/README.md)
-- [安全策略](SECURITY.md)
-- [隐私策略](PRIVACY.md)
-- [贡献指南](CONTRIBUTING.md)
-- [更新日志](CHANGELOG.md)
+- [架构与设计](design/README.md) — 控制平面、代码智能、MCP 安全模型
+- [安全策略](SECURITY.md) — 漏洞上报、已知限制
+- [隐私政策](PRIVACY.md) — 数据处理与存储
+- [贡献指南](CONTRIBUTING.md) — 开发环境与规范
+- [更新日志](CHANGELOG.md) — 发布历史
## 许可与署名
-DeepAgent Code 使用 **AGPL-3.0-or-later** 许可。如果你修改本项目并将其作为网络服务运行,必须向服务用户提供对应源代码。
+DeepAgent Code 采用 **AGPL-3.0-or-later** 许可。如果你修改并将其作为网络服务运行,必须向用户提供你的源代码。
-DeepAgent Code 基于 [opencode](https://github.com/sst/opencode) 的 MIT 许可代码演进而来。上游署名见 [NOTICE](NOTICE)。本项目不暗示 opencode 或其贡献者的任何背书。
+本项目衍生自 [opencode](https://github.com/sst/opencode)(MIT 许可)。完整的上游许可与署名见 [NOTICE](NOTICE)。本项目不暗示 opencode 或其贡献者的任何背书。
---
-Built by DeepAgent
+
+ 由 DeepAgent 打造
+
diff --git a/bun.lock b/bun.lock
index 82ed0bd7..5e95c744 100644
--- a/bun.lock
+++ b/bun.lock
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@deepagent-code/app",
- "version": "1.4.0",
+ "version": "1.0.0-beta",
"dependencies": {
"@codemirror/autocomplete": "6",
"@codemirror/commands": "6",
@@ -109,15 +109,20 @@
"lildax": "./bin/lildax.cjs",
},
"dependencies": {
+ "@agentclientprotocol/sdk": "^0.21.0",
"@deepagent-code/core": "workspace:*",
"@deepagent-code/sdk": "workspace:*",
"@deepagent-code/server": "workspace:*",
"@deepagent-code/tui": "workspace:*",
"@effect/platform-node": "catalog:",
"@opentui/core": "catalog:",
+ "@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@parcel/watcher": "2.5.1",
+ "deepagent-code": "workspace:*",
+ "drizzle-orm": "catalog:",
"effect": "catalog:",
+ "jsonc-parser": "^3.3.1",
"solid-js": "catalog:",
},
"devDependencies": {
@@ -344,7 +349,7 @@
},
"packages/desktop": {
"name": "@deepagent-code/desktop",
- "version": "1.4.0",
+ "version": "1.2.0",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"effect": "catalog:",
diff --git a/design/README.md b/design/README.md
index bd41fcfb..86c26ebc 100644
--- a/design/README.md
+++ b/design/README.md
@@ -1,266 +1,120 @@
-# DeepAgent Code Architecture & Design
+# DeepAgent Code — Architecture & Design
-> Public architecture baseline for DeepAgent Core V4.1.
+> **Public design overview.** Internal implementation details and roadmap docs live in the private `docs/` tree (not version-controlled). This directory contains the publicly visible architectural narrative.
-DeepAgent Code is a document-centered, event-driven AI coding system. It combines a coding-agent runtime with a durable control plane that owns context, planning, learning, collaboration, safety, and human oversight.
+---
-The architecture is designed around one requirement: a long-running agent must remain correct and governable after many model turns, tool calls, user interventions, process restarts, and worker handoffs.
+## What is DeepAgent Code?
-## Design Principles
+DeepAgent Code is an AI coding agent that adds a **control plane** on top of the [opencode](https://github.com/sst/opencode) runtime. It keeps the proven opencode foundations (runtime, tool, MCP, session, provider stack) and layers in:
-### One durable truth per concept
+- **Durable document memory** — knowledge base with retrieval gates, dedup, and merge
+- **Context assembly** — selective, evidence-backed context building (not raw file dumps)
+- **Plan system** — structured task planning with staleness detection and rollback
+- **Failure triage** — three-tier classifier (auto-fixable / needs-narrowing / not-auto-fixable)
+- **Domain adapters** — pluggable domain packs for specialized workflows
+- **AI IDE microservice** — LSP-backed semantic code navigation via `code_intel`
+- **MCP catalog** — curated, one-click-enable MCP servers with safety tiers
-Sessions, inputs, plans, documents, goals, events, approvals, and learning decisions each have one authoritative durable representation. In-memory state is a cache or an ownership hint, never a competing source of truth.
+---
-### Admission is separate from execution
+## Architectural Principles
-A user instruction is durably admitted before execution is scheduled. A successful API response therefore means the instruction is recorded, not merely present in a process-local queue.
+### 1. Enhance, don't replace
-### One provider-turn contract
+DeepAgent is built **on top of** the opencode agent/runtime/session/tool/MCP stack. It does not rewrite the execution engine, tool system, or provider layer. The default agent behavior is not degraded. The lower-strength `general` mode stays close to the inherited runtime contract.
-Native and AI SDK providers pass through the same model-turn boundary. Budget, permissions, tool policy, prompt assembly, artifacts, audit, learning, and close semantics cannot be bypassed by selecting another provider implementation.
+### 2. Control-plane only
-### Context is selected, not accumulated
+DeepAgent is responsible for **strategy / context / budget / audit / verification / document graph**. It does not directly spawn LSP processes or execute MCP tools — those go through the existing `LSP.Service` and `MCP.Service` respectively.
-The model receives a bounded working set assembled from explicit Context Sources. Full tool output and durable history remain referenceable artifacts; only admitted summaries, evidence, and snippets enter the active context.
+### 3. Full tool output does not enter context
-### Safety fails closed
+Per the deterministic task control contract: raw LSP results, diagnostic dumps, and capability indexes are written to **evidence artifacts** (ref-linked, tool-only visibility). Only summaries and `file:line` snippets appear in the model context.
-External events, credentials, tools, paths, autonomy, worker placement, and outbound messages are checked at their execution boundaries. Missing identity, trust, capability, or approval never widens access.
+### 4. Fail-closed on safety
-### Humans can always intervene
+MCP catalog entries default to **not connected** (zero startup overhead). Dangerous write operations (force-push, DROP, file delete) require explicit approval. Read-only DB connections enforce restricted-mode at the server level.
-Steering, plan editing, approval, pause, resume, takeover, rollback, and review are part of the runtime contract. They are not dashboard-only controls layered over an autonomous black box.
+---
-## System Map
+## Component Map
-```text
-┌─────────────────────────────────────────────────────────────────────┐
-│ Experience │
-│ Desktop · Web · TUI · IM · Repo & Wiki · Expert Panel · Oversight │
-└──────────────────────────────┬──────────────────────────────────────┘
- │
-┌──────────────────────────────▼──────────────────────────────────────┐
-│ Durable Session Runtime │
-│ Session V2 · System Context · Context Epoch · Steering · Queue │
-│ Prompt cache policy · Provider-turn lifecycle · Tool materialization│
-└──────────────────────────────┬──────────────────────────────────────┘
- │
-┌──────────────────────────────▼──────────────────────────────────────┐
-│ DeepAgent Control Plane │
-│ PlanController · GoalController · Context Graph · Learning │
-│ Event Router · Scheduler · Worker Pool · Handoff · Security gates │
-└──────────────────────────────┬──────────────────────────────────────┘
- │
-┌──────────────────────────────▼──────────────────────────────────────┐
-│ Durable State │
-│ DocumentStore · Session/Event database · Event Bus · Audit/Artifacts│
-└──────────────────────────────┬──────────────────────────────────────┘
- │
-┌──────────────────────────────▼──────────────────────────────────────┐
-│ Execution Services │
-│ Provider · Tool · LSP · MCP · Git/Worktree · Debug · Profile │
-└─────────────────────────────────────────────────────────────────────┘
+```
+┌─────────────────────────────────────────────────────────────┐
+│ DeepAgent Control Plane │
+│ │
+│ ┌──────────────┐ ┌─────────────┐ ┌───────────────────┐ │
+│ │ Plan System │ │ Doc Memory │ │ Failure Triage │ │
+│ │ (task/plan) │ │ (knowledge │ │ (3-tier classify)│ │
+│ └──────┬───────┘ │ store) │ └─────────┬─────────┘ │
+│ │ └──────┬──────┘ │ │
+│ ┌──────▼──────────────────▼──────────────────▼───────────┐ │
+│ │ Agent Gateway (core) │ │
+│ │ audit · budget · permission · capability index │ │
+│ └──────┬─────────────────────────────────────────────────┘ │
+└─────────│───────────────────────────────────────────────────┘
+ │
+┌─────────▼──────────────────────────────────────────────────┐
+│ opencode Foundation (unchanged) │
+│ │
+│ Session ─── Tool Registry ─── MCP Service │
+│ │ │ │ │
+│ Provider LSP Service 38 lang servers │
+│ (Claude/…) code_intel tool + MCP catalog │
+└────────────────────────────────────────────────────────────┘
```
-## Session V2
-
-Session V2 separates durable prompt admission from model execution.
-
-### Prompt admission
-
-- Each prompt creates one durable `session_input` before scheduling work.
-- Reusing a Session ID adopts that Session rather than creating a parallel execution entity.
-- Reusing a prompt message ID is accepted only for an exact retry with the same Session, content, and delivery mode.
-- Conflicting ID reuse fails instead of silently reconciling different user intent.
-- A prompt can be admitted without waking execution when the caller requests admit-only behavior.
-
-### Delivery vocabulary
-
-| Delivery | Meaning |
-|---|---|
-| normal turn | Start a normal activity when the Session is idle |
-| `steer` | Add guidance to the active activity at the next safe provider-turn boundary |
-| `goal_steer` | Add guidance to the next Goal tick |
-| `queue` | Open a future FIFO activity after the active activity settles |
-| interrupt | Target the active process-local ownership chain immediately |
-
-Steering never aborts an in-flight tool or stream. The input is persisted first, absorbed in stable order, and materialized into history exactly once.
-
-### Execution ownership
-
-`SessionExecution` is process-global and keyed by Session ID. A drain discovers placement from durable Session location only when it starts. SessionRunner, model resolution, tools, permissions, and filesystem services remain Location-scoped.
-
-Same-Session resumes join one coordinator; advisory wakes coalesce; different Sessions can run concurrently. Every provider turn performs one explicit `llm.stream(request)` call and reloads projected history before durable continuation.
-
-### Prompt cache invariant
-
-The system prompt is split into a byte-stable prefix and one append-only volatile tail:
-
-- Agent instructions, stable policy, and System Context baseline stay in the prefix.
-- Round state, budgets, plan snapshots, prior results, fan-out decisions, and steering stay in the tail.
-- OpenAI-compatible providers use a stable Session cache key; providers with cache markers use their protocol-native breakpoint.
-- Prefix hash and cache outcomes distinguish normal compaction misses from accidental prefix drift.
-
-## Document System
-
-DocumentStore is the durable body for DeepAgent state. Documents carry:
-
-- a stable ID and monotonic version;
-- type, scope, status, domain, tags, and description;
-- provenance and evidence references;
-- confidence, sensitivity, and approval risk;
-- typed links such as `supports`, `blocks`, `conflicts`, `validates`, `supersedes`, `contains`, `imports`, and `calls`.
-
-Writes use atomic replacement and conflict detection. Concurrent handles observe one authority, process-level writers coordinate through lock/CAS semantics, and migrations are incremental, restartable, and integrity-checked.
-
-### Scope
-
-| Scope | Purpose |
-|---|---|
-| `session-private` | Current conversation and run-local state |
-| `project-shared` | Knowledge and decisions shared by one project |
-| `user-global` | Cross-project preferences and explicitly promoted knowledge |
-| `public-system` | Built-in skills and domain-pack documents |
-| `sealed` | Audit/evaluator material that cannot enter model context |
-
-Plans, run context, worklogs, designs, diagnoses, evaluations, knowledge, memory, strategies, methodologies, skills, and failure dossiers share this document algebra instead of maintaining separate storage models.
-
-## System Context and the Four Graphs
-
-The Context System connects four projections over the same durable knowledge surface:
-
-1. **Code:** files, symbols, imports, calls, definitions, references, diagnostics.
-2. **Knowledge:** facts, strategies, methodologies, skills, and failure dossiers.
-3. **Memory:** decisions, constraints, project conventions, environment facts, and handoffs.
-4. **Documents:** plans, designs, run context, evidence, worklogs, and evaluations.
-
-Context Sources produce typed observations from their domains. Session-owned selection applies budget, relevance, scope, sensitivity, evidence strength, conflict, and snapshot rules. A Context Epoch records the selected baseline so a provider turn is reproducible and observable.
-
-The Event Router can attach a context strategy to an event. SessionRunner executes that strategy in the target Location, records query and admission decisions in trace, and degrades safely when an optional source is unavailable.
-
-Compaction preserves a stable structure: goal, constraints, completed and active work, blockers, decisions, next steps, critical facts/open questions, and relevant files. Durable references remain outside the prose summary and can be reloaded when needed.
-
-## Planning and Goal Execution
-
-### Plan authority
-
-A structural plan is a versioned DocumentStore document. Session hot state keeps only its plan pointer/version and stale latch. The model plan tool, human plan editor, Goal worker, UI, Grader, and archive all read and update the same plan through version-aware writes.
-
-The runtime derives plan staleness from facts it already observes:
-
-- the user adds new guidance;
-- a tool or execution step fails;
-- validation fails;
-- repeated work makes no progress;
-- the active domain-pack snapshot changes.
-
-Read and diagnosis tools remain available while stale. Mutating tools require the plan to be synchronized, with bounded anti-deadlock behavior.
-
-### Goal Loop
-
-A Goal has objective completion criteria, a plan, a durable run context, a budget ledger, and a bounded controller. Each tick:
-
-1. claims the expected durable Goal/plan version;
-2. applies pending user plan edits and steering;
-3. executes one coherent step;
-4. records tools, tokens, cost, time, evidence, and progress;
-5. evaluates objective criteria and stall state;
-6. emits facts and schedules the next tick only when eligible.
-
-`goal.tick.requested` is a durable command, not a post-hoc trace marker. Duplicate delivery cannot repeat provider or tool side effects. Pause, stop, takeover, hard limits, quiet hours, needs-human, and terminal state all stop self-continuation.
-
-Hot plan edits preserve reusable step IDs and completed evidence, increment the plan version, and reset progress/stall baselines without interrupting the in-flight tick.
-
-## Event-Driven Agent Runtime
-
-The Event Bus provides persist-before-dispatch delivery, idempotency, priority, retry, acknowledgement, dead-letter handling, retention, replay, and correlation. Local deployments use the embedded backend; distributed deployments use Redis Streams or Kafka through the same backend contract.
-
-The Router combines event type, trusted source, actor identity, Agent trigger/capability metadata, autonomy ceiling, approval intent, context strategy, deduplication, priority, and workspace backpressure into one traceable route decision.
-
-The Worker Pool owns bounded concurrency, placement, claims, leases, renewal, recovery, and handoff. Independent DAG nodes run concurrently; dependency edges remain ordered. File and symbol claims are shared across Workers so conflicting writes cannot run together.
-
-Write-capable Agents use isolated worktrees by default. Parent Agents receive bounded summaries, status, artifact/session references, and necessary diffs; complete child transcripts remain in their own Sessions.
-
-## Human Collaboration and Oversight
-
-### Repo & Wiki
-
-Repo & Wiki is a human-facing projection, never a second source of truth. It exposes document and code navigation, full-text search, docs-to-code links, knowledge governance, and execution archives. Organization and workspace identity are enforced on query, index, archive, and promotion paths.
-
-### Expert Panel
-
-Panelists receive the same frozen question and evidence under differentiated lenses. Anonymous multi-round debate avoids identity anchoring. A deterministic Arbiter applies quorum, preserves minority opinions, and routes unsafe ambiguity to the Approval Queue. Distributed panelists run through the Worker Pool.
-
-### IM and proactive delivery
-
-Project IM supports groups, direct conversations, threads, search, attachments, agent mentions, progress streaming, and permission revalidation when project bindings change. Event-driven notifications and digests pass through content safety, path ACL, external-link, rate, and quiet-hours policies before delivery.
-
-### Oversight
-
-Correlation IDs connect event, route, context, worker claim, Session, provider turn, tool, artifact, approval, and outbound action. Operators can inspect Approval Queue items, dead letters, budgets, conflicts, takeovers, rollbacks, and final delivery without reconstructing state from logs.
-
-## Learning and Knowledge Governance
-
-Learning runs outside the interactive turn on idle, pause, project switch, and Session finalization triggers. Candidates enter the same DocumentStore lifecycle used by human governance.
-
-- Low-risk project candidates can pass deterministic auto-review.
-- Medium-risk/global candidates use an isolated blank-thread reviewer with no Session history.
-- Sensitive, strategic, regulated, or irreversible candidates require human review.
-- Rejection status, reason, and fingerprint remain authoritative in DocumentStore; auxiliary indexes are rebuildable projections.
-- Promotion changes the same document's status/version instead of copying it into a second identity.
-- Released retrieval sets name a snapshot and carry evaluation matrix, baseline, repeats, and ablation verdict.
+---
-A failed release gate restores the previous knowledge snapshot. Selected and rejected refs remain reproducible in run artifacts.
+## code_intel — AI IDE Microservice
-## Code Intelligence
+The `code_intel` tool wraps the LSP stack as a **symbol-driven semantic API**. The agent specifies a symbol name and an intent; `code_intel` resolves line/column coordinates internally and returns `file:line` + code snippets.
-The AI IDE surface combines:
+```typescript
+code_intel({ symbol: "AgentGateway.open", intent: "overview" })
+// → definition + type + references + callers + callees + doc summary
+// full detail → evidence artifact (ref only in context)
+```
-- semantic symbol lookup and intent-oriented `code_intel` queries;
-- definitions, references, calls, imports, type hierarchy, diagnostics, and rename preview;
-- unsaved-buffer `textDocument/didOpen`, `didChange`, `didSave`, and `didClose` synchronization;
-- incremental code indexing with exact mtime I/O hints and content-SHA correctness authority;
-- deterministic fallback to repository search/read when a language server is unavailable.
+Supported intents: `definition · references · implementations · type · calls_in · calls_out · supertypes · subtypes · type_hints · hover · rename_preview · quick_fix · outline · diagnostics · overview`
-Full LSP output stays in evidence artifacts. The model receives bounded summaries and precise source references.
+Graceful degradation: if no LSP server is configured for the file type, returns a hint to use `grep/read`. Capability only grows, never drops.
-## MCP and Credential Security
+---
-MCP servers can be added from the curated catalog or configured manually through Desktop, HTTP, or CLI. Catalog risk tiers are derived from trusted templates rather than mutable user configuration. Servers default to disconnected; writes, external fetches, and privileged actions pass through runtime permission gates.
+## MCP Catalog — Safety Model
-Credentials use indirection rather than plaintext project configuration:
+Each catalog entry carries a **risk tier** derived at load time from the catalog template. The tier is **not user-writable** — it is computed from the entry definition, preventing config-injection attacks.
-- `${VAR}` and `${VAR:-default}` resolve at connection time;
-- `secret://` handles resolve through macOS Keychain, Linux Secret Service, or Windows Credential Manager/DPAPI;
-- environments without a native keyring require explicit approval for an audited local fallback outside the project repository;
-- logs, artifacts, outbound messages, and config views redact resolved values.
+| Tier | Examples | Default behavior |
+|------|----------|-----------------|
+| `read_only` | postgres-readonly | All ops auto-allowed |
+| `write_guarded` | filesystem, github, git | Write ops require explicit approval |
+| `external_fetch` | fetch, browser (Playwright) | External requests require explicit approval |
-## Security Boundaries
+**Credentials** are declared by key name in the catalog template (`CredentialSpec`). Values are filled at enable-time.
-Autonomous execution passes four independent gates:
+> **Known limitation (V3.4):** credential values are stored in plaintext in the local config file. Do not commit config files containing credentials to version control. A secure-storage mechanism (OS keyring, aligned with the codex approach) is planned for V3.5.
-1. the event source is trusted for the workspace;
-2. the actor has permission for the project and resource;
-3. the Agent descriptor permits the trigger, capability, and autonomy level;
-4. the runtime permits the concrete tool, path, network target, and side effect.
+---
-Additional controls include durable budgets, rate limits, quiet hours, secret/path/link filtering, worktree isolation, hardened read-only Git, human approval, takeover, rollback, and complete audit correlation.
+## Security Model Summary
-## Repository Map
+| Mechanism | Status |
+|-----------|--------|
+| MCP risk tier — catalog-derived, not config-injectable | ✅ V3.4 |
+| MCP catalog defaults to not connected | ✅ V3.4 |
+| Dangerous writes: approval gate (`ctx.ask`) | ✅ V3.4 |
+| Read-only DB: restricted-mode enforced at server | ✅ V3.4 |
+| Credential secure storage (OS keyring) | ⏳ V3.5 M-CRED |
-| Area | Path |
-|---|---|
-| Core Session, documents, context, event, and policy algebra | `packages/core/src/` |
-| CLI/server runtime, tools, Goal and event wiring | `packages/deepagent-code/src/` |
-| Desktop/Web application UI | `packages/app/src/` |
-| Electron host and isolated browser views | `packages/desktop/src/` |
-| Provider abstraction | `packages/llm/` |
-| Domain knowledge packs | `packages/domain-packs/` |
-| Generated JavaScript SDK | `packages/sdk/js/` |
+---
-## License and Source
+## License
-DeepAgent Code is licensed under **AGPL-3.0-or-later**. The canonical repository is [github.com/deepagent-ltd/deepagent-code](https://github.com/deepagent-ltd/deepagent-code).
+DeepAgent Code is licensed under **AGPL-3.0-or-later**.
+Source code: [github.com/lessweb/deepagent-code](https://github.com/lessweb/deepagent-code)
-The project is derived from [opencode](https://github.com/sst/opencode) under the MIT License. Upstream attribution is preserved in [NOTICE](../NOTICE).
+DeepAgent Code is derived from [opencode](https://github.com/sst/opencode) (MIT).
+See `NOTICE` in the repository root for the full upstream attribution.
diff --git a/packages/app/src/components/deepagent-settings-ux.test.ts b/packages/app/src/components/deepagent-settings-ux.test.ts
index f2e218fc..870cab56 100644
--- a/packages/app/src/components/deepagent-settings-ux.test.ts
+++ b/packages/app/src/components/deepagent-settings-ux.test.ts
@@ -23,23 +23,9 @@ describe("DeepAgent settings UX", () => {
expect(v2.indexOf('data-action="settings-deepagent-prompt-mode"')).toBeLessThan(
v2.indexOf('data-action="settings-deepagent-intelligence-model"'),
)
- })
-
- test("moves the permission approval control out of settings into the composer toolbar", async () => {
- const v2 = await readFile(path.join(here, "settings-v2/general.tsx"), "utf8")
- const composer = await readFile(path.join(here, "prompt-input.tsx"), "utf8")
- const control = await readFile(path.join(here, "deepagent/approval-control.tsx"), "utf8")
-
- // The auto-accept toggle no longer lives in settings…
- expect(v2).not.toContain('data-action="settings-auto-accept-permissions"')
- // …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"')
- // 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.readOnly")
- expect(control).toContain("composer.approval.fullAccess")
+ expect(v2.indexOf('data-action="settings-deepagent-intelligence-model"')).toBeLessThan(
+ v2.indexOf('data-action="settings-auto-accept-permissions"'),
+ )
})
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
deleted file mode 100644
index cabd2b57..00000000
--- a/packages/app/src/components/deepagent/approval-control.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { createMemo, type JSX } from "solid-js"
-import { Select } from "@deepagent-code/ui/select"
-import { useLanguage } from "@/context/language"
-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: 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 = DirectoryApprovalMode
-
-export function ApprovalControl(props: { directory: string; triggerStyle?: JSX.CSSProperties; onAfter?: () => void }) {
- const language = useLanguage()
- const permission = usePermission()
-
- const current = createMemo(() =>
- props.directory ? permission.directoryApprovalMode(props.directory) : "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
- if (mode === current()) return
- permission.setDirectoryApprovalMode(props.directory, mode)
- props.onAfter?.()
- }
-
- return (
- o}
- label={label}
- onSelect={onSelect}
- class="max-w-[180px] text-text-base"
- valueClass="truncate text-13-regular text-text-base"
- triggerStyle={props.triggerStyle}
- triggerProps={{ "data-action": "prompt-approval" }}
- variant="ghost"
- />
- )
-}
diff --git a/packages/app/src/components/deepagent/goal-plan-edit-dialog.tsx b/packages/app/src/components/deepagent/goal-plan-edit-dialog.tsx
deleted file mode 100644
index a7d3a00d..00000000
--- a/packages/app/src/components/deepagent/goal-plan-edit-dialog.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import { For, Index, Show, createSignal } from "solid-js"
-import { Dialog, DialogFooter } from "@deepagent-code/ui/v2/dialog-v2"
-import { Button } from "@deepagent-code/ui/button"
-import { Icon } from "@deepagent-code/ui/icon"
-import { useLanguage } from "@/context/language"
-import { showToast } from "@/utils/toast"
-import { editPlanGoal, type GoalPlanInput, type PanelGoalClient } from "./panel-goal.api"
-import type { SessionPlan } from "@/context/global-sync/types"
-
-/**
- * V4.1 §S2 — the goal plan HOT-EDIT dialog. Opened from the goal status bar for a running/paused goal.
- * Pre-fills from the live session_plan (the same plan the status bar reads), lets the user revise the
- * goal text + step titles/statuses (add/remove steps), and POSTs the revision via editPlanGoal. The
- * backend applies it between ticks (durable-doc upsert + stall re-baseline) — see goal-loop.applyPlanEdit.
- *
- * step_id is preserved for existing steps (so the backend reconciles ids + runtime evidence via
- * buildPlanFromInput); a NEW step is sent without an id and the backend assigns one. Evidence is
- * runtime-owned and never sent from the client.
- */
-
-// The status values a user can pick — mirrors PlanStepStatus. `active` is included so a user can point
-// the goal at a specific step; the backend re-derives active_step_id from the first active step.
-const STATUS_OPTIONS = ["pending", "active", "done", "blocked", "cancelled"] as const
-type StatusOption = (typeof STATUS_OPTIONS)[number]
-
-const STATUS_LABEL_KEY: Record = {
- pending: "composer.goal.editPlan.status.pending",
- active: "composer.goal.editPlan.status.active",
- done: "composer.goal.editPlan.status.done",
- blocked: "composer.goal.editPlan.status.blocked",
- cancelled: "composer.goal.editPlan.status.cancelled",
-}
-
-// A local editable step row. `step_id` is undefined for a freshly-added step (the backend assigns one).
-type EditStep = {
- step_id?: string
- title: string
- status: StatusOption
-}
-
-const normStatus = (s: string): StatusOption =>
- (STATUS_OPTIONS as readonly string[]).includes(s) ? (s as StatusOption) : "pending"
-
-export function GoalPlanEditDialog(props: {
- sessionID: string
- plan: SessionPlan | undefined
- client: PanelGoalClient
- onClose: () => void
-}) {
- const language = useLanguage()
- const [goal, setGoal] = createSignal(props.plan?.goal ?? "")
- const [steps, setSteps] = createSignal(
- (props.plan?.steps ?? []).map((s) => ({ step_id: s.step_id, title: s.title, status: normStatus(s.status) })),
- )
- const [busy, setBusy] = createSignal(false)
-
- const setStep = (i: number, patch: Partial) =>
- setSteps((prev) => prev.map((s, idx) => (idx === i ? { ...s, ...patch } : s)))
- const addStep = () => setSteps((prev) => [...prev, { title: "", status: "pending" }])
- const removeStep = (i: number) => setSteps((prev) => prev.filter((_, idx) => idx !== i))
-
- // Valid to submit: a non-empty goal and at least one step whose title is non-empty. Empty-title steps
- // are dropped on submit (a user adding a row then leaving it blank shouldn't create a titleless step).
- const canSave = () => goal().trim().length > 0 && steps().some((s) => s.title.trim().length > 0)
-
- const onSave = async () => {
- if (busy() || !canSave()) return
- setBusy(true)
- try {
- const plan: GoalPlanInput = {
- goal: goal().trim(),
- steps: steps()
- .filter((s) => s.title.trim().length > 0)
- .map((s) => ({
- ...(s.step_id ? { step_id: s.step_id } : {}),
- title: s.title.trim(),
- status: s.status,
- })),
- }
- const ok = await editPlanGoal(props.client, props.sessionID, plan)
- if (ok) {
- showToast({ title: language.t("composer.goal.editPlan.saved"), variant: "success" })
- props.onClose()
- } else {
- showToast({ title: language.t("composer.goal.editPlan.failed") })
- }
- } catch {
- showToast({ title: language.t("composer.goal.editPlan.failed") })
- } finally {
- setBusy(false)
- }
- }
-
- return (
-
-
-
- {language.t("composer.goal.editPlan.goalLabel")}
-
-
-
-
-
-
- props.onClose()} disabled={busy()}>
- {language.t("composer.goal.editPlan.cancel")}
-
-
- {language.t("composer.goal.editPlan.save")}
-
-
-
- )
-}
diff --git a/packages/app/src/components/deepagent/goal-start-button.tsx b/packages/app/src/components/deepagent/goal-start-button.tsx
deleted file mode 100644
index 24d0148f..00000000
--- a/packages/app/src/components/deepagent/goal-start-button.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { Show, createMemo, createResource, createSignal } from "solid-js"
-import { Button } from "@deepagent-code/ui/button"
-import { Icon } from "@deepagent-code/ui/icon"
-import { useServerSync } from "@/context/server-sync"
-import { useSDK } from "@/context/sdk"
-import { useLocal } from "@/context/local"
-import { useLanguage } from "@/context/language"
-import { showToast } from "@/utils/toast"
-import { fetchCapabilities, fetchGoalStartable, startGoal, type PanelGoalClient } from "./panel-goal.api"
-
-// The collaboration modes where "convert plan → supervised goal" makes sense. loop/design are BOTH
-// powered by the Goal Loop engine and are designed to have the human START the loop after the plan is
-// authored (loop: agent writes goal+plan.md; design: user writes it). auto is autonomous end-to-end in
-// the current turn — a supervised background goal would be a confusing, redundant second door there, so
-// the button must NOT appear in auto. plan is hidden and never the client-visible current mode.
-const GOAL_MODES = new Set(["loop", "design"])
-
-/**
- * V3.9 §D — "convert plan → goal" starter.
- *
- * The product path (per the goal-loop design): the user first produces a plan in plan mode, then
- * converts it into a supervised long-running goal. `startGoal({ sessionID })` with NO objective makes
- * the server read the session's existing plan (GoalManager.start → getPlan) and materialize it as the
- * goal carrier — so this button is a pure trigger with zero backend change.
- *
- * Visibility: only when goalLoop is enabled (capability), a plan exists for this session, and no goal
- * is already active (once a goal starts, GoalStatusBar takes over via the goal.updated event).
- */
-// The raw SDK client THROWS the parsed error BODY on a non-2xx response — which is a plain object,
-// not an Error. So `String(err)` yields "[object Object]". Extract a human-readable message from the
-// shapes the server returns: a 400 DeepAgentPromotionError `{message}`, or a 500 `{data:{message}}`.
-function errorMessage(err: unknown): string {
- if (err instanceof Error) return err.message
- if (err && typeof err === "object") {
- const o = err as { message?: unknown; data?: { message?: unknown } }
- if (typeof o.message === "string") return o.message
- if (o.data && typeof o.data.message === "string") return o.data.message
- }
- return String(err)
-}
-
-export function GoalStartButton(props: { sessionID: string }) {
- const sdk = useSDK()
- const serverSync = useServerSync()
- const local = useLocal()
- const language = useLanguage()
- const [busy, setBusy] = createSignal(false)
-
- const client = () => sdk.client as unknown as PanelGoalClient
-
- const [capabilities] = createResource(
- () => (props.sessionID ? "capabilities" : undefined),
- () => fetchCapabilities(client()),
- )
- const goalAvailable = createMemo(() => capabilities()?.goalLoop === true)
-
- // The current collaboration mode (auto/loop/design) — the button only applies to loop/design. Sourced
- // from local.agent.current() (session-scoped mode selection), the same source the mode selector uses.
- const currentMode = createMemo(() => local.agent.current()?.name)
- const modeAllows = createMemo(() => GOAL_MODES.has(currentMode() ?? ""))
-
- // A goal is "live" for this session iff the persistent session_goal pointer exists — while present,
- // GoalStatusBar owns the surface. Checked FIRST so we don't probe startability for an already-running
- // goal.
- const activeGoal = createMemo(() => (props.sessionID ? serverSync.data.session_goal[props.sessionID] : undefined))
-
- // Whether a plan actually exists to start, resolved server-side (session_plan OR repo goal+plan.md).
- // Re-fetched when the session, mode-eligibility, active-goal, or the in-session plan changes — the last
- // dependency makes the button appear promptly after the agent writes a plan mid-conversation. Only
- // probed when the mode allows and no goal is live, to avoid needless requests.
- const [startable] = createResource(
- () =>
- props.sessionID && goalAvailable() && modeAllows() && !activeGoal()
- ? ([props.sessionID, serverSync.data.session_plan[props.sessionID]?.steps.length ?? 0] as const)
- : undefined,
- ([sessionID]) => fetchGoalStartable(client(), sessionID),
- )
-
- const show = createMemo(
- () => goalAvailable() && modeAllows() && !activeGoal() && startable()?.startable === true,
- )
-
- const onStart = async () => {
- if (busy() || !props.sessionID) return
- setBusy(true)
- try {
- const snapshot = await startGoal(client(), { sessionID: props.sessionID })
- if (!snapshot) {
- showToast({ title: language.t("goal.start.failed") })
- } else {
- // The server emits an immediate goal.updated (phase=running) on start, so GoalStatusBar takes
- // over this surface right away. This toast is the belt-and-suspenders confirmation for the click.
- showToast({ title: language.t("goal.start.success"), variant: "success" })
- }
- } catch (err) {
- showToast({ title: language.t("goal.start.failed"), description: errorMessage(err) })
- } finally {
- setBusy(false)
- }
- }
-
- return (
-
-
-
- {language.t("goal.start.hint")}
-
- {language.t("goal.start.button")}
-
-
-
- )
-}
diff --git a/packages/app/src/components/deepagent/goal-status-bar.tsx b/packages/app/src/components/deepagent/goal-status-bar.tsx
index 6e51e028..f8838a6d 100644
--- a/packages/app/src/components/deepagent/goal-status-bar.tsx
+++ b/packages/app/src/components/deepagent/goal-status-bar.tsx
@@ -1,12 +1,9 @@
import { Show, createMemo, createSignal } from "solid-js"
import { Button } from "@deepagent-code/ui/button"
import { Icon } from "@deepagent-code/ui/icon"
-import { useDialog } from "@deepagent-code/ui/context/dialog"
import { useServerSync } from "@/context/server-sync"
import { useSDK } from "@/context/sdk"
-import { useLanguage } from "@/context/language"
import { pauseGoal, resumeGoal, stopGoal, type PanelGoalClient } from "./panel-goal.api"
-import { GoalPlanEditDialog } from "./goal-plan-edit-dialog"
/**
* V3.9 §D — the Goal status bar. Renders above the composer when a goal is running for this session
@@ -15,17 +12,17 @@ import { GoalPlanEditDialog } from "./goal-plan-edit-dialog"
* while the background loop ticks and after a terminal phase (until the user starts a new goal).
*/
-const PHASE_LABEL_KEY: Record = {
- running: "composer.goal.phase.running",
- paused: "composer.goal.phase.paused",
- done: "composer.goal.phase.done",
- needs_human: "composer.goal.phase.needsHuman",
- rolled_back: "composer.goal.phase.rolledBack",
- stopped: "composer.goal.phase.stopped",
+const PHASE_LABEL: Record = {
+ running: "Running",
+ paused: "Paused",
+ done: "Complete",
+ needs_human: "Needs you",
+ rolled_back: "Rolled back",
+ stopped: "Stopped",
}
const PHASE_ICON: Record[0]["name"]> = {
- running: "goal",
+ running: "status-active",
paused: "circle-ban-sign",
done: "circle-check",
needs_human: "circle-x",
@@ -39,8 +36,6 @@ const isTerminal = (phase: string) =>
export function GoalStatusBar(props: { sessionID: string }) {
const serverSync = useServerSync()
const sdk = useSDK()
- const language = useLanguage()
- const dialog = useDialog()
const [busy, setBusy] = createSignal(false)
const goal = createMemo(() => (props.sessionID ? serverSync.data.session_goal[props.sessionID] : undefined))
@@ -67,17 +62,6 @@ export function GoalStatusBar(props: { sessionID: string }) {
const onResume = withBusy(() => resumeGoal(client(), props.sessionID))
const onStop = withBusy(() => stopGoal(client(), props.sessionID))
const onDismiss = () => serverSync.goal.set(props.sessionID, undefined)
- // §S2 — open the plan hot-edit dialog, pre-filled from the live session_plan (the same plan this bar
- // reads). Only offered for a non-terminal goal (the backend refuses an edit once terminal anyway).
- const onEditPlan = () =>
- dialog.show(() => (
- dialog.close()}
- />
- ))
const tokens = () => goal()?.ledger.tokens ?? 0
const ticks = () => goal()?.ledger.ticks ?? 0
@@ -89,49 +73,32 @@ export function GoalStatusBar(props: { sessionID: string }) {
data-component="goal-status-bar"
class="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-surface-raised border border-border-subtle text-13-regular"
>
-
-
- {PHASE_LABEL_KEY[g().phase] ? language.t(PHASE_LABEL_KEY[g().phase] as never) : g().phase}
-
+
+ {PHASE_LABEL[g().phase] ?? g().phase}
- {language.t(ticks() === 1 ? "composer.goal.budget.one" : "composer.goal.budget.other", {
- ticks: ticks(),
- tokens: tokens().toLocaleString(),
- })}
+ {ticks()} {ticks() === 1 ? "tick" : "ticks"} · {tokens().toLocaleString()} tokens
0}>
— {g().gaps[0]}
-
-
-
-
-
- {language.t("composer.goal.pause")}
+ Pause
- {language.t("composer.goal.resume")}
+ Resume
-
+
-
+
diff --git a/packages/app/src/components/deepagent/panel-button.tsx b/packages/app/src/components/deepagent/panel-button.tsx
index 9d07ec1a..282b5b14 100644
--- a/packages/app/src/components/deepagent/panel-button.tsx
+++ b/packages/app/src/components/deepagent/panel-button.tsx
@@ -1,117 +1,105 @@
-import { createSignal, createResource } from "solid-js"
+import { Show, createSignal, createResource } from "solid-js"
import { Button } from "@deepagent-code/ui/button"
import { Icon } from "@deepagent-code/ui/icon"
-import { MenuV2 } from "@deepagent-code/ui/v2/menu-v2"
+import { Tooltip } from "@deepagent-code/ui/tooltip"
import { useDialog } from "@deepagent-code/ui/context/dialog"
import { useSDK } from "@/context/sdk"
-import { useLanguage } from "@/context/language"
-import { armPanel, consultPanel, fetchPanelStatus, type PanelGoalClient, type PanelRounds } from "./panel-goal.api"
+import { armPanel, consultPanel, fetchPanelStatus, type PanelGoalClient } from "./panel-goal.api"
import { PanelVerdictDialog } from "./panel-verdict-dialog"
/**
- * V4.0 §C — the Expert Panel control for the composer toolbar, as a THREE-STATE menu.
+ * V3.9 §C — the Expert Panel toggle button for the composer toolbar.
*
- * Clicking the button opens a menu with three mutually-exclusive choices (replaces the old
- * Shift/Alt-click "deep convene" gesture with an explicit, discoverable control):
- * - Off → disarm; icon NOT lit (ghost). No convene.
- * - Single-round → arm + convene ONE round now; icon lit (primary).
- * - Multi-round → arm + convene up to 3 anonymized debate rounds now (§C.4); icon lit (primary).
- * Both Single and Multi light the icon (armed); only Off is unlit. The chosen depth PERSISTS per
- * session (server-side), so re-convening later reuses it. Arm state seeds from the server's effective
- * status (explicit toggle, else global `expertPanelDefault`).
+ * Activation semantics (per the product spec):
+ * - Armed state is per-conversation, seeded from the global `expertPanelDefault` setting.
+ * - OFF → ON (user arms mid-conversation): immediately convene a panel on the CURRENT context and
+ * show the verdict, then go quiet ("等待唤醒") — no per-turn re-runs.
+ * - While ON, pressing again re-convenes on demand.
+ * - ON → OFF: disarm (no consult).
+ * The button reflects armed state; a spinner-ish disabled state covers the in-flight consult.
*/
export function PanelButton(props: { sessionID: string }) {
const sdk = useSDK()
const dialog = useDialog()
- const language = useLanguage()
const [busy, setBusy] = createSignal(false)
const [armedOverride, setArmedOverride] = createSignal
(undefined)
- const [roundsOverride, setRoundsOverride] = createSignal(undefined)
const client = () => sdk.client as unknown as PanelGoalClient
+ // Seed the armed state from the SERVER's effective status (explicit toggle, else global default),
+ // so the button reflects the server-configured default rather than a client-side guess. A local
+ // override wins once the user toggles this session.
const [status] = createResource(
() => props.sessionID || undefined,
(sessionID) => fetchPanelStatus(client(), sessionID),
)
const armed = () => armedOverride() ?? status()?.armed ?? false
- const rounds = (): PanelRounds => roundsOverride() ?? status()?.rounds ?? "single"
- // The server clamps the ceiling; multi requests 3 debate rounds, single requests 1.
- const DEEP_PANEL_ROUNDS = 3
- const consultNow = async (depth: PanelRounds) => {
- const verdict = await consultPanel(client(), {
- sessionID: props.sessionID,
- ...(depth === "multi" ? { maxRounds: DEEP_PANEL_ROUNDS } : {}),
- })
+ const consultNow = async () => {
+ const verdict = await consultPanel(client(), { sessionID: props.sessionID })
if (verdict) dialog.show(() => )
}
- // Off: disarm, no consult. Single/Multi: arm + persist the depth + convene once at that depth.
- const choose = async (choice: "off" | PanelRounds) => {
+ const onClick = async () => {
if (busy() || !props.sessionID) return
setBusy(true)
try {
- if (choice === "off") {
- await armPanel(client(), props.sessionID, false)
- setArmedOverride(false)
- return
+ if (!armed()) {
+ // OFF → ON: arm, then convene once on the current context.
+ await armPanel(client(), props.sessionID, true)
+ setArmedOverride(true)
+ await consultNow()
+ } else {
+ // Already armed: a press re-convenes on demand (stays armed).
+ await consultNow()
}
- await armPanel(client(), props.sessionID, true, choice)
- setArmedOverride(true)
- setRoundsOverride(choice)
- await consultNow(choice)
} finally {
setBusy(false)
}
}
- const activeKey = (): "off" | PanelRounds => (!armed() ? "off" : rounds())
+ const onDisarm = async (e: MouseEvent) => {
+ e.stopPropagation()
+ if (busy() || !props.sessionID) return
+ setBusy(true)
+ try {
+ await armPanel(client(), props.sessionID, false)
+ setArmedOverride(false)
+ } finally {
+ setBusy(false)
+ }
+ }
return (
-
-
-
- {language.t("composer.panel.label")}
-
-
-
-
- {language.t("composer.panel.label")}
- void choose("off")}
- badge={activeKey() === "off" ? : undefined}
- >
- {language.t("composer.panel.off")}
-
- void choose("single")}
- badge={activeKey() === "single" ? : undefined}
- >
- {language.t("composer.panel.single")}
-
- void choose("multi")}
- badge={activeKey() === "multi" ? : undefined}
- >
- {language.t("composer.panel.multi")}
-
-
-
-
-
+
+
+
+
+ Panel
+
+
+
+
+
+
+
+
)
}
diff --git a/packages/app/src/components/deepagent/panel-goal-contract.test.ts b/packages/app/src/components/deepagent/panel-goal-contract.test.ts
index 0a95d393..f1458e6f 100644
--- a/packages/app/src/components/deepagent/panel-goal-contract.test.ts
+++ b/packages/app/src/components/deepagent/panel-goal-contract.test.ts
@@ -8,7 +8,6 @@ import {
resumeGoal,
stopGoal,
goalStatus,
- editPlanGoal,
fetchCapabilities,
} from "./panel-goal.api"
@@ -52,30 +51,25 @@ describe("Expert Panel route contract (§C)", () => {
expect(result).toEqual(verdict)
})
- test("armPanel POSTs /deepagent/panel/arm with rounds and returns the effective armed state + depth", async () => {
+ test("armPanel POSTs /deepagent/panel/arm and returns the effective armed state", async () => {
const calls: Recorded[] = []
- const result = await armPanel(client(calls, { sessionID: "ses_1", armed: true, rounds: "multi" }), "ses_1", true, "multi")
+ const armed = await armPanel(client(calls, { sessionID: "ses_1", armed: true }), "ses_1", true)
expect(calls).toEqual([
- {
- method: "POST",
- url: "/deepagent/panel/arm",
- body: { sessionID: "ses_1", armed: true, rounds: "multi" },
- headers: JSON_HEADERS,
- },
+ { method: "POST", url: "/deepagent/panel/arm", body: { sessionID: "ses_1", armed: true }, headers: JSON_HEADERS },
])
- expect(result).toEqual({ armed: true, rounds: "multi" })
+ expect(armed).toBe(true)
})
- test("fetchPanelStatus GETs /deepagent/panel/status and reports armed + explicit + rounds", async () => {
+ test("fetchPanelStatus GETs /deepagent/panel/status and reports armed + explicit", async () => {
const calls: Recorded[] = []
- const status = await fetchPanelStatus(client(calls, { armed: true, explicit: false, rounds: "multi" }), "ses 1")
+ const status = await fetchPanelStatus(client(calls, { armed: true, explicit: false }), "ses 1")
expect(calls).toEqual([{ method: "GET", url: "/deepagent/panel/status?sessionID=ses%201" }])
- expect(status).toEqual({ armed: true, explicit: false, rounds: "multi" })
+ expect(status).toEqual({ armed: true, explicit: false })
})
- test("fetchPanelStatus tolerates a missing body (disarmed, not explicit, single-round default)", async () => {
+ test("fetchPanelStatus tolerates a missing body (disarmed, not explicit)", async () => {
const calls: Recorded[] = []
- expect(await fetchPanelStatus(client(calls, {}), "ses_1")).toEqual({ armed: false, explicit: false, rounds: "single" })
+ expect(await fetchPanelStatus(client(calls, {}), "ses_1")).toEqual({ armed: false, explicit: false })
})
})
@@ -117,39 +111,14 @@ describe("Goal Loop route contract (§D)", () => {
const calls: Recorded[] = []
expect(await goalStatus(client(calls, { goal: null }), "ses_1")).toBeNull()
})
-
- test("editPlanGoal POSTs /deepagent/goal/edit-plan with { sessionID, plan } and returns ok", async () => {
- const calls: Recorded[] = []
- const plan = {
- goal: "ship it",
- steps: [
- { step_id: "step_1", title: "revised", status: "pending" },
- { title: "new step" },
- ],
- }
- const ok = await editPlanGoal(client(calls, { ok: true }), "ses_1", plan)
- expect(calls).toEqual([
- { method: "POST", url: "/deepagent/goal/edit-plan", body: { sessionID: "ses_1", plan }, headers: JSON_HEADERS },
- ])
- expect(ok).toBe(true)
- })
-
- test("editPlanGoal returns false when the server refuses (no goal / terminal)", async () => {
- const calls: Recorded[] = []
- expect(await editPlanGoal(client(calls, { ok: false }), "ses_1", { goal: "g", steps: [] })).toBe(false)
- // ...and tolerates a missing body (older server) as false.
- expect(await editPlanGoal(client(calls, {}), "ses_1", { goal: "g", steps: [] })).toBe(false)
- })
})
describe("capabilities gating", () => {
test("fetchCapabilities GETs /global/capabilities and reads the feature flags", async () => {
const calls: Recorded[] = []
- const caps = await fetchCapabilities(
- client(calls, { features: { expertPanel: true, goalLoop: false, wiki: true, v4MultiAgentRuntime: true } }),
- )
+ const caps = await fetchCapabilities(client(calls, { features: { expertPanel: true, goalLoop: false } }))
expect(calls).toEqual([{ method: "GET", url: "/global/capabilities" }])
- expect(caps).toEqual({ expertPanel: true, goalLoop: false, wiki: true, v4MultiAgentRuntime: true })
+ expect(caps).toEqual({ expertPanel: true, goalLoop: false })
})
test("fetchCapabilities treats a server that omits the fields as disabled", async () => {
@@ -157,15 +126,8 @@ describe("capabilities gating", () => {
expect(await fetchCapabilities(client(calls, { features: {} }))).toEqual({
expertPanel: false,
goalLoop: false,
- wiki: false,
- v4MultiAgentRuntime: false,
})
// and a server with no features object at all
- expect(await fetchCapabilities(client(calls, {}))).toEqual({
- expertPanel: false,
- goalLoop: false,
- wiki: false,
- v4MultiAgentRuntime: false,
- })
+ expect(await fetchCapabilities(client(calls, {}))).toEqual({ expertPanel: false, goalLoop: false })
})
})
diff --git a/packages/app/src/components/deepagent/panel-goal.api.ts b/packages/app/src/components/deepagent/panel-goal.api.ts
index c5cdb8c6..af4e1416 100644
--- a/packages/app/src/components/deepagent/panel-goal.api.ts
+++ b/packages/app/src/components/deepagent/panel-goal.api.ts
@@ -47,15 +47,10 @@ type RawSdkClient = {
export type PanelGoalClient = RawSdkClient
-/** Which V3.9/V4 subsystems this server has enabled (from /global/capabilities.features). */
+/** Which V3.9 experimental subsystems this server has enabled (from /global/capabilities.features). */
export type DeepAgentCapabilities = {
expertPanel: boolean
goalLoop: boolean
- wiki: boolean
- // V4.0 §D2 — the Multi-Agent Runtime flag gates the Oversight Approval Queue's PRODUCERS
- // (goal-manager / panel-convene-consumer). When it is OFF the queue can never be fed, so the client
- // hides the Oversight entry rather than showing a permanently-empty dead-end (T1.1). Default OFF.
- v4MultiAgentRuntime: boolean
}
/**
@@ -71,8 +66,6 @@ export const fetchCapabilities = async (client: PanelGoalClient): Promise => {
- const response = await client.client.request<{ sessionID: string; armed: boolean; rounds: PanelRounds }>({
+): Promise => {
+ const response = await client.client.request<{ sessionID: string; armed: boolean }>({
method: "POST",
url: "/deepagent/panel/arm",
- body: { sessionID, armed, ...(rounds ? { rounds } : {}) },
+ body: { sessionID, armed },
headers: JSON_HEADERS,
})
- return { armed: response.data?.armed ?? armed, rounds: response.data?.rounds ?? rounds ?? "single" }
+ return response.data?.armed ?? armed
}
/**
- * Resolve the EFFECTIVE armed state + debate depth for a session: the explicit per-session toggle if
- * set, else the server's global expertPanelDefault. Lets the button seed from the server default
- * without the client guessing (the client setting is only a hint; the server is authoritative).
+ * Resolve the EFFECTIVE armed state for a session: the explicit per-session toggle if set, else the
+ * server's global expertPanelDefault. Lets the button seed from the server default without the client
+ * guessing (the client setting is only a hint; the server is authoritative).
*/
export const fetchPanelStatus = async (
client: PanelGoalClient,
sessionID: string,
-): Promise<{ armed: boolean; explicit: boolean; rounds: PanelRounds }> => {
- const response = await client.client.request<{ armed: boolean; explicit: boolean; rounds: PanelRounds }>({
+): Promise<{ armed: boolean; explicit: boolean }> => {
+ const response = await client.client.request<{ armed: boolean; explicit: boolean }>({
method: "GET",
url: `/deepagent/panel/status?sessionID=${encodeURIComponent(sessionID)}`,
})
- return {
- armed: response.data?.armed ?? false,
- explicit: response.data?.explicit ?? false,
- rounds: response.data?.rounds ?? "single",
- }
+ return { armed: response.data?.armed ?? false, explicit: response.data?.explicit ?? false }
}
// ── Goal Loop (§D) ───────────────────────────────────────────────────────────
@@ -181,42 +163,6 @@ export const pauseGoal = (client: PanelGoalClient, sessionID: string) => goalMut
export const resumeGoal = (client: PanelGoalClient, sessionID: string) => goalMutate(client, "resume", sessionID)
export const stopGoal = (client: PanelGoalClient, sessionID: string) => goalMutate(client, "stop", sessionID)
-/** A plan step as the edit-plan payload carries it (loose input: step_id/status optional, mirroring the
- * backend PlanInput — evidence is runtime-owned and never sent from the client). */
-export type GoalPlanStepInput = {
- step_id?: string
- title: string
- status?: string
- acceptance?: string | null
- assigned_agent?: string | null
- note?: string | null
-}
-export type GoalPlanInput = {
- goal: string
- steps: GoalPlanStepInput[]
- assumptions?: string[]
- active_step_id?: string | null
-}
-
-/**
- * V4.1 §S2 — hot-edit the plan of a RUNNING or PAUSED goal. POSTs the revised plan on the goal control
- * channel; the driver applies it between ticks (durable-doc upsert + stall re-baseline). Returns false
- * when no goal is running or it reached a terminal phase (the server's orphan guard).
- */
-export const editPlanGoal = async (
- client: PanelGoalClient,
- sessionID: string,
- plan: GoalPlanInput,
-): Promise => {
- const response = await client.client.request<{ ok: boolean }>({
- method: "POST",
- url: "/deepagent/goal/edit-plan",
- body: { sessionID, plan },
- headers: JSON_HEADERS,
- })
- return response.data?.ok ?? false
-}
-
export const goalStatus = async (
client: PanelGoalClient,
sessionID: string,
@@ -227,26 +173,3 @@ export const goalStatus = async (
})
return response.data?.goal ?? null
}
-
-export type GoalStartable = { startable: boolean; source: "plan" | "file" | "none" }
-
-/**
- * Whether a goal can be started for this session right now, resolved SERVER-SIDE with the same plan
- * precedence start() uses (session_plan → repo goal+plan.md → none). The button gates on this instead
- * of reading session_plan directly, because loop/design modes author the plan as the repo file (never
- * touching session_plan), so a client-only hasPlan() check would hide the button in exactly the modes
- * where it belongs. Tolerant of an older server that lacks the route (treated as not-startable).
- */
-export const fetchGoalStartable = async (
- client: PanelGoalClient,
- sessionID: string,
-): Promise => {
- const response = await client.client.request({
- method: "GET",
- url: `/deepagent/goal/startable?sessionID=${encodeURIComponent(sessionID)}`,
- })
- return {
- startable: response.data?.startable ?? false,
- source: response.data?.source ?? "none",
- }
-}
diff --git a/packages/app/src/components/deepagent/panel-verdict-dialog.tsx b/packages/app/src/components/deepagent/panel-verdict-dialog.tsx
index ecdcd61b..f8b0253d 100644
--- a/packages/app/src/components/deepagent/panel-verdict-dialog.tsx
+++ b/packages/app/src/components/deepagent/panel-verdict-dialog.tsx
@@ -1,6 +1,5 @@
import { For, Show } from "solid-js"
import { Dialog } from "@deepagent-code/ui/v2/dialog-v2"
-import { useLanguage } from "@/context/language"
import type { PanelVerdict } from "./panel-goal.api"
/**
@@ -8,32 +7,28 @@ import type { PanelVerdict } from "./panel-goal.api"
* arbiter's confidence + round count, the grounding evidence, and any preserved dissent (§C.8 不丢信息).
*/
-const DECISION_LABEL_KEY: Record = {
- approve: "composer.panel.verdict.approve",
- revise: "composer.panel.verdict.revise",
- block: "composer.panel.verdict.block",
- needs_human: "composer.panel.verdict.needsHuman",
+const DECISION_LABEL: Record = {
+ approve: "Approve",
+ revise: "Revise",
+ block: "Block",
+ needs_human: "Needs a human",
}
export function PanelVerdictDialog(props: { verdict: PanelVerdict }) {
- const language = useLanguage()
const v = () => props.verdict
return (
-
+
- {language.t(DECISION_LABEL_KEY[v().decision] as never)}
+ {DECISION_LABEL[v().decision]}
- {language.t(v().rounds === 1 ? "composer.panel.verdict.meta.one" : "composer.panel.verdict.meta.other", {
- confidence: (v().confidence * 100).toFixed(0),
- rounds: v().rounds,
- })}
+ {(v().confidence * 100).toFixed(0)}% confidence · {v().rounds} {v().rounds === 1 ? "round" : "rounds"}
0}>
-
{language.t("composer.panel.verdict.evidence")}
+
Evidence
{(e) => {e} }
@@ -44,7 +39,7 @@ export function PanelVerdictDialog(props: { verdict: PanelVerdict }) {
0}>
-
{language.t("composer.panel.verdict.dissent")}
+
Dissent (overruled)
{(d) => (
diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx
index 0a76cefc..acb86383 100644
--- a/packages/app/src/components/dialog-connect-provider.tsx
+++ b/packages/app/src/components/dialog-connect-provider.tsx
@@ -748,9 +748,7 @@ export function DialogConnectProvider(props: { provider: string }) {
const result = await serverSDK.client.provider.oauth
.callback({
providerID: props.provider,
- // This view only renders under an oauth method (see method()?.type gate), so methodIndex
- // is always set here; default to 0 to satisfy the required-number API type.
- method: store.methodIndex ?? 0,
+ method: store.methodIndex,
code,
})
.then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const }))
@@ -803,8 +801,7 @@ export function DialogConnectProvider(props: { provider: string }) {
const result = await serverSDK.client.provider.oauth
.callback({
providerID: props.provider,
- // Reached only via the oauth "auto" method branch, so methodIndex is set; default to 0.
- method: store.methodIndex ?? 0,
+ method: store.methodIndex,
})
.then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const }))
.catch((error) => ({ ok: false as const, error }))
diff --git a/packages/app/src/components/dialog-custom-provider-form.ts b/packages/app/src/components/dialog-custom-provider-form.ts
index cac792ea..1294b436 100644
--- a/packages/app/src/components/dialog-custom-provider-form.ts
+++ b/packages/app/src/components/dialog-custom-provider-form.ts
@@ -1,50 +1,11 @@
-import { isOfficialProvider } from "@deepagent-code/core/provider-official"
-import type { Provider as ResolvedProvider, ProviderConfig } from "@deepagent-code/sdk/v2"
-
const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
-const ANTHROPIC = "@ai-sdk/anthropic"
-
-export type ProviderProtocol = "openai-compatible" | "anthropic"
-
-// Per-model spec override written under `provider.
.models.`. Only the fields the user actually
-// set are emitted, so a blank field never clobbers the backend catalog-fill with a zero/false.
-export type CustomModelConfig = {
- name: string
- reasoning?: boolean
- temperature?: boolean
- limit?: { context: number; output?: number }
-}
-
-// The config payload written under `provider.`. `discovery` and `models` are mutually exclusive
-// in practice (discovery mode emits an empty models map), but both are typed optional so the emitted
-// object has one consistent shape instead of a union callers must narrow.
-export type CustomProviderConfig = {
- npm: string
- name: string
- env?: string[]
- options: {
- baseURL: string
- apiKey?: string
- headers?: Record
- }
- discovery?: boolean
- models: Record
-}
-
-const npmForProtocol = (kind: ProviderProtocol | undefined) => (kind === "anthropic" ? ANTHROPIC : OPENAI_COMPATIBLE)
-
-// Leading host labels that are generic service prefixes and make a poor provider id, so we skip past
-// them to reach the brand label (api.deepseek.com -> "deepseek", not "api"). Kept deliberately small:
-// only unambiguous service prefixes, never anything that could be a brand.
-const GENERIC_HOST_LABELS = new Set(["api", "www", "app", "gateway", "proxy", "open"])
type Translator = (key: string, vars?: Record) => string
export type ModelErr = {
id?: string
name?: string
- context?: string
}
export type HeaderErr = {
@@ -56,11 +17,6 @@ export type ModelRow = {
row: string
id: string
name: string
- // Editable spec overrides. `context` is a text field (parsed to a positive int on save; blank means
- // "let the backend/catalog fill it"). reasoning/temperature are booleans.
- context: string
- reasoning: boolean
- temperature: boolean
err: ModelErr
}
@@ -90,131 +46,37 @@ type ValidateArgs = {
t: Translator
disabledProviders: string[]
existingProviderIDs: Set
- // Protocol detected during model discovery; decides the SDK npm written to config. Defaults to
- // openai-compatible when omitted (backward compatible with the manual form).
- protocol?: ProviderProtocol
- // Runtime discovery mode: when true AND the user listed no manual models, persist `discovery: true`
- // with an empty model list so the backend refreshes models from the provider's /models endpoint on
- // every load instead of freezing them into config. Manual models always take precedence and turn
- // this off for that provider.
- discovery?: boolean
- // Edit mode: the provider being edited was persisted with `discovery: true`. We keep discovery on
- // (so the backend still refreshes the model list) AND emit the user's per-model spec overrides,
- // which the build loop merges over the discovered models (manual wins per-id). Without this, saving
- // spec edits would freeze the model snapshot and disable runtime refresh.
- editDiscovery?: boolean
- // Providers whose ids are already taken but belong to the provider being edited (so an edit doesn't
- // trip the "already exists" check on its own id).
- editingProviderID?: string
-}
-
-// Turn a base URL into a stable, unique provider id + a human display name so the user only has to
-// enter URL + key. Rules:
-// - id is derived from the registrable host label (api.deepseek.com -> "deepseek",
-// open.bigmodel.cn -> "bigmodel"), slugified to satisfy PROVIDER_ID.
-// - reserved official ids (openai/deepseek/anthropic/zhipuai/xai/google/...) and any id already in
-// use are avoided by appending a numeric suffix, since a third-party id that collides with an
-// official one is rejected by the backend (THIRD_PARTY_PROVIDER_CONFLICT).
-// - `disabledProviders` do NOT count as taken: re-adding a previously disabled provider should be
-// able to reuse its id.
-export function deriveProviderIdentity(input: {
- baseURL: string
- existingProviderIDs: Set
- disabledProviders?: string[]
-}): { providerID: string; name: string } {
- const disabled = new Set(input.disabledProviders ?? [])
- const taken = (id: string) =>
- (input.existingProviderIDs.has(id) && !disabled.has(id)) || (isOfficialProvider(id) && !disabled.has(id))
-
- const base = baseSlug(input.baseURL)
- let providerID = base
- let n = 2
- while (taken(providerID)) {
- providerID = `${base}-${n}`
- n++
- }
- return { providerID, name: displayName(base) }
-}
-
-function baseSlug(baseURL: string): string {
- let host = ""
- try {
- host = new URL(baseURL.trim()).hostname
- } catch {
- host = ""
- }
- const labels = host.split(".").filter(Boolean)
- // Drop leading generic service labels (api., www., ...) so we land on the brand label.
- while (labels.length > 1 && GENERIC_HOST_LABELS.has(labels[0].toLowerCase())) labels.shift()
- // Prefer the registrable label: for a.b.com pick "b"; for single-label/localhost keep as-is.
- const label = labels.length >= 2 ? labels[labels.length - 2] : (labels[0] ?? "")
- const slug = label
- .toLowerCase()
- .replace(/[^a-z0-9-_]+/g, "-")
- .replace(/^-+|-+$/g, "")
- // Must satisfy PROVIDER_ID (starts alphanumeric). Fall back to a safe default.
- return slug && PROVIDER_ID.test(slug) ? slug : "custom-provider"
-}
-
-function displayName(slug: string): string {
- const cleaned = slug.replace(/[-_]+/g, " ").trim()
- if (!cleaned) return "Custom Provider"
- return cleaned
- .split(" ")
- .map((word) => (word ? word[0].toUpperCase() + word.slice(1) : word))
- .join(" ")
}
export function validateCustomProvider(input: ValidateArgs) {
- const typedID = input.form.providerID.trim()
- const typedName = input.form.name.trim()
+ const providerID = input.form.providerID.trim()
+ const name = input.form.name.trim()
const baseURL = input.form.baseURL.trim()
const apiKey = input.form.apiKey.trim()
const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
const key = apiKey && !env ? apiKey : undefined
- const urlError = !baseURL
- ? input.t("provider.custom.error.baseURL.required")
- : !/^https?:\/\//.test(baseURL)
- ? input.t("provider.custom.error.baseURL.format")
- : undefined
-
- // Zero-config path: when the user leaves id/name blank we derive them from the URL, so those
- // fields are no longer required. Derivation needs a usable URL — if the URL itself is invalid we
- // skip it and let urlError drive the failure instead of emitting a spurious id/name error.
- const derived = !urlError && (!typedID || !typedName) ? deriveProviderIdentity({
- baseURL,
- existingProviderIDs: input.existingProviderIDs,
- disabledProviders: input.disabledProviders,
- }) : undefined
- const providerID = typedID || derived?.providerID || ""
- const name = typedName || derived?.name || ""
-
- // Only the user's explicitly-typed id is format-checked; a derived id is always valid by
- // construction. A blank id with no derivable URL still surfaces as "required".
const idError = !providerID
? input.t("provider.custom.error.providerID.required")
- : typedID && !PROVIDER_ID.test(typedID)
+ : !PROVIDER_ID.test(providerID)
? input.t("provider.custom.error.providerID.format")
: undefined
const nameError = !name ? input.t("provider.custom.error.name.required") : undefined
+ const urlError = !baseURL
+ ? input.t("provider.custom.error.baseURL.required")
+ : !/^https?:\/\//.test(baseURL)
+ ? input.t("provider.custom.error.baseURL.format")
+ : undefined
const disabled = input.disabledProviders.includes(providerID)
- // Editing a provider keeps its own id — don't flag that as a collision.
- const isSelf = input.editingProviderID === providerID
const existsError = idError
? undefined
- : input.existingProviderIDs.has(providerID) && !disabled && !isSelf
+ : input.existingProviderIDs.has(providerID) && !disabled
? input.t("provider.custom.error.providerID.exists")
: undefined
- // Discovery mode is only active when the user listed no manual models: the model list then comes
- // from the backend at runtime, so the empty model rows must not fail validation.
- const hasManualModels = input.form.models.some((m) => m.id.trim().length > 0)
- const discoveryMode = !!input.discovery && !hasManualModels
-
const seenModels = new Set()
const models = input.form.models.map((m) => {
const id = m.id.trim()
@@ -227,25 +89,10 @@ export function validateCustomProvider(input: ValidateArgs) {
return undefined
})()
const nameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
- const ctx = m.context.trim()
- // Blank context is allowed (backend/catalog fills it); a non-empty value must be a positive int.
- const contextError = ctx && !/^\d+$/.test(ctx) ? input.t("provider.custom.error.context") : undefined
- return { id: idError, name: nameError, context: contextError }
+ return { id: idError, name: nameError }
})
- const modelsValid =
- (discoveryMode || models.every((m) => !m.id && !m.name)) && models.every((m) => !m.context)
- const modelConfig = Object.fromEntries(
- input.form.models.map((m) => {
- const ctx = m.context.trim()
- const spec: CustomModelConfig = {
- name: m.name.trim(),
- ...(m.reasoning ? { reasoning: true } : {}),
- ...(m.temperature ? { temperature: true } : {}),
- ...(ctx ? { limit: { context: Number(ctx) } } : {}),
- }
- return [m.id.trim(), spec]
- }),
- )
+ const modelsValid = models.every((m) => !m.id && !m.name)
+ const modelConfig = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
const seenHeaders = new Set()
const headers = input.form.headers.map((h) => {
@@ -281,70 +128,26 @@ export function validateCustomProvider(input: ValidateArgs) {
const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
if (!ok) return { err, models, headers }
- const config: CustomProviderConfig = {
- npm: npmForProtocol(input.protocol),
- name,
- ...(env ? { env: [env] } : {}),
- options: {
- baseURL,
- ...(key ? { apiKey: key } : {}),
- ...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}),
- },
- // Edit-of-discovery: keep discovery on (runtime refresh) AND persist the spec overrides — the
- // build loop merges these over the discovered models (manual wins per-id).
- // New discovery mode: persist the opt-in flag and an empty model list (backend refreshes).
- // Manual mode: freeze the listed models and leave discovery off.
- ...(input.editDiscovery
- ? { discovery: true, models: modelConfig }
- : discoveryMode
- ? { discovery: true, models: {} }
- : { models: modelConfig }),
- }
-
return {
err,
models,
headers,
- result: { providerID, name, key, config },
- }
-}
-
-// Build the dialog form state for editing an existing custom provider. Fields (URL/key/headers/name)
-// come from the raw config entry; model rows are seeded from the RESOLVED provider so the user sees the
-// actual context/reasoning/temperature values (a discovery provider has no models in config — its
-// specs only exist post-resolve). Each row is pre-filled so edits override just those fields.
-export function formStateFromProvider(input: {
- config: ProviderConfig
- resolved: ResolvedProvider | undefined
-}): FormState {
- const { config, resolved } = input
- const headers = config.options?.headers
- const headerRows =
- headers && typeof headers === "object" && Object.keys(headers).length
- ? Object.entries(headers as Record).map(([key, value]) =>
- headerRow2(String(key), String(value)),
- )
- : [headerRow()]
-
- const resolvedModels = resolved?.models ?? {}
- const modelRows = Object.entries(resolvedModels).map(([id, m]) =>
- modelRow({
- id,
- name: m.name || id,
- context: m.limit?.context ? String(m.limit.context) : "",
- reasoning: !!m.capabilities?.reasoning,
- temperature: !!m.capabilities?.temperature,
- }),
- )
-
- return {
- providerID: resolved?.id ?? config.id ?? "",
- name: config.name ?? resolved?.name ?? "",
- baseURL: (config.options?.baseURL as string | undefined) ?? "",
- apiKey: (config.options?.apiKey as string | undefined) ?? "",
- models: modelRows.length ? modelRows : [modelRow()],
- headers: headerRows,
- err: {},
+ result: {
+ providerID,
+ name,
+ key,
+ config: {
+ npm: OPENAI_COMPATIBLE,
+ name,
+ ...(env ? { env: [env] } : {}),
+ options: {
+ baseURL,
+ ...(key ? { apiKey: key } : {}),
+ ...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}),
+ },
+ models: modelConfig,
+ },
+ },
}
}
@@ -352,15 +155,5 @@ let row = 0
const nextRow = () => `row-${row++}`
-export const modelRow = (init?: Partial): ModelRow => ({
- row: nextRow(),
- id: "",
- name: "",
- context: "",
- reasoning: false,
- temperature: false,
- err: {},
- ...init,
-})
+export const modelRow = (): ModelRow => ({ row: nextRow(), id: "", name: "", err: {} })
export const headerRow = (): HeaderRow => ({ row: nextRow(), key: "", value: "", err: {} })
-const headerRow2 = (key: string, value: string): HeaderRow => ({ row: nextRow(), key, value, err: {} })
diff --git a/packages/app/src/components/dialog-custom-provider.test.ts b/packages/app/src/components/dialog-custom-provider.test.ts
index 4e0ff95e..07dd26ec 100644
--- a/packages/app/src/components/dialog-custom-provider.test.ts
+++ b/packages/app/src/components/dialog-custom-provider.test.ts
@@ -1,10 +1,5 @@
import { describe, expect, test } from "bun:test"
-import {
- deriveProviderIdentity,
- formStateFromProvider,
- modelRow,
- validateCustomProvider,
-} from "./dialog-custom-provider-form"
+import { validateCustomProvider } from "./dialog-custom-provider-form"
const t = (key: string) => key
@@ -16,7 +11,7 @@ describe("validateCustomProvider", () => {
name: " Custom Provider ",
baseURL: "https://api.example.com ",
apiKey: " {env: CUSTOM_PROVIDER_KEY} ",
- models: [modelRow({ id: " model-a ", name: " Model A " })],
+ models: [{ row: "m0", id: " model-a ", name: " Model A ", err: {} }],
headers: [
{ row: "h0", key: " X-Test ", value: " enabled ", err: {} },
{ row: "h1", key: "", value: "", err: {} },
@@ -56,7 +51,10 @@ describe("validateCustomProvider", () => {
name: "Provider",
baseURL: "https://api.example.com",
apiKey: "secret",
- models: [modelRow({ id: "model-a", name: "Model A" }), modelRow({ id: "model-a", name: "Model A 2" })],
+ models: [
+ { row: "m0", id: "model-a", name: "Model A", err: {} },
+ { row: "m1", id: "model-a", name: "Model A 2", err: {} },
+ ],
headers: [
{ row: "h0", key: "Authorization", value: "one", err: {} },
{ row: "h1", key: "authorization", value: "two", err: {} },
@@ -73,308 +71,10 @@ describe("validateCustomProvider", () => {
expect(result.models[1]).toEqual({
id: "provider.custom.error.duplicate",
name: undefined,
- context: undefined,
})
expect(result.headers[1]).toEqual({
key: "provider.custom.error.duplicate",
value: undefined,
})
})
-
- test("derives provider id and name from the URL when left blank", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "",
- name: "",
- baseURL: "https://api.moonshot.cn/v1",
- apiKey: "secret",
- // No manual models: discovery would fill these; validation must not require them here since
- // the dialog auto-fills discovered models before calling validate. Simulate a filled row.
- models: [modelRow({ id: "kimi", name: "Kimi" })],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- })
-
- expect(result.err.providerID).toBeUndefined()
- expect(result.err.name).toBeUndefined()
- expect(result.result?.providerID).toBe("moonshot")
- expect(result.result?.name).toBe("Moonshot")
- })
-
- test("persists anthropic npm when protocol is anthropic", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "claude-relay",
- name: "Claude Relay",
- baseURL: "https://relay.example.com",
- apiKey: "secret",
- models: [modelRow({ id: "claude-x", name: "Claude X" })],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- protocol: "anthropic",
- })
-
- expect(result.result?.config.npm).toBe("@ai-sdk/anthropic")
- })
-
- test("defaults to openai-compatible npm when protocol omitted", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "relay",
- name: "Relay",
- baseURL: "https://relay.example.com",
- apiKey: "secret",
- models: [modelRow({ id: "m", name: "M" })],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- })
-
- expect(result.result?.config.npm).toBe("@ai-sdk/openai-compatible")
- })
-
- test("discovery mode persists discovery flag and empty models when no manual models", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "",
- name: "",
- baseURL: "https://api.moonshot.cn/v1",
- apiKey: "secret",
- // No manual models: the backend owns the list at runtime.
- models: [modelRow()],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- discovery: true,
- })
-
- expect(result.err.providerID).toBeUndefined()
- expect(result.result?.providerID).toBe("moonshot")
- expect(result.result?.config.discovery).toBe(true)
- expect(result.result?.config.models).toEqual({})
- })
-
- test("explicit anthropic protocol choice persists the anthropic npm even without detection", () => {
- // Simulates the user picking Anthropic in the protocol selector: the dialog passes protocol
- // "anthropic" to the form, which must win regardless of any auto-detection.
- const result = validateCustomProvider({
- form: {
- providerID: "relay",
- name: "Relay",
- baseURL: "https://relay.example.com",
- apiKey: "secret",
- models: [modelRow({ id: "m", name: "M" })],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- protocol: "anthropic",
- })
-
- expect(result.result?.config.npm).toBe("@ai-sdk/anthropic")
- })
-
- test("manual models override discovery mode: freeze models, no discovery flag", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "relay",
- name: "Relay",
- baseURL: "https://relay.example.com",
- apiKey: "secret",
- models: [modelRow({ id: "custom-model", name: "Custom Model" })],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- // Even with discovery requested, a hand-listed model wins and turns discovery off.
- discovery: true,
- })
-
- expect(result.result?.config.discovery).toBeUndefined()
- expect(result.result?.config.models).toEqual({ "custom-model": { name: "Custom Model" } })
- })
-
- test("emits spec overrides only for fields the user set", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "relay",
- name: "Relay",
- baseURL: "https://relay.example.com",
- apiKey: "secret",
- models: [
- modelRow({ id: "with-specs", name: "With Specs", context: "128000", reasoning: true, temperature: true }),
- modelRow({ id: "bare", name: "Bare" }),
- ],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- })
-
- expect(result.result?.config.models).toEqual({
- "with-specs": { name: "With Specs", reasoning: true, temperature: true, limit: { context: 128000 } },
- bare: { name: "Bare" },
- })
- })
-
- test("rejects a non-numeric context and produces no result", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "relay",
- name: "Relay",
- baseURL: "https://relay.example.com",
- apiKey: "secret",
- models: [modelRow({ id: "m", name: "M", context: "12x" })],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- existingProviderIDs: new Set(),
- })
-
- expect(result.result).toBeUndefined()
- expect(result.models[0].context).toBe("provider.custom.error.context")
- })
-
- test("edit mode keeps discovery on and layers spec overrides", () => {
- const result = validateCustomProvider({
- form: {
- providerID: "relay",
- name: "Relay",
- baseURL: "https://relay.example.com",
- apiKey: "secret",
- models: [modelRow({ id: "gpt-4o", name: "GPT-4o", context: "200000" })],
- headers: [{ row: "h0", key: "", value: "", err: {} }],
- err: {},
- },
- t,
- disabledProviders: [],
- // Its own id is "already in use" but must not be flagged when editing it.
- existingProviderIDs: new Set(["relay"]),
- editDiscovery: true,
- editingProviderID: "relay",
- })
-
- expect(result.err.providerID).toBeUndefined()
- expect(result.result?.config.discovery).toBe(true)
- expect(result.result?.config.models).toEqual({ "gpt-4o": { name: "GPT-4o", limit: { context: 200000 } } })
- })
-})
-
-describe("formStateFromProvider", () => {
- test("prefills fields from config and specs from the resolved provider", () => {
- const form = formStateFromProvider({
- config: {
- name: "My Relay",
- npm: "@ai-sdk/openai-compatible",
- discovery: true,
- options: { baseURL: "https://relay.example.com", apiKey: "secret", headers: { "X-Env": "prod" } },
- },
- resolved: {
- id: "relay",
- name: "My Relay",
- source: "custom",
- env: [],
- options: {},
- models: {
- "gpt-4o": {
- id: "gpt-4o",
- providerID: "relay",
- api: { id: "gpt-4o", url: "", npm: "@ai-sdk/openai-compatible" },
- name: "GPT-4o",
- capabilities: {
- temperature: true,
- reasoning: false,
- attachment: false,
- toolcall: true,
- input: { text: true, audio: false, image: false, video: false, pdf: false },
- output: { text: true, audio: false, image: false, video: false, pdf: false },
- interleaved: false,
- },
- cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
- limit: { context: 128000, output: 16384 },
- status: "active",
- options: {},
- headers: {},
- release_date: "",
- },
- },
- },
- })
-
- expect(form.providerID).toBe("relay")
- expect(form.name).toBe("My Relay")
- expect(form.baseURL).toBe("https://relay.example.com")
- expect(form.apiKey).toBe("secret")
- expect(form.headers[0]).toMatchObject({ key: "X-Env", value: "prod" })
- expect(form.models).toHaveLength(1)
- expect(form.models[0]).toMatchObject({
- id: "gpt-4o",
- name: "GPT-4o",
- context: "128000",
- reasoning: false,
- temperature: true,
- })
- })
-})
-
-describe("deriveProviderIdentity", () => {
- test("strips generic service labels and uses the brand label", () => {
- expect(deriveProviderIdentity({ baseURL: "https://api.moonshot.cn/v1", existingProviderIDs: new Set() })).toEqual({
- providerID: "moonshot",
- name: "Moonshot",
- })
- expect(
- deriveProviderIdentity({ baseURL: "https://open.bigmodel.cn/api/paas/v4", existingProviderIDs: new Set() }),
- ).toEqual({ providerID: "bigmodel", name: "Bigmodel" })
- })
-
- test("avoids reserved official provider ids by suffixing", () => {
- // openai is a reserved official id; a third-party endpoint must not claim it.
- const result = deriveProviderIdentity({ baseURL: "https://api.openai.com/v1", existingProviderIDs: new Set() })
- expect(result.providerID).toBe("openai-2")
- })
-
- test("avoids ids already in use", () => {
- const result = deriveProviderIdentity({
- baseURL: "https://api.moonshot.cn/v1",
- existingProviderIDs: new Set(["moonshot", "moonshot-2"]),
- })
- expect(result.providerID).toBe("moonshot-3")
- })
-
- test("lets a disabled provider reclaim its id", () => {
- const result = deriveProviderIdentity({
- baseURL: "https://api.moonshot.cn/v1",
- existingProviderIDs: new Set(["moonshot"]),
- disabledProviders: ["moonshot"],
- })
- expect(result.providerID).toBe("moonshot")
- })
-
- test("falls back to a safe id for an unparseable URL", () => {
- const result = deriveProviderIdentity({ baseURL: "not a url", existingProviderIDs: new Set() })
- expect(result.providerID).toBe("custom-provider")
- })
})
diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx
index 1b4a0054..90dfb92e 100644
--- a/packages/app/src/components/dialog-custom-provider.tsx
+++ b/packages/app/src/components/dialog-custom-provider.tsx
@@ -6,37 +6,17 @@ import { ProviderIcon } from "@deepagent-code/ui/provider-icon"
import { useMutation } from "@tanstack/solid-query"
import { TextField } from "@deepagent-code/ui/text-field"
import { showToast } from "@/utils/toast"
-import { batch, createSignal, For, Show } from "solid-js"
+import { batch, For } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
-import {
- type FormState,
- type ProviderProtocol,
- deriveProviderIdentity,
- formStateFromProvider,
- headerRow,
- modelRow,
- validateCustomProvider,
-} from "./dialog-custom-provider-form"
+import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
import { DialogSelectProvider } from "./dialog-select-provider"
-import { Select } from "@deepagent-code/ui/select"
-import { Switch } from "@deepagent-code/ui/switch"
-
-// "auto" keeps the zero-config behavior (probe openai-compatible then anthropic and persist whichever
-// answered). The explicit choices let the user pin the protocol when auto-detection is wrong or the
-// endpoint doesn't implement /models. The choice decides both the discovery probe `kind` and the SDK
-// npm persisted to config.
-type ProtocolChoice = "auto" | ProviderProtocol
-const PROTOCOL_CHOICES: ProtocolChoice[] = ["auto", "openai-compatible", "anthropic"]
type Props = {
back?: "providers" | "close"
- // Edit mode: the id of an existing custom provider to load and edit. When set, the dialog prefills
- // from config + resolved provider data and persists spec overrides (keeping discovery on if it was).
- edit?: string
}
export function DialogCustomProvider(props: Props) {
@@ -45,45 +25,15 @@ export function DialogCustomProvider(props: Props) {
const serverSDK = useServerSDK()
const language = useLanguage()
- const editConfig = () => (props.edit ? serverSync.data.config.provider?.[props.edit] : undefined)
- const editResolved = () => (props.edit ? serverSync.data.provider.all.get(props.edit) : undefined)
- // A provider persisted with `discovery: true`: on save we keep discovery on and layer the spec
- // overrides, so runtime model refresh survives the edit.
- const isEditDiscovery = () => props.edit != null && editConfig()?.discovery === true
-
- const initialForm = (): FormState => {
- const cfg = editConfig()
- if (props.edit && cfg) return formStateFromProvider({ config: cfg, resolved: editResolved() })
- return {
- providerID: "",
- name: "",
- baseURL: "",
- apiKey: "",
- models: [modelRow()],
- headers: [headerRow()],
- err: {},
- }
- }
-
- const [form, setForm] = createStore(initialForm())
-
- // Advanced section (provider id / display name / headers / manual models) is collapsed by default:
- // the zero-config path only needs Base URL + API key. In edit mode we open it so the user sees the
- // model rows they came to edit. Protocol detected during discovery decides which SDK npm persists.
- const [showAdvanced, setShowAdvanced] = createSignal(!!props.edit)
- // In edit mode, seed the detected protocol from the persisted SDK npm so a save that skips
- // re-discovery still writes the correct npm.
- const [detectedProtocol, setDetectedProtocol] = createSignal(
- editConfig()?.npm === "@ai-sdk/anthropic" ? "anthropic" : props.edit ? "openai-compatible" : undefined,
- )
- // User's explicit protocol choice. "auto" (default) preserves auto-detection; an explicit choice
- // pins both the discovery probe kind and the persisted npm.
- const [protocolChoice, setProtocolChoice] = createSignal("auto")
-
- // The protocol persisted to config: an explicit choice wins; otherwise the detected one (or
- // openai-compatible default when nothing was detected).
- const resolvedProtocol = (): ProviderProtocol | undefined =>
- protocolChoice() === "auto" ? detectedProtocol() : (protocolChoice() as ProviderProtocol)
+ const [form, setForm] = createStore({
+ providerID: "",
+ name: "",
+ baseURL: "",
+ apiKey: "",
+ models: [modelRow()],
+ headers: [headerRow()],
+ err: {},
+ })
const goBack = () => {
if (props.back === "close") {
@@ -137,18 +87,13 @@ export function DialogCustomProvider(props: Props) {
setForm("err", key, undefined)
}
- const setModel = (index: number, key: "id" | "name" | "context", value: string) => {
+ const setModel = (index: number, key: "id" | "name", value: string) => {
batch(() => {
setForm("models", index, key, value)
- const errKey = key === "context" ? "context" : key
- setForm("models", index, "err", errKey, undefined)
+ setForm("models", index, "err", key, undefined)
})
}
- const setModelBool = (index: number, key: "reasoning" | "temperature", value: boolean) => {
- setForm("models", index, key, value)
- }
-
const setHeader = (index: number, key: "key" | "value", value: string) => {
batch(() => {
setForm("headers", index, key, value)
@@ -164,16 +109,15 @@ export function DialogCustomProvider(props: Props) {
.map((h) => [h.key, h.value]),
)
- const validate = (nextForm: FormState = form, discovery = false) => {
+ const discoveredRows = (models: Array<{ id: string; name: string }>) =>
+ models.map((model) => ({ ...modelRow(), id: model.id, name: model.name || model.id }))
+
+ const validate = (nextForm: FormState = form) => {
const output = validateCustomProvider({
form: nextForm,
t: language.t,
disabledProviders: serverSync.data.config.disabled_providers ?? [],
existingProviderIDs: new Set(serverSync.data.provider.all.keys()),
- protocol: resolvedProtocol(),
- discovery,
- editDiscovery: isEditDiscovery(),
- editingProviderID: props.edit,
})
batch(() => {
setForm("err", output.err)
@@ -215,62 +159,39 @@ export function DialogCustomProvider(props: Props) {
void (async () => {
let nextForm: FormState = form
+ const providerID = form.providerID.trim()
const baseURL = form.baseURL.trim()
const key = form.apiKey.trim()
const env = key.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
// Whether the user already typed at least one model id by hand.
const hasManualModels = form.models.some((m) => m.id.trim().length > 0)
- // providerID is optional now (derived from URL when blank); discovery only needs URL + key.
- // Pass a best-effort id so the backend has something to label the request with.
- const discoverID = form.providerID.trim() || deriveProviderIdentity({
- baseURL,
- existingProviderIDs: new Set(serverSync.data.provider.all.keys()),
- disabledProviders: serverSync.data.config.disabled_providers ?? [],
- }).providerID
- // Runtime discovery mode: when the user provides only URL+key (no manual models) and the
- // endpoint answers discovery, persist `discovery: true` with an empty model list so the backend
- // refreshes models on every load instead of freezing this snapshot into config.
- let discoveryMode = false
- // Edit mode never re-runs discovery: we're persisting spec overrides for an already-connected
- // provider. editDiscovery in the validator keeps `discovery: true` when it was set originally.
- if (!props.edit && baseURL && key && !env) {
+ if (providerID && baseURL && key && !env) {
// Model discovery is a convenience, not a requirement: many OpenAI-compatible servers
// (some vLLM setups, local runtimes) don't implement GET /v1/models and return 400/404.
- // Treat discovery as best-effort — probe to detect the protocol (→ SDK npm) and confirm the
- // endpoint works. If it fails we fall back to manual models and let validation handle the
- // empty case instead of blocking save on a missing endpoint.
- // Auto (kind omitted) lets the backend probe openai-compatible then anthropic and report which
- // protocol answered. An explicit choice pins the probe to that protocol.
- const choice = protocolChoice()
- // Defensive client-side timeout: the backend already caps its /models fetch, but guard the
- // whole round-trip too so a stalled request can never leave the submit button hung. On
- // timeout (or any error) we fall through to manual/validation handling instead of blocking.
- const res = await Promise.race([
- serverSDK.client.provider.models
- .discover(
- {
- providerID: discoverID,
- baseURL,
- apiKey: key,
- headers: headerConfig(),
- ...(choice === "auto" ? {} : { kind: choice }),
- },
- { throwOnError: true },
- )
- .then((res) => res.data),
- new Promise((resolve) => setTimeout(() => resolve(undefined), 20_000)),
- ]).catch(() => undefined)
- if (res?.kind) setDetectedProtocol(res.kind)
- const discovered = res?.models ?? []
+ // Treat discovery as best-effort — only use its result to auto-fill models when the user
+ // hasn't entered any. If it fails (or returns nothing) we keep the user's manual models and
+ // let validation handle the empty case, instead of blocking save on a missing endpoint.
+ const discovered = await serverSDK.client.provider.models
+ .discover(
+ {
+ providerID,
+ baseURL,
+ apiKey: key,
+ headers: headerConfig(),
+ kind: "openai-compatible",
+ },
+ { throwOnError: true },
+ )
+ .then((res) => res.data?.models ?? [])
+ .catch(() => [])
if (discovered.length > 0 && !hasManualModels) {
- // Backend owns the model list at runtime, so we leave the form's manual-model rows empty
- // and persist `discovery: true` instead of freezing this snapshot. (nextForm stays the
- // empty-models form, keeping the validator in discovery mode.)
- discoveryMode = true
+ const rows = discoveredRows(discovered)
+ setForm("models", rows)
+ nextForm = { ...form, models: rows }
}
}
- const result = validate(nextForm, discoveryMode)
+ const result = validate(nextForm)
if (!result) return
saveMutation.mutate(result)
})().catch((err) => {
@@ -295,9 +216,7 @@ export function DialogCustomProvider(props: Props) {
-
- {language.t(props.edit ? "provider.custom.edit.title" : "provider.custom.title")}
-
+
{language.t("provider.custom.title")}
-
-
- Press Enter to
- send, Shift+Enter {" "}
- for new line
-
-
-
- fileInputRef?.click()}
- aria-label="Attach file"
- >
- {uploading() ? "Uploading…" : "Attach"}
-
-
+
+ Press Enter to
+ send, Shift+Enter {" "}
+ for new line
)
diff --git a/packages/app/src/components/im/message-item.tsx b/packages/app/src/components/im/message-item.tsx
index 117c2eb0..af532e10 100644
--- a/packages/app/src/components/im/message-item.tsx
+++ b/packages/app/src/components/im/message-item.tsx
@@ -9,8 +9,6 @@ interface MessageItemProps {
agentStatus?: { agentID: string; status: string }
/** Live reasoning/tool/text parts for the turn this message triggered. */
agentProgress?: AgentProgressPart[]
- /** §B3 thread — open this message's reply chain. Hidden when undefined. */
- onOpenThread?: (message: LocalMessage) => void
}
export function MessageItem(props: MessageItemProps) {
@@ -60,16 +58,6 @@ export function MessageItem(props: MessageItemProps) {
-
- props.onOpenThread!(props.message)}
- >
- Reply in thread
-
-
-
)
diff --git a/packages/app/src/components/im/message-list.tsx b/packages/app/src/components/im/message-list.tsx
index 051f8bdd..4bd3e5d5 100644
--- a/packages/app/src/components/im/message-list.tsx
+++ b/packages/app/src/components/im/message-list.tsx
@@ -6,8 +6,6 @@ interface MessageListProps {
messages: LocalMessage[]
agentStatuses: Map
agentProgress: Map
- // §B3 thread — when provided, each message shows a "Reply in thread" affordance.
- onOpenThread?: (message: LocalMessage) => void
}
export function MessageList(props: MessageListProps) {
@@ -41,7 +39,6 @@ export function MessageList(props: MessageListProps) {
message={message}
agentStatus={props.agentStatuses.get(message.id)}
agentProgress={props.agentProgress.get(message.id)}
- onOpenThread={props.onOpenThread}
/>
)}
diff --git a/packages/app/src/components/im/message-search.tsx b/packages/app/src/components/im/message-search.tsx
deleted file mode 100644
index 48fc3091..00000000
--- a/packages/app/src/components/im/message-search.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { createSignal, For, Show, type Component } from "solid-js"
-import { Button } from "@deepagent-code/ui/button"
-import { useIMClient } from "@/utils/im-client"
-import type { IMMessage } from "./types"
-
-// V4.0 §B3 — full-text message search (FTS + metadata) across the caller's group memberships.
-// Backed by `GET /api/v1/im/search?q=…`. Results are keyset-paginated (nextCursor). Clicking a result
-// hands the group + message id back to the panel so it can open the conversation.
-export const MessageSearch: Component<{
- onSelect?: (result: { groupID: string; messageID: string }) => void
-}> = (props) => {
- const client = useIMClient()
- const [query, setQuery] = createSignal("")
- const [results, setResults] = createSignal([])
- const [cursor, setCursor] = createSignal(null)
- const [hasMore, setHasMore] = createSignal(false)
- const [loading, setLoading] = createSignal(false)
- const [searched, setSearched] = createSignal(false)
-
- const run = async (append: boolean) => {
- const q = query().trim()
- if (!q) return
- setLoading(true)
- try {
- const page = await client.searchMessages({
- q,
- limit: 30,
- cursor: append ? cursor() ?? undefined : undefined,
- })
- const incoming = page.messages ?? []
- setResults((prev) => (append ? [...prev, ...incoming] : incoming))
- setCursor(page.nextCursor)
- setHasMore(page.hasMore)
- setSearched(true)
- } catch (error) {
- console.error("Search failed:", error)
- const { showToast } = await import("@/utils/toast")
- showToast({
- variant: "error",
- title: "Search failed",
- description: error instanceof Error ? error.message : String(error),
- })
- } finally {
- setLoading(false)
- }
- }
-
- return (
-
-
- setQuery(e.currentTarget.value)}
- onKeyDown={(e) => {
- e.stopPropagation()
- if (e.key === "Enter") void run(false)
- }}
- />
- run(false)} disabled={!query().trim() || loading()}>
- {loading() ? "…" : "Search"}
-
-
-
-
- 0}
- fallback={No matching messages.
}
- >
-
-
- {(msg) => (
- props.onSelect?.({ groupID: msg.groupID, messageID: msg.id })}
- >
- {msg.content}
-
- {msg.senderType} · {new Date(msg.createdAt).toLocaleString()}
-
-
- )}
-
-
-
- run(true)} disabled={loading()}>
- {loading() ? "Loading…" : "Load more"}
-
-
-
-
-
-
-
- )
-}
diff --git a/packages/app/src/components/im/thread-view.tsx b/packages/app/src/components/im/thread-view.tsx
deleted file mode 100644
index c4fcd05b..00000000
--- a/packages/app/src/components/im/thread-view.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { createSignal, createEffect, For, Show, type Component } from "solid-js"
-import { IconButton } from "@deepagent-code/ui/icon-button"
-import { Button } from "@deepagent-code/ui/button"
-import { MessageItem } from "./message-item"
-import { MessageComposer } from "./message-composer"
-import { useIMClient } from "@/utils/im-client"
-import type { LocalMessage } from "@/hooks/use-im-websocket"
-import type { AgentDescriptor } from "./types"
-
-// V4.0 §B3 — a message's reply thread. Overlays the group chat: shows the root message + its replies
-// (listThread, keyset paginated via nextCursor), and posts new replies with `replyToID = root.id`.
-// The thread endpoint is `GET /groups/:groupId/messages/:messageId/thread`.
-export const ThreadView: Component<{
- groupID: string
- root: LocalMessage
- agents: AgentDescriptor[]
- onClose: () => void
-}> = (props) => {
- const client = useIMClient()
- const [replies, setReplies] = createSignal([])
- const [cursor, setCursor] = createSignal(null)
- const [hasMore, setHasMore] = createSignal(false)
- const [loading, setLoading] = createSignal(true)
-
- const load = (append: boolean) => {
- setLoading(true)
- client
- .listThread(props.groupID, props.root.id, 50, append ? cursor() ?? undefined : undefined)
- .then((page) => {
- const incoming = page.messages ?? []
- setReplies((prev) => (append ? [...prev, ...incoming] : incoming))
- setCursor(page.nextCursor)
- setHasMore(page.hasMore)
- setLoading(false)
- })
- .catch((error) => {
- console.error("Failed to load thread:", error)
- setLoading(false)
- })
- }
-
- // Reload whenever the root message changes.
- createEffect(() => {
- props.root.id
- setReplies([])
- setCursor(null)
- load(false)
- })
-
- const sendReply = async (content: string) => {
- try {
- const created = await client.createMessage(props.groupID, {
- content,
- type: "text",
- replyToID: props.root.id,
- })
- setReplies((prev) => [...prev, created as LocalMessage])
- } catch (error) {
- console.error("Failed to send reply:", error)
- const { showToast } = await import("@/utils/toast")
- showToast({
- variant: "error",
- title: "Failed to send reply",
- description: error instanceof Error ? error.message : String(error),
- })
- }
- }
-
- return (
-
-
- Thread
-
-
-
-
- {/* root message */}
-
-
-
-
- {replies().length} {replies().length === 1 ? "reply" : "replies"}
-
-
-
0}
- fallback={Loading thread…
}
- >
- {(reply) => }
-
-
- load(true)} disabled={loading()}>
- {loading() ? "Loading…" : "Load more"}
-
-
-
-
-
-
-
-
- )
-}
diff --git a/packages/app/src/components/im/types.ts b/packages/app/src/components/im/types.ts
index 17dbb276..9366a73c 100644
--- a/packages/app/src/components/im/types.ts
+++ b/packages/app/src/components/im/types.ts
@@ -40,19 +40,3 @@ export interface IMMessage {
updatedAt: number
deletedAt: number | null
}
-
-// V4.0 §B3 file attachments. Mirrors the server's `IMAttachmentResponse`
-// (packages/deepagent-code/src/server/routes/instance/httpapi/groups/im.ts).
-export interface IMAttachment {
- id: string
- workspaceID: string
- projectID: string | null
- groupID: string | null
- messageID: string | null
- uploadedBy: string
- filename: string
- mime: string
- sizeBytes: number
- checksum: string
- createdAt: number
-}
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 9aee1c96..580fd9a3 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -37,7 +37,6 @@ import { useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { Button } from "@deepagent-code/ui/button"
-import { Spinner } from "@deepagent-code/ui/spinner"
import { DockShellForm, DockTray } from "@deepagent-code/ui/dock-surface"
import { Icon, type IconProps } from "@deepagent-code/ui/icon"
import { ProviderIcon } from "@deepagent-code/ui/provider-icon"
@@ -57,8 +56,6 @@ import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionTabs } from "@/pages/session/helpers"
import { PanelButton } from "@/components/deepagent/panel-button"
import { fetchCapabilities } from "@/components/deepagent/panel-goal.api"
-import { ApprovalControl } from "@/components/deepagent/approval-control"
-import { ModeSelector, useModeLabel } from "@/components/deepagent/mode-selector"
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
import { createPromptAttachments } from "./prompt-input/attachments"
import { ACCEPTED_FILE_TYPES, pickAttachmentFiles } from "./prompt-input/files"
@@ -146,7 +143,6 @@ export const PromptInput: Component = (props) => {
const command = useCommand()
const permission = usePermission()
const language = useLanguage()
- const modeLabel = useModeLabel()
const platform = usePlatform()
const pickDirectory = useDirectoryPicker()
const { params, tabs, view } = useSessionLayout()
@@ -159,10 +155,6 @@ export const PromptInput: Component = (props) => {
let draftPreparePrompt: { prompt: Prompt; cursor: number } | undefined
const [draftPreparing, setDraftPreparing] = createSignal(false)
- // Live text streamed from intelligence refinement. Shown in a panel BELOW the editor while the raw
- // user input stays untouched in the editor above. Preserved on cancel so the user keeps both their
- // input and whatever draft was generated so far; cleared only on approved submit or explicit dismiss.
- const [draftPreview, setDraftPreview] = createSignal("")
const [draftReview, setDraftReview] = createSignal<
| {
draft: DeepAgentPromptPrepareResult
@@ -248,11 +240,10 @@ export const PromptInput: Component = (props) => {
draftPreparePrompt = { prompt: current, cursor: prompt.cursor() ?? promptLength(current) }
}
setDraftPreparing(true)
- // Reset any leftover preview so the panel starts clean, then show a generating placeholder until
- // the first delta arrives. The raw user input stays in the editor above — we do NOT overwrite it.
- setDraftPreview("")
setStore("mode", "normal")
setStore("popover", null)
+ const text = language.t("prompt.generating")
+ prompt.set(makeTextPrompt(text), text.length)
requestAnimationFrame(() => {
editorRef.blur()
queueScroll()
@@ -261,41 +252,12 @@ export const PromptInput: Component = (props) => {
const stopDraftPrepare = () => {
setDraftPreparing(false)
- // Keep the raw input reference around while a draft (partial or reviewable) is still on screen so
- // the user can restore it; only drop it once nothing references the original prompt anymore.
requestAnimationFrame(() => {
- if (draftReview() || draftPreview()) return
+ if (draftReview()) return
draftPreparePrompt = undefined
})
}
- const updateDraftPrepare = (preview: string) => {
- if (!draftPreparing() || !preview) return
- setDraftPreview(preview)
- queueScroll()
- }
-
- // Dismiss a preserved partial draft (from a canceled preparation) without touching the editor input.
- const dismissDraftPreview = () => {
- setDraftPreview("")
- if (!draftReview() && !draftPreparing()) draftPreparePrompt = undefined
- }
-
- // Copy the preserved partial draft into the editor so the user can edit and resubmit it. A canceled
- // stream has no server-persisted draft id, so it can't be submitted as an intelligence draft directly.
- const useDraftPreview = () => {
- const preview = draftPreview().trim()
- if (!preview) return
- prompt.set(makeTextPrompt(preview), preview.length)
- setDraftPreview("")
- draftPreparePrompt = undefined
- requestAnimationFrame(() => {
- editorRef.focus()
- setCursorPosition(editorRef, preview.length)
- queueScroll()
- })
- }
-
const activeFileTab = createSessionTabs({
tabs,
pathFromTab: files.pathFromTab,
@@ -427,7 +389,7 @@ export const PromptInput: Component = (props) => {
.join("")
return text.trim().length === 0 && imageAttachments().length === 0 && commentCount() === 0
})
- const stopping = createMemo(() => draftPreparing() || (working() && blank()))
+ const stopping = createMemo(() => working() && blank())
const tip = () => {
if (stopping()) {
return (
@@ -1281,9 +1243,6 @@ export const PromptInput: Component = (props) => {
const editable = draft.preview.trim() || draft.goal.trim()
draftReviewResolve = resolve
setDraftPreparing(false)
- // Preparation succeeded and produced a persisted, submittable draft: transition from the live
- // preview panel to the editable review. The editor now holds the draft for the user to approve.
- setDraftPreview("")
setDraftReview({ draft, originalPrompt: original.prompt, originalCursor: original.cursor })
setStore("mode", "normal")
setStore("popover", null)
@@ -1318,7 +1277,6 @@ export const PromptInput: Component = (props) => {
onAbort: props.onAbort,
onSubmit: props.onSubmit,
onPromptPrepareStart: startDraftPrepare,
- onPromptPrepareProgress: updateDraftPrepare,
onPromptPrepareEnd: stopDraftPrepare,
confirmPromptDraft,
})
@@ -1329,12 +1287,7 @@ export const PromptInput: Component = (props) => {
event.preventDefault()
return
}
- if (draftPreparing()) {
- event.preventDefault()
- void abort()
- return
- }
- if (composing()) {
+ if (draftPreparing() || composing()) {
event.preventDefault()
return
}
@@ -1343,14 +1296,11 @@ export const PromptInput: Component = (props) => {
confirmCurrentDraft()
return
}
- // A fresh submission supersedes any paused draft from a prior canceled preparation.
- if (draftPreview()) setDraftPreview("")
handleSubmit(event)
}
const handleKeyDown = (event: KeyboardEvent) => {
if (draftPreparing()) {
- if (event.key === "Escape" || (event.ctrlKey && event.code === "KeyG")) void abort()
event.preventDefault()
event.stopPropagation()
return
@@ -1666,53 +1616,6 @@ export const PromptInput: Component = (props) => {
onPress: () => void addProject(),
}))
- // Live/paused draft panel shown BELOW the editor. While preparing it streams the refined prompt;
- // after a cancel it stays as a preserved partial draft the user can adopt (→ editor) or dismiss.
- const draftPreviewPanel = () => (
- 0}>
-
-
-
-
-
-
- {draftPreparing()
- ? language.t("prompt.draft.preview.streaming")
- : language.t("prompt.draft.preview.paused")}
-
-
-
- {draftPreview().trim() || language.t("prompt.generating")}
-
-
0}>
-
-
- {language.t("prompt.draft.preview.dismiss")}
-
-
- {language.t("prompt.draft.preview.use")}
-
-
-
-
-
- )
-
const draftReviewBar = () => (
{(_review) => (
@@ -1855,9 +1758,7 @@ export const PromptInput: Component = (props) => {
"[&_[data-type=file]]:text-syntax-property": true,
"[&_[data-type=agent]]:text-syntax-type": true,
"font-mono!": store.mode === "shell",
- // Editor keeps the raw user input during preparation (the streamed draft shows in the
- // panel below), so it reads normally — locked to edits, not greyed as a placeholder.
- "cursor-wait": draftPreparing(),
+ "cursor-wait text-text-weak": draftPreparing(),
}}
style={{ "padding-bottom": space }}
/>
@@ -1896,11 +1797,11 @@ export const PromptInput: Component = (props) => {
-
+
= (props) => {
- {draftPreviewPanel()}
@@ -1979,28 +1879,23 @@ export const PromptInput: Component = (props) => {
title={language.t("command.agent.cycle")}
keybind={command.keybind("agent.cycle")}
>
- {
+ local.agent.set(value)
+ restoreFocus()
}}
- onClose={restoreFocus}
- >
- {modeLabel(local.agent.current()?.name)}
-
-
+ class="capitalize max-w-[160px] text-text-base"
+ valueClass="truncate text-13-regular text-text-base"
+ triggerStyle={control()}
+ triggerProps={{ "data-action": "prompt-agent" }}
+ variant="ghost"
+ />
-
-
-
diff --git a/packages/app/src/components/prompt-input/scenario-toggle.tsx b/packages/app/src/components/prompt-input/scenario-toggle.tsx
index de7a27f4..b7d25e2c 100644
--- a/packages/app/src/components/prompt-input/scenario-toggle.tsx
+++ b/packages/app/src/components/prompt-input/scenario-toggle.tsx
@@ -17,7 +17,7 @@ const scenarios = [
},
{
mode: "intelligence" as const,
- icon: "intelligence" as const,
+ icon: "speech-bubble" as const,
label: "prompt.scenario.intelligence" as const,
tooltip: "prompt.scenario.intelligence.tooltip" as const,
},
diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts
index 1eb5e2da..50935f36 100644
--- a/packages/app/src/components/prompt-input/submit.test.ts
+++ b/packages/app/src/components/prompt-input/submit.test.ts
@@ -29,7 +29,6 @@ const preparedDrafts: Array<{
}> = []
const sentPromptAsync: Array<{ directory: string; metadata?: unknown; text?: string }> = []
const promptPrepareEvents: string[] = []
-const promptPrepareProgress: string[] = []
let params: { id?: string } = {}
let selected = "/repo/worktree-a"
@@ -73,10 +72,8 @@ const clientFor = (directory: string) => {
},
client: {
request: async (payload: {
- url?: string
path?: { sessionID?: string }
body?: { mode?: string; output_language?: string; parts?: Array<{ type: string; text?: string }> }
- signal?: AbortSignal
}) => {
const text = payload.body?.parts?.find((part) => part.type === "text")?.text
preparedDrafts.push({
@@ -94,43 +91,16 @@ const clientFor = (directory: string) => {
},
})
}
- if (text === "prepare waits") {
- return {
- data: new ReadableStream({
- start(controller) {
- payload.signal?.addEventListener(
- "abort",
- () => controller.error(new DOMException("Aborted", "AbortError")),
- { once: true },
- )
- },
- }),
- }
- }
- const result = {
- prompt_draft_id: "prompt_draft:test:1",
- context_plan_id: "context_plan:test:1",
- state: "draft_ready",
- mode: payload.body?.mode ?? "intelligence",
- route: text === "hello" ? "general" : "code",
- goal: "Prepared goal",
- preview: "# Prepared prompt",
- }
return {
- data: new ReadableStream({
- start(controller) {
- controller.enqueue(
- new TextEncoder().encode(
- [
- `data: ${JSON.stringify({ type: "progress", preview: "Prepared" })}\n\n`,
- `data: ${JSON.stringify({ type: "progress", preview: "Prepared goal" })}\n\n`,
- `data: ${JSON.stringify({ type: "result", result })}\n\n`,
- ].join(""),
- ),
- )
- controller.close()
- },
- }),
+ data: {
+ prompt_draft_id: "prompt_draft:test:1",
+ context_plan_id: "context_plan:test:1",
+ state: "draft_ready",
+ mode: payload.body?.mode ?? "intelligence",
+ route: text === "hello" ? "general" : "code",
+ goal: "Prepared goal",
+ preview: "# Prepared prompt",
+ },
}
},
},
@@ -313,7 +283,6 @@ beforeEach(() => {
preparedDrafts.length = 0
sentPromptAsync.length = 0
promptPrepareEvents.length = 0
- promptPrepareProgress.length = 0
promptValue[0] = { type: "text", content: "ls", start: 0, end: 2 }
selected = "/repo/worktree-a"
variant = undefined
@@ -470,7 +439,6 @@ describe("prompt submit worktree selection", () => {
setMode: () => undefined,
setPopover: () => undefined,
onPromptPrepareStart: () => promptPrepareEvents.push("start"),
- onPromptPrepareProgress: (preview) => promptPrepareProgress.push(preview),
onPromptPrepareEnd: () => promptPrepareEvents.push("end"),
confirmPromptDraft: async () => ({ editedGoal: "Edited prepared goal" }),
onSubmit: () => undefined,
@@ -482,7 +450,6 @@ describe("prompt submit worktree selection", () => {
await flushAsyncSubmit()
expect(promptPrepareEvents).toEqual(["start", "end"])
- expect(promptPrepareProgress).toEqual(["Prepared", "Prepared goal"])
expect(preparedDrafts).toEqual([
{ directory: "/repo/main", sessionID: "session-1", mode: "intelligence", outputLanguage: "english", text: "ls" },
])
@@ -565,13 +532,7 @@ describe("prompt submit worktree selection", () => {
expect(confirms).toEqual([])
expect(preparedDrafts).toEqual([
- {
- directory: "/repo/main",
- sessionID: "session-1",
- mode: "intelligence",
- outputLanguage: "english",
- text: "hello",
- },
+ { directory: "/repo/main", sessionID: "session-1", mode: "intelligence", outputLanguage: "english", text: "hello" },
])
expect(sentPromptAsync[0]?.text).toBe("hello")
expect(sentPromptAsync[0]?.metadata).toEqual({
@@ -624,38 +585,4 @@ describe("prompt submit worktree selection", () => {
expect(sentPromptAsync).toEqual([])
promptValue[0] = { type: "text", content: "ls", start: 0, end: 2 }
})
-
- test("stops intelligence prompt preparation without submitting", async () => {
- params = { id: "session-1" }
- promptMode = "intelligence"
- promptValue[0] = { type: "text", content: "prepare waits", start: 0, end: 13 }
-
- const submit = createPromptSubmit({
- info: () => ({ id: "session-1" }),
- imageAttachments: () => [],
- commentCount: () => 0,
- autoAccept: () => false,
- mode: () => "normal",
- working: () => false,
- editor: () => undefined,
- queueScroll: () => undefined,
- promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
- addToHistory: () => undefined,
- resetHistoryNavigation: () => undefined,
- setMode: () => undefined,
- setPopover: () => undefined,
- onPromptPrepareStart: () => promptPrepareEvents.push("start"),
- onPromptPrepareEnd: () => promptPrepareEvents.push("end"),
- onSubmit: () => undefined,
- })
-
- await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event)
- await flushAsyncSubmit()
- await submit.abort()
- await flushAsyncSubmit()
-
- expect(promptPrepareEvents).toEqual(["start", "end"])
- expect(sentPromptAsync).toEqual([])
- promptValue[0] = { type: "text", content: "ls", start: 0, end: 2 }
- })
})
diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts
index 7e98fdf9..baa67555 100644
--- a/packages/app/src/components/prompt-input/submit.ts
+++ b/packages/app/src/components/prompt-input/submit.ts
@@ -77,17 +77,10 @@ type RawSdkClient = {
path?: Record
body?: unknown
headers?: Record
- parseAs?: "stream"
- signal?: AbortSignal
}): Promise<{ data?: TData }>
}
}
-type DeepAgentPromptPrepareStreamEvent =
- | { type: "progress"; preview: string }
- | { type: "result"; result: DeepAgentPromptPrepareResult }
- | { type: "error"; message: string }
-
type FollowupSendInput = {
client: ReturnType["client"]
serverSync: ReturnType
@@ -98,9 +91,7 @@ type FollowupSendInput = {
before?: () => Promise | boolean
onBeforeSubmit?: () => void
onPromptPrepareStart?: () => void
- onPromptPrepareProgress?: (preview: string) => void
onPromptPrepareEnd?: () => void
- promptPrepareSignal?: AbortSignal
promptOutputLanguage?: DeepAgentPromptOutputLanguage
confirmPromptDraft?: (draft: DeepAgentPromptPrepareResult) => Promise
}
@@ -120,13 +111,11 @@ async function prepareDeepAgentPromptDraft(input: {
mode: DeepAgentPromptModeForConfirmation
outputLanguage: DeepAgentPromptOutputLanguage
parts: SessionPromptAsyncInput["parts"]
- signal?: AbortSignal
- onProgress?: (preview: string) => void
}) {
const raw = input.client as unknown as RawSdkClient
- const response = await raw.client.request>({
+ const response = await raw.client.request({
method: "POST",
- url: "/session/{sessionID}/prompt_prepare_stream",
+ url: "/session/{sessionID}/prompt_prepare",
path: { sessionID: input.sessionID },
body: {
mode: input.mode,
@@ -136,43 +125,9 @@ async function prepareDeepAgentPromptDraft(input: {
headers: {
"Content-Type": "application/json",
},
- parseAs: "stream",
- signal: input.signal,
})
- if (!response.data) throw new Error("Prompt draft prepare returned no stream")
-
- const decoder = new TextDecoder()
- const reader = response.data.getReader()
- let buffer = ""
- let prepared: DeepAgentPromptPrepareResult | undefined
- const readEvent = (block: string) => {
- const data = block
- .split("\n")
- .filter((line) => line.startsWith("data:"))
- .map((line) => line.slice(5).trimStart())
- .join("\n")
- if (!data) return
- const event = JSON.parse(data) as DeepAgentPromptPrepareStreamEvent
- if (event.type === "progress") {
- input.onProgress?.(event.preview)
- return
- }
- if (event.type === "error") throw new Error(event.message)
- prepared = event.result
- }
-
- while (true) {
- const part = await reader.read()
- if (part.done) break
- buffer += decoder.decode(part.value, { stream: true })
- const blocks = buffer.split("\n\n")
- buffer = blocks.pop() ?? ""
- blocks.forEach(readEvent)
- }
- buffer += decoder.decode()
- if (buffer.trim()) readEvent(buffer)
- if (!prepared) throw new Error("Prompt draft prepare returned no result")
- return prepared
+ if (!response.data) throw new Error("Prompt draft prepare returned no data")
+ return response.data
}
export type DeepAgentPromptSuggestion = {
@@ -285,13 +240,10 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
mode,
outputLanguage: input.promptOutputLanguage ?? "english",
parts: preparedParts.requestParts,
- signal: input.promptPrepareSignal,
- onProgress: input.onPromptPrepareProgress,
})
} catch (err) {
setIdle()
input.onPromptPrepareEnd?.()
- if (input.promptPrepareSignal?.aborted) return false
throw err
}
input.onPromptPrepareEnd?.()
@@ -402,7 +354,6 @@ type PromptSubmitInput = {
onAbort?: () => void
onSubmit?: () => void
onPromptPrepareStart?: () => void
- onPromptPrepareProgress?: (preview: string) => void
onPromptPrepareEnd?: () => void
confirmPromptDraft?: (draft: DeepAgentPromptPrepareResult) => Promise
}
@@ -430,7 +381,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const language = useLanguage()
const params = useParams()
const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID)
- let activePreparation: { sessionID: string; controller: AbortController } | undefined
const errorMessage = (err: unknown) => {
if (err && typeof err === "object" && "data" in err) {
@@ -442,7 +392,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}
const abort = async () => {
- const sessionID = activePreparation?.sessionID ?? params.id
+ const sessionID = params.id
if (!sessionID) return Promise.resolve()
// D3: any stop resets the scenario to `direct` and pauses scenario automation for this
@@ -452,12 +402,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
input.onAbort?.()
- if (activePreparation) {
- activePreparation.controller.abort()
- activePreparation = undefined
- return Promise.resolve()
- }
-
const key = pendingKey(sessionID)
const queued = pending.get(key)
if (queued) {
@@ -734,7 +678,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
const messageID = Identifier.ascending("message")
const preparesPromptDraft = promptPipelineMode(draft.metadata) === "intelligence"
- const preparationAbort = preparesPromptDraft ? new AbortController() : undefined
const removeOptimisticMessage = () => {
sync.session.optimistic.remove({
@@ -757,7 +700,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
sync.set("session_status", session.id, { type: "busy" })
}
- const controller = preparationAbort ?? new AbortController()
+ const controller = new AbortController()
const cleanup = () => {
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
@@ -806,12 +749,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
return true
}
- if (preparationAbort) activePreparation = { sessionID: session.id, controller: preparationAbort }
- const clearActivePreparation = () => {
- if (activePreparation?.controller !== preparationAbort) return
- activePreparation = undefined
- }
-
void sendFollowupDraft({
client,
sync,
@@ -822,14 +759,11 @@ export function createPromptSubmit(input: PromptSubmitInput) {
before: waitForWorktree,
onBeforeSubmit: input.onSubmit,
onPromptPrepareStart: input.onPromptPrepareStart,
- onPromptPrepareProgress: input.onPromptPrepareProgress,
onPromptPrepareEnd: input.onPromptPrepareEnd,
- promptPrepareSignal: preparationAbort?.signal,
promptOutputLanguage: promptOutputLanguage(language.locale()),
confirmPromptDraft: input.confirmPromptDraft,
})
.then((sent) => {
- clearActivePreparation()
pending.delete(pendingKey(session.id))
if (sent) {
if (preparesPromptDraft) {
@@ -846,7 +780,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
restoreInput()
})
.catch((err) => {
- clearActivePreparation()
pending.delete(pendingKey(session.id))
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
diff --git a/packages/app/src/components/review/dialog-review.api.ts b/packages/app/src/components/review/dialog-review.api.ts
index 3f293b78..3dc44596 100644
--- a/packages/app/src/components/review/dialog-review.api.ts
+++ b/packages/app/src/components/review/dialog-review.api.ts
@@ -10,9 +10,6 @@ export type KnowledgeItem = {
evidence_strength: "strong" | "medium" | "weak" | "none"
evidence_refs: string[]
approval_status: "pending" | "approved" | "rejected"
- // Storage scope, for grouping by project vs global: "durable" (global) or
- // "durable:project:". Absent from older servers → treated as global.
- scope?: string
}
type RawSdkClient = {
diff --git a/packages/app/src/components/review/dialog-review.tsx b/packages/app/src/components/review/dialog-review.tsx
index c3c8a165..ec85e370 100644
--- a/packages/app/src/components/review/dialog-review.tsx
+++ b/packages/app/src/components/review/dialog-review.tsx
@@ -150,30 +150,29 @@ export const DialogReview: Component<{ client: ReviewClient }> = (props) => {
const pending = createMemo(() => (items() ?? []).filter((i) => i.approval_status === "pending" && matchesQuery(i)))
const approved = createMemo(() => (items() ?? []).filter((i) => i.approval_status === "approved" && matchesQuery(i)))
- // Group by SCOPE, not by type: the governance view shows the user's learned facts split into
- // "this project" vs "global". A doc scoped `durable:project:` is project-local; anything else
- // (`durable`, or legacy untagged) is global. (Skills + domain-pack seed docs are already excluded
- // server-side, so every item here is a human-readable, user-learned fact.)
- const scopeOf = (item: KnowledgeItem): "project" | "global" =>
- item.scope?.startsWith("durable:project:") ? "project" : "global"
- const scopeLabel = (scope: string) =>
- scope === "project" ? language.t("review.scope.project") : language.t("review.scope.global")
+ // Stable display order for the type boxes. Any unknown type falls back into "other".
+ const TYPE_ORDER = ["memory", "failure_dossier", "knowledge", "strategy", "methodology", "skill"] as const
+ const typeLabel = (type: string) =>
+ (TYPE_ORDER as readonly string[]).includes(type)
+ ? language.t(`review.type.${type}`)
+ : language.t("review.type.other")
- // Project bucket first (most specific to what the user is working on), then global.
- const SCOPE_ORDER = ["project", "global"] as const
+ // Group pending items by type, preserving TYPE_ORDER; trailing "other" bucket for unknown types.
const pendingGroups = createMemo(() => {
- const byScope = new Map()
+ const byType = new Map()
for (const item of pending()) {
- const key = scopeOf(item)
- const list = byScope.get(key)
+ const key = (TYPE_ORDER as readonly string[]).includes(item.type) ? item.type : "other"
+ const list = byType.get(key)
if (list) list.push(item)
- else byScope.set(key, [item])
+ else byType.set(key, [item])
}
- const ordered: Array<{ scope: string; items: KnowledgeItem[] }> = []
- for (const scope of SCOPE_ORDER) {
- const list = byScope.get(scope)
- if (list?.length) ordered.push({ scope, items: list })
+ const ordered: Array<{ type: string; items: KnowledgeItem[] }> = []
+ for (const type of TYPE_ORDER) {
+ const list = byType.get(type)
+ if (list?.length) ordered.push({ type, items: list })
}
+ const other = byType.get("other")
+ if (other?.length) ordered.push({ type: "other", items: other })
return ordered
})
@@ -527,7 +526,7 @@ export const DialogReview: Component<{ client: ReviewClient }> = (props) => {
}
>
- {(group) => GroupBox({ boxKey: group.scope, label: scopeLabel(group.scope), items: group.items })}
+ {(group) => GroupBox({ boxKey: group.type, label: typeLabel(group.type), items: group.items })}
0}>
{GroupBox({ boxKey: "approved", label: language.t("review.status.approved"), items: approved() })}
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index 2e9b6754..c3cbe55a 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -20,7 +20,6 @@ import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal"
import { focusTerminalById } from "@/pages/session/helpers"
import { useSessionLayout } from "@/pages/session/session-layout"
-import { StatusPopover } from "@/components/status-popover"
import { messageAgentColor } from "@/utils/agent"
import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist"
@@ -226,9 +225,7 @@ export function SessionHeader() {
view().reviewPanel.close()
layout.fileTree.close()
- // T3.2: the right panel now has an always-on icon rail; there is no separate "menu" list. Opening
- // the panel means opening a default content panel (review) — the rail lets the user switch from there.
- view().rightPanel.open("review")
+ view().rightPanel.open("menu")
}
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
@@ -490,7 +487,6 @@ export function SessionHeader() {
-
diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx
index 4594c5fc..8c7d5a82 100644
--- a/packages/app/src/components/settings-v2/general.tsx
+++ b/packages/app/src/components/settings-v2/general.tsx
@@ -6,7 +6,9 @@ import { Switch } from "@deepagent-code/ui/v2/switch-v2"
import { TextInputV2 } from "@deepagent-code/ui/v2/text-input-v2"
import { Tooltip } from "@deepagent-code/ui/tooltip"
import { useTheme, type ColorScheme } from "@deepagent-code/ui/theme/context"
+import { useParams } from "@solidjs/router"
import { useLanguage } from "@/context/language"
+import { usePermission } from "@/context/permission"
import { usePlatform, type DisplayBackend } from "@/context/platform"
import { ZOOM_LEVELS } from "@/zoom-levels"
import { useServerSync } from "@/context/server-sync"
@@ -24,6 +26,7 @@ import {
terminalInput,
useSettings,
} from "@/context/settings"
+import { decode64 } from "@/utils/base64"
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "../link"
import { SettingsListV2 } from "./parts/list"
@@ -97,13 +100,40 @@ const playDemoSound = (id: string | undefined) => {
export const SettingsGeneralV2: Component = () => {
const theme = useTheme()
const language = useLanguage()
+ const permission = usePermission()
const platform = usePlatform()
+ const params = useParams()
const settings = useSettings()
const models = useModels()
const updater = useUpdaterAction()
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
+ const dir = createMemo(() => decode64(params.dir))
+ const accepting = createMemo(() => {
+ const value = dir()
+ if (!value) return false
+ if (!params.id) return permission.isAutoAcceptingDirectory(value)
+ return permission.isAutoAccepting(params.id, value)
+ })
+
+ const toggleAccept = (checked: boolean) => {
+ const value = dir()
+ if (!value) return
+
+ if (!params.id) {
+ if (permission.isAutoAcceptingDirectory(value) === checked) return
+ permission.toggleAutoAcceptDirectory(value)
+ return
+ }
+
+ if (checked) {
+ permission.enableAutoAccept(params.id, value)
+ return
+ }
+
+ permission.disableAutoAccept(params.id, value)
+ }
const desktop = createMemo(() => platform.platform === "desktop")
const themeOptions = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
@@ -485,6 +515,14 @@ export const SettingsGeneralV2: Component = () => {
/>
+
+
+
+
+
)
diff --git a/packages/app/src/components/settings-v2/import-history.tsx b/packages/app/src/components/settings-v2/import-history.tsx
index a351bfe7..918c30d8 100644
--- a/packages/app/src/components/settings-v2/import-history.tsx
+++ b/packages/app/src/components/settings-v2/import-history.tsx
@@ -228,7 +228,7 @@ export const ImportSection: Component = () => {
value={customPath()}
onInput={(e) => setCustomPath(e.currentTarget.value)}
disabled={running()}
- placeholder="~/.codex_backup"
+ placeholder="/Users/you/.codex_backup"
spellcheck={false}
autocorrect="off"
autocomplete="off"
@@ -296,7 +296,7 @@ export const ImportSection: Component = () => {
value={cwdFilter()}
onInput={(e) => setCwdFilter(e.currentTarget.value)}
disabled={running()}
- placeholder="~/projects/..."
+ placeholder="/Users/you/projects/..."
spellcheck={false}
autocorrect="off"
autocomplete="off"
@@ -305,22 +305,18 @@ export const ImportSection: Component = () => {
-
-
- {running() ? t("settings.import.running", "Importing…") : t("settings.import.run.btn", "Import")}
-
-
-
- {t("settings.import.cancel", "Cancel")}
+
+
+
+ {running() ? t("settings.import.running", "Importing…") : t("settings.import.run.btn", "Import")}
-
-
+
+
+ {t("settings.import.cancel", "Cancel")}
+
+
+
+
diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx
index 000f8a7d..e77b5968 100644
--- a/packages/app/src/components/settings-v2/providers.tsx
+++ b/packages/app/src/components/settings-v2/providers.tsx
@@ -91,13 +91,7 @@ export const SettingsProvidersV2: Component = () => {
await serverSync
.updateConfig({ disabled_providers: next })
- .then(async () => {
- // updateConfig only forks the backend instance rebuild (fire-and-forget), so the provider
- // list the immediate refresh reads back can still include the just-disabled provider. Await an
- // explicit dispose so the rebuild completes before refreshing — otherwise the row lingers in
- // the connected list even though the toast reports success.
- await serverSdk.client.global.dispose().catch(() => undefined)
- serverSync.refreshProviders()
+ .then(() => {
showToast({
variant: "success",
icon: "circle-check",
@@ -113,13 +107,7 @@ export const SettingsProvidersV2: Component = () => {
}
const disconnect = async (providerID: string, name: string) => {
- // Any config-defined third-party provider — including discovery-mode ones whose config has no
- // models (isConfigCustom misses those, so before this they fell through to the standard branch,
- // which only clears a keystore credential the provider doesn't use and left it in the config →
- // it reappeared on every restart). Route them through disableProvider, which writes
- // `disabled_providers`; isProviderAllowed then filters the provider out and it stays gone across
- // restarts.
- if (isCustomProvider(providerID)) {
+ if (isConfigCustom(providerID)) {
await serverSdk.client.auth.remove({ providerID }).catch(() => undefined)
await disableProvider(providerID, name)
return
@@ -144,21 +132,7 @@ export const SettingsProvidersV2: Component = () => {
})
}
- // A third-party provider defined via config (including discovery-mode providers whose config has no
- // models). These open the custom-provider dialog in edit mode so the user can adjust auto-filled
- // specs; official/api/env providers use the standard connect dialog.
- const isCustomProvider = (providerID: string) => {
- const cfg = serverSync.data.config.provider?.[providerID]
- if (!cfg) return false
- const resolvedSource = serverSync.data.provider.all.get(providerID)?.source
- return resolvedSource === "custom" || isConfigCustom(providerID)
- }
-
const openProviderConfig = (providerID: string) => {
- if (isCustomProvider(providerID)) {
- dialog.push(() => )
- return
- }
dialog.push(() => )
}
diff --git a/packages/app/src/components/settings-v2/servers.tsx b/packages/app/src/components/settings-v2/servers.tsx
index d477bca4..09125316 100644
--- a/packages/app/src/components/settings-v2/servers.tsx
+++ b/packages/app/src/components/settings-v2/servers.tsx
@@ -2,7 +2,6 @@ import { ButtonV2 } from "@deepagent-code/ui/v2/button-v2"
import { Tag } from "@deepagent-code/ui/v2/badge-v2"
import { Icon as IconV2 } from "@deepagent-code/ui/v2/icon"
import { IconButtonV2 } from "@deepagent-code/ui/v2/icon-button-v2"
-import { MenuV2 } from "@deepagent-code/ui/v2/menu-v2"
import { TextInputV2 } from "@deepagent-code/ui/v2/text-input-v2"
import { useDialog } from "@deepagent-code/ui/context/dialog"
import fuzzysort from "fuzzysort"
@@ -61,23 +60,12 @@ export const SettingsServersV2: Component = () => {
>
diff --git a/packages/app/src/components/settings-v2/settings-v2.css b/packages/app/src/components/settings-v2/settings-v2.css
index 445265d0..ca6d0919 100644
--- a/packages/app/src/components/settings-v2/settings-v2.css
+++ b/packages/app/src/components/settings-v2/settings-v2.css
@@ -730,19 +730,6 @@
color: var(--v2-text-text-faint);
}
-/* Import: the run action is a full-width bar button under the options list, not a small
- right-aligned row control. Cancel (only while running) sits beside it, sized to content. */
-.settings-v2-import-actions {
- display: flex;
- gap: 8px;
- margin-top: 4px;
-}
-
-.settings-v2-import-run {
- flex: 1;
- justify-content: center;
-}
-
.settings-v2-import-log {
margin: 0;
padding: 10px 12px;
diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx
index 6ec5f86a..4f436f86 100644
--- a/packages/app/src/components/status-popover-body.tsx
+++ b/packages/app/src/components/status-popover-body.tsx
@@ -1,11 +1,12 @@
import { Button } from "@deepagent-code/ui/button"
import { useDialog } from "@deepagent-code/ui/context/dialog"
import { Icon } from "@deepagent-code/ui/icon"
+import { Switch } from "@deepagent-code/ui/switch"
import { Tabs } from "@deepagent-code/ui/tabs"
+import { showToast } from "@/utils/toast"
import { useNavigate } from "@solidjs/router"
-import { type Accessor, createEffect, createMemo, For, onCleanup, Show } from "solid-js"
+import { type Accessor, createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
-import { McpManagement } from "@/components/mcp-management"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
@@ -13,6 +14,9 @@ import { ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { type ServerHealth } from "@/utils/server-health"
import { useGlobal } from "@/context/global"
+import { useMcpRemove, useMcpToggle } from "@/context/mcp"
+import { DialogAddMcp } from "@/components/dialog-add-mcp"
+import { DialogConfigureMcp } from "@/components/dialog-configure-mcp"
const listServersByHealth = (
list: ServerConnection.Any[],
@@ -242,6 +246,14 @@ export function StatusPopoverBody(props: { shown: Accessor }) {
const language = useLanguage()
const navigate = useNavigate()
+ const fail = (err: unknown) => {
+ showToast({
+ variant: "error",
+ title: language.t("common.requestFailed"),
+ description: err instanceof Error ? err.message : String(err),
+ })
+ }
+
createEffect(() => {
if (!props.shown()) return
})
@@ -253,11 +265,30 @@ export function StatusPopoverBody(props: { shown: Accessor }) {
dialogRun += 1
})
const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
+ const toggleMcp = useMcpToggle()
+ const removeMcp = useMcpRemove()
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
+ const [selectedMcp, setSelectedMcp] = createSignal()
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
+ createEffect(() => {
+ const names = mcpNames()
+ if (names.length === 0) {
+ setSelectedMcp(undefined)
+ return
+ }
+ if (!selectedMcp() || !names.includes(selectedMcp()!)) setSelectedMcp(names[0])
+ })
+
+ const deleteSelectedMcp = () => {
+ const name = selectedMcp()
+ if (!name || removeMcp.isPending) return
+ if (!window.confirm(language.t("dialog.mcp.delete.confirm", { name }))) return
+ removeMcp.mutate(name)
+ }
+
return (
}) {
-
+
+ dialog.show(() => )}>
+ {language.t("dialog.mcp.add")}
+
+ {
+ const name = selectedMcp()
+ if (!name) return
+ dialog.show(() => )
+ }}
+ >
+ {language.t("dialog.mcp.configure")}
+
+
+ {language.t("dialog.mcp.delete")}
+
+
+
+
0}
+ fallback={
+ {language.t("dialog.mcp.empty")}
+ }
+ >
+
+ {(name) => {
+ const status = () => mcpStatus(name)
+ const enabled = () => status() === "connected"
+ return (
+ {
+ setSelectedMcp(name)
+ }}
+ >
+
+
+
+ {name}
+
+
+
+ {language.t("mcp.auth.clickToAuthenticate")}
+
+
+
+ event.stopPropagation()}>
+ {
+ if (toggleMcp.isPending) return
+ toggleMcp.mutate(name)
+ }}
+ />
+
+
+ )
+ }}
+
+
+
diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx
index 41663f9d..f0e6f136 100644
--- a/packages/app/src/components/status-popover.tsx
+++ b/packages/app/src/components/status-popover.tsx
@@ -6,16 +6,28 @@ import { Popover } from "@deepagent-code/ui/popover"
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
import { useLanguage } from "@/context/language"
import { useServer } from "@/context/server"
+import { useSync } from "@/context/sync"
import { useGlobal } from "@/context/global"
+const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
export function StatusPopover() {
const language = useLanguage()
const server = useServer()
const global = useGlobal()
+ const sync = useSync()
const [shown, setShown] = createSignal(false)
- const serverHealth = () => global.servers.health[server.key]?.healthy
+ const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
+ const mcpIssue = createMemo(() => {
+ const mcp = Object.values(sync.data.mcp ?? {})
+ const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
+ const warn = mcp.some((item) => item.status === "needs_auth")
+ if (failed) return "critical" as const
+ if (warn) return "warning" as const
+ })
+ const serverHealthy = () => global.servers.health[server.key]?.healthy === true
+ const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue())
return (
-
+
@@ -54,15 +67,50 @@ export function StatusPopover() {
}
>
-
+
)
}
-export function StatusPopoverV2(_props: { scope?: "server" }) {
- return
+export function StatusPopoverV2(props: { scope?: "server" }) {
+ if (props.scope === "server") return
+ return
+}
+
+function DirectoryStatusPopover() {
+ const language = useLanguage()
+ const server = useServer()
+ const global = useGlobal()
+ const sync = useSync()
+ const [shown, setShown] = createSignal(false)
+ const serverHealth = () => global.servers.health[server.key]?.healthy
+ const ready = createMemo(() => serverHealth() === false || sync.data.mcp_ready)
+ const mcpIssue = createMemo(() => {
+ const mcp = Object.values(sync.data.mcp ?? {})
+ const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
+ const warn = mcp.some((item) => item.status === "needs_auth")
+ if (failed) return "critical" as const
+ if (warn) return "warning" as const
+ })
+ const healthy = createMemo(() => serverHealth() === true && !mcpIssue())
+ const state = createMemo(() => ({
+ shown: shown(),
+ ready: ready(),
+ healthy: healthy(),
+ serverHealth: serverHealth(),
+ issue: mcpIssue(),
+ label: language.t("status.popover.trigger"),
+ onOpenChange: setShown,
+ body: () => (
+
+
+
+ ),
+ }))
+
+ return
}
function ServerStatusPopover() {
@@ -76,7 +124,7 @@ function ServerStatusPopover() {
ready: serverHealth() !== undefined,
healthy: serverHealth() === true,
serverHealth: serverHealth(),
- label: language.t("status.popover.tab.servers"),
+ label: language.t("status.popover.trigger"),
onOpenChange: setShown,
body: () => (
@@ -93,6 +141,7 @@ type StatusPopoverState = {
ready: boolean
healthy: boolean
serverHealth: boolean | undefined
+ issue?: "critical" | "warning"
label: string
onOpenChange: (value: boolean) => void
body: () => JSX.Element
@@ -114,7 +163,10 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
const statusDotClass = () => ({
"absolute rounded-full": true,
"bg-icon-success-base": props.state.ready && props.state.healthy,
- "bg-icon-critical-base": props.state.serverHealth === false,
+ "bg-icon-warning-base": props.state.ready && props.state.serverHealth === true && props.state.issue === "warning",
+ "bg-icon-critical-base":
+ props.state.serverHealth === false ||
+ (props.state.ready && props.state.serverHealth === true && props.state.issue === "critical"),
"bg-border-weak-base": props.state.serverHealth === undefined || !props.state.ready,
})
@@ -140,7 +192,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
}}
trigger={
-
+
{
const gone = () =>
client.pty
.get({ ptyID: id }, { throwOnError: false })
- .then((result) => result.response?.status === 404)
+ .then((result) => result.response.status === 404)
.catch((err) => {
debugTerminal("failed to inspect terminal session", err)
return false
@@ -511,15 +511,11 @@ export const Terminal = (props: TerminalProps) => {
throw err
})
if (!result) return
- // With throwOnError:false a completed request always carries a response; the SDK types it
- // optional (it can be absent when the request itself failed to build), so guard once.
- const response = result.response
- if (!response) throw new Error("PTY connect ticket failed: no response from server")
- if (response.status === 200 && result.data?.ticket) return result.data.ticket
- if (response.status === 404 || response.status === 405) return
- if (response.status === 403)
+ if (result.response.status === 200 && result.data?.ticket) return result.data.ticket
+ if (result.response.status === 404 || result.response.status === 405) return
+ if (result.response.status === 403)
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
- throw new Error(`PTY connect ticket failed with ${response.status}`)
+ throw new Error(`PTY connect ticket failed with ${result.response.status}`)
}
const retry = (err: unknown) => {
diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts
index 417e2f8b..7663c2cf 100644
--- a/packages/app/src/context/directory-sync.ts
+++ b/packages/app/src/context/directory-sync.ts
@@ -300,7 +300,7 @@ export const createDirSyncContext = (
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id))
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
- const cursor = messages.response?.headers.get("x-next-cursor") ?? undefined
+ const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
return {
session,
part,
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index 8792e665..6d0f32c5 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -60,17 +60,6 @@ function errors(list: PromiseSettledResult
[]) {
return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason)
}
-// A bootstrap query can be cancelled rather than genuinely failing: refreshProviders() both refetches
-// and invalidates the same providers query key, so the superseded in-flight fetch rejects with a
-// TanStack CancelledError; disposing/rebuilding the backend aborts in-flight requests the same way.
-// These are benign races (a newer load replaces this one), not load failures — surfacing them as an
-// error toast ("无法加载 …" / cancelled) is a false alarm, so skip the toast for them.
-function isCancellation(error: unknown): boolean {
- if (!error || typeof error !== "object") return false
- const name = (error as { name?: unknown }).name
- return name === "CancelledError" || name === "AbortError"
-}
-
const providerRev = new Map()
export function clearProviderRev(scope: ServerScope, directory: string) {
@@ -325,7 +314,6 @@ export async function bootstrapDirectory(input: {
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.sdk))),
() =>
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.sdk)).catch((err) => {
- if (isCancellation(err)) return
const project = getFilename(input.directory)
showToast({
variant: "error",
@@ -336,7 +324,7 @@ export async function bootstrapDirectory(input: {
].filter(Boolean) as (() => Promise)[]
await waitForPaint()
- const slowErrs = errors(await runAll(slow)).filter((err) => !isCancellation(err))
+ const slowErrs = errors(await runAll(slow))
if (slowErrs.length > 0) {
console.error("Failed to finish bootstrap instance", slowErrs[0])
const project = getFilename(input.directory)
diff --git a/packages/app/src/context/global-sync/queue.test.ts b/packages/app/src/context/global-sync/queue.test.ts
index 647bccb0..c9919855 100644
--- a/packages/app/src/context/global-sync/queue.test.ts
+++ b/packages/app/src/context/global-sync/queue.test.ts
@@ -43,24 +43,4 @@ describe("createRefreshQueue", () => {
expect(calls).toEqual(["C:\\tmp\\demo"])
queue.dispose()
})
-
- test("does not enqueue rejected directories", async () => {
- const calls: string[] = []
- const queue = createRefreshQueue({
- paused: () => false,
- accept: (directory) => directory !== "/",
- bootstrap: async () => {},
- bootstrapInstance: (directory) => {
- calls.push(directory)
- },
- })
-
- queue.push("/")
- queue.push("/project")
-
- await tick()
-
- expect(calls).toEqual(["/project"])
- queue.dispose()
- })
})
diff --git a/packages/app/src/context/global-sync/queue.ts b/packages/app/src/context/global-sync/queue.ts
index b5653717..947e31ac 100644
--- a/packages/app/src/context/global-sync/queue.ts
+++ b/packages/app/src/context/global-sync/queue.ts
@@ -3,7 +3,6 @@ type QueueInput = {
bootstrap: () => Promise
bootstrapInstance: (directory: string) => Promise | void
key?: (directory: string) => string
- accept?: (directory: string) => boolean
}
export function createRefreshQueue(input: QueueInput) {
@@ -37,7 +36,6 @@ export function createRefreshQueue(input: QueueInput) {
const push = (directory: string) => {
if (!directory) return
- if (input.accept && !input.accept(directory)) return
queued.set(key(directory), directory)
if (input.paused()) return
schedule()
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx
index 7344ad4b..07e21f6f 100644
--- a/packages/app/src/context/layout.tsx
+++ b/packages/app/src/context/layout.tsx
@@ -34,16 +34,8 @@ const DEFAULT_FILE_TREE_WIDTH = 200
// visually consistent across screen sizes / zoom levels. px is derived live from the current
// window width and clamped to [MIN_RIGHT_PANEL_PX, window * MAX_RIGHT_PANEL_RATIO].
const DEFAULT_RIGHT_PANEL_RATIO = 0.26
-// T3.3 — the right panel remembers TWO widths, one per "bucket": WIDE panels (diff/files) want
-// horizontal room; NARROW panels (subagent/im/oversight lists, browser, terminal-ish) read fine in a
-// slimmer column. Each bucket keeps its own ratio so switching between a diff and a list does not drag
-// one to the other's width. A slightly slimmer default for the narrow bucket.
-const DEFAULT_RIGHT_PANEL_NARROW_RATIO = 0.2
const MIN_RIGHT_PANEL_PX = 300
const MAX_RIGHT_PANEL_RATIO = 0.6
-// T3.2 — the always-on vertical icon rail is a fixed, non-resizable strip beside the content area.
-export const RIGHT_PANEL_RAIL_PX = 44
-export type RightPanelWidthBucket = "wide" | "narrow"
const FALLBACK_WINDOW_WIDTH = 1280
const DEFAULT_SESSION_WIDTH = 600
const DEFAULT_TERMINAL_HEIGHT = 280
@@ -82,19 +74,18 @@ type SessionView = {
reviewOpen?: string[]
// U3/U4/U7: added "worktree" (isolated worktree diff/merge), "subagents" (child-session list),
// "browser" (isolated WebContentsView).
- // T3.2: the "menu" mode is gone — an always-on icon rail replaced the full-panel menu list.
rightPanelMode?:
+ | "menu"
| "review"
| "files"
+ | "status"
| "worktree"
| "subagents"
| "browser"
- | "mcp"
| "plugins"
| "profile"
| "debug"
| "im"
- | "oversight"
pendingMessage?: string
pendingMessageAt?: number
todoCollapsed?: boolean
@@ -313,11 +304,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
tab: "changes" as "changes" | "all",
},
rightPanel: {
- // `ratio` = the WIDE bucket (legacy field name, kept so the existing migration keeps working);
- // `narrowRatio` = the NARROW bucket (T3.3). An older store without narrowRatio falls back to
- // the default at read time.
ratio: DEFAULT_RIGHT_PANEL_RATIO,
- narrowRatio: DEFAULT_RIGHT_PANEL_NARROW_RATIO,
},
session: {
width: DEFAULT_SESSION_WIDTH,
@@ -743,31 +730,21 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
rightPanel: {
// Stored value is a ratio of the window width; px is derived live from the current window
// width and clamped so the panel stays proportional across screens and can stretch with the
- // window (no fixed 720px ceiling). T3.3: the ratio is per-BUCKET (wide/narrow) so each panel
- // family remembers its own width.
+ // window (no fixed 720px ceiling).
maxWidth: createMemo(() => Math.round(windowWidth() * MAX_RIGHT_PANEL_RATIO)),
minWidth: MIN_RIGHT_PANEL_PX,
- // Fixed, non-resizable icon-rail strip beside the content area (T3.2).
- railWidth: RIGHT_PANEL_RAIL_PX,
- // Called inside a tracking scope (JSX / createMemo), so reading store.rightPanel + windowWidth()
- // here is reactive without wrapping in its own memo. Defaults to the wide bucket for callers
- // that don't care (e.g. the shared session-width calc).
- width: (bucket: RightPanelWidthBucket = "wide") => {
- const ratio =
- bucket === "narrow"
- ? store.rightPanel?.narrowRatio ?? DEFAULT_RIGHT_PANEL_NARROW_RATIO
- : store.rightPanel?.ratio ?? DEFAULT_RIGHT_PANEL_RATIO
+ width: createMemo(() => {
+ const ratio = store.rightPanel?.ratio ?? DEFAULT_RIGHT_PANEL_RATIO
const max = Math.max(MIN_RIGHT_PANEL_PX, Math.round(windowWidth() * MAX_RIGHT_PANEL_RATIO))
return Math.min(max, Math.max(MIN_RIGHT_PANEL_PX, Math.round(windowWidth() * ratio)))
- },
- resize(width: number, bucket: RightPanelWidthBucket = "wide") {
+ }),
+ resize(width: number) {
const ratio = windowWidth() > 0 ? width / windowWidth() : DEFAULT_RIGHT_PANEL_RATIO
- const field = bucket === "narrow" ? "narrowRatio" : "ratio"
if (!store.rightPanel) {
- setStore("rightPanel", field === "narrowRatio" ? { narrowRatio: ratio } : { ratio })
+ setStore("rightPanel", { ratio })
return
}
- setStore("rightPanel", field, ratio)
+ setStore("rightPanel", "ratio", ratio)
},
},
session: {
diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx
index 292fd5f8..6aed2145 100644
--- a/packages/app/src/context/notification.tsx
+++ b/packages/app/src/context/notification.tsx
@@ -13,9 +13,25 @@ import { decode64 } from "@/utils/base64"
import { EventSessionError } from "@deepagent-code/sdk/v2"
import { Persist, persisted } from "@/utils/persist"
import { playSoundById } from "@/utils/sound"
-import { pruneNotifications, type Notification } from "./notification-state"
-export type { Notification } from "./notification-state"
+type NotificationBase = {
+ directory?: string
+ session?: string
+ metadata?: unknown
+ time: number
+ viewed: boolean
+}
+
+type TurnCompleteNotification = NotificationBase & {
+ type: "turn-complete"
+}
+
+type ErrorNotification = NotificationBase & {
+ type: "error"
+ error: EventSessionError["properties"]["error"]
+}
+
+export type Notification = TurnCompleteNotification | ErrorNotification
type NotificationIndex = {
session: {
@@ -32,6 +48,16 @@ type NotificationIndex = {
}
}
+const MAX_NOTIFICATIONS = 500
+const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30
+
+function pruneNotifications(list: Notification[]) {
+ const cutoff = Date.now() - NOTIFICATION_TTL_MS
+ const pruned = list.filter((n) => n.time >= cutoff)
+ if (pruned.length <= MAX_NOTIFICATIONS) return pruned
+ return pruned.slice(pruned.length - MAX_NOTIFICATIONS)
+}
+
function createNotificationIndex(): NotificationIndex {
return {
session: {
diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts
index 31571998..7097ae9f 100644
--- a/packages/app/src/context/permission-auto-respond.test.ts
+++ b/packages/app/src/context/permission-auto-respond.test.ts
@@ -1,12 +1,7 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, Session } from "@deepagent-code/sdk/v2/client"
import { base64Encode } from "@deepagent-code/core/util/encode"
-import {
- autoRespondsPermission,
- isDirectoryAutoAccepting,
- isDirectoryReadOnly,
- isMutatingPermission,
-} from "./permission-auto-respond"
+import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
const session = (input: { id: string; parentID?: string }) =>
({
@@ -105,31 +100,3 @@ describe("isDirectoryAutoAccepting", () => {
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false)
})
})
-
-describe("isDirectoryReadOnly", () => {
- test("returns true when the directory read-only key is set", () => {
- const directory = "/tmp/project"
- const readOnly = { [`${base64Encode(directory)}/*`]: true }
- expect(isDirectoryReadOnly(readOnly, directory)).toBe(true)
- })
-
- test("returns false when unset or explicitly false", () => {
- const directory = "/tmp/project"
- expect(isDirectoryReadOnly({}, directory)).toBe(false)
- expect(isDirectoryReadOnly({ [`${base64Encode(directory)}/*`]: false }, directory)).toBe(false)
- })
-})
-
-describe("isMutatingPermission", () => {
- test("treats write/execute tools as mutating", () => {
- for (const tool of ["edit", "write", "patch", "bash", "task", "external_directory"]) {
- expect(isMutatingPermission(tool)).toBe(true)
- }
- })
-
- test("treats read-style tools as non-mutating", () => {
- for (const tool of ["read", "glob", "grep", "list", "lsp", "webfetch", "websearch", "todowrite"]) {
- expect(isMutatingPermission(tool)).toBe(false)
- }
- })
-})
diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts
index 38670af2..8b1c867f 100644
--- a/packages/app/src/context/permission-auto-respond.ts
+++ b/packages/app/src/context/permission-auto-respond.ts
@@ -9,22 +9,6 @@ export function directoryAcceptKey(directory: string) {
return `${base64Encode(directory)}/*`
}
-// Directory-scoped read-only state reuses the same directory key space as auto-accept
-// (the two are mutually exclusive per directory).
-export function isDirectoryReadOnly(readOnly: Record, directory: string) {
- const key = directoryAcceptKey(directory)
- return readOnly[key] ?? false
-}
-
-// Tools that write, execute, or otherwise mutate state — denied in read-only mode.
-// Everything else (read/glob/grep/list/lsp/webfetch/websearch/todowrite/…) is left to the
-// normal permission flow so the agent can still inspect the workspace.
-const MUTATING_PERMISSIONS = new Set(["edit", "write", "patch", "bash", "task", "external_directory"])
-
-export function isMutatingPermission(permission: string) {
- return MUTATING_PERMISSIONS.has(permission)
-}
-
function accepted(autoAccept: Record, sessionID: string, directory?: string) {
const key = acceptKey(sessionID, directory)
const directoryKey = directory ? directoryAcceptKey(directory) : undefined
diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx
index 90de9025..a9db32a2 100644
--- a/packages/app/src/context/permission.tsx
+++ b/packages/app/src/context/permission.tsx
@@ -11,13 +11,9 @@ import {
acceptKey,
directoryAcceptKey,
isDirectoryAutoAccepting,
- isDirectoryReadOnly,
- isMutatingPermission,
autoRespondsPermission,
} from "./permission-auto-respond"
-export type DirectoryApprovalMode = "read-only" | "request" | "full-access"
-
type PermissionRespondFn = (input: {
sessionID: string
permissionID: string
@@ -63,13 +59,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
return hasPermissionPromptRules(store.config.permission)
})
- // Tri-state Read-Only / Request / Full-Access: v4 adds a parallel `readOnly` directory map
- // alongside the pre-existing `autoAccept` map (the two are mutually exclusive per directory).
- // Back-compat: the live persisted key is "permission" (the "v3"/"v4" naming lives in the shape,
- // not the storage key — the real key was never "permission.v3"). We keep the key unchanged so
- // existing data is read in place, and version the shape additively: `migrate` back-fills the new
- // `readOnly` map and `merge` supplies the default, so an existing auto-accepting directory keeps
- // its `autoAccept` entry and reads back as "full-access" with no data loss.
const [store, setStore, _, ready] = persisted(
{
...Persist.serverGlobal(serverSDK.scope, "permission", ["permission.v3"]),
@@ -77,31 +66,19 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
if (!value || typeof value !== "object" || Array.isArray(value)) return value
const data = value as Record
- const withAutoAccept = data.autoAccept
- ? data
- : {
- ...data,
- autoAccept:
- typeof data.autoAcceptEdits === "object" &&
- data.autoAcceptEdits &&
- !Array.isArray(data.autoAcceptEdits)
- ? data.autoAcceptEdits
- : {},
- }
-
- if (
- (withAutoAccept as Record).readOnly &&
- typeof (withAutoAccept as Record).readOnly === "object"
- ) {
- return withAutoAccept
+ if (data.autoAccept) return value
+
+ return {
+ ...data,
+ autoAccept:
+ typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits)
+ ? data.autoAcceptEdits
+ : {},
}
-
- return { ...withAutoAccept, readOnly: {} }
},
},
createStore({
autoAccept: {} as Record,
- readOnly: {} as Record,
}),
)
@@ -171,39 +148,11 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
return isDirectoryAutoAccepting(store.autoAccept, directory)
}
- function isDirectoryReadOnlyMode(directory: string) {
- return isDirectoryReadOnly(store.readOnly, directory)
- }
-
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : []
return autoRespondsPermission(store.autoAccept, session, permission, directory)
}
- // Read-only is a directory-level guard: auto-REJECT any mutating tool (edit/write/bash/…)
- // while leaving read-style tools to the normal flow. The mirror image of full-access
- // auto-approve. Directory-scoped only (session-lineage overrides are auto-accept's concern).
- function shouldAutoReject(permission: PermissionRequest, directory?: string) {
- if (!directory) return false
- if (!isDirectoryReadOnlyMode(directory)) return false
- return isMutatingPermission(permission.permission)
- }
-
- function respondReject(permission: PermissionRequest, directory?: string) {
- const now = Date.now()
- const hit = responded.has(permission.id)
- responded.delete(permission.id)
- responded.set(permission.id, now)
- pruneResponded(now)
- if (hit) return
- respond({
- sessionID: permission.sessionID,
- permissionID: permission.id,
- response: "reject",
- directory,
- })
- }
-
function bumpEnableVersion(sessionID: string, directory?: string) {
const key = acceptKey(sessionID, directory)
const next = (enableVersion.get(key) ?? 0) + 1
@@ -216,10 +165,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
if (event?.type !== "permission.asked") return
const perm = event.properties
- if (shouldAutoReject(perm, e.name)) {
- respondReject(perm, e.name)
- return
- }
if (!shouldAutoRespond(perm, e.name)) return
respondOnce(perm, e.name)
@@ -231,8 +176,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
setStore(
produce((draft) => {
draft.autoAccept[key] = true
- // full-access and read-only are mutually exclusive.
- draft.readOnly[key] = false
}),
)
@@ -258,63 +201,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
)
}
- // Read-only directory mode: mirror of enableDirectory, but eagerly REJECTS the pending
- // mutating requests instead of approving them.
- function enableReadOnlyDirectory(directory: string) {
- const key = directoryAcceptKey(directory)
- setStore(
- produce((draft) => {
- draft.readOnly[key] = true
- // read-only and full-access are mutually exclusive.
- draft.autoAccept[key] = false
- }),
- )
-
- serverSDK.client.permission
- .list({ directory })
- .then((x) => {
- if (!isDirectoryReadOnlyMode(directory)) return
- for (const perm of x.data ?? []) {
- if (!perm?.id) continue
- if (!shouldAutoReject(perm, directory)) continue
- respondReject(perm, directory)
- }
- })
- .catch(() => undefined)
- }
-
- function disableReadOnlyDirectory(directory: string) {
- const key = directoryAcceptKey(directory)
- setStore(
- produce((draft) => {
- draft.readOnly[key] = false
- }),
- )
- }
-
- function directoryApprovalMode(directory: string): DirectoryApprovalMode {
- if (isAutoAcceptingDirectory(directory)) return "full-access"
- if (isDirectoryReadOnlyMode(directory)) return "read-only"
- return "request"
- }
-
- function setDirectoryApprovalMode(directory: string, mode: DirectoryApprovalMode) {
- if (directoryApprovalMode(directory) === mode) return
- switch (mode) {
- case "full-access":
- disableReadOnlyDirectory(directory)
- enableDirectory(directory)
- return
- case "read-only":
- disableDirectory(directory)
- enableReadOnlyDirectory(directory)
- return
- default:
- disableDirectory(directory)
- disableReadOnlyDirectory(directory)
- }
- }
-
function enable(sessionID: string, directory: string) {
const key = acceptKey(sessionID, directory)
const version = bumpEnableVersion(sessionID, directory)
@@ -357,16 +243,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
autoResponds(permission: PermissionRequest, directory?: string) {
return shouldAutoRespond(permission, directory)
},
- autoRejects(permission: PermissionRequest, directory?: string) {
- return shouldAutoReject(permission, directory)
- },
isAutoAccepting,
isAutoAcceptingDirectory,
- // Tri-state directory approval mode (Read-Only / Request / Full-Access).
- // isAutoAcceptingDirectory / toggleAutoAcceptDirectory remain valid: full-access == auto-accept.
- directoryApprovalMode,
- setDirectoryApprovalMode,
- isDirectoryReadOnly: isDirectoryReadOnlyMode,
toggleAutoAccept(sessionID: string, directory: string) {
if (isAutoAccepting(sessionID, directory)) {
disable(sessionID, directory)
diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx
index 91f79cc5..423d73de 100644
--- a/packages/app/src/context/server-sync.tsx
+++ b/packages/app/src/context/server-sync.tsx
@@ -44,12 +44,23 @@ import { ServerConnection, useServer } from "./server"
import { retry } from "@deepagent-code/core/util/retry"
import type { ServerScope } from "@/utils/server-scope"
import { persisted } from "@/utils/persist"
-import { isFilesystemRootDir } from "@/utils/filesystem-root"
import { toggleMcp } from "./global-sync/mcp"
import type { SessionPlan, SessionPlanStep, SessionGoal } from "./global-sync/types"
export type { SessionPlan, SessionPlanStep, SessionGoal }
+// True when `dir` is a filesystem root: posix "/" or a Windows drive/UNC root ("C:\", "C:/", "\\").
+// Rooting an instance here is refused server-side (assertSafeInstanceRoot); we check on the client
+// too so we never fire the doomed boot. Kept dependency-free (no node:path in the renderer).
+function isFilesystemRootDir(dir: string): boolean {
+ const trimmed = dir.trim()
+ if (!trimmed) return false
+ const normalized = trimmed.replace(/\\/g, "/").replace(/\/+$/, "")
+ if (normalized === "") return true // was "/" or "\" (all separators)
+ if (/^[A-Za-z]:$/.test(normalized)) return true // "C:" (drive root after trailing-slash strip)
+ return false
+}
+
type GlobalStore = {
ready: boolean
error?: InitError
@@ -286,7 +297,6 @@ export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
const queue = createRefreshQueue({
paused,
key: directoryKey,
- accept: (directory) => !isFilesystemRootDir(directory),
bootstrap: () => queryClient.fetchQuery({ queryKey: [serverSDK.scope, "bootstrap"] }),
bootstrapInstance,
})
@@ -412,8 +422,18 @@ export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
async function bootstrapInstance(directory: string) {
const key = directoryKey(directory)
if (!key) return
+ // Fail-closed against a filesystem-root directory. The server refuses to boot an instance
+ // rooted at "/" (assertSafeInstanceRoot — it would make the file-tool permission boundary the
+ // whole disk), so a stored session/route pointing at "/" would otherwise trigger an endless
+ // boot→fail→retry storm surfacing only as "unexpected server error". Mirror the guard here so
+ // the doomed request is never sent and the user gets a clear reason instead. Legacy "/" data
+ // (pre-guard) is the only way to reach this now that the boot path rejects it.
if (isFilesystemRootDir(directory)) {
- children.disposeDirectory(key)
+ showToast({
+ variant: "error",
+ title: language.t("toast.project.rootRefused.title"),
+ description: language.t("toast.project.rootRefused.description"),
+ })
return
}
const pending = booting.get(key)
diff --git a/packages/app/src/context/server.test.ts b/packages/app/src/context/server.test.ts
index cd21a182..ae34e3a1 100644
--- a/packages/app/src/context/server.test.ts
+++ b/packages/app/src/context/server.test.ts
@@ -118,23 +118,6 @@ describe("createServerProjects", () => {
})
describe("migrateCanonicalLocalServerState", () => {
- test("removes persisted filesystem-root projects and last-project pointers", () => {
- expect(
- migrateCanonicalLocalServerState({
- projects: {
- local: [
- { worktree: "/", expanded: true },
- { worktree: "/project", expanded: true },
- ],
- },
- lastProject: { local: "/", remote: "/project" },
- }),
- ).toEqual({
- projects: { local: [{ worktree: "/project", expanded: true }] },
- lastProject: { remote: "/project" },
- })
- })
-
test("moves an existing canonical web bucket into local scope", () => {
expect(
migrateCanonicalLocalServerState(
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 41f68cd6..1efe91a0 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -3,7 +3,6 @@ import { type Accessor, batch, createMemo } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import { ServerScope } from "@/utils/server-scope"
-import { isFilesystemRootDir } from "@/utils/filesystem-root"
type StoredProject = { worktree: string; expanded: boolean }
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http | ServerConnection.Server
@@ -33,10 +32,6 @@ function isRecord(value: unknown): value is Record {
}
export function migrateCanonicalLocalServerState(value: unknown, canonicalLocalServer?: ServerConnection.Key) {
- return removeFilesystemRootServerState(migrateCanonicalServerScope(value, canonicalLocalServer))
-}
-
-function migrateCanonicalServerScope(value: unknown, canonicalLocalServer?: ServerConnection.Key) {
if (!canonicalLocalServer || canonicalLocalServer === "local") return value
if (!isRecord(value)) return value
const projects = isRecord(value.projects) ? value.projects : undefined
@@ -70,31 +65,6 @@ function migrateCanonicalServerScope(value: unknown, canonicalLocalServer?: Serv
return next
}
-function removeFilesystemRootServerState(value: unknown) {
- if (!isRecord(value)) return value
- const projects = isRecord(value.projects)
- ? Object.fromEntries(
- Object.entries(value.projects).map(([scope, entries]) => [
- scope,
- Array.isArray(entries)
- ? entries.filter(
- (project) =>
- !isRecord(project) || typeof project.worktree !== "string" || !isFilesystemRootDir(project.worktree),
- )
- : entries,
- ]),
- )
- : value.projects
- const lastProject = isRecord(value.lastProject)
- ? Object.fromEntries(
- Object.entries(value.lastProject).filter(
- ([, directory]) => typeof directory !== "string" || !isFilesystemRootDir(directory),
- ),
- )
- : value.lastProject
- return { ...value, projects, lastProject }
-}
-
export function createServerProjects(input: {
scope: Accessor
store: Store
@@ -301,7 +271,11 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
// Normalize a stored entry to a typed connection so we can key it uniformly
// (Http keys by url, Server keys by `server:`).
const toConnection = (x: StoredServer): ServerConnection.Any =>
- typeof x === "string" ? { type: "http", http: { url: x } } : "type" in x ? x : { type: "http", http: x }
+ typeof x === "string"
+ ? { type: "http", http: { url: x } }
+ : "type" in x
+ ? x
+ : { type: "http", http: x }
const allServers = createMemo((): Array => {
return resolveServerList({ stored: store.list, props: props.servers })
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index 181b543c..807a7b6b 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -553,12 +553,6 @@ export const dict = {
"settings.general.section.updates": "التحديثات",
"settings.general.section.sounds": "المؤثرات الصوتية",
"settings.general.section.feed": "الخلاصة",
- "settings.general.section.sharing": "المشاركة",
- "settings.general.row.shareUrl.title": "عنوان URL لخادم المشاركة",
- "settings.general.row.shareUrl.description": "عنوان URL الأساسي للخادم المستخدم عند مشاركة الجلسات. اتركه فارغًا لاستخدام الافتراضي.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "تفعيل لجنة الخبراء افتراضيًا",
- "settings.general.row.expertPanelDefault.description": "بدء المحادثات الجديدة مع تفعيل لجنة الخبراء، بحيث يبدأ زرها مراجعة السياق الحالي عند الطلب.",
"settings.general.section.display": "شاشة العرض",
"settings.general.row.language.title": "اللغة",
"settings.general.row.language.description": "تغيير لغة العرض لـ DeepAgent Code",
diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts
index b2ff7bef..db2de284 100644
--- a/packages/app/src/i18n/br.ts
+++ b/packages/app/src/i18n/br.ts
@@ -560,12 +560,6 @@ export const dict = {
"settings.general.section.updates": "Atualizações",
"settings.general.section.sounds": "Efeitos sonoros",
"settings.general.section.feed": "Feed",
- "settings.general.section.sharing": "Compartilhamento",
- "settings.general.row.shareUrl.title": "URL do servidor de compartilhamento",
- "settings.general.row.shareUrl.description": "URL base do servidor usado ao compartilhar sessões. Deixe em branco para usar o padrão.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Ativar painel de especialistas por padrão",
- "settings.general.row.expertPanelDefault.description": "Iniciar novas conversas com o painel de especialistas ativado, para que seu botão inicie uma revisão do contexto atual sob demanda.",
"settings.general.section.display": "Tela",
"settings.general.row.language.title": "Idioma",
"settings.general.row.language.description": "Alterar o idioma de exibição do DeepAgent Code",
diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts
index cc9f9115..7190b688 100644
--- a/packages/app/src/i18n/bs.ts
+++ b/packages/app/src/i18n/bs.ts
@@ -625,12 +625,6 @@ export const dict = {
"settings.general.section.updates": "Ažuriranja",
"settings.general.section.sounds": "Zvučni efekti",
"settings.general.section.feed": "Feed",
- "settings.general.section.sharing": "Dijeljenje",
- "settings.general.row.shareUrl.title": "URL servera za dijeljenje",
- "settings.general.row.shareUrl.description": "Osnovni URL servera koji se koristi pri dijeljenju sesija. Ostavite prazno za zadano.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Zadano uključi ekspertni panel",
- "settings.general.row.expertPanelDefault.description": "Pokreni nove razgovore s uključenim ekspertnim panelom, tako da njegovo dugme po potrebi pokreće pregled trenutnog konteksta.",
"settings.general.section.display": "Prikaz",
"settings.general.row.language.title": "Jezik",
diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts
index 6af26897..fe57ab9f 100644
--- a/packages/app/src/i18n/da.ts
+++ b/packages/app/src/i18n/da.ts
@@ -620,12 +620,6 @@ export const dict = {
"settings.general.section.updates": "Opdateringer",
"settings.general.section.sounds": "Lydeffekter",
"settings.general.section.feed": "Feed",
- "settings.general.section.sharing": "Deling",
- "settings.general.row.shareUrl.title": "Delings-server-URL",
- "settings.general.row.shareUrl.description": "Basis-URL for serveren, der bruges ved deling af sessioner. Lad stå tomt for standard.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Aktivér ekspertpanel som standard",
- "settings.general.row.expertPanelDefault.description": "Start nye samtaler med ekspertpanelet aktiveret, så knappen kan gennemgå den aktuelle kontekst efter behov.",
"settings.general.section.display": "Skærm",
"settings.general.row.language.title": "Sprog",
diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts
index e682c164..ae2da2b4 100644
--- a/packages/app/src/i18n/de.ts
+++ b/packages/app/src/i18n/de.ts
@@ -569,12 +569,6 @@ export const dict = {
"settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Soundeffekte",
"settings.general.section.feed": "Feed",
- "settings.general.section.sharing": "Teilen",
- "settings.general.row.shareUrl.title": "Server-URL für Freigabe",
- "settings.general.row.shareUrl.description": "Basis-URL des Servers, der beim Teilen von Sitzungen verwendet wird. Leer lassen für Standard.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Expertengremium standardmäßig aktivieren",
- "settings.general.row.expertPanelDefault.description": "Neue Unterhaltungen mit aktiviertem Expertengremium starten, damit dessen Schaltfläche den aktuellen Kontext bei Bedarf prüft.",
"settings.general.section.display": "Anzeige",
"settings.general.row.language.title": "Sprache",
"settings.general.row.language.description": "Die Anzeigesprache für DeepAgent Code ändern",
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index ebea8dba..bdc0ea4c 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -182,12 +182,9 @@ export const dict = {
"provider.connect.transport.error.number": "Enter a positive whole number, or leave blank.",
"provider.custom.title": "Custom provider",
- "provider.custom.description.prefix": "Enter the base URL and API key. We detect the protocol and available models automatically. See the ",
+ "provider.custom.description.prefix": "Configure an OpenAI-compatible provider. See the ",
"provider.custom.description.link": "provider config docs",
"provider.custom.description.suffix": ".",
- "provider.custom.advanced.toggle": "Advanced settings",
- "provider.custom.models.autoHint": "Detected automatically from the provider. Add models here only to override.",
- "provider.custom.field.providerID.autoDescription": "Optional. Derived from the base URL when left blank.",
"provider.custom.field.providerID.label": "Provider ID",
"provider.custom.field.providerID.placeholder": "myprovider",
"provider.custom.field.providerID.description": "Lowercase letters, numbers, hyphens, or underscores",
@@ -198,22 +195,10 @@ export const dict = {
"provider.custom.field.apiKey.label": "API key",
"provider.custom.field.apiKey.placeholder": "API key",
"provider.custom.field.apiKey.description": "Optional. Leave empty if you manage auth via headers.",
- "provider.custom.field.protocol.label": "Protocol",
- "provider.custom.field.protocol.description": "Auto-detects by default. Pick a protocol if detection is wrong or the endpoint has no model list.",
- "provider.custom.field.protocol.option.auto": "Auto-detect",
- "provider.custom.field.protocol.option.openai-compatible": "OpenAI-compatible",
- "provider.custom.field.protocol.option.anthropic": "Anthropic",
- "provider.custom.edit.title": "Edit custom provider",
- "provider.custom.specWarning":
- "Model specs (context window, reasoning) are best-effort defaults auto-filled from a public catalog. If you edit them, we can't guarantee the model still works.",
"provider.custom.models.label": "Models",
"provider.custom.models.id.label": "ID",
"provider.custom.models.id.placeholder": "model-id",
"provider.custom.models.name.label": "Name",
- "provider.custom.models.context.label": "Context tokens",
- "provider.custom.models.context.placeholder": "e.g. 128000",
- "provider.custom.models.reasoning.label": "Reasoning",
- "provider.custom.models.temperature.label": "Temperature",
"provider.custom.models.name.placeholder": "Display Name",
"provider.custom.models.remove": "Remove model",
"provider.custom.models.add": "Add model",
@@ -232,7 +217,6 @@ export const dict = {
"provider.custom.error.baseURL.format": "Must start with http:// or https://",
"provider.custom.error.required": "Required",
"provider.custom.error.duplicate": "Duplicate",
- "provider.custom.error.context": "Enter a positive whole number, or leave blank",
"provider.disconnect.toast.disconnected.title": "{{provider}} disconnected",
"provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.",
@@ -325,64 +309,8 @@ export const dict = {
"prompt.action.attachFile": "Add files",
"prompt.attachment.remove": "Remove attachment",
"prompt.generating": "Generating prompt...",
- "prompt.draft.preview.streaming": "Preparing prompt draft",
- "prompt.draft.preview.paused": "Draft paused",
- "prompt.draft.preview.dismiss": "Dismiss",
- "prompt.draft.preview.use": "Use this draft",
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
- "composer.approval.request": "Request approval",
- "composer.approval.readOnly": "Read-only",
- "composer.approval.fullAccess": "Full access",
- "composer.mode.auto": "Auto",
- "composer.mode.loop": "Loop",
- "composer.mode.design": "Design",
- "composer.mode.auto.desc": "Agent decides how to plan and act on your request",
- "composer.mode.loop.desc": "Agent works autonomously toward a goal, iterating until done",
- "composer.mode.design.desc": "Agent explores the problem and proposes a design before building",
- "composer.panel.label": "Expert Panel",
- "composer.panel.off": "Off",
- "composer.panel.single": "Single-round review",
- "composer.panel.multi": "Multi-round debate",
- "composer.panel.verdict.title": "Expert panel verdict",
- "composer.panel.verdict.approve": "Approve",
- "composer.panel.verdict.revise": "Revise",
- "composer.panel.verdict.block": "Block",
- "composer.panel.verdict.needsHuman": "Needs a human",
- "composer.panel.verdict.meta.one": "{{confidence}}% confidence · {{rounds}} round",
- "composer.panel.verdict.meta.other": "{{confidence}}% confidence · {{rounds}} rounds",
- "composer.panel.verdict.evidence": "Evidence",
- "composer.panel.verdict.dissent": "Dissent (overruled)",
- "composer.goal.phase.running": "Running",
- "composer.goal.phase.paused": "Paused",
- "composer.goal.phase.done": "Complete",
- "composer.goal.phase.needsHuman": "Needs you",
- "composer.goal.phase.rolledBack": "Rolled back",
- "composer.goal.phase.stopped": "Stopped",
- "composer.goal.budget.one": "{{ticks}} tick · {{tokens}} tokens",
- "composer.goal.budget.other": "{{ticks}} ticks · {{tokens}} tokens",
- "composer.goal.pause": "Pause",
- "composer.goal.resume": "Resume",
- "composer.goal.stop": "Stop goal",
- "composer.goal.dismiss": "Dismiss goal",
- "composer.goal.editPlan": "Edit plan",
- "composer.goal.editPlan.title": "Edit goal plan",
- "composer.goal.editPlan.desc": "Revise the running goal's plan. Changes apply on the next tick.",
- "composer.goal.editPlan.goalLabel": "Goal",
- "composer.goal.editPlan.stepsLabel": "Steps",
- "composer.goal.editPlan.addStep": "Add step",
- "composer.goal.editPlan.removeStep": "Remove step",
- "composer.goal.editPlan.stepPlaceholder": "Step description",
- "composer.goal.editPlan.save": "Apply changes",
- "composer.goal.editPlan.cancel": "Cancel",
- "composer.goal.editPlan.saved": "Plan updated — takes effect next tick",
- "composer.goal.editPlan.failed": "Couldn't update the plan (goal not running?)",
- "composer.goal.editPlan.status.pending": "Pending",
- "composer.goal.editPlan.status.active": "Active",
- "composer.goal.editPlan.status.done": "Done",
- "composer.goal.editPlan.status.blocked": "Blocked",
- "composer.goal.editPlan.status.cancelled": "Cancelled",
- "composer.steer.hint": "Session is working — your message will steer the current turn",
"prompt.scenario.label": "Scenario mode",
"prompt.scenario.direct": "Direct",
"prompt.scenario.intelligence": "Intelligence",
@@ -477,7 +405,6 @@ export const dict = {
"dialog.server.add.error": "Could not connect to server",
"dialog.server.add.checking": "Checking...",
"dialog.server.add.button": "Add server",
- "dialog.server.add.menu.http": "Add HTTP server",
"dialog.server.connect.button": "Connect to server",
"dialog.server.add.name": "Server name (optional)",
"dialog.server.add.namePlaceholder": "Localhost",
@@ -790,66 +717,6 @@ export const dict = {
"session.files.all": "All files",
"session.files.empty": "No files",
"session.files.binaryContent": "Binary file (content cannot be displayed)",
- "session.files.heading": "Files",
- "session.files.newFile": "New file",
- "session.files.newFolder": "New folder",
- "session.files.newFile.placeholder": "File name",
- "session.files.newFolder.placeholder": "Folder name",
- "session.files.create.confirm": "Confirm",
- "session.files.create.cancel": "Cancel",
- "session.files.create.failed": "Failed to create file",
- "session.files.createFolder.failed": "Failed to create folder",
- "session.panel.oversight": "Oversight",
- "session.panel.debug": "Debug",
- "session.panel.profile": "Profiler",
- "oversight.common.refresh": "Refresh",
- "oversight.dashboard.title": "Agent Dashboard",
- "oversight.dashboard.loading": "Loading metrics…",
- "oversight.dashboard.empty": "No metrics available.",
- "oversight.metric.taskSuccessRate": "Task success rate",
- "oversight.metric.conflictRate": "Conflict rate",
- "oversight.metric.dlqEvents": "DLQ events",
- "oversight.metric.pushRejected": "Push rejected",
- "oversight.metric.tasksCompleted": "Tasks completed",
- "oversight.metric.tasksFailed": "Tasks failed",
- "oversight.metric.publishLatencyP50": "Publish latency P50",
- "oversight.metric.publishLatencyP95": "Publish latency P95",
- "oversight.metric.eventToAgentP50": "Event→agent P50",
- "oversight.metric.eventToAgentP95": "Event→agent P95",
- "oversight.metric.humanTakeovers": "Human takeovers",
- "oversight.metric.rollbacks": "Rollbacks",
- "oversight.metric.pushRejectedByReason": "Push rejected by reason",
- "oversight.metric.window": "Window: {{from}} → {{to}}",
- "oversight.approvals.title": "Approval Queue",
- "oversight.approvals.empty": "No pending approvals.",
- "oversight.approvals.approve": "Approve",
- "oversight.approvals.reject": "Reject",
- "oversight.approvals.acknowledge": "Acknowledge",
- "oversight.approvals.viewTrace": "View trace",
- "oversight.approvals.resolveFailed": "Failed to resolve",
- "oversight.trace.title": "Event Trace",
- "oversight.trace.placeholder": "correlationID…",
- "oversight.trace.action": "Trace",
- "oversight.trace.loading": "Loading trace…",
- "oversight.trace.empty": "No events for this correlationID.",
- "oversight.trace.source": "source:",
- "oversight.trace.causedBy": "caused by:",
- "oversight.takeover.title": "Human Takeover",
- "oversight.takeover.description": "Record that a human is taking over from the autonomous agents (pauses autonomy escalation).",
- "oversight.takeover.placeholder": "Reason for taking over…",
- "oversight.takeover.action": "Take over",
- "oversight.takeover.recorded": "Takeover recorded.",
- "oversight.takeover.unsupported": "Takeover recording activates once the backend endpoint (P3.10) is available.",
- "oversight.takeover.failed": "Failed: {{error}}",
- "oversight.rollback.title": "Rollback",
- "oversight.rollback.description": "Revert an agent-produced change over a session (via SessionRevert). Only affects a session in this workspace. This reverts real changes — use with care.",
- "oversight.rollback.sessionPlaceholder": "Session ID (ses_…)",
- "oversight.rollback.reasonPlaceholder": "Reason for rolling back… (optional)",
- "oversight.rollback.action": "Roll back",
- "oversight.rollback.noop": "Nothing to revert — rollback recorded as a no-op.",
- "oversight.rollback.applied": "Rollback applied — session reverted.",
- "oversight.rollback.notFound": "No such session in this workspace.",
- "oversight.rollback.failed": "Failed: {{error}}",
"session.messages.renderEarlier": "Render earlier messages",
"session.messages.loadingEarlier": "Loading earlier messages...",
@@ -937,7 +804,7 @@ export const dict = {
"session.header.open.app.powershell": "PowerShell",
"session.header.open.app.sublimeText": "Sublime Text",
- "status.popover.trigger": "Servers",
+ "status.popover.trigger": "Servers & MCP",
"status.popover.ariaLabel": "Server configurations",
"status.popover.tab.servers": "Servers",
"status.popover.tab.mcp": "MCP",
@@ -945,7 +812,7 @@ export const dict = {
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Manage servers",
// V3.6 Phase 3C: right-panel extensions hub label (unifies plugin/packs/MCP concept)
- "sidebar.extensions": "Plugins",
+ "sidebar.extensions": "Extensions & Services",
"session.share.popover.title": "Publish on web",
"session.share.popover.description.shared":
@@ -1019,30 +886,6 @@ export const dict = {
"sidebar.settings": "Settings",
"sidebar.help": "Help",
"sidebar.review": "Knowledge Governance",
- "sidebar.wiki": "Repo & Wiki",
- "goal.start.hint": "Plan ready — run it as a supervised goal",
- "goal.start.button": "Run as goal",
- "goal.start.success": "Goal started — running in the background",
- "goal.start.failed": "Couldn't start the goal",
- "wiki.title": "Repo & Wiki",
- "wiki.description": "Read, search, and govern the four graphs. Knowledge and Memory are editable; Documents and Code are read-only.",
- "wiki.search": "Search pages and symbols",
- "wiki.searchEmpty": "No pages match your search",
- "wiki.empty": "No pages yet",
- "wiki.loading": "Loading…",
- "wiki.selectPrompt": "Select a page to read",
- "wiki.readOnly": "Read-only",
- "wiki.version": "v{{version}}",
- "wiki.links": "Linked code & docs",
- "wiki.stale": "stale",
- "wiki.type.knowledge": "Knowledge",
- "wiki.type.memory": "Memory",
- "wiki.type.document": "Documents",
- "wiki.type.code": "Code",
- "wiki.edit.button": "Edit",
- "wiki.edit.save": "Save",
- "wiki.edit.saved": "Knowledge page updated",
- "wiki.edit.failed": "Couldn't save the edit",
"sidebar.history": "History projects",
"sidebar.history.description": "Projects with imported or past sessions that are not currently active. Click one to open it.",
"sidebar.workspaces.enable": "Enable workspaces",
@@ -1195,8 +1038,6 @@ export const dict = {
"review.type.skill": "Skill",
"review.type.failure_dossier": "Failure dossiers",
"review.type.other": "Other",
- "review.scope.project": "This project",
- "review.scope.global": "Global",
"review.groupCount": "{{count}}",
"review.envFacts.title": "Shared environment facts",
"review.envFacts.description":
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index 5ec07a63..0d7123d7 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -628,12 +628,6 @@ export const dict = {
"settings.general.section.updates": "Actualizaciones",
"settings.general.section.sounds": "Efectos de sonido",
"settings.general.section.feed": "Feed",
- "settings.general.section.sharing": "Compartir",
- "settings.general.row.shareUrl.title": "URL del servidor para compartir",
- "settings.general.row.shareUrl.description": "URL base del servidor usado al compartir sesiones. Déjalo en blanco para usar el predeterminado.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Activar el panel de expertos por defecto",
- "settings.general.row.expertPanelDefault.description": "Iniciar nuevas conversaciones con el panel de expertos activado, para que su botón revise el contexto actual cuando se solicite.",
"settings.general.section.display": "Pantalla",
"settings.general.row.language.title": "Idioma",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index 9e01f181..bea1dcc1 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -567,12 +567,6 @@ export const dict = {
"settings.general.section.updates": "Mises à jour",
"settings.general.section.sounds": "Effets sonores",
"settings.general.section.feed": "Flux",
- "settings.general.section.sharing": "Partage",
- "settings.general.row.shareUrl.title": "URL du serveur de partage",
- "settings.general.row.shareUrl.description": "URL de base du serveur utilisé pour partager les sessions. Laissez vide pour la valeur par défaut.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Activer le panel d'experts par défaut",
- "settings.general.row.expertPanelDefault.description": "Démarrer les nouvelles conversations avec le panel d'experts activé, afin que son bouton examine le contexte actuel à la demande.",
"settings.general.section.display": "Affichage",
"settings.general.row.language.title": "Langue",
"settings.general.row.language.description": "Changer la langue d'affichage pour DeepAgent Code",
diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts
index ad5e45cb..3fbb6ed1 100644
--- a/packages/app/src/i18n/ja.ts
+++ b/packages/app/src/i18n/ja.ts
@@ -559,12 +559,6 @@ export const dict = {
"settings.general.section.updates": "アップデート",
"settings.general.section.sounds": "効果音",
"settings.general.section.feed": "フィード",
- "settings.general.section.sharing": "共有",
- "settings.general.row.shareUrl.title": "共有サーバー URL",
- "settings.general.row.shareUrl.description": "セッション共有時に使用するサーバーのベース URL。空欄でデフォルトを使用します。",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "エキスパートパネルをデフォルトで有効化",
- "settings.general.row.expertPanelDefault.description": "新しい会話をエキスパートパネル有効の状態で開始し、ボタンで現在のコンテキストをいつでも会議レビューできるようにします。",
"settings.general.section.display": "ディスプレイ",
"settings.general.row.language.title": "言語",
"settings.general.row.language.description": "DeepAgent Codeの表示言語を変更します",
diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts
index d8f0e408..0f1d882f 100644
--- a/packages/app/src/i18n/ko.ts
+++ b/packages/app/src/i18n/ko.ts
@@ -555,12 +555,6 @@ export const dict = {
"settings.general.section.updates": "업데이트",
"settings.general.section.sounds": "효과음",
"settings.general.section.feed": "피드",
- "settings.general.section.sharing": "공유",
- "settings.general.row.shareUrl.title": "공유 서버 URL",
- "settings.general.row.shareUrl.description": "세션 공유 시 사용하는 서버의 기본 URL입니다. 비워 두면 기본값을 사용합니다.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "전문가 패널을 기본으로 활성화",
- "settings.general.row.expertPanelDefault.description": "새 대화를 전문가 패널이 활성화된 상태로 시작하여 버튼으로 현재 컨텍스트를 필요할 때 검토할 수 있게 합니다.",
"settings.general.section.display": "디스플레이",
"settings.general.row.language.title": "언어",
"settings.general.row.language.description": "DeepAgent Code 표시 언어 변경",
diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts
index 2d21e9a8..68ec2d5c 100644
--- a/packages/app/src/i18n/no.ts
+++ b/packages/app/src/i18n/no.ts
@@ -628,12 +628,6 @@ export const dict = {
"settings.general.section.updates": "Oppdateringer",
"settings.general.section.sounds": "Lydeffekter",
"settings.general.section.feed": "Feed",
- "settings.general.section.sharing": "Deling",
- "settings.general.row.shareUrl.title": "Delingsserver-URL",
- "settings.general.row.shareUrl.description": "Basis-URL for serveren som brukes ved deling av økter. La stå tomt for standard.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Aktiver ekspertpanel som standard",
- "settings.general.row.expertPanelDefault.description": "Start nye samtaler med ekspertpanelet aktivert, slik at knappen kan gjennomgå gjeldende kontekst ved behov.",
"settings.general.section.display": "Skjerm",
"settings.general.row.language.title": "Språk",
diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts
index 8ade8e49..2db7354f 100644
--- a/packages/app/src/i18n/pl.ts
+++ b/packages/app/src/i18n/pl.ts
@@ -559,12 +559,6 @@ export const dict = {
"settings.general.section.updates": "Aktualizacje",
"settings.general.section.sounds": "Efekty dźwiękowe",
"settings.general.section.feed": "Kanał",
- "settings.general.section.sharing": "Udostępnianie",
- "settings.general.row.shareUrl.title": "URL serwera udostępniania",
- "settings.general.row.shareUrl.description": "Podstawowy adres URL serwera używany podczas udostępniania sesji. Pozostaw puste, aby użyć domyślnego.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Domyślnie włącz panel ekspertów",
- "settings.general.row.expertPanelDefault.description": "Rozpoczynaj nowe rozmowy z włączonym panelem ekspertów, aby jego przycisk mógł na żądanie przeglądać bieżący kontekst.",
"settings.general.section.display": "Ekran",
"settings.general.row.language.title": "Język",
"settings.general.row.language.description": "Zmień język wyświetlania dla DeepAgent Code",
diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts
index c88ca046..6bd99bf4 100644
--- a/packages/app/src/i18n/ru.ts
+++ b/packages/app/src/i18n/ru.ts
@@ -624,12 +624,6 @@ export const dict = {
"settings.general.section.updates": "Обновления",
"settings.general.section.sounds": "Звуковые эффекты",
"settings.general.section.feed": "Лента",
- "settings.general.section.sharing": "Общий доступ",
- "settings.general.row.shareUrl.title": "URL сервера для общего доступа",
- "settings.general.row.shareUrl.description": "Базовый URL сервера, используемого при общем доступе к сеансам. Оставьте пустым для значения по умолчанию.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Включить экспертную панель по умолчанию",
- "settings.general.row.expertPanelDefault.description": "Начинать новые беседы с включённой экспертной панелью, чтобы её кнопка по запросу проверяла текущий контекст.",
"settings.general.section.display": "Дисплей",
"settings.general.row.language.title": "Язык",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index d9822fb2..6a08684d 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -619,12 +619,6 @@ export const dict = {
"settings.general.section.updates": "การอัปเดต",
"settings.general.section.sounds": "เสียงเอฟเฟกต์",
"settings.general.section.feed": "ฟีด",
- "settings.general.section.sharing": "การแชร์",
- "settings.general.row.shareUrl.title": "URL เซิร์ฟเวอร์สำหรับแชร์",
- "settings.general.row.shareUrl.description": "URL พื้นฐานของเซิร์ฟเวอร์ที่ใช้เมื่อแชร์เซสชัน เว้นว่างไว้เพื่อใช้ค่าเริ่มต้น",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "เปิดใช้คณะผู้เชี่ยวชาญโดยค่าเริ่มต้น",
- "settings.general.row.expertPanelDefault.description": "เริ่มการสนทนาใหม่โดยเปิดใช้คณะผู้เชี่ยวชาญ เพื่อให้ปุ่มสามารถตรวจทานบริบทปัจจุบันได้ตามต้องการ",
"settings.general.section.display": "การแสดงผล",
"settings.general.row.language.title": "ภาษา",
diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts
index ce03a0a0..24ffe8c6 100644
--- a/packages/app/src/i18n/tr.ts
+++ b/packages/app/src/i18n/tr.ts
@@ -630,12 +630,6 @@ export const dict = {
"settings.general.section.updates": "Güncellemeler",
"settings.general.section.sounds": "Ses efektleri",
"settings.general.section.feed": "Akış",
- "settings.general.section.sharing": "Paylaşım",
- "settings.general.row.shareUrl.title": "Paylaşım sunucusu URL'si",
- "settings.general.row.shareUrl.description": "Oturumları paylaşırken kullanılan sunucunun temel URL'si. Varsayılanı kullanmak için boş bırakın.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Uzman panelini varsayılan olarak etkinleştir",
- "settings.general.row.expertPanelDefault.description": "Yeni sohbetleri uzman paneli etkin olarak başlatın; böylece düğmesi istendiğinde mevcut bağlamı inceler.",
"settings.general.section.display": "Ekran",
"settings.general.row.language.title": "Dil",
diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts
index 931f58d4..6835497b 100644
--- a/packages/app/src/i18n/uk.ts
+++ b/packages/app/src/i18n/uk.ts
@@ -728,12 +728,6 @@ export const dict = {
"settings.general.section.updates": "Оновлення",
"settings.general.section.sounds": "Звукові ефекти",
"settings.general.section.feed": "Стрічка",
- "settings.general.section.sharing": "Спільний доступ",
- "settings.general.row.shareUrl.title": "URL сервера спільного доступу",
- "settings.general.row.shareUrl.description": "Базовий URL сервера, що використовується під час надання спільного доступу до сеансів. Залиште порожнім для типового.",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
- "settings.general.row.expertPanelDefault.title": "Типово вмикати експертну панель",
- "settings.general.row.expertPanelDefault.description": "Починати нові розмови з увімкненою експертною панеллю, щоб її кнопка за потреби переглядала поточний контекст.",
"settings.general.section.display": "Дисплей",
"settings.general.row.language.title": "Мова",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index 9facb0d1..0e2008c6 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -220,18 +220,11 @@ export const dict = {
"provider.custom.field.apiKey.label": "API 密钥",
"provider.custom.field.apiKey.placeholder": "API 密钥",
"provider.custom.field.apiKey.description": "可选。如果你通过请求头管理认证,可留空。",
- "provider.custom.edit.title": "编辑自定义提供方",
- "provider.custom.specWarning":
- "模型规格(上下文窗口、推理能力)是根据公共目录自动补全的默认值,仅供参考。如果你修改它们,我们无法保证模型仍能正常使用。",
"provider.custom.models.label": "模型",
"provider.custom.models.id.label": "ID",
"provider.custom.models.id.placeholder": "model-id",
"provider.custom.models.name.label": "名称",
"provider.custom.models.name.placeholder": "显示名称",
- "provider.custom.models.context.label": "上下文 tokens",
- "provider.custom.models.context.placeholder": "例如 128000",
- "provider.custom.models.reasoning.label": "推理",
- "provider.custom.models.temperature.label": "温度",
"provider.custom.models.remove": "移除模型",
"provider.custom.models.add": "添加模型",
"provider.custom.headers.label": "请求头(可选)",
@@ -249,7 +242,6 @@ export const dict = {
"provider.custom.error.baseURL.format": "必须以 http:// 或 https:// 开头",
"provider.custom.error.required": "必填",
"provider.custom.error.duplicate": "重复",
- "provider.custom.error.context": "请输入正整数,或留空",
"provider.disconnect.toast.disconnected.title": "{{provider}} 已断开连接",
"provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。",
@@ -336,64 +328,8 @@ export const dict = {
"prompt.action.attachFile": "附加文件",
"prompt.attachment.remove": "移除附件",
"prompt.generating": "提示生成中...",
- "prompt.draft.preview.streaming": "正在准备提示草稿",
- "prompt.draft.preview.paused": "草稿已暂停",
- "prompt.draft.preview.dismiss": "丢弃",
- "prompt.draft.preview.use": "使用此草稿",
"prompt.action.send": "发送",
"prompt.action.stop": "停止",
- "composer.approval.request": "请求批准",
- "composer.approval.readOnly": "只读",
- "composer.approval.fullAccess": "完全访问",
- "composer.mode.auto": "自动",
- "composer.mode.loop": "目标",
- "composer.mode.design": "设计",
- "composer.mode.auto.desc": "智能体自行定目标、做计划并执行到完成",
- "composer.mode.loop.desc": "你说需求,智能体生成 goal+plan.md,再由监督循环自主执行到达成",
- "composer.mode.design.desc": "你自己写好 goal+plan.md,智能体读取并忠实执行你的方案",
- "composer.panel.label": "专家团",
- "composer.panel.off": "关闭",
- "composer.panel.single": "单轮评审",
- "composer.panel.multi": "多轮辩论",
- "composer.panel.verdict.title": "专家团裁定",
- "composer.panel.verdict.approve": "批准",
- "composer.panel.verdict.revise": "修订",
- "composer.panel.verdict.block": "阻止",
- "composer.panel.verdict.needsHuman": "需人工介入",
- "composer.panel.verdict.meta.one": "{{confidence}}% 置信度 · {{rounds}} 轮",
- "composer.panel.verdict.meta.other": "{{confidence}}% 置信度 · {{rounds}} 轮",
- "composer.panel.verdict.evidence": "依据",
- "composer.panel.verdict.dissent": "异议(已被否决)",
- "composer.goal.phase.running": "运行中",
- "composer.goal.phase.paused": "已暂停",
- "composer.goal.phase.done": "已完成",
- "composer.goal.phase.needsHuman": "需你处理",
- "composer.goal.phase.rolledBack": "已回滚",
- "composer.goal.phase.stopped": "已停止",
- "composer.goal.budget.one": "{{ticks}} 次 · {{tokens}} tokens",
- "composer.goal.budget.other": "{{ticks}} 次 · {{tokens}} tokens",
- "composer.goal.pause": "暂停",
- "composer.goal.resume": "继续",
- "composer.goal.stop": "停止",
- "composer.goal.dismiss": "忽略",
- "composer.goal.editPlan": "编辑计划",
- "composer.goal.editPlan.title": "编辑目标计划",
- "composer.goal.editPlan.desc": "修改正在运行的目标计划。更改将在下一个 tick 生效。",
- "composer.goal.editPlan.goalLabel": "目标",
- "composer.goal.editPlan.stepsLabel": "步骤",
- "composer.goal.editPlan.addStep": "添加步骤",
- "composer.goal.editPlan.removeStep": "删除步骤",
- "composer.goal.editPlan.stepPlaceholder": "步骤描述",
- "composer.goal.editPlan.save": "应用更改",
- "composer.goal.editPlan.cancel": "取消",
- "composer.goal.editPlan.saved": "计划已更新——下一个 tick 生效",
- "composer.goal.editPlan.failed": "无法更新计划(目标未在运行?)",
- "composer.goal.editPlan.status.pending": "待处理",
- "composer.goal.editPlan.status.active": "进行中",
- "composer.goal.editPlan.status.done": "已完成",
- "composer.goal.editPlan.status.blocked": "受阻",
- "composer.goal.editPlan.status.cancelled": "已取消",
- "composer.steer.hint": "会话正在运行——你的消息将引导当前回合",
"prompt.scenario.label": "情景模式",
"prompt.scenario.direct": "直接",
"prompt.scenario.intelligence": "智能",
@@ -484,7 +420,6 @@ export const dict = {
"dialog.server.add.error": "无法连接到服务器",
"dialog.server.add.checking": "检查中...",
"dialog.server.add.button": "添加服务器",
- "dialog.server.add.menu.http": "添加 HTTP 服务器",
"dialog.server.connect.button": "连接到服务端",
"dialog.server.add.name": "服务器名称(可选)",
"dialog.server.add.namePlaceholder": "Localhost",
@@ -693,66 +628,6 @@ export const dict = {
"session.files.all": "所有文件",
"session.files.empty": "无文件",
"session.files.binaryContent": "二进制文件(无法显示内容)",
- "session.files.heading": "文件",
- "session.files.newFile": "新建文件",
- "session.files.newFolder": "新建文件夹",
- "session.files.newFile.placeholder": "文件名",
- "session.files.newFolder.placeholder": "文件夹名",
- "session.files.create.confirm": "确认",
- "session.files.create.cancel": "取消",
- "session.files.create.failed": "创建失败",
- "session.files.createFolder.failed": "创建文件夹失败",
- "session.panel.oversight": "监督",
- "session.panel.debug": "调试",
- "session.panel.profile": "性能剖析",
- "oversight.common.refresh": "刷新",
- "oversight.dashboard.title": "Agent 仪表盘",
- "oversight.dashboard.loading": "正在加载指标…",
- "oversight.dashboard.empty": "暂无可用指标。",
- "oversight.metric.taskSuccessRate": "任务成功率",
- "oversight.metric.conflictRate": "冲突率",
- "oversight.metric.dlqEvents": "死信事件",
- "oversight.metric.pushRejected": "推送被拒",
- "oversight.metric.tasksCompleted": "已完成任务",
- "oversight.metric.tasksFailed": "失败任务",
- "oversight.metric.publishLatencyP50": "发布延迟 P50",
- "oversight.metric.publishLatencyP95": "发布延迟 P95",
- "oversight.metric.eventToAgentP50": "事件→Agent P50",
- "oversight.metric.eventToAgentP95": "事件→Agent P95",
- "oversight.metric.humanTakeovers": "人工接管次数",
- "oversight.metric.rollbacks": "回滚次数",
- "oversight.metric.pushRejectedByReason": "按原因统计的推送拒绝",
- "oversight.metric.window": "时间窗口:{{from}} → {{to}}",
- "oversight.approvals.title": "审批队列",
- "oversight.approvals.empty": "没有待审批项。",
- "oversight.approvals.approve": "批准",
- "oversight.approvals.reject": "拒绝",
- "oversight.approvals.acknowledge": "知悉",
- "oversight.approvals.viewTrace": "查看轨迹",
- "oversight.approvals.resolveFailed": "处理失败",
- "oversight.trace.title": "事件轨迹",
- "oversight.trace.placeholder": "correlationID…",
- "oversight.trace.action": "追踪",
- "oversight.trace.loading": "正在加载轨迹…",
- "oversight.trace.empty": "该 correlationID 没有事件。",
- "oversight.trace.source": "来源:",
- "oversight.trace.causedBy": "起因:",
- "oversight.takeover.title": "人工接管",
- "oversight.takeover.description": "记录由人工从自主 Agent 处接管(暂停自主性升级)。",
- "oversight.takeover.placeholder": "接管原因…",
- "oversight.takeover.action": "接管",
- "oversight.takeover.recorded": "接管已记录。",
- "oversight.takeover.unsupported": "接管记录将在后端端点(P3.10)可用后启用。",
- "oversight.takeover.failed": "失败:{{error}}",
- "oversight.rollback.title": "回滚",
- "oversight.rollback.description": "回滚某个会话中 Agent 产生的更改(通过 SessionRevert)。仅影响本工作区内的会话。此操作会回滚真实更改——请谨慎使用。",
- "oversight.rollback.sessionPlaceholder": "会话 ID(ses_…)",
- "oversight.rollback.reasonPlaceholder": "回滚原因…(可选)",
- "oversight.rollback.action": "回滚",
- "oversight.rollback.noop": "无内容可回滚——已记录为空操作。",
- "oversight.rollback.applied": "已回滚——会话已还原。",
- "oversight.rollback.notFound": "本工作区内无此会话。",
- "oversight.rollback.failed": "失败:{{error}}",
"session.messages.renderEarlier": "显示更早的消息",
"session.messages.loadingEarlier": "正在加载更早的消息...",
"session.messages.loadEarlier": "加载更早的消息",
@@ -814,7 +689,7 @@ export const dict = {
"session.header.open.menu": "打开选项",
"session.header.open.copyPath": "复制路径",
- "status.popover.trigger": "服务器",
+ "status.popover.trigger": "服务器与MCP",
"status.popover.ariaLabel": "服务器配置",
"status.popover.tab.servers": "服务器",
"status.popover.tab.mcp": "MCP",
@@ -822,7 +697,7 @@ export const dict = {
"status.popover.tab.plugins": "插件",
"status.popover.action.manageServers": "管理服务器",
// V3.6 Phase 3C
- "sidebar.extensions": "插件",
+ "sidebar.extensions": "扩展与服务",
"session.share.popover.title": "发布到网页",
"session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。",
@@ -872,30 +747,6 @@ export const dict = {
"sidebar.settings": "设置",
"sidebar.help": "帮助",
"sidebar.review": "知识治理",
- "sidebar.wiki": "仓库与百科",
- "goal.start.hint": "计划已就绪 — 转为受监督的长跑目标",
- "goal.start.button": "转为 Goal",
- "goal.start.success": "目标已启动,正在后台运行",
- "goal.start.failed": "无法启动目标",
- "wiki.title": "仓库与百科",
- "wiki.description": "阅读、检索、治理四张图。知识与记忆可编辑;文档与代码只读。",
- "wiki.search": "搜索页面与符号",
- "wiki.searchEmpty": "没有匹配的页面",
- "wiki.empty": "暂无页面",
- "wiki.loading": "加载中…",
- "wiki.selectPrompt": "选择一个页面阅读",
- "wiki.readOnly": "只读",
- "wiki.version": "v{{version}}",
- "wiki.links": "关联代码与文档",
- "wiki.stale": "已失效",
- "wiki.type.knowledge": "知识",
- "wiki.type.memory": "记忆",
- "wiki.type.document": "文档",
- "wiki.type.code": "代码",
- "wiki.edit.button": "编辑",
- "wiki.edit.save": "保存",
- "wiki.edit.saved": "知识页已更新",
- "wiki.edit.failed": "保存失败",
"sidebar.history": "历史项目",
"sidebar.history.description": "包含已导入或历史会话、但当前未激活的项目。点击可打开。",
"sidebar.workspaces.enable": "启用工作区",
@@ -926,10 +777,6 @@ export const dict = {
"settings.general.section.sounds": "音效",
"settings.general.section.feed": "动态",
"settings.general.section.display": "显示",
- "settings.general.section.sharing": "分享",
- "settings.general.row.shareUrl.title": "分享服务器地址",
- "settings.general.row.shareUrl.description": "分享会话时使用的服务器基础地址。留空则使用默认值。",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
"settings.general.row.language.title": "语言",
"settings.general.row.language.description": "更改 DeepAgent Code 的显示语言",
"settings.general.deepagent.mode.title": "智能体",
@@ -987,8 +834,6 @@ export const dict = {
"review.type.skill": "技能",
"review.type.failure_dossier": "失败档案",
"review.type.other": "其他",
- "review.scope.project": "当前项目",
- "review.scope.global": "全局",
"review.groupCount": "{{count}}",
"packs.title": "领域包",
"packs.active": "自动检测",
@@ -1028,8 +873,6 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
"settings.general.row.showSessionProgressBar.title": "显示会话进度条",
"settings.general.row.showSessionProgressBar.description": "当智能体正在工作时,在会话顶部显示动画进度条",
- "settings.general.row.expertPanelDefault.title": "默认启用专家团",
- "settings.general.row.expertPanelDefault.description": "新会话默认激活专家团,其按钮可随时对当前上下文发起会诊",
"settings.general.row.wayland.title": "使用原生 Wayland",
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index 608fadcb..6a87085d 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -171,18 +171,11 @@ export const dict = {
"provider.custom.field.apiKey.label": "API 金鑰",
"provider.custom.field.apiKey.placeholder": "API 金鑰",
"provider.custom.field.apiKey.description": "選填。若您透過標頭管理驗證,可留空。",
- "provider.custom.edit.title": "編輯自訂提供方",
- "provider.custom.specWarning":
- "模型規格(上下文視窗、推理能力)是根據公開目錄自動補全的預設值,僅供參考。若您修改它們,我們無法保證模型仍能正常使用。",
"provider.custom.models.label": "模型",
"provider.custom.models.id.label": "ID",
"provider.custom.models.id.placeholder": "model-id",
"provider.custom.models.name.label": "名稱",
"provider.custom.models.name.placeholder": "顯示名稱",
- "provider.custom.models.context.label": "上下文 tokens",
- "provider.custom.models.context.placeholder": "例如 128000",
- "provider.custom.models.reasoning.label": "推理",
- "provider.custom.models.temperature.label": "溫度",
"provider.custom.models.remove": "移除模型",
"provider.custom.models.add": "新增模型",
"provider.custom.headers.label": "標頭(選填)",
@@ -200,7 +193,6 @@ export const dict = {
"provider.custom.error.baseURL.format": "必須以 http:// 或 https:// 開頭",
"provider.custom.error.required": "必填",
"provider.custom.error.duplicate": "重複",
- "provider.custom.error.context": "請輸入正整數,或留空",
"provider.disconnect.toast.disconnected.title": "{{provider}} 已中斷連線",
"provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。",
@@ -286,58 +278,6 @@ export const dict = {
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "傳送",
"prompt.action.stop": "停止",
- "composer.approval.request": "請求批准",
- "composer.approval.readOnly": "唯讀",
- "composer.approval.fullAccess": "完全存取",
- "composer.mode.auto": "自動",
- "composer.mode.loop": "目標",
- "composer.mode.design": "設計",
- "composer.mode.auto.desc": "智能體自行定目標、做計劃並執行到完成",
- "composer.mode.loop.desc": "你說需求,智能體生成 goal+plan.md,再由監督循環自主執行到達成",
- "composer.mode.design.desc": "你自己寫好 goal+plan.md,智能體讀取並忠實執行你的方案",
- "composer.panel.label": "專家團",
- "composer.panel.off": "關閉",
- "composer.panel.single": "單輪評審",
- "composer.panel.multi": "多輪辯論",
- "composer.panel.verdict.title": "專家團裁定",
- "composer.panel.verdict.approve": "批准",
- "composer.panel.verdict.revise": "修訂",
- "composer.panel.verdict.block": "阻止",
- "composer.panel.verdict.needsHuman": "需人工介入",
- "composer.panel.verdict.meta.one": "{{confidence}}% 置信度 · {{rounds}} 輪",
- "composer.panel.verdict.meta.other": "{{confidence}}% 置信度 · {{rounds}} 輪",
- "composer.panel.verdict.evidence": "依據",
- "composer.panel.verdict.dissent": "異議(已被否決)",
- "composer.goal.phase.running": "執行中",
- "composer.goal.phase.paused": "已暫停",
- "composer.goal.phase.done": "已完成",
- "composer.goal.phase.needsHuman": "需你處理",
- "composer.goal.phase.rolledBack": "已回滾",
- "composer.goal.phase.stopped": "已停止",
- "composer.goal.budget.one": "{{ticks}} 次 · {{tokens}} tokens",
- "composer.goal.budget.other": "{{ticks}} 次 · {{tokens}} tokens",
- "composer.goal.pause": "暫停",
- "composer.goal.resume": "繼續",
- "composer.goal.stop": "停止",
- "composer.goal.dismiss": "忽略",
- "composer.goal.editPlan": "編輯計劃",
- "composer.goal.editPlan.title": "編輯目標計劃",
- "composer.goal.editPlan.desc": "修改正在執行的目標計劃。變更將在下一個 tick 生效。",
- "composer.goal.editPlan.goalLabel": "目標",
- "composer.goal.editPlan.stepsLabel": "步驟",
- "composer.goal.editPlan.addStep": "新增步驟",
- "composer.goal.editPlan.removeStep": "刪除步驟",
- "composer.goal.editPlan.stepPlaceholder": "步驟描述",
- "composer.goal.editPlan.save": "套用變更",
- "composer.goal.editPlan.cancel": "取消",
- "composer.goal.editPlan.saved": "計劃已更新——下一個 tick 生效",
- "composer.goal.editPlan.failed": "無法更新計劃(目標未在執行?)",
- "composer.goal.editPlan.status.pending": "待處理",
- "composer.goal.editPlan.status.active": "進行中",
- "composer.goal.editPlan.status.done": "已完成",
- "composer.goal.editPlan.status.blocked": "受阻",
- "composer.goal.editPlan.status.cancelled": "已取消",
- "composer.steer.hint": "工作階段正在執行——你的訊息將引導目前回合",
"prompt.toast.pasteUnsupported.title": "不支援的附件",
"prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。",
@@ -378,8 +318,6 @@ export const dict = {
"dialog.server.add.error": "無法連線到伺服器",
"dialog.server.add.checking": "檢查中...",
"dialog.server.add.button": "新增伺服器",
- "dialog.server.add.menu.http": "新增 HTTP 伺服器",
- "dialog.server.connect.button": "連線到伺服器",
"dialog.server.add.name": "伺服器名稱(選填)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "使用者名稱(選填)",
@@ -578,66 +516,6 @@ export const dict = {
"session.files.all": "所有檔案",
"session.files.empty": "沒有檔案",
"session.files.binaryContent": "二進位檔案(無法顯示內容)",
- "session.files.heading": "檔案",
- "session.files.newFile": "新增檔案",
- "session.files.newFolder": "新增資料夾",
- "session.files.newFile.placeholder": "檔案名稱",
- "session.files.newFolder.placeholder": "資料夾名稱",
- "session.files.create.confirm": "確認",
- "session.files.create.cancel": "取消",
- "session.files.create.failed": "建立失敗",
- "session.files.createFolder.failed": "建立資料夾失敗",
- "session.panel.oversight": "監督",
- "session.panel.debug": "偵錯",
- "session.panel.profile": "效能分析",
- "oversight.common.refresh": "重新整理",
- "oversight.dashboard.title": "Agent 儀表板",
- "oversight.dashboard.loading": "正在載入指標…",
- "oversight.dashboard.empty": "暫無可用指標。",
- "oversight.metric.taskSuccessRate": "任務成功率",
- "oversight.metric.conflictRate": "衝突率",
- "oversight.metric.dlqEvents": "死信事件",
- "oversight.metric.pushRejected": "推送被拒",
- "oversight.metric.tasksCompleted": "已完成任務",
- "oversight.metric.tasksFailed": "失敗任務",
- "oversight.metric.publishLatencyP50": "發布延遲 P50",
- "oversight.metric.publishLatencyP95": "發布延遲 P95",
- "oversight.metric.eventToAgentP50": "事件→Agent P50",
- "oversight.metric.eventToAgentP95": "事件→Agent P95",
- "oversight.metric.humanTakeovers": "人工接管次數",
- "oversight.metric.rollbacks": "回滾次數",
- "oversight.metric.pushRejectedByReason": "按原因統計的推送拒絕",
- "oversight.metric.window": "時間視窗:{{from}} → {{to}}",
- "oversight.approvals.title": "審批佇列",
- "oversight.approvals.empty": "沒有待審批項。",
- "oversight.approvals.approve": "批准",
- "oversight.approvals.reject": "拒絕",
- "oversight.approvals.acknowledge": "知悉",
- "oversight.approvals.viewTrace": "檢視軌跡",
- "oversight.approvals.resolveFailed": "處理失敗",
- "oversight.trace.title": "事件軌跡",
- "oversight.trace.placeholder": "correlationID…",
- "oversight.trace.action": "追蹤",
- "oversight.trace.loading": "正在載入軌跡…",
- "oversight.trace.empty": "該 correlationID 沒有事件。",
- "oversight.trace.source": "來源:",
- "oversight.trace.causedBy": "起因:",
- "oversight.takeover.title": "人工接管",
- "oversight.takeover.description": "記錄由人工從自主 Agent 處接管(暫停自主性升級)。",
- "oversight.takeover.placeholder": "接管原因…",
- "oversight.takeover.action": "接管",
- "oversight.takeover.recorded": "接管已記錄。",
- "oversight.takeover.unsupported": "接管記錄將在後端端點(P3.10)可用後啟用。",
- "oversight.takeover.failed": "失敗:{{error}}",
- "oversight.rollback.title": "回滾",
- "oversight.rollback.description": "回滾某個工作階段中 Agent 產生的變更(透過 SessionRevert)。僅影響本工作區內的工作階段。此操作會回滾真實變更——請謹慎使用。",
- "oversight.rollback.sessionPlaceholder": "工作階段 ID(ses_…)",
- "oversight.rollback.reasonPlaceholder": "回滾原因…(可選)",
- "oversight.rollback.action": "回滾",
- "oversight.rollback.noop": "無內容可回滾——已記錄為空操作。",
- "oversight.rollback.applied": "已回滾——工作階段已還原。",
- "oversight.rollback.notFound": "本工作區內無此工作階段。",
- "oversight.rollback.failed": "失敗:{{error}}",
"session.messages.renderEarlier": "顯示更早的訊息",
"session.messages.loadingEarlier": "正在載入更早的訊息...",
"session.messages.loadEarlier": "載入更早的訊息",
@@ -723,32 +601,6 @@ export const dict = {
"sidebar.nav.projectsAndSessions": "專案與工作階段",
"sidebar.settings": "設定",
"sidebar.help": "說明",
- "sidebar.wiki": "倉庫與百科",
- "review.scope.project": "當前專案",
- "review.scope.global": "全域",
- "goal.start.hint": "計劃已就緒 — 轉為受監督的長跑目標",
- "goal.start.button": "轉為 Goal",
- "goal.start.success": "目標已啟動,正在背景執行",
- "goal.start.failed": "無法啟動目標",
- "wiki.title": "倉庫與百科",
- "wiki.description": "閱讀、檢索、治理四張圖。知識與記憶可編輯;文件與程式碼唯讀。",
- "wiki.search": "搜尋頁面與符號",
- "wiki.searchEmpty": "沒有符合的頁面",
- "wiki.empty": "暫無頁面",
- "wiki.loading": "載入中…",
- "wiki.selectPrompt": "選擇一個頁面閱讀",
- "wiki.readOnly": "唯讀",
- "wiki.version": "v{{version}}",
- "wiki.links": "關聯程式碼與文件",
- "wiki.stale": "已失效",
- "wiki.type.knowledge": "知識",
- "wiki.type.memory": "記憶",
- "wiki.type.document": "文件",
- "wiki.type.code": "程式碼",
- "wiki.edit.button": "編輯",
- "wiki.edit.save": "儲存",
- "wiki.edit.saved": "知識頁已更新",
- "wiki.edit.failed": "儲存失敗",
"sidebar.workspaces.enable": "啟用工作區",
"sidebar.workspaces.disable": "停用工作區",
"sidebar.gettingStarted.title": "開始使用",
@@ -773,10 +625,6 @@ export const dict = {
"settings.general.section.sounds": "音效",
"settings.general.section.feed": "資訊流",
"settings.general.section.display": "顯示",
- "settings.general.section.sharing": "分享",
- "settings.general.row.shareUrl.title": "分享伺服器網址",
- "settings.general.row.shareUrl.description": "分享工作階段時使用的伺服器基礎網址。留空則使用預設值。",
- "settings.general.row.shareUrl.placeholder": "https://opncd.ai",
"settings.general.row.language.title": "語言",
"settings.general.row.language.description": "變更 DeepAgent Code 的顯示語言",
@@ -805,8 +653,6 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
"settings.general.row.showSessionProgressBar.title": "顯示工作階段進度列",
"settings.general.row.showSessionProgressBar.description": "當代理程式正在運作時,在工作階段頂部顯示動畫進度列",
- "settings.general.row.expertPanelDefault.title": "預設啟用專家團",
- "settings.general.row.expertPanelDefault.description": "新工作階段預設啟用專家團,其按鈕可隨時對目前上下文發起會診",
"settings.general.row.wayland.title": "使用原生 Wayland",
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx
index 5f6517aa..abef9f0b 100644
--- a/packages/app/src/pages/directory-layout.tsx
+++ b/packages/app/src/pages/directory-layout.tsx
@@ -4,16 +4,10 @@ import { base64Encode } from "@deepagent-code/core/util/encode"
import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
import { useLanguage } from "@/context/language"
-import { useLayout } from "@/context/layout"
import { LocalProvider } from "@/context/local"
import { SDKProvider } from "@/context/sdk"
-import { useServer } from "@/context/server"
-import { useServerSDK } from "@/context/server-sdk"
-import { useServerSync } from "@/context/server-sync"
import { useSync } from "@/context/sync"
import { decode64 } from "@/utils/base64"
-import { isFilesystemRootDir, recoverFilesystemRootRoute } from "@/utils/filesystem-root"
-import { formatServerError } from "@/utils/server-errors"
import { Schema } from "effect"
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
@@ -59,13 +53,8 @@ export function decodeDirectory(dir: string): ProjectDirString | undefined {
export default function Layout(props: ParentProps) {
const params = useParams()
const language = useLanguage()
- const layout = useLayout()
const navigate = useNavigate()
- const server = useServer()
- const serverSDK = useServerSDK()
- const serverSync = useServerSync()
let invalid = ""
- let recovering = ""
const resolved = createMemo(() => {
if (!params.dir) return ""
@@ -89,51 +78,8 @@ export default function Layout(props: ParentProps) {
navigate("/", { replace: true })
})
- createEffect(() => {
- const directory = resolved()
- if (!directory || !isFilesystemRootDir(directory)) return
- const dataDir = serverSync.data.path.data
- if (!dataDir) return
- const key = `${params.dir}/${params.id ?? ""}`
- if (recovering === key) return
- recovering = key
-
- void recoverFilesystemRootRoute({
- dataDir,
- sessionID: params.id,
- getSession: (sessionID) =>
- serverSDK.client.session
- .get({ sessionID })
- .then((response) => response.data)
- .catch(() => undefined),
- mkdir: async (destination) => {
- await serverSDK.createClient({ directory: destination, throwOnError: true }).file.mkdir({ path: "." })
- },
- moveSession: async (sessionID, destination) => {
- await serverSDK.client.experimental.controlPlane.moveSession({
- sessionID,
- destination: { directory: destination },
- })
- },
- })
- .then((result) => {
- layout.projects.open(result.directory)
- server.projects.touch(result.directory)
- const session = result.sessionID ? `/session/${result.sessionID}` : "/session"
- navigate(`/${base64Encode(result.directory)}${session}`, { replace: true })
- })
- .catch((error) => {
- showToast({
- variant: "error",
- title: language.t("toast.project.rootRecoveryFailed.title"),
- description: formatServerError(error, language.t),
- })
- navigate("/", { replace: true })
- })
- })
-
return (
-
+
{(resolved) => (
{props.children}
diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx
index 3b6448e1..7c0e4ed5 100644
--- a/packages/app/src/pages/error.tsx
+++ b/packages/app/src/pages/error.tsx
@@ -6,6 +6,7 @@ import { Component, createSignal, onMount, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
+import { Icon } from "@deepagent-code/ui/icon"
import { errorDescriptionKey } from "./error-description"
export type InitError = {
@@ -15,7 +16,6 @@ export type InitError = {
type Translator = ReturnType["t"]
const CHAIN_SEPARATOR = "\n" + "─".repeat(40) + "\n"
-const OFFICIAL_FEEDBACK_URL: string = ""
function isIssue(value: unknown): value is { message: string; path: string[] } {
if (!value || typeof value !== "object") return false
@@ -346,20 +346,17 @@ export const ErrorPage: Component = (props) => {
{(message) => {message()}
}