Skip to content

Commit ab7b13e

Browse files
committed
feat: support deleting archived sessions
1 parent 2323fc1 commit ab7b13e

17 files changed

Lines changed: 882 additions & 74 deletions

File tree

apps/kimi-code/src-tauri/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,22 @@ async fn list_archived_sessions(state: State<'_, AppState>) -> Result<Vec<Sessio
327327
state.client.list_archived_sessions().await
328328
}
329329

330+
#[tauri::command]
331+
async fn delete_archived_sessions(
332+
state: State<'_, AppState>,
333+
session_ids: Vec<String>,
334+
) -> Result<Vec<String>, String> {
335+
let session_ids = state.client.delete_archived_sessions(&session_ids).await?;
336+
if !session_ids.is_empty() {
337+
state
338+
.web_server
339+
.desktop_state_changed(DesktopStateChange::SessionsDeleted {
340+
session_ids: session_ids.clone(),
341+
});
342+
}
343+
Ok(session_ids)
344+
}
345+
330346
#[tauri::command]
331347
async fn fork_session(state: State<'_, AppState>, session_id: String) -> Result<String, String> {
332348
let session_id = state.client.fork_session(&session_id).await?;
@@ -750,6 +766,7 @@ pub fn run() {
750766
remove_workspace,
751767
list_workspace_sessions,
752768
list_archived_sessions,
769+
delete_archived_sessions,
753770
fork_session,
754771
archive_session,
755772
restore_session,

apps/kimi-code/src/ArchivedSessionsSettings.tsx

Lines changed: 203 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@ import {
66
RefreshCw,
77
RotateCcw,
88
Search,
9+
Trash2,
910
} from "lucide-react";
1011
import { useEffect, useMemo, useState } from "react";
1112

12-
import { listArchivedSessions, restoreSession } from "./agentRpc";
1313
import {
14+
deleteArchivedSessions,
15+
listArchivedSessions,
16+
restoreSession,
17+
} from "./agentRpc";
18+
import {
19+
archivedSessionIdsForWorkspace,
1420
archivedSessionPath,
1521
archivedSessionTitle,
1622
filterArchivedSessions,
1723
formatArchivedTime,
1824
groupArchivedSessions,
25+
removeArchivedSessions,
1926
type ArchivedSessionSort,
2027
} from "./archivedSessions";
2128
import SettingsSelect from "./components/SettingsSelect";
@@ -25,6 +32,18 @@ import { conciseError } from "./utils/errors";
2532

2633
const ALL_WORKSPACES = "all";
2734

35+
type DeleteTarget =
36+
| {
37+
kind: "session";
38+
sessionIds: string[];
39+
label: string;
40+
}
41+
| {
42+
kind: "workspace";
43+
sessionIds: string[];
44+
label: string;
45+
};
46+
2847
export default function ArchivedSessionsSettings() {
2948
const [sessions, setSessions] = useState<SessionSummary[]>([]);
3049
const [loading, setLoading] = useState(true);
@@ -33,6 +52,8 @@ export default function ArchivedSessionsSettings() {
3352
const [workspacePath, setWorkspacePath] = useState(ALL_WORKSPACES);
3453
const [sort, setSort] = useState<ArchivedSessionSort>("archived-desc");
3554
const [restoringId, setRestoringId] = useState<string>();
55+
const [deleteTarget, setDeleteTarget] = useState<DeleteTarget>();
56+
const [deleting, setDeleting] = useState(false);
3657
const [reloadKey, setReloadKey] = useState(0);
3758

3859
useEffect(() => {
@@ -54,6 +75,18 @@ export default function ArchivedSessionsSettings() {
5475
};
5576
}, [reloadKey]);
5677

78+
useEffect(() => {
79+
if (!deleteTarget) return;
80+
const handleKeyDown = (event: KeyboardEvent): void => {
81+
if (event.key !== "Escape") return;
82+
event.preventDefault();
83+
event.stopImmediatePropagation();
84+
if (!deleting) setDeleteTarget(undefined);
85+
};
86+
document.addEventListener("keydown", handleKeyDown, true);
87+
return () => document.removeEventListener("keydown", handleKeyDown, true);
88+
}, [deleteTarget, deleting]);
89+
5790
const untitledLabel = t("conversation.new");
5891
const unknownWorkspaceLabel = t("settings.archivedUnknownWorkspace");
5992
const locale = getLanguage() === "zh" ? "zh-CN" : "en";
@@ -72,6 +105,17 @@ export default function ArchivedSessionsSettings() {
72105
];
73106
}, [locale, sessions, unknownWorkspaceLabel]);
74107

108+
useEffect(() => {
109+
if (
110+
loading ||
111+
workspacePath === ALL_WORKSPACES ||
112+
workspaceOptions.some((option) => option.value === workspacePath)
113+
) {
114+
return;
115+
}
116+
setWorkspacePath(ALL_WORKSPACES);
117+
}, [loading, workspaceOptions, workspacePath]);
118+
75119
const filtered = useMemo(
76120
() =>
77121
filterArchivedSessions(sessions, {
@@ -96,23 +140,58 @@ export default function ArchivedSessionsSettings() {
96140
() => groupArchivedSessions(filtered, unknownWorkspaceLabel),
97141
[filtered, unknownWorkspaceLabel],
98142
);
143+
const mutationBusy = restoringId !== undefined || deleting;
99144

100145
const handleRestore = async (sessionId: string): Promise<void> => {
101-
if (restoringId) return;
146+
if (mutationBusy) return;
102147
setRestoringId(sessionId);
103148
setError(undefined);
104149
try {
105150
await restoreSession(sessionId);
106-
setSessions((current) =>
107-
current.filter((session) => session.id !== sessionId),
108-
);
151+
setSessions((current) => removeArchivedSessions(current, [sessionId]));
109152
} catch (restoreError) {
110153
setError(conciseError(restoreError));
111154
} finally {
112155
setRestoringId(undefined);
113156
}
114157
};
115158

159+
const requestWorkspaceDelete = (
160+
workspaceId: string,
161+
path: string,
162+
): void => {
163+
if (mutationBusy) return;
164+
const sessionIds = archivedSessionIdsForWorkspace(sessions, workspaceId);
165+
if (sessionIds.length === 0) return;
166+
setDeleteTarget({ kind: "workspace", sessionIds, label: path });
167+
};
168+
169+
const handleDelete = async (): Promise<void> => {
170+
if (!deleteTarget || mutationBusy) return;
171+
setDeleting(true);
172+
setError(undefined);
173+
try {
174+
const deletedSessionIds = await deleteArchivedSessions(
175+
deleteTarget.sessionIds,
176+
);
177+
setSessions((current) =>
178+
removeArchivedSessions(current, deletedSessionIds),
179+
);
180+
setDeleteTarget(undefined);
181+
} catch (deleteError) {
182+
const message = conciseError(deleteError);
183+
try {
184+
const items = await listArchivedSessions();
185+
setSessions(items.filter((session) => session.archived));
186+
} catch {
187+
// Preserve the original mutation error; Retry performs a full reload.
188+
}
189+
setError(message);
190+
} finally {
191+
setDeleting(false);
192+
}
193+
};
194+
116195
return (
117196
<section
118197
className="settings-archived"
@@ -196,7 +275,7 @@ export default function ArchivedSessionsSettings() {
196275
) : groups.length > 0 ? (
197276
<div className="settings-archived-list">
198277
{groups.map((group) => (
199-
<section className="settings-archived-group" key={group.path}>
278+
<section className="settings-archived-group" key={group.workspaceId}>
200279
<div className="settings-archived-workspace">
201280
<Folder size={15} aria-hidden="true" />
202281
<span title={group.path}>{group.path}</span>
@@ -205,36 +284,70 @@ export default function ArchivedSessionsSettings() {
205284
count: group.sessions.length,
206285
})}
207286
</small>
287+
<button
288+
className="settings-archived-delete-all"
289+
type="button"
290+
disabled={mutationBusy}
291+
aria-label={t("settings.archivedDeleteAllLabel", {
292+
path: group.path,
293+
})}
294+
onClick={() =>
295+
requestWorkspaceDelete(group.workspaceId, group.path)
296+
}
297+
>
298+
<Trash2 size={13} />
299+
{t("settings.archivedDeleteAll")}
300+
</button>
208301
</div>
209302
<div className="settings-archived-card">
210-
{group.sessions.map((session) => (
211-
<div className="settings-archived-row" key={session.id}>
212-
<div className="settings-archived-meta">
213-
<strong title={archivedSessionTitle(session, untitledLabel)}>
214-
{archivedSessionTitle(session, untitledLabel)}
215-
</strong>
216-
<span>
217-
{t("settings.archivedAt", {
218-
time: formatArchivedTime(session.updatedAt),
219-
})}
220-
</span>
303+
{group.sessions.map((session) => {
304+
const title = archivedSessionTitle(session, untitledLabel);
305+
return (
306+
<div className="settings-archived-row" key={session.id}>
307+
<div className="settings-archived-meta">
308+
<strong title={title}>{title}</strong>
309+
<span>
310+
{t("settings.archivedAt", {
311+
time: formatArchivedTime(session.updatedAt),
312+
})}
313+
</span>
314+
</div>
315+
<div className="settings-archived-actions">
316+
<button
317+
className="settings-archived-restore"
318+
type="button"
319+
disabled={mutationBusy}
320+
onClick={() => void handleRestore(session.id)}
321+
>
322+
<RotateCcw
323+
size={14}
324+
className={
325+
restoringId === session.id ? "spinning" : undefined
326+
}
327+
/>
328+
{restoringId === session.id
329+
? t("settings.archivedRestoring")
330+
: t("settings.archivedRestore")}
331+
</button>
332+
<button
333+
className="settings-archived-delete"
334+
type="button"
335+
disabled={mutationBusy}
336+
onClick={() =>
337+
setDeleteTarget({
338+
kind: "session",
339+
sessionIds: [session.id],
340+
label: title,
341+
})
342+
}
343+
>
344+
<Trash2 size={14} />
345+
{t("settings.archivedDelete")}
346+
</button>
347+
</div>
221348
</div>
222-
<button
223-
className="settings-archived-restore"
224-
type="button"
225-
disabled={restoringId !== undefined}
226-
onClick={() => void handleRestore(session.id)}
227-
>
228-
<RotateCcw
229-
size={14}
230-
className={restoringId === session.id ? "spinning" : undefined}
231-
/>
232-
{restoringId === session.id
233-
? t("settings.archivedRestoring")
234-
: t("settings.archivedRestore")}
235-
</button>
236-
</div>
237-
))}
349+
);
350+
})}
238351
</div>
239352
</section>
240353
))}
@@ -246,6 +359,63 @@ export default function ArchivedSessionsSettings() {
246359
: t("settings.archivedNoMatch")}
247360
</div>
248361
)}
362+
363+
{deleteTarget && (
364+
<div
365+
className="settings-archived-confirm-backdrop"
366+
onMouseDown={(event) => {
367+
if (event.target === event.currentTarget && !deleting) {
368+
setDeleteTarget(undefined);
369+
}
370+
}}
371+
>
372+
<div
373+
className="settings-archived-confirm"
374+
role="alertdialog"
375+
aria-modal="true"
376+
aria-labelledby="archived-delete-confirm-title"
377+
>
378+
<span className="settings-archived-confirm-icon">
379+
<Trash2 size={19} />
380+
</span>
381+
<h4 id="archived-delete-confirm-title">
382+
{t(
383+
deleteTarget.kind === "session"
384+
? "settings.archivedDeleteSessionTitle"
385+
: "settings.archivedDeleteWorkspaceTitle",
386+
)}
387+
</h4>
388+
<p>
389+
{deleteTarget.kind === "session"
390+
? t("settings.archivedDeleteSessionCopy", {
391+
title: deleteTarget.label,
392+
})
393+
: t("settings.archivedDeleteWorkspaceCopy", {
394+
path: deleteTarget.label,
395+
count: deleteTarget.sessionIds.length,
396+
})}
397+
</p>
398+
<div className="settings-archived-confirm-actions">
399+
<button
400+
type="button"
401+
autoFocus
402+
disabled={deleting}
403+
onClick={() => setDeleteTarget(undefined)}
404+
>
405+
{t("common.cancel")}
406+
</button>
407+
<button
408+
className="danger"
409+
type="button"
410+
disabled={deleting}
411+
onClick={() => void handleDelete()}
412+
>
413+
{deleting ? t("settings.archivedDeleting") : t("settings.archivedConfirmDelete")}
414+
</button>
415+
</div>
416+
</div>
417+
</div>
418+
)}
249419
</section>
250420
);
251421
}

apps/kimi-code/src/agentRpc.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,12 @@ export function listArchivedSessions(): Promise<SessionSummary[]> {
309309
return invoke<SessionSummary[]>("list_archived_sessions");
310310
}
311311

312+
export function deleteArchivedSessions(
313+
sessionIds: string[],
314+
): Promise<string[]> {
315+
return invoke<string[]>("delete_archived_sessions", { sessionIds });
316+
}
317+
312318
export function forkSession(sessionId: string): Promise<string> {
313319
return invoke<string>("fork_session", { sessionId });
314320
}

0 commit comments

Comments
 (0)