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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ The features below start from a real need — something a plain coding agent, op

## What You Can Do

### Bring your history over — switch tools without starting from zero

**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.

**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.

### Keep one conversation going indefinitely

**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.
Expand Down
6 changes: 6 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体

## 你可以做什么

### 把历史一起带过来——换工具不必从零开始

**需求:** 你已经在另一个智能体里——Codex 或 Claude Code——积累了几个月的上下文,而换工具通常意味着把这一切统统丢下:那些对话、沉淀下来的记忆、你调好的技能。单是这份代价,就足以把人留在早该告别的工具上。

**DeepAgent 的做法:** 一键导入你已有的历史。把它指向一个 Codex 或 Claude Code 的安装目录,它就会把你的会话、记忆和技能热导入 DeepAgent——读取各工具的本地格式,归一化后重放进文档图,让导入的对话表现得和原生对话一样。导入过程中会对密钥做脱敏,导入的项目彼此隔离、不会和你正在进行的工作冲突,重复导入会收敛而不是产生重复。入口有三处:设置里的"导入历史"面板、侧栏的历史视图,以及 `import-history` 命令行。迁移只需几分钟,而不是推倒重来。

### 一直对话下去,不必换新

**需求:** 长任务会撑爆上下文窗口。多数智能体的应对方式是截断历史,或在阈值处一刀切地整体摘要——于是任务进行到一半,智能体忘了你一小时前拍的板,或者窗口被陈旧的工具输出塞满,质量断崖式下跌。
Expand Down
99 changes: 99 additions & 0 deletions packages/app/src/components/history/dialog-history-projects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Component, createMemo, createResource, For, Show } from "solid-js"
import { Dialog } from "@deepagent-code/ui/v2/dialog-v2"
import { Icon } from "@deepagent-code/ui/icon"
import { getFilename } from "@deepagent-code/core/util/path"
import { useLanguage } from "@/context/language"
import "../settings-v2/settings-v2.css"

type ProjectListItem = {
id: string
worktree: string
name?: string
}

type RawSdkClient = {
client: {
request<TData>(options: {
method: string
url: string
body?: unknown
headers?: Record<string, string>
}): Promise<{ data?: TData }>
}
}

export const listAllProjects = async (client: RawSdkClient): Promise<ProjectListItem[]> => {
try {
const response = await client.client.request<{ items?: ProjectListItem[] } | ProjectListItem[]>({
method: "GET",
url: "/global/projects",
})
const data = response.data
if (Array.isArray(data)) return data
return data?.items ?? []
} catch {
// Endpoint missing (stale sidecar build) or transient error — degrade to an
// empty list instead of surfacing a scary error / crashing the dialog.
return []
}
}

export const DialogHistoryProjects: Component<{
client: RawSdkClient
activeWorktrees: ReadonlySet<string>
onOpen: (directory: string) => void
}> = (props) => {
const language = useLanguage()
const [items] = createResource(async () => listAllProjects(props.client))

const historical = createMemo(() => {
const all = items() ?? []
// History = projects the user is NOT currently viewing as active.
return all
.filter((p) => p.worktree && !props.activeWorktrees.has(p.worktree))
.sort((a, b) => (a.name ?? a.worktree).localeCompare(b.name ?? b.worktree))
})

return (
<Dialog size="x-large" variant="settings" title={language.t("sidebar.history")}>
<div class="settings-v2-panel" data-component="history-projects-dialog">
<div class="settings-v2-tab-body deepagent-dialog-body">
<p class="text-12-regular text-v2-text-text-faint">
{language.t("sidebar.history.description")}
</p>

<div class="deepagent-dialog-scroll rounded-lg border border-v2-border-border-muted">
<Show
when={!items.loading}
fallback={<div class="p-4 text-13-regular text-v2-text-text-faint">{language.t("review.loading")}</div>}
>
<Show
when={historical().length > 0}
fallback={<div class="p-4 text-13-regular text-v2-text-text-faint">{language.t("review.empty")}</div>}
>
<For each={historical()}>
{(project) => (
<button
type="button"
data-action="history-project"
class="flex w-full items-center gap-3 border-b border-v2-border-border-muted px-3 py-2.5 text-left last:border-b-0 hover:bg-v2-background-bg-layer-01"
onClick={() => props.onOpen(project.worktree)}
>
<Icon name="folder" size="small" class="text-icon-weak" />
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<span class="break-words text-13-medium text-v2-text-text-base">
{project.name ?? getFilename(project.worktree)}
</span>
<span class="break-words text-11-regular text-v2-text-text-faint">{project.worktree}</span>
</div>
</button>
)}
</For>
</Show>
</Show>
</div>
</div>
</div>
</Dialog>
)
}
14 changes: 13 additions & 1 deletion packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const working = createMemo(() => sync.data.session_working(params.id ?? ""))
// A finished subagent is read-only: it did its one turn and was marked done by the task tool
// (metadata.deepagent.subagent.finished). The panel still lists it so its history is viewable,
// but new prompts must be refused — the parent orchestrates work, you don't chat with a done child.
const subagentFinished = createMemo(() => {
const meta = info()?.metadata as { deepagent?: { subagent?: { finished?: boolean } } } | undefined
return meta?.deepagent?.subagent?.finished === true
})
const imageAttachments = createMemo(() =>
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
)
Expand Down Expand Up @@ -1264,6 +1271,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})

const handlePromptSubmit = (event: Event) => {
// Read-only finished subagent: swallow the submit so no new turn is started.
if (subagentFinished()) {
event.preventDefault()
return
}
if (draftPreparing() || composing()) {
event.preventDefault()
return
Expand Down Expand Up @@ -1778,7 +1790,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<IconButton
data-action="prompt-submit"
type="submit"
disabled={draftPreparing() || (!working() && blank())}
disabled={subagentFinished() || draftPreparing() || (!working() && blank())}
tabIndex={store.mode === "normal" ? undefined : -1}
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
variant="primary"
Expand Down
116 changes: 59 additions & 57 deletions packages/app/src/components/review/dialog-review.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, For, Show, createMemo, createResource, createSignal } from "solid-js"
import { Component, createMemo, createResource, createSignal, For, Show } from "solid-js"
import { Dialog } 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 "../settings-v2/settings-v2.css"
Expand All @@ -9,15 +10,11 @@ type KnowledgeItem = {
id: string
type: "knowledge" | "strategy" | "methodology" | "memory" | "skill" | "failure_dossier"
summary: string
// The durable model carries a discrete evidence strength, NOT a raw confidence number.
// (Backend route /deepagent/knowledge/pending returns DeepAgentKnowledgeItem.)
evidence_strength: "strong" | "medium" | "weak" | "none"
evidence_refs: string[]
approval_status: "pending" | "approved" | "rejected"
}

// The new id-based review routes are not in the generated SDK; use the raw-request escape hatch
// (the dir-scoped client injects the workspace directory). Mirrors submit.ts's RawSdkClient.
type RawSdkClient = {
client: {
request<TData>(options: {
Expand All @@ -29,13 +26,8 @@ type RawSdkClient = {
}
}

// The dialog mounts outside SDKProvider (DialogProvider sits above it), so the dir-scoped sdk
// client is passed in by the opener instead of read from useSDK. Shape mirrors submit.ts: the
// generated client exposes the low-level request fn at `.client.request`.
type ReviewClient = RawSdkClient

// Exported for the route-contract test (review-dialog-contract.test.ts): these are the live V3.1
// self-learning Review routes. The test asserts method/url/body so a backend rename breaks CI.
export const listPending = async (client: ReviewClient): Promise<KnowledgeItem[]> => {
const response = await client.client.request<{ items: KnowledgeItem[] }>({
method: "GET",
Expand All @@ -61,21 +53,27 @@ export const DialogReview: Component<{ client: ReviewClient }> = (props) => {
const language = useLanguage()
const [selected, setSelected] = createSignal<ReadonlySet<string>>(new Set())
const [busy, setBusy] = createSignal(false)
const [showApproved, setShowApproved] = createSignal(false)
const [items, { refetch }] = createResource(async () => listPending(props.client))

const allItems = createMemo(() => items() ?? [])
// Rejected (and superseded, which the backend already excludes) are noise —
// hide them. Approved collapses into a single expandable row so the list is
// dominated by what actually needs attention (pending).
const pending = createMemo(() => (items() ?? []).filter((i) => i.approval_status === "pending"))
const approved = createMemo(() => (items() ?? []).filter((i) => i.approval_status === "approved"))

const toggle = (id: string) => {
const next = new Set<string>(selected())
if (next.has(id)) next.delete(id)
else next.add(id)
setSelected(next)
}
const selectAll = () => setSelected(new Set(allItems().map((i) => i.id)))
const selectAll = () => setSelected(new Set(pending().map((i) => i.id)))
const invert = () => {
const cur = selected()
setSelected(
new Set(
allItems()
pending()
.map((i) => i.id)
.filter((id) => !cur.has(id)),
),
Expand All @@ -101,6 +99,31 @@ export const DialogReview: Component<{ client: ReviewClient }> = (props) => {
}
}

const Row = (item: KnowledgeItem) => {
const checked = createMemo(() => selected().has(item.id))
return (
<label
data-action="review-item"
data-status={item.approval_status}
class="flex cursor-pointer items-start gap-3 border-b border-v2-border-border-muted px-3 py-2.5 last:border-b-0 hover:bg-v2-background-bg-layer-01"
>
<input type="checkbox" class="mt-0.5" checked={checked()} onChange={() => toggle(item.id)} />
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<span class="break-words text-13-medium text-v2-text-text-base">{item.summary}</span>
<span class="break-words text-11-regular text-v2-text-text-faint">
{item.type}
{" · "}
{language.t("review.strength", { value: language.t(`review.strength.${item.evidence_strength}`) })}
<Show when={item.evidence_refs.length > 0}>
{" · "}
{language.t("review.evidence", { count: item.evidence_refs.length })}
</Show>
</span>
</div>
</label>
)
}

return (
<Dialog size="x-large" variant="settings" title={language.t("review.title")}>
<div class="settings-v2-panel" data-component="review-dialog">
Expand All @@ -113,60 +136,39 @@ export const DialogReview: Component<{ client: ReviewClient }> = (props) => {
fallback={<div class="p-4 text-13-regular text-v2-text-text-faint">{language.t("review.loading")}</div>}
>
<Show
when={allItems().length > 0}
when={pending().length > 0 || approved().length > 0}
fallback={<div class="p-4 text-13-regular text-v2-text-text-faint">{language.t("review.empty")}</div>}
>
<For each={allItems()}>
{(item) => {
const checked = createMemo(() => selected().has(item.id))
// P2-J: render all THREE states. The backend returns approved entries too so a
// reviewer can REVOKE a prior approval (select it, then Reject). Collapsing
// approved into "pending" made Approve look like a no-op and hid revoke entirely.
const statusKey = createMemo(() =>
item.approval_status === "rejected"
? "review.status.rejected"
: item.approval_status === "approved"
? "review.status.approved"
: "review.status.pending",
)
return (
<label
data-action="review-item"
data-status={item.approval_status}
data-rejected={item.approval_status === "rejected" ? "true" : "false"}
class="flex cursor-pointer items-start gap-3 border-b border-v2-border-border-muted px-3 py-2.5 last:border-b-0 hover:bg-v2-background-bg-layer-01 data-[rejected=true]:opacity-60"
>
<input type="checkbox" class="mt-0.5" checked={checked()} onChange={() => toggle(item.id)} />
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<span class="break-words text-13-medium text-v2-text-text-base">{item.summary}</span>
<span class="break-words text-11-regular text-v2-text-text-faint">
{item.type}
{" · "}
{language.t(statusKey())}
{" · "}
{language.t("review.strength", {
value: language.t(`review.strength.${item.evidence_strength}`),
})}
<Show when={item.evidence_refs.length > 0}>
{" · "}
{language.t("review.evidence", { count: item.evidence_refs.length })}
</Show>
</span>
</div>
</label>
)
}}
</For>
{/* Pending: laid out flat — this is what needs attention. */}
<For each={pending()}>{(item) => Row(item)}</For>

{/* Approved: collapsed into one expandable row. */}
<Show when={approved().length > 0}>
<button
type="button"
data-action="review-approved-toggle"
class="flex w-full items-center gap-2 border-b border-v2-border-border-muted bg-v2-background-bg-layer-01 px-3 py-2.5 text-left hover:bg-v2-background-bg-layer-02"
onClick={() => setShowApproved(!showApproved())}
>
<Icon name={showApproved() ? "chevron-down" : "chevron-right"} size="small" />
<span class="text-13-medium text-v2-text-text-base">
{language.t("review.status.approved")} ({approved().length})
</span>
</button>
<Show when={showApproved()}>
<For each={approved()}>{(item) => Row(item)}</For>
</Show>
</Show>
</Show>
</Show>
</div>

<div class="deepagent-dialog-actions flex items-center justify-between">
<div class="flex flex-wrap items-center gap-2">
<Button variant="secondary" size="small" onClick={selectAll} disabled={allItems().length === 0}>
<Button variant="secondary" size="small" onClick={selectAll} disabled={pending().length === 0}>
{language.t("review.selectAll")}
</Button>
<Button variant="secondary" size="small" onClick={invert} disabled={allItems().length === 0}>
<Button variant="secondary" size="small" onClick={invert} disabled={pending().length === 0}>
{language.t("review.invertSelection")}
</Button>
<Show when={selected().size > 0}>
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/components/settings-v2/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "../link"
import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
import { ImportSection } from "./import-history"
import "./settings-v2.css"
import {
deepAgentModeFromConfig,
Expand Down Expand Up @@ -923,6 +924,8 @@ export const SettingsGeneralV2: Component = () => {
<UpdatesSection />

<DisplaySection />

<ImportSection />
</div>
</>
)
Expand Down
Loading
Loading