-
Notifications
You must be signed in to change notification settings - Fork 623
feat(web): hide ACP-created sessions and workspaces from the sidebar #1908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
70a559b
02b5b17
cd4a8f0
7d66cc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@moonshot-ai/acp-adapter": patch | ||
| "@moonshot-ai/kimi-web": patch | ||
| --- | ||
|
|
||
| Tag ACP-created sessions with `source: 'acp'` (plus the client's `clientInfo.name`) in their persisted custom metadata, and add a kimi-web setting (on by default) that hides ACP-created sessions and ACP-only workspaces from the sidebar and archived lists. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import { | |
| type WorkspaceSortMode, | ||
| } from '../lib/workspaceOrder'; | ||
| import { mergeWorkspaces } from '../lib/mergeWorkspaces'; | ||
| import { acpOnlyWorkspaceRoots, isAcpSession } from '../lib/acpSessions'; | ||
| import { workspaceRootKey } from '../lib/rootKey'; | ||
| import { mergeSnapshotMessages } from '../lib/snapshotMessages'; | ||
| import { mergeSnapshotSubagents } from '../lib/taskMerge'; | ||
|
|
@@ -1386,7 +1387,9 @@ async function handleSessionNotFound(sessionId: string): Promise<void> { | |
|
|
||
| if (rawState.activeSessionId !== sessionId) return; | ||
|
|
||
| const next = rawState.sessions[0]; | ||
| // Skip sessions hidden from the sidebar (e.g. ACP-created), matching the | ||
| // other pick-next paths in useWorkspaceState. | ||
| const next = rawState.sessions.find((s) => !(hideAcpSessions.value && isAcpSession(s))); | ||
| if (next) { | ||
| await workspaceState.selectSession(next.id, { urlMode: 'replace' }); | ||
| } else { | ||
|
|
@@ -2350,6 +2353,36 @@ function workspaceIdForSession(s: { workspaceId?: string; cwd: string }): string | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Hide ACP-created sessions/workspaces from the sidebar. Default ON: sessions | ||
| * created via the ACP adapter (e.g. bot clients driving kimi-code headlessly) | ||
| * carry `source: 'acp'` in their persisted custom metadata, and their | ||
| * workspaces are usually machine-owned directories the user never edits in. | ||
| * Persisted as '1'/'0' like the notification prefs. | ||
| */ | ||
| function loadHideAcpSessions(): boolean { | ||
| const v = safeGetString(STORAGE_KEYS.hideAcpSessions); | ||
| return v === null ? true : v === '1'; | ||
| } | ||
|
|
||
| const hideAcpSessions = ref(loadHideAcpSessions()); | ||
|
|
||
| function setHideAcpSessions(on: boolean): void { | ||
| hideAcpSessions.value = on; | ||
| safeSetString(STORAGE_KEYS.hideAcpSessions, on ? '1' : '0'); | ||
| } | ||
|
|
||
| /** | ||
| * Roots whose loaded sessions are ALL ACP-created (see `acpOnlyWorkspaceRoots` | ||
| * for the semantics and the pagination caveat). Deliberately NOT written into | ||
| * `rawState.hiddenWorkspaceRoots`: that set is user-owned, gets persisted, | ||
| * and is auto-cleared by workspace WS broadcasts. These roots only exist at | ||
| * the `mergeWorkspaces` call site below. | ||
| */ | ||
| const acpHiddenWorkspaceRoots = computed<string[]>(() => | ||
| hideAcpSessions.value ? acpOnlyWorkspaceRoots(rawState.sessions) : [], | ||
| ); | ||
|
|
||
| /** | ||
| * Merge real (daemon) workspaces with workspaces DERIVED from the current | ||
| * sessions' cwds. Each distinct cwd with no matching real workspace becomes one | ||
|
|
@@ -2359,8 +2392,10 @@ function workspaceIdForSession(s: { workspaceId?: string; cwd: string }): string | |
| const mergedWorkspaces = computed<AppWorkspace[]>(() => | ||
| mergeWorkspaces({ | ||
| workspaces: rawState.workspaces, | ||
| sessions: rawState.sessions, | ||
| hiddenWorkspaceRoots: rawState.hiddenWorkspaceRoots, | ||
| sessions: hideAcpSessions.value | ||
| ? rawState.sessions.filter((s) => !isAcpSession(s)) | ||
| : rawState.sessions, | ||
| hiddenWorkspaceRoots: [...rawState.hiddenWorkspaceRoots, ...acpHiddenWorkspaceRoots.value], | ||
| sessionsHasMoreByWorkspace: rawState.sessionsHasMoreByWorkspace, | ||
| }), | ||
| ); | ||
|
|
@@ -2483,7 +2518,12 @@ const sessionsForView = computed<Session[]>(() => { | |
| // excluded too, so this flat list matches what the grouped sidebar renders | ||
| // and sidebar search can't resurrect sessions from a removed workspace. | ||
| return rawState.sessions | ||
| .filter((s) => !s.parentSessionId && visibleWorkspaceIds.has(workspaceIdForSession(s))) | ||
| .filter( | ||
| (s) => | ||
| !s.parentSessionId && | ||
| visibleWorkspaceIds.has(workspaceIdForSession(s)) && | ||
| !(hideAcpSessions.value && isAcpSession(s)), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| ) | ||
| .map((s) => { | ||
| const workspaceId = workspaceIdForSession(s); | ||
| return { | ||
|
|
@@ -2508,6 +2548,7 @@ const workspaceGroups = computed<WorkspaceGroup[]>(() => { | |
| (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), | ||
| )) { | ||
| if (s.parentSessionId) continue; // child sessions stay out of the list | ||
| if (hideAcpSessions.value && isAcpSession(s)) continue; // ACP sessions hidden by preference | ||
| const wid = workspaceIdForSession(s); | ||
| const view: Session = { | ||
| id: s.id, | ||
|
|
@@ -2654,6 +2695,7 @@ const workspaceState = useWorkspaceState(rawState, { | |
| persistSessionProfile, | ||
| mergedWorkspaces, | ||
| workspacesView, | ||
| isSessionHiddenFromList: (s: AppSession) => hideAcpSessions.value && isAcpSession(s), | ||
| status, | ||
| workspaceIdForSession, | ||
| savePermissionToStorage, | ||
|
|
@@ -2889,6 +2931,10 @@ export function useKimiWebClient() { | |
| conversationToc, | ||
| setConversationToc, | ||
|
|
||
| // Hide ACP-created sessions/workspaces from the sidebar | ||
| hideAcpSessions, | ||
| setHideAcpSessions, | ||
|
|
||
| // Color scheme | ||
| colorScheme: appearance.colorScheme, | ||
| setColorScheme: appearance.setColorScheme, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| // apps/kimi-web/src/lib/acpSessions.test.ts | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import type { AppSession } from '../api/types'; | ||
| import { acpOnlyWorkspaceRoots, isAcpSession } from './acpSessions'; | ||
| import { workspaceRootKey } from './rootKey'; | ||
|
|
||
| function makeSession(overrides: Partial<AppSession>): AppSession { | ||
| return { | ||
| id: 'session_1', | ||
| title: 't', | ||
| createdAt: '2026-01-01T00:00:00Z', | ||
| updatedAt: '2026-01-01T00:00:00Z', | ||
| busy: false, | ||
| archived: false, | ||
| cwd: '/repo', | ||
| model: 'kimi', | ||
| usage: { | ||
| inputTokens: 0, | ||
| outputTokens: 0, | ||
| cacheReadTokens: 0, | ||
| cacheCreationTokens: 0, | ||
| totalCostUsd: 0, | ||
| contextTokens: 0, | ||
| contextLimit: 0, | ||
| turnCount: 0, | ||
| }, | ||
| messageCount: 0, | ||
| lastSeq: 0, | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| describe('isAcpSession', () => { | ||
| it('matches exactly the acp source tag', () => { | ||
| expect(isAcpSession(makeSession({ source: 'acp' }))).toBe(true); | ||
| expect(isAcpSession(makeSession({ source: 'vscode' }))).toBe(false); | ||
| expect(isAcpSession(makeSession({}))).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('acpOnlyWorkspaceRoots', () => { | ||
| it('reports roots whose sessions are all ACP-created', () => { | ||
| const roots = acpOnlyWorkspaceRoots([ | ||
| makeSession({ id: 'a', cwd: '/agent/s_1', source: 'acp' }), | ||
| makeSession({ id: 'b', cwd: '/agent/s_1', source: 'acp' }), | ||
| makeSession({ id: 'c', cwd: '/repo', source: undefined }), | ||
| ]); | ||
| expect(roots).toEqual([workspaceRootKey('/agent/s_1')]); | ||
| }); | ||
|
|
||
| it('hides nothing when a workspace has any non-ACP session', () => { | ||
| const roots = acpOnlyWorkspaceRoots([ | ||
| makeSession({ id: 'a', cwd: '/mixed', source: 'acp' }), | ||
| makeSession({ id: 'b', cwd: '/mixed' }), | ||
| ]); | ||
| expect(roots).toEqual([]); | ||
| }); | ||
|
|
||
| it('never reports a root with no ACP session (no vacuous truth on empty/ACP-free sets)', () => { | ||
| expect(acpOnlyWorkspaceRoots([])).toEqual([]); | ||
| expect(acpOnlyWorkspaceRoots([makeSession({ id: 'a', cwd: '/repo' })])).toEqual([]); | ||
| }); | ||
|
|
||
| it('skips sessions without a cwd', () => { | ||
| const roots = acpOnlyWorkspaceRoots([makeSession({ id: 'a', cwd: '', source: 'acp' })]); | ||
| expect(roots).toEqual([]); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On mobile,
App.vueopensMobileSettingsSheetinstead of the desktopSettingsDialog, and this newly added filter is controlled byclient.hideAcpSessions, which defaults to true. Because the mobile sheet has no corresponding toggle/event, mobile users cannot turn the filter off to find or restore archived ACP sessions, or reveal hidden ACP workspaces, from the mobile UI; add the same setting row here or pass through the existing setter.Useful? React with 👍 / 👎.