Skip to content

Commit 2323fc1

Browse files
committed
feat(settings): add archived session management
1 parent 40d3948 commit 2323fc1

11 files changed

Lines changed: 919 additions & 5 deletions

File tree

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,11 @@ async fn list_workspace_sessions(
322322
state.client.list_workspace_sessions(&workspace_id).await
323323
}
324324

325+
#[tauri::command]
326+
async fn list_archived_sessions(state: State<'_, AppState>) -> Result<Vec<SessionSummary>, String> {
327+
state.client.list_archived_sessions().await
328+
}
329+
325330
#[tauri::command]
326331
async fn fork_session(state: State<'_, AppState>, session_id: String) -> Result<String, String> {
327332
let session_id = state.client.fork_session(&session_id).await?;
@@ -342,6 +347,20 @@ async fn archive_session(state: State<'_, AppState>, session_id: String) -> Resu
342347
Ok(())
343348
}
344349

350+
#[tauri::command]
351+
async fn restore_session(
352+
state: State<'_, AppState>,
353+
session_id: String,
354+
) -> Result<SessionSummary, String> {
355+
let session = state.client.restore_session(&session_id).await?;
356+
state
357+
.web_server
358+
.desktop_state_changed(DesktopStateChange::SessionRestored {
359+
session_id: session.id.clone(),
360+
});
361+
Ok(session)
362+
}
363+
345364
#[tauri::command]
346365
async fn prepare_session(
347366
state: State<'_, AppState>,
@@ -730,8 +749,10 @@ pub fn run() {
730749
create_or_touch_workspace,
731750
remove_workspace,
732751
list_workspace_sessions,
752+
list_archived_sessions,
733753
fork_session,
734754
archive_session,
755+
restore_session,
735756
prepare_session,
736757
conversation_context_usage,
737758
list_conversation_messages,
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import {
2+
CalendarDays,
3+
Clock3,
4+
Folder,
5+
ListOrdered,
6+
RefreshCw,
7+
RotateCcw,
8+
Search,
9+
} from "lucide-react";
10+
import { useEffect, useMemo, useState } from "react";
11+
12+
import { listArchivedSessions, restoreSession } from "./agentRpc";
13+
import {
14+
archivedSessionPath,
15+
archivedSessionTitle,
16+
filterArchivedSessions,
17+
formatArchivedTime,
18+
groupArchivedSessions,
19+
type ArchivedSessionSort,
20+
} from "./archivedSessions";
21+
import SettingsSelect from "./components/SettingsSelect";
22+
import { getLanguage, t } from "./i18n";
23+
import type { SessionSummary } from "./types";
24+
import { conciseError } from "./utils/errors";
25+
26+
const ALL_WORKSPACES = "all";
27+
28+
export default function ArchivedSessionsSettings() {
29+
const [sessions, setSessions] = useState<SessionSummary[]>([]);
30+
const [loading, setLoading] = useState(true);
31+
const [error, setError] = useState<string>();
32+
const [query, setQuery] = useState("");
33+
const [workspacePath, setWorkspacePath] = useState(ALL_WORKSPACES);
34+
const [sort, setSort] = useState<ArchivedSessionSort>("archived-desc");
35+
const [restoringId, setRestoringId] = useState<string>();
36+
const [reloadKey, setReloadKey] = useState(0);
37+
38+
useEffect(() => {
39+
let active = true;
40+
setLoading(true);
41+
setError(undefined);
42+
void listArchivedSessions()
43+
.then((items) => {
44+
if (active) setSessions(items.filter((session) => session.archived));
45+
})
46+
.catch((loadError) => {
47+
if (active) setError(conciseError(loadError));
48+
})
49+
.finally(() => {
50+
if (active) setLoading(false);
51+
});
52+
return () => {
53+
active = false;
54+
};
55+
}, [reloadKey]);
56+
57+
const untitledLabel = t("conversation.new");
58+
const unknownWorkspaceLabel = t("settings.archivedUnknownWorkspace");
59+
const locale = getLanguage() === "zh" ? "zh-CN" : "en";
60+
61+
const workspaceOptions = useMemo(() => {
62+
const paths = new Set(
63+
sessions.map((session) =>
64+
archivedSessionPath(session, unknownWorkspaceLabel),
65+
),
66+
);
67+
return [
68+
{ value: ALL_WORKSPACES, label: t("settings.archivedAllWorkspaces") },
69+
...[...paths]
70+
.sort((left, right) => left.localeCompare(right, locale))
71+
.map((path) => ({ value: path, label: path })),
72+
];
73+
}, [locale, sessions, unknownWorkspaceLabel]);
74+
75+
const filtered = useMemo(
76+
() =>
77+
filterArchivedSessions(sessions, {
78+
query,
79+
workspacePath,
80+
sort,
81+
untitledLabel,
82+
unknownWorkspaceLabel,
83+
locale,
84+
}),
85+
[
86+
locale,
87+
query,
88+
sessions,
89+
sort,
90+
unknownWorkspaceLabel,
91+
untitledLabel,
92+
workspacePath,
93+
],
94+
);
95+
const groups = useMemo(
96+
() => groupArchivedSessions(filtered, unknownWorkspaceLabel),
97+
[filtered, unknownWorkspaceLabel],
98+
);
99+
100+
const handleRestore = async (sessionId: string): Promise<void> => {
101+
if (restoringId) return;
102+
setRestoringId(sessionId);
103+
setError(undefined);
104+
try {
105+
await restoreSession(sessionId);
106+
setSessions((current) =>
107+
current.filter((session) => session.id !== sessionId),
108+
);
109+
} catch (restoreError) {
110+
setError(conciseError(restoreError));
111+
} finally {
112+
setRestoringId(undefined);
113+
}
114+
};
115+
116+
return (
117+
<section
118+
className="settings-archived"
119+
aria-labelledby="archived-sessions-heading"
120+
>
121+
<header className="settings-archived-header">
122+
<h3 id="archived-sessions-heading">
123+
{t("settings.archivedTitle")}
124+
</h3>
125+
<p>{t("settings.archivedDescription")}</p>
126+
</header>
127+
128+
<div className="settings-archived-toolbar">
129+
<label className="settings-archived-search">
130+
<Search size={15} aria-hidden="true" />
131+
<input
132+
type="search"
133+
value={query}
134+
placeholder={t("settings.archivedSearch")}
135+
aria-label={t("settings.archivedSearch")}
136+
onChange={(event) => setQuery(event.target.value)}
137+
/>
138+
</label>
139+
<SettingsSelect
140+
className="settings-archived-workspace-select"
141+
value={workspacePath}
142+
options={workspaceOptions}
143+
ariaLabel={t("settings.archivedAllWorkspaces")}
144+
onChange={setWorkspacePath}
145+
/>
146+
</div>
147+
148+
<div
149+
className="settings-segmented settings-archived-sort"
150+
role="group"
151+
aria-label={t("settings.archivedSortLabel")}
152+
>
153+
<button
154+
className={sort === "archived-desc" ? "active" : ""}
155+
type="button"
156+
aria-pressed={sort === "archived-desc"}
157+
onClick={() => setSort("archived-desc")}
158+
>
159+
<Clock3 size={14} />
160+
{t("settings.archivedSortArchived")}
161+
</button>
162+
<button
163+
className={sort === "created-desc" ? "active" : ""}
164+
type="button"
165+
aria-pressed={sort === "created-desc"}
166+
onClick={() => setSort("created-desc")}
167+
>
168+
<CalendarDays size={14} />
169+
{t("settings.archivedSortCreated")}
170+
</button>
171+
<button
172+
className={sort === "name-asc" ? "active" : ""}
173+
type="button"
174+
aria-pressed={sort === "name-asc"}
175+
onClick={() => setSort("name-asc")}
176+
>
177+
<ListOrdered size={14} />
178+
{t("settings.archivedSortName")}
179+
</button>
180+
</div>
181+
182+
{error && (
183+
<div className="settings-archived-error" role="alert">
184+
<span>{error}</span>
185+
<button type="button" onClick={() => setReloadKey((value) => value + 1)}>
186+
<RefreshCw size={13} />
187+
{t("common.retry")}
188+
</button>
189+
</div>
190+
)}
191+
192+
{loading ? (
193+
<div className="settings-archived-empty" role="status">
194+
{t("settings.archivedLoading")}
195+
</div>
196+
) : groups.length > 0 ? (
197+
<div className="settings-archived-list">
198+
{groups.map((group) => (
199+
<section className="settings-archived-group" key={group.path}>
200+
<div className="settings-archived-workspace">
201+
<Folder size={15} aria-hidden="true" />
202+
<span title={group.path}>{group.path}</span>
203+
<small>
204+
{t("settings.archivedCount", {
205+
count: group.sessions.length,
206+
})}
207+
</small>
208+
</div>
209+
<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>
221+
</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+
))}
238+
</div>
239+
</section>
240+
))}
241+
</div>
242+
) : (
243+
<div className="settings-archived-empty">
244+
{sessions.length === 0
245+
? t("settings.archivedEmpty")
246+
: t("settings.archivedNoMatch")}
247+
</div>
248+
)}
249+
</section>
250+
);
251+
}

apps/kimi-code/src/SettingsDialog.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
22
import { relaunch } from "@tauri-apps/plugin-process";
33
import { check, type Update } from "@tauri-apps/plugin-updater";
4-
import { Copy, ExternalLink, Globe, Info, Puzzle, RefreshCw, SlidersHorizontal, User, X, Zap } from "lucide-react";
4+
import { Archive, Copy, ExternalLink, Globe, Info, Puzzle, RefreshCw, SlidersHorizontal, User, X, Zap } from "lucide-react";
55

66
import type { ColorScheme } from "./appearance";
77
import AccountSettings from "./AccountSettings";
8+
import ArchivedSessionsSettings from "./ArchivedSessionsSettings";
89
import SettingsSelect from "./components/SettingsSelect";
910
import { LANGUAGE_OPTIONS, t, type Language } from "./i18n";
1011
import PluginSettings from "./PluginSettings";
1112
import ProviderSettings from "./ProviderSettings";
1213
import { invoke, isDesktop, openExternalUrl } from "./transport";
1314
import type { AccountUsage, AuthStatus, ManagedUserInfo } from "./types";
1415

15-
type SettingsTab = "general" | "account" | "providers" | "plugins" | "web" | "about";
16+
type SettingsTab = "general" | "account" | "providers" | "plugins" | "web" | "archived" | "about";
1617
type WebServerListenScope = "local" | "global";
1718

1819
interface WebServerStatus {
@@ -397,6 +398,15 @@ export default function SettingsDialog({
397398
{t("settings.tabWeb")}
398399
</button>
399400
)}
401+
<button
402+
className={`settings-tab ${activeTab === "archived" ? "active" : ""}`}
403+
type="button"
404+
aria-current={activeTab === "archived" ? "page" : undefined}
405+
onClick={() => setActiveTab("archived")}
406+
>
407+
<Archive size={15} />
408+
{t("settings.tabArchived")}
409+
</button>
400410
<button
401411
className={`settings-tab ${activeTab === "about" ? "active" : ""}`}
402412
type="button"
@@ -516,6 +526,8 @@ export default function SettingsDialog({
516526
<ProviderSettings onChanged={onProvidersChanged} />
517527
) : activeTab === "plugins" ? (
518528
<PluginSettings onChanged={onPluginsChanged} />
529+
) : activeTab === "archived" ? (
530+
<ArchivedSessionsSettings />
519531
) : activeTab === "web" ? (
520532
<section
521533
className="settings-section settings-web"

apps/kimi-code/src/agentRpc.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,10 @@ export function listWorkspaceSessions(
305305
return invoke<SessionSummary[]>("list_workspace_sessions", { workspaceId });
306306
}
307307

308+
export function listArchivedSessions(): Promise<SessionSummary[]> {
309+
return invoke<SessionSummary[]>("list_archived_sessions");
310+
}
311+
308312
export function forkSession(sessionId: string): Promise<string> {
309313
return invoke<string>("fork_session", { sessionId });
310314
}
@@ -313,6 +317,10 @@ export function archiveSession(sessionId: string): Promise<void> {
313317
return invoke<void>("archive_session", { sessionId });
314318
}
315319

320+
export function restoreSession(sessionId: string): Promise<SessionSummary> {
321+
return invoke<SessionSummary>("restore_session", { sessionId });
322+
}
323+
316324
export function listSkills(sessionId: string): Promise<SkillDescriptor[]> {
317325
return invoke<SkillDescriptor[]>("list_skills", { sessionId });
318326
}

0 commit comments

Comments
 (0)