From 326237c421bef303c493d662fe71c3275b83d1ef Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Tue, 23 Jun 2026 21:17:05 +0800 Subject: [PATCH 1/8] feat(server): support restoring and listing archived sessions - add a `:restore` session action that clears the archived flag in state.json and returns the restored session - add an `archived_only` list query param, mutually exclusive with `include_archive`, that post-filters to archived sessions - keep the implementation in the server layer as a temporary measure until agent-core exposes restore natively --- .changeset/restore-archived-sessions.md | 5 + packages/server/src/lib/sessionArchive.ts | 109 ++++++++++++++++++++++ packages/server/src/routes/sessions.ts | 46 ++++++++- packages/server/test/sessions.e2e.test.ts | 100 ++++++++++++++++++++ 4 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 .changeset/restore-archived-sessions.md create mode 100644 packages/server/src/lib/sessionArchive.ts diff --git a/.changeset/restore-archived-sessions.md b/.changeset/restore-archived-sessions.md new file mode 100644 index 0000000000..2fd35cbf90 --- /dev/null +++ b/.changeset/restore-archived-sessions.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Add server APIs to restore archived sessions and list only archived sessions. diff --git a/packages/server/src/lib/sessionArchive.ts b/packages/server/src/lib/sessionArchive.ts new file mode 100644 index 0000000000..c784662377 --- /dev/null +++ b/packages/server/src/lib/sessionArchive.ts @@ -0,0 +1,109 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { basename, isAbsolute, join, relative, resolve } from 'node:path'; + +import { SessionNotFoundError } from '@moonshot-ai/agent-core'; + +/** + * Temporary server-side session restore. + * + * TODO: remove once `@moonshot-ai/agent-core` exposes `ISessionService.restore` + * natively. At that point the `:restore` route should delegate to the service + * instead of rewriting `state.json` here. + * + * Archive is a boolean flag (`archived`) persisted in each session's + * `/state.json`. agent-core's `SessionStore` can set it to `true` + * (`archive`) but has no inverse; while agent-core is being refactored we flip + * it back from the server by: + * 1. reading `/session_index.jsonl` to resolve `sessionId -> sessionDir`; + * 2. validating the resolved dir is inside `/sessions` (defense + * against a tampered index); + * 3. read-modify-write `state.json` with `archived: false`. + * + * This mirrors `SessionStore.archive` and publishes no event (same as archive). + * The query read-model rebuilds from the store on every call, so a restored + * session shows up in subsequent lists with no extra invalidation. + */ + +interface SessionIndexEntry { + readonly sessionId: string; + readonly sessionDir: string; + readonly workDir: string; +} + +export async function restoreArchivedSession(homeDir: string, sessionId: string): Promise { + const sessionDir = await findSessionDir(homeDir, sessionId); + if (sessionDir === undefined) { + throw new SessionNotFoundError(sessionId); + } + + const statePath = join(sessionDir, 'state.json'); + let parsed: unknown; + try { + parsed = JSON.parse(await readFile(statePath, 'utf-8')) as unknown; + } catch { + throw new SessionNotFoundError(sessionId); + } + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new SessionNotFoundError(sessionId); + } + + const next: Record = { + ...(parsed as Record), + archived: false, + updatedAt: new Date().toISOString(), + }; + await writeFile(statePath, `${JSON.stringify(next, null, 2)}\n`, 'utf-8'); +} + +async function findSessionDir(homeDir: string, sessionId: string): Promise { + const indexPath = join(homeDir, 'session_index.jsonl'); + let raw: string; + try { + raw = await readFile(indexPath, 'utf-8'); + } catch { + return undefined; + } + + const sessionsDir = join(homeDir, 'sessions'); + let found: string | undefined; + for (const line of raw.split(/\r?\n/)) { + const trimmed = line.trim(); + if (trimmed === '') continue; + const entry = parseIndexLine(trimmed); + if (entry === undefined || entry.sessionId !== sessionId) continue; + const sessionDir = resolve(entry.sessionDir); + if (!isAbsolute(entry.sessionDir)) continue; + if (!isPathInside(sessionsDir, sessionDir)) continue; + if (basename(sessionDir) !== entry.sessionId) continue; + // Last valid line wins, matching `readSessionIndex`'s Map semantics. + found = sessionDir; + } + return found; +} + +function parseIndexLine(line: string): SessionIndexEntry | undefined { + try { + const parsed = JSON.parse(line) as unknown; + if (typeof parsed !== 'object' || parsed === null) return undefined; + const entry = parsed as Partial; + if ( + typeof entry.sessionId !== 'string' || + typeof entry.sessionDir !== 'string' || + typeof entry.workDir !== 'string' + ) { + return undefined; + } + return { + sessionId: entry.sessionId, + sessionDir: entry.sessionDir, + workDir: entry.workDir, + }; + } catch { + return undefined; + } +} + +function isPathInside(parent: string, child: string): boolean { + const rel = relative(resolve(parent), resolve(child)); + return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); +} diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index 2c69bdb807..7f118216ba 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -23,11 +23,12 @@ import { workspaceIdSchema, type Event, } from '@moonshot-ai/protocol'; -import { IPromptService, ISessionService, SessionNotFoundError, SessionUndoUnavailableError, ErrorCodes, KimiError, IWorkspaceRegistry, WorkspaceNotFoundError, IEventService, type IInstantiationService, type SessionClientTelemetry } from '@moonshot-ai/agent-core'; +import { IPromptService, ISessionService, SessionNotFoundError, SessionUndoUnavailableError, ErrorCodes, KimiError, IEnvironmentService, IWorkspaceRegistry, WorkspaceNotFoundError, IEventService, type IInstantiationService, type SessionClientTelemetry } from '@moonshot-ai/agent-core'; import { z } from 'zod'; import { errEnvelope, okEnvelope } from '../envelope'; +import { restoreArchivedSession } from '../lib/sessionArchive'; import { defineRoute } from '../middleware/defineRoute'; import { parseActionSuffix } from './action-suffix'; @@ -84,6 +85,7 @@ const sessionsListQueryCoercion = z status: sessionStatusSchema.optional(), include_archive: booleanQueryParam, exclude_empty: booleanQueryParam, + archived_only: booleanQueryParam, workspace_id: workspaceIdSchema.optional(), }) @@ -96,6 +98,14 @@ const sessionsListQueryCoercion = z params: { code: ErrorCode.VALIDATION_FAILED }, }); } + if (value.archived_only === true && value.include_archive === true) { + ctx.addIssue({ + code: 'custom', + message: 'archived_only and include_archive are mutually exclusive', + path: ['archived_only'], + params: { code: ErrorCode.VALIDATION_FAILED }, + }); + } }); const sessionChildrenListQueryCoercion = z @@ -267,12 +277,15 @@ export function registerSessionsRoutes( async (req, reply) => { try { const raw = req.query; + const archivedOnly = raw.archived_only === true; const baseQuery = { before_id: raw.before_id, after_id: raw.after_id, page_size: raw.page_size, status: raw.status, - includeArchive: raw.include_archive, + // archived_only needs the mixed set so we can post-filter to + // archived-only below (temporary server-side filter; see comment). + includeArchive: archivedOnly ? true : raw.include_archive, excludeEmpty: raw.exclude_empty, }; let query; @@ -295,6 +308,23 @@ export function registerSessionsRoutes( query = baseQuery; } const page = await ix.invokeFunction((a) => a.get(ISessionService).list(query)); + if (archivedOnly) { + // Temporary server-side archived-only filter. The post-filter happens + // after pagination, so `has_more` still reflects the mixed set: a page + // may come back short (or empty) while `has_more` is true, and clients + // must keep paging. No data is lost. Remove once agent-core supports + // archived-only natively. + reply.send( + okEnvelope( + { + items: page.items.filter((session) => session.archived === true), + has_more: page.has_more, + }, + req.id, + ), + ); + return; + } reply.send(okEnvelope(page, req.id)); } catch (err) { sendMappedError(reply, req.id, err); @@ -427,7 +457,7 @@ export function registerSessionsRoutes( const { tail } = req.params; const parsed = parseActionSuffix({ tail, - allowedActions: ['fork', 'compact', 'undo', 'abort', 'btw', 'archive'] as const, + allowedActions: ['fork', 'compact', 'undo', 'abort', 'btw', 'archive', 'restore'] as const, resourceLabel: 'session', }); if (parsed.kind !== 'action') { @@ -485,6 +515,16 @@ export function registerSessionsRoutes( return; } + if (parsed.action === 'restore') { + const homeDir = ix.invokeFunction((a) => a.get(IEnvironmentService)).homeDir; + await restoreArchivedSession(homeDir, parsed.id); + const session = await ix.invokeFunction((a) => + a.get(ISessionService).get(parsed.id), + ); + reply.send(okEnvelope(session, req.id)); + return; + } + const body = undoSessionRequestSchema.parse(req.body); const result = await ix.invokeFunction((a) => a.get(ISessionService).undo(parsed.id, body), diff --git a/packages/server/test/sessions.e2e.test.ts b/packages/server/test/sessions.e2e.test.ts index 46d662d792..63ea2abae7 100644 --- a/packages/server/test/sessions.e2e.test.ts +++ b/packages/server/test/sessions.e2e.test.ts @@ -856,3 +856,103 @@ describe('POST /api/v1/sessions/{session_id}:archive — archive', () => { expect(env.code).toBe(40401); }); }); + +describe('GET /api/v1/sessions?archived_only — archived-only list', () => { + it('returns only archived sessions and hides them from the default list', async () => { + const r = await bootDaemon(); + const cwd = join(tmpDir, 'workspace-archived-only'); + const created = envelopeOf<{ id: string }>( + (await appOf(r).inject({ + method: 'POST', + url: '/api/v1/sessions', + payload: { metadata: { cwd } }, + })).json(), + ).data!; + + await appOf(r).inject({ + method: 'POST', + url: `/api/v1/sessions/${created.id}:archive`, + payload: {}, + }); + + const defaultList = envelopeOf<{ items: Array<{ id: string }> }>( + (await appOf(r).inject({ method: 'GET', url: '/api/v1/sessions' })).json(), + ); + expect(defaultList.data!.items.find((s) => s.id === created.id)).toBeUndefined(); + + const archivedOnly = envelopeOf<{ items: Array<{ id: string; archived?: boolean }>; has_more: boolean }>( + (await appOf(r).inject({ method: 'GET', url: '/api/v1/sessions?archived_only=true' })).json(), + ); + expect(archivedOnly.code).toBe(0); + const listed = archivedOnly.data!.items.find((s) => s.id === created.id); + expect(listed).toBeDefined(); + expect(listed!.archived).toBe(true); + // No live session should leak into the archived-only view. + expect(archivedOnly.data!.items.every((s) => s.archived === true)).toBe(true); + }); + + it('rejects archived_only together with include_archive', async () => { + const r = await bootDaemon(); + const res = await appOf(r).inject({ + method: 'GET', + url: '/api/v1/sessions?archived_only=true&include_archive=true', + }); + const env = envelopeOf(res.json()); + expect(env.code).toBe(40001); + }); +}); + +describe('POST /api/v1/sessions/{session_id}:restore — restore', () => { + it('restores an archived session so it reappears in the default list', async () => { + const r = await bootDaemon(); + const cwd = join(tmpDir, 'workspace-restore'); + const created = envelopeOf<{ id: string }>( + (await appOf(r).inject({ + method: 'POST', + url: '/api/v1/sessions', + payload: { metadata: { cwd } }, + })).json(), + ).data!; + + await appOf(r).inject({ + method: 'POST', + url: `/api/v1/sessions/${created.id}:archive`, + payload: {}, + }); + + const restoreRes = await appOf(r).inject({ + method: 'POST', + url: `/api/v1/sessions/${created.id}:restore`, + payload: {}, + }); + const restoreEnv = envelopeOf(restoreRes.json()); + expect(restoreEnv.code).toBe(0); + const session = sessionSchema.parse(restoreEnv.data); + expect(session.id).toBe(created.id); + expect(session.archived).toBe(false); + + const defaultList = envelopeOf<{ items: Array<{ id: string; archived?: boolean }> }>( + (await appOf(r).inject({ method: 'GET', url: '/api/v1/sessions' })).json(), + ); + const relisted = defaultList.data!.items.find((s) => s.id === created.id); + expect(relisted).toBeDefined(); + expect(relisted!.archived).toBe(false); + + const getRes = envelopeOf( + (await appOf(r).inject({ method: 'GET', url: `/api/v1/sessions/${created.id}` })).json(), + ); + expect(getRes.code).toBe(0); + expect(sessionSchema.parse(getRes.data).archived).toBe(false); + }); + + it('returns 40401 for unknown id', async () => { + const r = await bootDaemon(); + const res = await appOf(r).inject({ + method: 'POST', + url: '/api/v1/sessions/sess_missing:restore', + payload: {}, + }); + const env = envelopeOf(res.json()); + expect(env.code).toBe(40401); + }); +}); From 9f46512734adcca940ec3ff9124f94f3254d4da8 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 24 Jun 2026 19:27:53 +0800 Subject: [PATCH 2/8] fix(server): paginate archived-only sessions before response --- packages/server/src/routes/sessions.ts | 96 +++++++++++++++++------ packages/server/test/sessions.e2e.test.ts | 64 +++++++++++++++ 2 files changed, 135 insertions(+), 25 deletions(-) diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index 7f118216ba..94b1a5298c 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -167,6 +167,56 @@ function headerString(headers: Record, key: string): string | u return trimmed.length === 0 ? undefined : trimmed; } +const DEFAULT_SESSION_LIST_PAGE_SIZE = 20; +const MAX_SESSION_LIST_PAGE_SIZE = 100; + +type SessionListRequest = Parameters[0]; +type SessionListPage = Awaited>; +type SessionListItem = SessionListPage['items'][number]; +type SessionListBaseQuery = Omit; +type SessionListCursor = Pick; + +function normalizeSessionListPageSize(cursor: SessionListCursor): number { + const requested = cursor.page_size ?? DEFAULT_SESSION_LIST_PAGE_SIZE; + return Math.min(Math.max(requested, 1), MAX_SESSION_LIST_PAGE_SIZE); +} + +async function listSessionsWithRouteFilter( + fetchPage: (query: SessionListRequest) => Promise, + baseQuery: SessionListBaseQuery, + cursor: SessionListCursor, + predicate: (session: SessionListItem) => boolean, +): Promise { + const targetSize = normalizeSessionListPageSize(cursor); + const matches: SessionListItem[] = []; + let beforeId = cursor.before_id; + let afterId = cursor.after_id; + let coreHasMore = true; + + while (matches.length <= targetSize && coreHasMore) { + const page = await fetchPage({ + ...baseQuery, + before_id: beforeId, + after_id: afterId, + page_size: MAX_SESSION_LIST_PAGE_SIZE, + }); + if (page.items.length === 0) break; + + matches.push(...page.items.filter(predicate)); + coreHasMore = page.has_more; + + const nextBeforeId = page.items[page.items.length - 1]?.id; + if (!coreHasMore || nextBeforeId === undefined || nextBeforeId === beforeId) break; + beforeId = nextBeforeId; + afterId = undefined; + } + + return { + items: matches.slice(0, targetSize), + has_more: matches.length > targetSize, + }; +} + export function registerSessionsRoutes( app: SessionRouteHost, ix: IInstantiationService, @@ -278,17 +328,11 @@ export function registerSessionsRoutes( try { const raw = req.query; const archivedOnly = raw.archived_only === true; - const baseQuery = { - before_id: raw.before_id, - after_id: raw.after_id, - page_size: raw.page_size, - status: raw.status, - // archived_only needs the mixed set so we can post-filter to - // archived-only below (temporary server-side filter; see comment). + const status = raw.status; + let baseQuery: SessionListBaseQuery = { includeArchive: archivedOnly ? true : raw.include_archive, excludeEmpty: raw.exclude_empty, }; - let query; if (raw.workspace_id !== undefined) { const registry = ix.invokeFunction((a) => a.get(IWorkspaceRegistry)); let root: string; @@ -303,28 +347,30 @@ export function registerSessionsRoutes( } throw err; } - query = { ...baseQuery, workDir: root }; - } else { - query = baseQuery; + baseQuery = { ...baseQuery, workDir: root }; } - const page = await ix.invokeFunction((a) => a.get(ISessionService).list(query)); + if (archivedOnly) { - // Temporary server-side archived-only filter. The post-filter happens - // after pagination, so `has_more` still reflects the mixed set: a page - // may come back short (or empty) while `has_more` is true, and clients - // must keep paging. No data is lost. Remove once agent-core supports - // archived-only natively. - reply.send( - okEnvelope( - { - items: page.items.filter((session) => session.archived === true), - has_more: page.has_more, - }, - req.id, - ), + const page = await listSessionsWithRouteFilter( + (query) => ix.invokeFunction((a) => a.get(ISessionService).list(query)), + baseQuery, + raw, + (session) => + session.archived === true && (status === undefined || session.status === status), ); + reply.send(okEnvelope(page, req.id)); return; } + + const page = await ix.invokeFunction((a) => + a.get(ISessionService).list({ + ...baseQuery, + before_id: raw.before_id, + after_id: raw.after_id, + page_size: raw.page_size, + status, + }), + ); reply.send(okEnvelope(page, req.id)); } catch (err) { sendMappedError(reply, req.id, err); diff --git a/packages/server/test/sessions.e2e.test.ts b/packages/server/test/sessions.e2e.test.ts index 63ea2abae7..103bd22f8b 100644 --- a/packages/server/test/sessions.e2e.test.ts +++ b/packages/server/test/sessions.e2e.test.ts @@ -891,6 +891,70 @@ describe('GET /api/v1/sessions?archived_only — archived-only list', () => { expect(archivedOnly.data!.items.every((s) => s.archived === true)).toBe(true); }); + it('paginates archived_only without returning empty filtered pages', async () => { + const r = await bootDaemon(); + const cwd = join(tmpDir, 'workspace-archived-only-pagination'); + + const archivedOlder = envelopeOf<{ id: string }>( + (await appOf(r).inject({ + method: 'POST', + url: '/api/v1/sessions', + payload: { metadata: { cwd } }, + })).json(), + ).data!; + await appOf(r).inject({ + method: 'POST', + url: `/api/v1/sessions/${archivedOlder.id}:archive`, + payload: {}, + }); + + const archivedNewer = envelopeOf<{ id: string }>( + (await appOf(r).inject({ + method: 'POST', + url: '/api/v1/sessions', + payload: { metadata: { cwd } }, + })).json(), + ).data!; + await appOf(r).inject({ + method: 'POST', + url: `/api/v1/sessions/${archivedNewer.id}:archive`, + payload: {}, + }); + + await appOf(r).inject({ + method: 'POST', + url: '/api/v1/sessions', + payload: { metadata: { cwd } }, + }); + await appOf(r).inject({ + method: 'POST', + url: '/api/v1/sessions', + payload: { metadata: { cwd } }, + }); + + const first = envelopeOf<{ items: Array<{ id: string; archived?: boolean }>; has_more: boolean }>( + (await appOf(r).inject({ + method: 'GET', + url: '/api/v1/sessions?archived_only=true&page_size=1', + })).json(), + ); + expect(first.code).toBe(0); + expect(first.data!.items).toHaveLength(1); + expect(first.data!.items[0]).toMatchObject({ id: archivedNewer.id, archived: true }); + expect(first.data!.has_more).toBe(true); + + const second = envelopeOf<{ items: Array<{ id: string; archived?: boolean }>; has_more: boolean }>( + (await appOf(r).inject({ + method: 'GET', + url: `/api/v1/sessions?archived_only=true&page_size=1&before_id=${archivedNewer.id}`, + })).json(), + ); + expect(second.code).toBe(0); + expect(second.data!.items).toHaveLength(1); + expect(second.data!.items[0]).toMatchObject({ id: archivedOlder.id, archived: true }); + expect(second.data!.has_more).toBe(false); + }); + it('rejects archived_only together with include_archive', async () => { const r = await bootDaemon(); const res = await appOf(r).inject({ From d011fcec53a23c5d3934498015ae2fef8a28bc4c Mon Sep 17 00:00:00 2001 From: qer Date: Mon, 6 Jul 2026 15:13:03 +0800 Subject: [PATCH 3/8] feat(web): add archived sessions page in Settings Browse, search, filter by workspace, sort, and restore archived sessions from a new Archived tab in Settings, backed by the server archived_only list and :restore action. --- .changeset/web-archived-sessions-settings.md | 5 + apps/kimi-web/src/api/daemon/client.ts | 12 ++ apps/kimi-web/src/api/types.ts | 3 +- .../components/settings/SettingsDialog.vue | 203 +++++++++++++++++- .../composables/client/useWorkspaceState.ts | 26 +++ .../src/composables/useKimiWebClient.ts | 2 + apps/kimi-web/src/i18n/locales/en/settings.ts | 16 ++ apps/kimi-web/src/i18n/locales/zh/settings.ts | 16 ++ 8 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 .changeset/web-archived-sessions-settings.md diff --git a/.changeset/web-archived-sessions-settings.md b/.changeset/web-archived-sessions-settings.md new file mode 100644 index 0000000000..b12f05239b --- /dev/null +++ b/.changeset/web-archived-sessions-settings.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +web: Add an Archived sessions page in Settings to browse and restore archived sessions. Open Settings → Archived to find it. diff --git a/apps/kimi-web/src/api/daemon/client.ts b/apps/kimi-web/src/api/daemon/client.ts index 5c5049b3c3..531ff3cb13 100644 --- a/apps/kimi-web/src/api/daemon/client.ts +++ b/apps/kimi-web/src/api/daemon/client.ts @@ -293,6 +293,7 @@ export class DaemonKimiWebApi implements KimiWebApi { status?: AppSessionStatus; workspaceId?: string; includeArchive?: boolean; + archivedOnly?: boolean; excludeEmpty?: boolean; }, ): Promise> { @@ -302,6 +303,7 @@ export class DaemonKimiWebApi implements KimiWebApi { page_size: input?.pageSize, status: input?.status ? toWireSessionStatus(input.status) : undefined, include_archive: input?.includeArchive, + archived_only: input?.archivedOnly, exclude_empty: input?.excludeEmpty, // PRESUMED — daemon supports ?workspace_id= once the registry ships; it // ignores unknown query params until then, so this is safe to always send. @@ -420,6 +422,16 @@ export class DaemonKimiWebApi implements KimiWebApi { return data; } + // POST /sessions/{id}:restore — clear the archived flag. The daemon returns + // the full restored session, so callers can merge it straight back into lists. + async restoreSession(sessionId: string): Promise { + const data = await this.http.post( + `/sessions/${encodeURIComponent(sessionId)}:restore`, + {}, + ); + return toAppSession(data); + } + // ------------------------------------------------------------------------- // Messages // ------------------------------------------------------------------------- diff --git a/apps/kimi-web/src/api/types.ts b/apps/kimi-web/src/api/types.ts index 16adbbcd3c..08b6ae5174 100644 --- a/apps/kimi-web/src/api/types.ts +++ b/apps/kimi-web/src/api/types.ts @@ -645,7 +645,7 @@ export interface AppSessionWarning { export interface KimiWebApi { getHealth(): Promise<{ status: 'ok'; uptimeSec: number }>; getMeta(): Promise<{ serverVersion: string; serverId: string; startedAt: string; capabilities: Record; openInApps: string[]; dangerousBypassAuth: boolean }>; - listSessions(input?: PageRequest & { status?: AppSessionStatus; workspaceId?: string; includeArchive?: boolean; excludeEmpty?: boolean }): Promise>; + listSessions(input?: PageRequest & { status?: AppSessionStatus; workspaceId?: string; includeArchive?: boolean; archivedOnly?: boolean; excludeEmpty?: boolean }): Promise>; createSession(input: { title?: string; cwd?: string; model?: string; workspaceId?: string }): Promise; /** Fetch one session by id (deep links beyond the first listSessions page). */ getSession(sessionId: string): Promise; @@ -653,6 +653,7 @@ export interface KimiWebApi { getSessionStatus(sessionId: string): Promise; getSessionWarnings(sessionId: string): Promise; archiveSession(sessionId: string): Promise<{ archived: true }>; + restoreSession(sessionId: string): Promise; listMessages(sessionId: string, input?: PageRequest & { role?: AppMessageRole }): Promise>; /** v2 initial sync: atomic session state + `asOfSeq` watermark + epoch. */ getSessionSnapshot(sessionId: string): Promise; diff --git a/apps/kimi-web/src/components/settings/SettingsDialog.vue b/apps/kimi-web/src/components/settings/SettingsDialog.vue index b910332c21..f3ed0c753f 100644 --- a/apps/kimi-web/src/components/settings/SettingsDialog.vue +++ b/apps/kimi-web/src/components/settings/SettingsDialog.vue @@ -3,8 +3,10 @@ scattered in the sidebar account popover: appearance, language, account, connection, plus notifications and the troubleshooting-log export. -->