Skip to content

Commit 1db9d29

Browse files
deepagent-aiclaude
andauthored
Feat/v3.9 core (#43)
### Issue for this PR Closes # ### Type of change - [ ] Bug fix - [ ] New feature - [ ] Refactor / code improvement - [ ] Documentation ### What does this PR do? Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? ### Screenshots / recordings _If this is a UI change, please include a screenshot or recording._ ### Checklist - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR _If you do not follow this template your PR will be automatically rejected._ --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3827f37 commit 1db9d29

61 files changed

Lines changed: 8417 additions & 105 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import type { GlobalSession } from "@deepagent-code/sdk/v2/client"
2+
import { Component, createMemo, createResource, createSignal, For, Show } from "solid-js"
3+
import { Dialog } from "@deepagent-code/ui/v2/dialog-v2"
4+
import { Icon } from "@deepagent-code/ui/icon"
5+
import { IconButton } from "@deepagent-code/ui/icon-button"
6+
import { getFilename } from "@deepagent-code/core/util/path"
7+
import { useLanguage } from "@/context/language"
8+
import { sessionTitle } from "@/utils/session-title"
9+
import { formatSessionTime } from "@/utils/session-time"
10+
import { showToast } from "@/utils/toast"
11+
import "../settings-v2/settings-v2.css"
12+
13+
// The archived list carries an optional `preview` (first user-message snippet) that the backend is
14+
// still landing. Widen the generated GlobalSession locally so the UI can render it the moment it
15+
// arrives without waiting on an SDK regen. TODO: drop once GlobalSession gains `preview`.
16+
type ArchivedSession = GlobalSession & { preview?: string }
17+
18+
// Minimal shape of the serverSDK client this dialog needs. Kept structural so the caller can pass
19+
// the shared serverSDK.client without a cast fight.
20+
type ArchivedClient = {
21+
experimental: {
22+
session: {
23+
list(input: {
24+
archived: boolean | "true" | "false"
25+
roots?: boolean | "true" | "false"
26+
limit?: number
27+
}): Promise<{ data?: GlobalSession[] }>
28+
}
29+
}
30+
session: {
31+
update(input: {
32+
directory: string
33+
sessionID: string
34+
time: { archived: number | null }
35+
}): Promise<unknown>
36+
delete(input: { directory: string; sessionID: string }): Promise<unknown>
37+
}
38+
}
39+
40+
const ARCHIVED_FETCH_LIMIT = 200
41+
42+
export const listArchivedSessions = async (client: ArchivedClient): Promise<ArchivedSession[]> => {
43+
try {
44+
const response = await client.experimental.session.list({
45+
archived: true,
46+
// Only top-level sessions — hide subagent child sessions (parentID set) from the drawer.
47+
roots: true,
48+
limit: ARCHIVED_FETCH_LIMIT,
49+
})
50+
return (response.data ?? []) as ArchivedSession[]
51+
} catch {
52+
// Endpoint missing (stale sidecar build) or transient error — surface via the error state below.
53+
throw new Error("archived-list-failed")
54+
}
55+
}
56+
57+
export const DialogArchivedSessions: Component<{ client: ArchivedClient }> = (props) => {
58+
const language = useLanguage()
59+
const [query, setQuery] = createSignal("")
60+
// Locally removed ids (restored or deleted) — so the row disappears without a refetch.
61+
const [removed, setRemoved] = createSignal<ReadonlySet<string>>(new Set())
62+
const [confirmId, setConfirmId] = createSignal<string | undefined>(undefined)
63+
64+
const [items, { mutate }] = createResource(async () => listArchivedSessions(props.client))
65+
66+
const drop = (id: string) => {
67+
setRemoved((prev) => new Set(prev).add(id))
68+
// Keep the backing resource in sync so a later re-render / refetch stays consistent.
69+
mutate((prev) => (prev ?? []).filter((s) => s.id !== id))
70+
}
71+
72+
const sorted = createMemo(() => {
73+
const all = (items() ?? []).filter((s) => !removed().has(s.id))
74+
return all.slice().sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
75+
})
76+
77+
const filtered = createMemo(() => {
78+
const q = query().trim().toLowerCase()
79+
if (!q) return sorted()
80+
return sorted().filter((s) => {
81+
const title = (sessionTitle(s.title) ?? "").toLowerCase()
82+
const preview = (s.preview ?? "").toLowerCase()
83+
const dir = s.directory.toLowerCase()
84+
const project = (s.project?.name ?? "").toLowerCase()
85+
return title.includes(q) || preview.includes(q) || dir.includes(q) || project.includes(q)
86+
})
87+
})
88+
89+
const rowTitle = (s: ArchivedSession) => sessionTitle(s.title) ?? language.t("command.session.new")
90+
91+
const projectLabel = (s: ArchivedSession) =>
92+
s.project?.name ?? (s.project?.worktree ? getFilename(s.project.worktree) : getFilename(s.directory))
93+
94+
const secondaryLine = (s: ArchivedSession) => {
95+
if (s.preview && s.preview.trim()) return s.preview.trim()
96+
return formatSessionTime(s.time.updated ?? s.time.created, language.intl())
97+
}
98+
99+
const handleRestore = async (s: ArchivedSession) => {
100+
drop(s.id)
101+
await props.client.session
102+
.update({ directory: s.directory, sessionID: s.id, time: { archived: null } })
103+
.then(() => {
104+
showToast({
105+
title: language.t("session.archived.restored", { name: rowTitle(s) }),
106+
variant: "success",
107+
})
108+
})
109+
.catch((err: unknown) => {
110+
// Restore failed — put the row back so the user can retry.
111+
setRemoved((prev) => {
112+
const next = new Set(prev)
113+
next.delete(s.id)
114+
return next
115+
})
116+
mutate((prev) => {
117+
const list = prev ?? []
118+
return list.some((x) => x.id === s.id) ? list : [...list, s]
119+
})
120+
showToast({
121+
title: language.t("session.archived.restore.failed"),
122+
description: err instanceof Error ? err.message : String(err),
123+
})
124+
})
125+
}
126+
127+
const handleDelete = async (s: ArchivedSession) => {
128+
setConfirmId(undefined)
129+
drop(s.id)
130+
await props.client.session.delete({ directory: s.directory, sessionID: s.id }).catch((err: unknown) => {
131+
setRemoved((prev) => {
132+
const next = new Set(prev)
133+
next.delete(s.id)
134+
return next
135+
})
136+
mutate((prev) => {
137+
const list = prev ?? []
138+
return list.some((x) => x.id === s.id) ? list : [...list, s]
139+
})
140+
showToast({
141+
title: language.t("session.delete.failed.title"),
142+
description: err instanceof Error ? err.message : String(err),
143+
})
144+
})
145+
}
146+
147+
return (
148+
<Dialog size="x-large" variant="settings" title={language.t("session.archived.title")}>
149+
<div class="settings-v2-panel" data-component="archived-sessions-dialog">
150+
<div class="settings-v2-tab-body deepagent-dialog-body">
151+
<div class="flex items-center gap-2 rounded-lg border border-v2-border-border-muted px-3 py-2">
152+
<Icon name="magnifying-glass" size="small" class="text-icon-weak" />
153+
<input
154+
type="text"
155+
autofocus
156+
value={query()}
157+
onInput={(e) => setQuery(e.currentTarget.value)}
158+
placeholder={language.t("session.archived.search")}
159+
class="min-w-0 flex-1 bg-transparent text-13-regular text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint"
160+
aria-label={language.t("session.archived.search")}
161+
/>
162+
</div>
163+
164+
<div class="deepagent-dialog-scroll rounded-lg border border-v2-border-border-muted">
165+
<Show
166+
when={!items.loading}
167+
fallback={<div class="p-4 text-13-regular text-v2-text-text-faint">{language.t("review.loading")}</div>}
168+
>
169+
<Show
170+
when={!items.error}
171+
fallback={
172+
<div class="p-4 text-13-regular text-v2-text-text-faint">
173+
{language.t("session.archived.error")}
174+
</div>
175+
}
176+
>
177+
<Show
178+
when={filtered().length > 0}
179+
fallback={
180+
<div class="p-4 text-13-regular text-v2-text-text-faint">
181+
{query().trim() ? language.t("review.empty") : language.t("session.archived.empty")}
182+
</div>
183+
}
184+
>
185+
<For each={filtered()}>
186+
{(session) => (
187+
<div class="flex items-center 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">
188+
<Icon name="archive" size="small" class="shrink-0 text-icon-weak" />
189+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
190+
<span class="truncate text-13-medium text-v2-text-text-base">{rowTitle(session)}</span>
191+
<span class="truncate text-11-regular text-v2-text-text-faint">{secondaryLine(session)}</span>
192+
<span class="truncate text-11-regular text-v2-text-text-faint">{projectLabel(session)}</span>
193+
</div>
194+
<Show
195+
when={confirmId() === session.id}
196+
fallback={
197+
<div class="flex shrink-0 items-center gap-1">
198+
<IconButton
199+
icon="arrow-undo-down"
200+
variant="ghost"
201+
size="small"
202+
onClick={() => void handleRestore(session)}
203+
aria-label={language.t("session.archived.restore")}
204+
title={language.t("session.archived.restore")}
205+
/>
206+
<IconButton
207+
icon="trash"
208+
variant="ghost"
209+
size="small"
210+
onClick={() => setConfirmId(session.id)}
211+
aria-label={language.t("common.delete")}
212+
title={language.t("common.delete")}
213+
/>
214+
</div>
215+
}
216+
>
217+
<div class="flex shrink-0 flex-col items-end gap-1">
218+
<span class="text-11-regular text-v2-text-text-base">
219+
{language.t("session.delete.confirm", { name: rowTitle(session) })}
220+
</span>
221+
<div class="flex items-center gap-2">
222+
<button
223+
type="button"
224+
class="text-11-medium text-v2-text-text-faint hover:text-v2-text-text-base"
225+
onClick={() => setConfirmId(undefined)}
226+
>
227+
{language.t("common.cancel")}
228+
</button>
229+
<button
230+
type="button"
231+
class="text-11-medium text-v2-text-text-danger hover:opacity-80"
232+
onClick={() => void handleDelete(session)}
233+
>
234+
{language.t("session.delete.button")}
235+
</button>
236+
</div>
237+
</div>
238+
</Show>
239+
</div>
240+
)}
241+
</For>
242+
</Show>
243+
</Show>
244+
</Show>
245+
</div>
246+
</div>
247+
</div>
248+
</Dialog>
249+
)
250+
}

0 commit comments

Comments
 (0)