From 3627087b96bcfe7e7bb12566eaf26ce4eb3d440c Mon Sep 17 00:00:00 2001 From: qer Date: Sun, 28 Jun 2026 02:16:17 +0800 Subject: [PATCH 1/4] chore(web): remove the /sessions slash command --- .changeset/web-remove-sessions-command.md | 5 + apps/kimi-web/src/App.vue | 17 - .../src/components/dialogs/SessionsDialog.vue | 394 ------------------ apps/kimi-web/src/i18n/locales/en/commands.ts | 1 - apps/kimi-web/src/i18n/locales/en/sessions.ts | 7 - apps/kimi-web/src/i18n/locales/zh/commands.ts | 1 - apps/kimi-web/src/i18n/locales/zh/sessions.ts | 7 - apps/kimi-web/src/lib/slashCommands.ts | 1 - 8 files changed, 5 insertions(+), 428 deletions(-) create mode 100644 .changeset/web-remove-sessions-command.md delete mode 100644 apps/kimi-web/src/components/dialogs/SessionsDialog.vue diff --git a/.changeset/web-remove-sessions-command.md b/.changeset/web-remove-sessions-command.md new file mode 100644 index 0000000000..ae052a2666 --- /dev/null +++ b/.changeset/web-remove-sessions-command.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Remove the /sessions slash command from the web UI; the sidebar already covers session browsing. diff --git a/apps/kimi-web/src/App.vue b/apps/kimi-web/src/App.vue index 51a75ba5de..4a610657e8 100644 --- a/apps/kimi-web/src/App.vue +++ b/apps/kimi-web/src/App.vue @@ -16,7 +16,6 @@ import ProviderManager from './components/settings/ProviderManager.vue'; import LoginDialog from './components/dialogs/LoginDialog.vue'; import NewSessionDialog from './components/dialogs/NewSessionDialog.vue'; import SettingsDialog from './components/settings/SettingsDialog.vue'; -import SessionsDialog from './components/dialogs/SessionsDialog.vue'; import AddWorkspaceDialog from './components/dialogs/AddWorkspaceDialog.vue'; import StatusPanel from './components/chat/StatusPanel.vue'; import WarningToasts from './components/WarningToasts.vue'; @@ -229,7 +228,6 @@ const showModelPicker = ref(false); const showProviders = ref(false); const showLogin = ref(false); const showNewSession = ref(false); -const showSessions = ref(false); const showAddWorkspace = ref(false); const showStatusPanel = ref(false); const showSettings = ref(false); @@ -249,7 +247,6 @@ const anyOverlayOpen = computed(() => showProviders.value || showLogin.value || showNewSession.value || - showSessions.value || showAddWorkspace.value || showStatusPanel.value || showSettings.value || @@ -400,9 +397,6 @@ function handleCommand(cmd: string): void { case '/clear': handleCreateSession(); break; - case '/sessions': - showSessions.value = true; - break; case '/fork': void client.forkSession(); break; @@ -868,17 +862,6 @@ function openPr(url: string): void { @close="showNewSession = false" /> - - - - - - - - - - - - diff --git a/apps/kimi-web/src/i18n/locales/en/commands.ts b/apps/kimi-web/src/i18n/locales/en/commands.ts index 1027cb50cf..691cf863fe 100644 --- a/apps/kimi-web/src/i18n/locales/en/commands.ts +++ b/apps/kimi-web/src/i18n/locales/en/commands.ts @@ -1,7 +1,6 @@ export default { help: { desc: 'Show the list of available commands' }, new: { desc: 'Create a new session' }, - sessions: { desc: 'Browse & switch sessions' }, clear: { desc: 'Clear and start a new session' }, model: { desc: 'Switch model' }, provider: { desc: 'Manage providers (add / remove / refresh)' }, diff --git a/apps/kimi-web/src/i18n/locales/en/sessions.ts b/apps/kimi-web/src/i18n/locales/en/sessions.ts index 378168311f..cdc86a56a4 100644 --- a/apps/kimi-web/src/i18n/locales/en/sessions.ts +++ b/apps/kimi-web/src/i18n/locales/en/sessions.ts @@ -1,10 +1,3 @@ export default { - title: 'Sessions', - close: 'Close', - searchPlaceholder: 'Search sessions by title…', - noWorkspace: 'No workspace', - emptyNone: 'No sessions yet', - emptyNoMatch: 'No matching sessions', - footerHint: '↑↓ to navigate · Enter to switch · Esc to close', justNow: 'just now', } as const; diff --git a/apps/kimi-web/src/i18n/locales/zh/commands.ts b/apps/kimi-web/src/i18n/locales/zh/commands.ts index 9f0b6e323d..1d0ddf7092 100644 --- a/apps/kimi-web/src/i18n/locales/zh/commands.ts +++ b/apps/kimi-web/src/i18n/locales/zh/commands.ts @@ -1,7 +1,6 @@ export default { help: { desc: '显示可用命令列表' }, new: { desc: '创建新会话' }, - sessions: { desc: '浏览/切换会话' }, clear: { desc: '清空并新建会话' }, model: { desc: '切换模型' }, provider: { desc: '管理提供商 (添加/删除/刷新)' }, diff --git a/apps/kimi-web/src/i18n/locales/zh/sessions.ts b/apps/kimi-web/src/i18n/locales/zh/sessions.ts index 3d2f16aa77..9bec2efa69 100644 --- a/apps/kimi-web/src/i18n/locales/zh/sessions.ts +++ b/apps/kimi-web/src/i18n/locales/zh/sessions.ts @@ -1,10 +1,3 @@ export default { - title: '会话', - close: '关闭', - searchPlaceholder: '按标题搜索会话…', - noWorkspace: '无工作区', - emptyNone: '暂无会话', - emptyNoMatch: '没有匹配的会话', - footerHint: '↑↓ 切换 · Enter 进入 · Esc 关闭', justNow: '刚刚', }; diff --git a/apps/kimi-web/src/lib/slashCommands.ts b/apps/kimi-web/src/lib/slashCommands.ts index b9fa53d521..cca86e1b3f 100644 --- a/apps/kimi-web/src/lib/slashCommands.ts +++ b/apps/kimi-web/src/lib/slashCommands.ts @@ -24,7 +24,6 @@ export interface SlashCommand { export const SLASH_COMMANDS: SlashCommand[] = [ { name: '/help', desc: 'commands.help.desc' }, { name: '/new', desc: 'commands.new.desc' }, - { name: '/sessions', desc: 'commands.sessions.desc' }, { name: '/clear', desc: 'commands.clear.desc' }, { name: '/model', desc: 'commands.model.desc' }, { name: '/provider', desc: 'commands.provider.desc' }, From 95b7d7cee0fbe0e59939baee49f90254e81f6d9d Mon Sep 17 00:00:00 2001 From: qer Date: Sun, 28 Jun 2026 02:16:24 +0800 Subject: [PATCH 2/4] feat(web): hide empty sessions from the session list Add an optional exclude_empty parameter to the session list API; the web client passes it so unused "New Session" entries are hidden by default, with pagination and has_more computed on the filtered set. --- .changeset/add-session-list-exclude-empty.md | 5 +++ .changeset/web-hide-empty-sessions.md | 5 +++ apps/kimi-web/src/api/daemon/client.ts | 8 +++- apps/kimi-web/src/api/types.ts | 2 +- .../composables/client/useWorkspaceState.ts | 13 +++++- .../src/services/session/session.ts | 2 + .../src/services/session/sessionService.ts | 17 +++++--- .../test/services/session-service.test.ts | 40 +++++++++++++++++++ packages/server/src/routes/sessions.ts | 2 + 9 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 .changeset/add-session-list-exclude-empty.md create mode 100644 .changeset/web-hide-empty-sessions.md diff --git a/.changeset/add-session-list-exclude-empty.md b/.changeset/add-session-list-exclude-empty.md new file mode 100644 index 0000000000..dff6430114 --- /dev/null +++ b/.changeset/add-session-list-exclude-empty.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add an optional exclude_empty parameter to the session list API to omit sessions that have no messages. diff --git a/.changeset/web-hide-empty-sessions.md b/.changeset/web-hide-empty-sessions.md new file mode 100644 index 0000000000..d259d35a11 --- /dev/null +++ b/.changeset/web-hide-empty-sessions.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Hide unused "New Session" entries from the web session list by default. diff --git a/apps/kimi-web/src/api/daemon/client.ts b/apps/kimi-web/src/api/daemon/client.ts index 691bc85d6f..83e44541af 100644 --- a/apps/kimi-web/src/api/daemon/client.ts +++ b/apps/kimi-web/src/api/daemon/client.ts @@ -286,7 +286,12 @@ export class DaemonKimiWebApi implements KimiWebApi { // ------------------------------------------------------------------------- async listSessions( - input?: PageRequest & { status?: AppSessionStatus; workspaceId?: string; includeArchive?: boolean }, + input?: PageRequest & { + status?: AppSessionStatus; + workspaceId?: string; + includeArchive?: boolean; + excludeEmpty?: boolean; + }, ): Promise> { const query: Record = { before_id: input?.beforeId, @@ -294,6 +299,7 @@ export class DaemonKimiWebApi implements KimiWebApi { page_size: input?.pageSize, status: input?.status ? toWireSessionStatus(input.status) : undefined, include_archive: input?.includeArchive, + 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. workspace_id: input?.workspaceId, diff --git a/apps/kimi-web/src/api/types.ts b/apps/kimi-web/src/api/types.ts index 620cdad1a1..fe12d91b1b 100644 --- a/apps/kimi-web/src/api/types.ts +++ b/apps/kimi-web/src/api/types.ts @@ -605,7 +605,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[] }>; - listSessions(input?: PageRequest & { status?: AppSessionStatus; workspaceId?: string; includeArchive?: boolean }): Promise>; + listSessions(input?: PageRequest & { status?: AppSessionStatus; workspaceId?: string; includeArchive?: 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; diff --git a/apps/kimi-web/src/composables/client/useWorkspaceState.ts b/apps/kimi-web/src/composables/client/useWorkspaceState.ts index 66cf6ae8da..967ef58f34 100644 --- a/apps/kimi-web/src/composables/client/useWorkspaceState.ts +++ b/apps/kimi-web/src/composables/client/useWorkspaceState.ts @@ -299,7 +299,11 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta const items: AppSession[] = []; let beforeId: string | undefined; for (;;) { - const page = await api.listSessions({ pageSize: SESSION_PAGE_SIZE, beforeId }); + const page = await api.listSessions({ + pageSize: SESSION_PAGE_SIZE, + beforeId, + excludeEmpty: true, + }); items.push(...page.items); if (!page.hasMore || page.items.length === 0) break; beforeId = page.items[page.items.length - 1]!.id; @@ -326,7 +330,11 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta const pages = await Promise.all( workspaces.map((w) => api - .listSessions({ workspaceId: w.id, pageSize: SESSIONS_INITIAL_PAGE_SIZE }) + .listSessions({ + workspaceId: w.id, + pageSize: SESSIONS_INITIAL_PAGE_SIZE, + excludeEmpty: true, + }) .then((page) => ({ workspaceId: w.id, page })) .catch(() => ({ workspaceId: w.id, @@ -372,6 +380,7 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta workspaceId, pageSize: SESSIONS_LOAD_MORE_SIZE, beforeId, + excludeEmpty: true, }); // Append de-duped against the latest list so a concurrently added/removed // session is respected. diff --git a/packages/agent-core/src/services/session/session.ts b/packages/agent-core/src/services/session/session.ts index 1d83feead3..fde54ddcca 100644 --- a/packages/agent-core/src/services/session/session.ts +++ b/packages/agent-core/src/services/session/session.ts @@ -24,6 +24,8 @@ export interface SessionListQuery extends CursorQuery { status?: import('@moonshot-ai/protocol').SessionStatus; workDir?: string; includeArchive?: boolean; + /** When true, hide sessions the user has never interacted with (no prompt yet). */ + excludeEmpty?: boolean; } export interface SessionClientTelemetry { diff --git a/packages/agent-core/src/services/session/sessionService.ts b/packages/agent-core/src/services/session/sessionService.ts index 3af85e3264..3b684a0dc7 100644 --- a/packages/agent-core/src/services/session/sessionService.ts +++ b/packages/agent-core/src/services/session/sessionService.ts @@ -260,21 +260,26 @@ export class SessionService extends Disposable implements ISessionService { }; const all = await this.core.rpc.listSessions(corePayload); const sorted = all.toSorted((a, b) => b.updatedAt - a.updatedAt); + // Hide sessions the user has never interacted with: a session is "empty" when + // it has no lastPrompt (the first prompt has not been sent yet). Filtered + // before cursor pagination so each returned page is filled with non-empty + // sessions and has_more reflects the filtered set. + const visible = query.excludeEmpty ? sorted.filter((s) => s.lastPrompt) : sorted; let pivotIndex = -1; if (query.before_id !== undefined) { - pivotIndex = sorted.findIndex((s) => s.id === query.before_id); + pivotIndex = visible.findIndex((s) => s.id === query.before_id); } else if (query.after_id !== undefined) { - pivotIndex = sorted.findIndex((s) => s.id === query.after_id); + pivotIndex = visible.findIndex((s) => s.id === query.after_id); } - let slice: typeof sorted; + let slice: typeof visible; if (query.before_id !== undefined && pivotIndex >= 0) { - slice = sorted.slice(pivotIndex + 1); + slice = visible.slice(pivotIndex + 1); } else if (query.after_id !== undefined && pivotIndex >= 0) { - slice = sorted.slice(0, pivotIndex); + slice = visible.slice(0, pivotIndex); } else { - slice = sorted; + slice = visible; } const requestedSize = query.page_size ?? DEFAULT_PAGE_SIZE; diff --git a/packages/agent-core/test/services/session-service.test.ts b/packages/agent-core/test/services/session-service.test.ts index 6d8ece1386..b84de9286c 100644 --- a/packages/agent-core/test/services/session-service.test.ts +++ b/packages/agent-core/test/services/session-service.test.ts @@ -608,6 +608,46 @@ describe('SessionService.list', () => { expect(page.items).toEqual([]); expect(page.has_more).toBe(false); }); + + it('excludeEmpty drops sessions without a lastPrompt before pagination', async () => { + const ts = (n: number) => 1_000_000 + n * 1_000; + const summary = ( + id: string, + updatedAt: number, + lastPrompt?: string, + ): SessionSummary => ({ + id, + workDir: '/tmp/a', + sessionDir: `/tmp/sessions/${id}`, + createdAt: updatedAt, + updatedAt, + metadata: { cwd: '/tmp/a' }, + title: undefined, + lastPrompt, + }); + state.sessions = [ + summary('e1', ts(3)), + summary('u1', ts(2), 'hi'), + summary('e2', ts(1)), + summary('u2', ts(0), 'yo'), + ]; + + const all = await svc.list({}); + expect(all.items.map((s) => s.id)).toEqual(['e1', 'u1', 'e2', 'u2']); + + const visible = await svc.list({ excludeEmpty: true }); + expect(visible.items.map((s) => s.id)).toEqual(['u1', 'u2']); + expect(visible.has_more).toBe(false); + + // Pagination + cursor operate on the filtered set. + const first = await svc.list({ excludeEmpty: true, page_size: 1 }); + expect(first.items.map((s) => s.id)).toEqual(['u1']); + expect(first.has_more).toBe(true); + + const next = await svc.list({ excludeEmpty: true, page_size: 1, before_id: 'u1' }); + expect(next.items.map((s) => s.id)).toEqual(['u2']); + expect(next.has_more).toBe(false); + }); }); describe('SessionService.get', () => { diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index 245817b804..2c69bdb807 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -83,6 +83,7 @@ const sessionsListQueryCoercion = z page_size: z.coerce.number().int().min(1).max(100).optional(), status: sessionStatusSchema.optional(), include_archive: booleanQueryParam, + exclude_empty: booleanQueryParam, workspace_id: workspaceIdSchema.optional(), }) @@ -272,6 +273,7 @@ export function registerSessionsRoutes( page_size: raw.page_size, status: raw.status, includeArchive: raw.include_archive, + excludeEmpty: raw.exclude_empty, }; let query; if (raw.workspace_id !== undefined) { From d73002e18f7ad2bc0d5100087dcaf18e67c70e99 Mon Sep 17 00:00:00 2001 From: qer Date: Sun, 28 Jun 2026 02:24:49 +0800 Subject: [PATCH 3/4] fix(protocol): add exclude_empty to the session list query schema Keep the shared protocol schema in sync with the server route so clients using the protocol type see the new parameter. --- packages/protocol/src/__tests__/rest-session.test.ts | 9 +++++++++ packages/protocol/src/rest/session.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/protocol/src/__tests__/rest-session.test.ts b/packages/protocol/src/__tests__/rest-session.test.ts index db64818ebf..51e07e7a0b 100644 --- a/packages/protocol/src/__tests__/rest-session.test.ts +++ b/packages/protocol/src/__tests__/rest-session.test.ts @@ -98,6 +98,15 @@ describe('listSessionsQuerySchema', () => { include_archive: false, }); }); + + it('parses exclude_empty to boolean', () => { + expect(listSessionsQuerySchema.parse({ exclude_empty: 'true' })).toEqual({ + exclude_empty: true, + }); + expect(listSessionsQuerySchema.parse({ exclude_empty: 'false' })).toEqual({ + exclude_empty: false, + }); + }); }); describe('getSessionProfileResponseSchema', () => { diff --git a/packages/protocol/src/rest/session.ts b/packages/protocol/src/rest/session.ts index a6d7bacc47..cde14b18ff 100644 --- a/packages/protocol/src/rest/session.ts +++ b/packages/protocol/src/rest/session.ts @@ -46,6 +46,7 @@ export const listSessionsQuerySchema = cursorQuerySchema.and( z.object({ status: sessionStatusSchema.optional(), include_archive: booleanQueryParam, + exclude_empty: booleanQueryParam, }), ); export type ListSessionsQuery = z.infer; From e4d0171a64cb0150c82c796992114ba45bcd9c36 Mon Sep 17 00:00:00 2001 From: qer Date: Sun, 28 Jun 2026 02:36:24 +0800 Subject: [PATCH 4/4] fix(protocol): keep exclude_empty off the child session list schema listSessionChildrenQuerySchema aliased the main list schema, so it inherited exclude_empty even though the /sessions/{id}/children route does not filter by it. Split it so generated clients are not misled. --- packages/protocol/src/__tests__/rest-session.test.ts | 8 ++++++++ packages/protocol/src/rest/session.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/protocol/src/__tests__/rest-session.test.ts b/packages/protocol/src/__tests__/rest-session.test.ts index 51e07e7a0b..d53c4e5e55 100644 --- a/packages/protocol/src/__tests__/rest-session.test.ts +++ b/packages/protocol/src/__tests__/rest-session.test.ts @@ -11,6 +11,7 @@ import { forkSessionRequestSchema, forkSessionResponseSchema, getSessionProfileResponseSchema, + listSessionChildrenQuerySchema, listSessionChildrenResponseSchema, listSessionsQuerySchema, sessionStatusResponseSchema, @@ -109,6 +110,13 @@ describe('listSessionsQuerySchema', () => { }); }); +describe('listSessionChildrenQuerySchema', () => { + it('does not advertise exclude_empty (child lists do not filter by it)', () => { + const parsed = listSessionChildrenQuerySchema.parse({ exclude_empty: true }); + expect(parsed).not.toHaveProperty('exclude_empty'); + }); +}); + describe('getSessionProfileResponseSchema', () => { it('accepts a Session payload', () => { const parsed = getSessionProfileResponseSchema.parse({ diff --git a/packages/protocol/src/rest/session.ts b/packages/protocol/src/rest/session.ts index cde14b18ff..f937de4dd2 100644 --- a/packages/protocol/src/rest/session.ts +++ b/packages/protocol/src/rest/session.ts @@ -86,7 +86,14 @@ export const startBtwSessionResponseSchema = z.object({ }); export type StartBtwSessionResponse = z.infer; -export const listSessionChildrenQuerySchema = listSessionsQuerySchema; +// Child lists intentionally omit exclude_empty: the /sessions/{id}/children route +// does not filter by it, so advertising it would mislead generated clients. +export const listSessionChildrenQuerySchema = cursorQuerySchema.and( + z.object({ + status: sessionStatusSchema.optional(), + include_archive: booleanQueryParam, + }), +); export type ListSessionChildrenQuery = z.infer; export const listSessionChildrenResponseSchema = pageResponseSchema(sessionSchema);