From 479515e9333a041dee2d5e900683e36eb951c6fc Mon Sep 17 00:00:00 2001 From: qer Date: Thu, 2 Jul 2026 21:09:20 +0800 Subject: [PATCH 1/3] feat(web): collapse loaded sessions back to the first page The workspace session list's load-more control was one-way: once expanded, the only way to hide the extra sessions was to collapse the whole group. Add a Show less / Show all toggle so an expanded list can be collapsed back to its first page and re-expanded without losing the loaded data. Restyle the control as a session-row-shaped pill whose label aligns with the session titles, per design-system section 07, and mirror the behavior in the mobile switcher. --- .changeset/web-sidebar-collapse-sessions.md | 5 + apps/kimi-web/design/design-system.html | 12 + .../design/sidebar-show-more-demo.html | 649 ++++++++++++++++++ apps/kimi-web/public/design-system.html | 12 + apps/kimi-web/src/components/Sidebar.vue | 34 +- .../src/components/WorkspaceGroup.vue | 78 ++- .../components/mobile/MobileSwitcherSheet.vue | 47 +- .../composables/client/useWorkspaceState.ts | 13 +- .../src/composables/useKimiWebClient.ts | 7 +- apps/kimi-web/src/i18n/locales/en/sidebar.ts | 4 +- apps/kimi-web/src/i18n/locales/zh/sidebar.ts | 4 +- apps/kimi-web/src/types.ts | 3 + 12 files changed, 845 insertions(+), 23 deletions(-) create mode 100644 .changeset/web-sidebar-collapse-sessions.md create mode 100644 apps/kimi-web/design/sidebar-show-more-demo.html diff --git a/.changeset/web-sidebar-collapse-sessions.md b/.changeset/web-sidebar-collapse-sessions.md new file mode 100644 index 000000000..0fc63b3c8 --- /dev/null +++ b/.changeset/web-sidebar-collapse-sessions.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Let the web sidebar collapse an expanded workspace session list back to its first page. diff --git a/apps/kimi-web/design/design-system.html b/apps/kimi-web/design/design-system.html index 3694066d9..7f412eae9 100644 --- a/apps/kimi-web/design/design-system.html +++ b/apps/kimi-web/design/design-system.html @@ -2211,6 +2211,18 @@

Workspace group

  • The group is collapsible; when collapsed its session list is hidden.
  • +

    Show more & collapse

    +

    The "load more / show less" control at the bottom of each workspace group is a session-row-shaped compact list control (same family as search, New chat, inline rename — not a Button). It doubles as the pagination trigger and the in-group expand / collapse toggle.

    + + + + + + + + +
    PartRule
    Containersession-row pill: display:flex; gap:--sb-gap; min-height:26px, same padding as a session row, radius-md; hover = surface-sunken (no text recolor); :focus-visible uses --p-focus-ring
    Lead slotempty, --sb-gutter wide, so the label's start x aligns with the session titles (--sb-pad-x + --sb-gutter + --sb-gap)
    Labelfont-ui, text-xs, --color-text; flex:1, truncated
    Behavior"Load more" fetches the next page and auto-expands; once more than the first page is loaded, "Show less" appears and collapses back to the first page (view-layer trim — data is kept, no refetch); "Show all" re-expands
    +

    ResizeHandle

    A 4px vertical drag bar, layered over the 1px column border (margin: 0 -2px makes the whole 4px grabbable), turning accent on hover / drag.

    diff --git a/apps/kimi-web/design/sidebar-show-more-demo.html b/apps/kimi-web/design/sidebar-show-more-demo.html new file mode 100644 index 000000000..d42cf7c5d --- /dev/null +++ b/apps/kimi-web/design/sidebar-show-more-demo.html @@ -0,0 +1,649 @@ + + + + + +kimi-web · 侧栏「展开更多」Demo + + + +
    +

    侧栏「展开更多」Demo

    + + + +
    + +
    +
    +
    方案 A · 加载更多 + 整组折叠
    (点工作区标题折叠整组)
    + +
    +
    +
    方案 B · 加载更多 + 组内展开/收起
    (收回到第一页)
    + +
    +
    +

    怎么看这个 demo

    +
      +
    • 左右两个侧栏是同一组数据(kimi-code 工作区共 23 个会话,每页 5 条),可独立点按。
    • +
    • 顶部 按钮样式 切换「当前实现」与「规范合规」,两套样式同时作用在两个方案上,方便对比。
    • +
    • 第二个工作区 docs 用来看「上面的列表展开后会把下面的工作区顶下去」——这就是想收起的动机。
    • +
    + +

    方案 A:加载更多 + 整组折叠

    +
      +
    • 只有 加载更多 一个按钮,拉下一页追加。
    • +
    • 收起靠 点工作区标题(整组会话全部隐藏)。这是 §07 已经定义的折叠。
    • +
    • 改动最小,完全符合现有规范。
    • +
    • 一折就整组全收,连第一页也看不见。
    • +
    + +

    方案 B:加载更多 + 组内展开/收起

    +
      +
    • 加载超过第一页后,多一个 收起 按钮;收回后变成 展开
    • +
    • 收起只回到第一页(前 5 条),数据不丢、不重新请求。
    • +
    • 能收回第一页之外的会话,缓解把下面工作区顶下去的问题。
    • +
    • §07 目前没有定义这个组内控件,需要扩展规范。
    • +
    + +

    样式差异(切到「当前实现」看)

    +
      +
    • 当前:裸 button —— mono 字体、无圆角、hover 文字变 accent、无焦点环,且文字比 session 标题偏右 8px
    • +
    • 设计稿:做成「会话行同款」的紧凑列表控件 —— 行首留空使文字与 session 标题精确对齐font-uiradius-md、26px 行高、hover 只出 sunken 底、:focus-visible 焦点环。
    • +
    +

    规范参考:design-system.html §07(侧栏)/ §02(token / 字体)/ §08(焦点环)。

    +
    +
    + + + + diff --git a/apps/kimi-web/public/design-system.html b/apps/kimi-web/public/design-system.html index 3694066d9..7f412eae9 100644 --- a/apps/kimi-web/public/design-system.html +++ b/apps/kimi-web/public/design-system.html @@ -2211,6 +2211,18 @@

    Workspace group

  • The group is collapsible; when collapsed its session list is hidden.
  • +

    Show more & collapse

    +

    The "load more / show less" control at the bottom of each workspace group is a session-row-shaped compact list control (same family as search, New chat, inline rename — not a Button). It doubles as the pagination trigger and the in-group expand / collapse toggle.

    +
    + + + + + + + +
    PartRule
    Containersession-row pill: display:flex; gap:--sb-gap; min-height:26px, same padding as a session row, radius-md; hover = surface-sunken (no text recolor); :focus-visible uses --p-focus-ring
    Lead slotempty, --sb-gutter wide, so the label's start x aligns with the session titles (--sb-pad-x + --sb-gutter + --sb-gap)
    Labelfont-ui, text-xs, --color-text; flex:1, truncated
    Behavior"Load more" fetches the next page and auto-expands; once more than the first page is loaded, "Show less" appears and collapses back to the first page (view-layer trim — data is kept, no refetch); "Show all" re-expands
    +

    ResizeHandle

    A 4px vertical drag bar, layered over the 1px column border (margin: 0 -2px makes the whole 4px grabbable), turning accent on hover / drag.

    diff --git a/apps/kimi-web/src/components/Sidebar.vue b/apps/kimi-web/src/components/Sidebar.vue index c8c92ef2b..b89cf4d95 100644 --- a/apps/kimi-web/src/components/Sidebar.vue +++ b/apps/kimi-web/src/components/Sidebar.vue @@ -146,6 +146,36 @@ const allCollapsed = computed( props.groups.every((g) => collapsedIds.value.has(g.workspace.id)), ); +// --------------------------------------------------------------------------- +// In-group expand / collapse (show-more pagination) +// --------------------------------------------------------------------------- +// Tracks which workspace groups are "expanded" past their first page. Ephemeral +// (not persisted): a refresh reloads only the first page, so everything starts +// collapsed. Loading more expands automatically; the user can collapse back to +// the first page without losing the already-loaded data. +const expandedIds = ref>(new Set()); + +function isExpanded(id: string): boolean { + return expandedIds.value.has(id); +} + +function toggleExpand(id: string): void { + const next = new Set(expandedIds.value); + if (next.has(id)) next.delete(id); + else next.add(id); + expandedIds.value = next; +} + +function onLoadMore(id: string): void { + // Loading more should reveal the new rows immediately. + if (!expandedIds.value.has(id)) { + const next = new Set(expandedIds.value); + next.add(id); + expandedIds.value = next; + } + emit('loadMoreSessions', id); +} + // --------------------------------------------------------------------------- // Workspace path display (toggle in the Workspaces section header) // --------------------------------------------------------------------------- @@ -646,6 +676,7 @@ onBeforeUnmount(() => { :ws-menu-open-id="wsMenuOpenId" :dragging="draggingWsId === g.workspace.id" :is-collapsed="isCollapsed" + :is-expanded="isExpanded" :show-path="showWorkspacePaths" @group-click="handleGhClick" @group-contextmenu="openGhMenu" @@ -655,7 +686,8 @@ onBeforeUnmount(() => { @rename-session="(id, title) => emit('rename', id, title)" @archive-session="(id) => emit('archive', id)" @fork-session="(id) => emit('fork', id)" - @load-more="(id) => emit('loadMoreSessions', id)" + @load-more="onLoadMore" + @toggle-expand="toggleExpand" @confirm-rename="confirmRenameWorkspace" @cancel-rename="cancelRenameWorkspace" @update-rename-value="onUpdateRenameValue" diff --git a/apps/kimi-web/src/components/WorkspaceGroup.vue b/apps/kimi-web/src/components/WorkspaceGroup.vue index eaff3dfa4..07d854a33 100644 --- a/apps/kimi-web/src/components/WorkspaceGroup.vue +++ b/apps/kimi-web/src/components/WorkspaceGroup.vue @@ -30,6 +30,9 @@ const props = defineProps<{ /** When true, render the workspace root path as a stable subtitle line. */ showPath: boolean; isCollapsed: (id: string) => boolean; + /** When true, render all loaded sessions; otherwise only the first page + * (`group.initialCount`). Drives the in-group show-more / show-less toggle. */ + isExpanded: (id: string) => boolean; }>(); const emit = defineEmits<{ @@ -42,6 +45,7 @@ const emit = defineEmits<{ archiveSession: [id: string]; forkSession: [id: string]; loadMore: [workspaceId: string]; + toggleExpand: [workspaceId: string]; confirmRename: []; cancelRename: []; updateRenameValue: [value: string]; @@ -57,6 +61,25 @@ const renameValueModel = computed({ set: (value: string) => emit('updateRenameValue', value), }); +// Sessions to render: all when expanded, otherwise only the first page. The +// collapse is a pure view-layer trim — data, cursor and hasMore stay intact, so +// re-expanding never refetches. +const visibleSessions = computed(() => + props.isExpanded(props.group.workspace.id) + ? props.group.sessions + : props.group.sessions.slice(0, props.group.initialCount), +); +// True once more than the first page is loaded — gates the show-less/show-all toggle. +const canToggleExpand = computed( + () => props.group.sessions.length > props.group.initialCount, +); +function showMoreCount(): number { + return Math.max(0, props.group.workspace.sessionCount - props.group.sessions.length); +} +function showAllCount(): number { + return props.group.sessions.length - props.group.initialCount; +} + // Hand the rename input element back to the parent's ref so Sidebar keeps // owning focus (startRenameWorkspace focuses renameInputRef on nextTick). Only // one group's input is mounted at a time, so sibling groups never collide. @@ -140,7 +163,7 @@ function onHeaderDragStart(event: DragEvent): void { :inert="isCollapsed(group.workspace.id)" > - {{ - group.loadingMore - ? t('sidebar.loadingMore') - : t('sidebar.showMore', { count: Math.max(0, group.workspace.sessionCount - group.sessions.length) }) - }} + + {{ + group.loadingMore ? t('sidebar.loadingMore') : t('sidebar.showMore', { count: showMoreCount() }) + }} + +
    {{ t('sidebar.noSessions') }}
    @@ -281,21 +315,37 @@ function onHeaderDragStart(event: DragEvent): void { color: var(--color-text-faint); font-family: var(--font-mono); } +/* Show-more / show-less — a session-row-shaped compact list control (§07). The + empty lead slot mirrors a session row's status gutter, so the label text lands + at the exact same x as the session titles (--sb-pad-x + --sb-gutter + --sb-gap + from the sidebar edge). Hover washes the row in the sunken surface, matching + New chat / session rows; no text recolor. */ .show-more { - display: block; + display: flex; + align-items: center; + gap: var(--sb-gap); width: 100%; - padding: var(--space-1) var(--space-2) var(--space-1) calc(var(--sb-pad-x) + var(--sb-gutter) + var(--sb-gap)); - background: none; + min-height: 26px; + margin: 0; + padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2)); border: none; + border-radius: var(--radius-md); + background: transparent; color: var(--color-text); + font-family: var(--font-ui); font-size: var(--text-xs); - font-family: var(--font-mono); - cursor: pointer; text-align: left; + cursor: pointer; } -.show-more:hover { - color: var(--color-accent-hover); - background: var(--color-surface-sunken); +.show-more:hover { background: var(--color-surface-sunken); } +.show-more:focus-visible { outline: none; box-shadow: var(--p-focus-ring); } +.show-more-lead { width: var(--sb-gutter); flex: none; } +.show-more-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Inline workspace rename input */ diff --git a/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue b/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue index 8dcd40cb3..6f6b3a267 100644 --- a/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue +++ b/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue @@ -93,6 +93,37 @@ function toggleCollapse(id: string): void { wsMenuFor.value = null; } +// --------------------------------------------------------------------------- +// In-group expand / collapse (show-more pagination) — mirrors the desktop +// sidebar. Local to the sheet; a refresh reloads only the first page. +// --------------------------------------------------------------------------- +const expandedIds = ref>(new Set()); + +function isExpanded(id: string): boolean { + return expandedIds.value.has(id); +} + +function toggleExpand(id: string): void { + const next = new Set(expandedIds.value); + if (next.has(id)) next.delete(id); + else next.add(id); + expandedIds.value = next; +} + +function visibleSessions(g: WorkspaceGroup): Session[] { + return isExpanded(g.workspace.id) ? g.sessions : g.sessions.slice(0, g.initialCount); +} + +function onLoadMore(id: string): void { + // Loading more should reveal the new rows immediately. + if (!expandedIds.value.has(id)) { + const next = new Set(expandedIds.value); + next.add(id); + expandedIds.value = next; + } + emit('loadMore', id); +} + function wsAttention(id: string): number { return props.attentionByWorkspace[id] ?? 0; } @@ -228,7 +259,7 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise {
    {{ t('sidebar.noSessions') }}
    { type="button" class="mshow-more" :disabled="g.loadingMore" - @click.stop="emit('loadMore', g.workspace.id)" + @click.stop="onLoadMore(g.workspace.id)" > {{ g.loadingMore @@ -267,6 +298,18 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise { : t('sidebar.showMore', { count: Math.max(0, g.workspace.sessionCount - g.sessions.length) }) }} +
    diff --git a/apps/kimi-web/src/composables/client/useWorkspaceState.ts b/apps/kimi-web/src/composables/client/useWorkspaceState.ts index 033fe1f68..b1906d061 100644 --- a/apps/kimi-web/src/composables/client/useWorkspaceState.ts +++ b/apps/kimi-web/src/composables/client/useWorkspaceState.ts @@ -45,6 +45,10 @@ import type { UseSideChat } from './useSideChat'; import type { UseTaskPoller } from './useTaskPoller'; const MESSAGES_PAGE_SIZE = 50; +// Sessions fetched per workspace on first load — keeps the initial request +// count at (number of workspaces) and each response small. Exported so the +// sidebar can fall back to it when a workspace's first-page size is unknown. +export const SESSIONS_INITIAL_PAGE_SIZE = 5; const PROMPT_NOT_FOUND_CODE = 40402; const WORKSPACE_NOT_FOUND_CODE = 40410; // Shared "already resolved" conflict (40902). The daemon reuses it for both @@ -327,9 +331,6 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta // Backend max page size for GET /sessions. Bigger pages mean fewer round-trips // when draining the full session list. const SESSION_PAGE_SIZE = 100; - // Sessions fetched per workspace on first load — keeps the initial request - // count at (number of workspaces) and each response small. - const SESSIONS_INITIAL_PAGE_SIZE = 5; // Sessions fetched per "load more" click within a workspace. const SESSIONS_LOAD_MORE_SIZE = 30; // On initial load, if the oldest session of the first page is still within @@ -429,6 +430,7 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta const fallback = await listAllSessionsGlobal().catch(() => [] as AppSession[]); rawState.sessionsHasMoreByWorkspace = {}; rawState.sessionsCursorByWorkspace = {}; + rawState.sessionsInitialCountByWorkspace = {}; rawState.sessionsFullyLoaded = true; return fallback; } @@ -443,6 +445,7 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta const loaded: AppSession[] = []; const hasMore: Record = {}; const cursors: Record = {}; + const counts: Record = {}; for (const { workspaceId, page } of pages) { loaded.push(...page.items); // Trust the server's hasMore — the per-workspace session_count is only a @@ -453,9 +456,13 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta // out of band cannot shift the cursor and skip intervening sessions. cursors[workspaceId] = page.items.length > 0 ? page.items[page.items.length - 1]!.id : undefined; + // First-page size = how many were loaded on first paint. Used by the + // sidebar's in-group "show less" control as the collapse target. + counts[workspaceId] = page.items.length; } rawState.sessionsHasMoreByWorkspace = hasMore; rawState.sessionsCursorByWorkspace = cursors; + rawState.sessionsInitialCountByWorkspace = counts; rawState.sessionsFullyLoaded = false; // Keep rawState.sessions newest-first for readers that pick sessions[0] // (e.g. auto-selecting the most recent session on first load). diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts index 5791758c4..79e2d29e1 100644 --- a/apps/kimi-web/src/composables/useKimiWebClient.ts +++ b/apps/kimi-web/src/composables/useKimiWebClient.ts @@ -33,7 +33,7 @@ import { useSoundNotification } from './client/useSoundNotification'; import { useTaskPoller } from './client/useTaskPoller'; import { useModelProviderState } from './client/useModelProviderState'; import { useSideChat } from './client/useSideChat'; -import { useWorkspaceState } from './client/useWorkspaceState'; +import { SESSIONS_INITIAL_PAGE_SIZE, useWorkspaceState } from './client/useWorkspaceState'; const appearance = useAppearance(); const notification = useNotification(); @@ -334,6 +334,9 @@ export interface ExtendedState extends KimiClientState { * the end of the last fetched page so a deep-linked older session appended * out of band does not shift the cursor and skip intervening sessions. */ sessionsCursorByWorkspace: Record; + /** Number of sessions loaded on first paint per workspace (the "first page"). + * Drives the sidebar's in-group show-less collapse target. */ + sessionsInitialCountByWorkspace: Record; /** True once every session has been loaded (after a search-triggered full drain). */ sessionsFullyLoaded: boolean; } @@ -375,6 +378,7 @@ const rawState: ExtendedState = reactive({ sessionsHasMoreByWorkspace: {}, sessionsLoadingMoreByWorkspace: {}, sessionsCursorByWorkspace: {}, + sessionsInitialCountByWorkspace: {}, sessionsFullyLoaded: false, }); @@ -2002,6 +2006,7 @@ const workspaceGroups = computed(() => { sessions: byId.get(w.id) ?? [], hasMore: rawState.sessionsHasMoreByWorkspace[w.id] ?? false, loadingMore: rawState.sessionsLoadingMoreByWorkspace[w.id] ?? false, + initialCount: rawState.sessionsInitialCountByWorkspace[w.id] ?? SESSIONS_INITIAL_PAGE_SIZE, })); }); diff --git a/apps/kimi-web/src/i18n/locales/en/sidebar.ts b/apps/kimi-web/src/i18n/locales/en/sidebar.ts index e70846a48..bb53bc6c0 100644 --- a/apps/kimi-web/src/i18n/locales/en/sidebar.ts +++ b/apps/kimi-web/src/i18n/locales/en/sidebar.ts @@ -31,7 +31,9 @@ export default { language: 'Language', daemon: 'Daemon', noSessions: 'No conversations yet', - showMore: 'Show more ({count})', + showMore: 'Load more ({count})', + showLess: 'Show less', + showAll: 'Show all ({count})', loadingMore: 'Loading…', collapseSidebar: 'Collapse sidebar', expandSidebar: 'Expand sidebar', diff --git a/apps/kimi-web/src/i18n/locales/zh/sidebar.ts b/apps/kimi-web/src/i18n/locales/zh/sidebar.ts index b76bb0954..95698ae77 100644 --- a/apps/kimi-web/src/i18n/locales/zh/sidebar.ts +++ b/apps/kimi-web/src/i18n/locales/zh/sidebar.ts @@ -31,7 +31,9 @@ export default { language: '语言', daemon: '后台', noSessions: '暂无对话', - showMore: '展开更多 ({count})', + showMore: '加载更多 ({count})', + showLess: '收起', + showAll: '展开 ({count})', loadingMore: '加载中…', collapseSidebar: '收起侧边栏', expandSidebar: '展开侧边栏', diff --git a/apps/kimi-web/src/types.ts b/apps/kimi-web/src/types.ts index 444e2eba8..9c5d95fe8 100644 --- a/apps/kimi-web/src/types.ts +++ b/apps/kimi-web/src/types.ts @@ -78,6 +78,9 @@ export interface WorkspaceGroup { hasMore: boolean; /** True while the next page of sessions is being fetched for this workspace. */ loadingMore: boolean; + /** Number of sessions loaded on first paint (the "first page"); the collapse + * target for the in-group show-less control. */ + initialCount: number; } /** Sidebar session-list scope: only the active workspace, or all workspaces. */ From ca6604992539fdbd1fbfa2ea31decc2d366a6d55 Mon Sep 17 00:00:00 2001 From: qer Date: Thu, 2 Jul 2026 21:22:48 +0800 Subject: [PATCH 2/3] fix(web): preserve first-page capacity for sparse workspaces The collapse target was seeded with the exact number of sessions loaded on first paint, which is 0 for an empty workspace and below a full page for a sparse one. Newly created sessions are prepended without bumping that count, so a workspace that was empty on load would hide its first new session behind a Show all control, and a sparse one would hide an older row on each new session even when it had never paged. Floor the collapse target at one full page so the first-page capacity is preserved. --- .../src/composables/client/useWorkspaceState.ts | 10 +++++++--- apps/kimi-web/src/composables/useKimiWebClient.ts | 4 ++-- apps/kimi-web/src/types.ts | 5 +++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/kimi-web/src/composables/client/useWorkspaceState.ts b/apps/kimi-web/src/composables/client/useWorkspaceState.ts index b1906d061..30d95d2ed 100644 --- a/apps/kimi-web/src/composables/client/useWorkspaceState.ts +++ b/apps/kimi-web/src/composables/client/useWorkspaceState.ts @@ -456,9 +456,13 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta // out of band cannot shift the cursor and skip intervening sessions. cursors[workspaceId] = page.items.length > 0 ? page.items[page.items.length - 1]!.id : undefined; - // First-page size = how many were loaded on first paint. Used by the - // sidebar's in-group "show less" control as the collapse target. - counts[workspaceId] = page.items.length; + // Collapse target for the sidebar's in-group "show less" control: the + // first-page capacity, floored at a full page so a workspace that was + // empty or sparse on first paint does not hide sessions created later. + // If the initial load pulled more than a page (recent-window + // continuations), keep the larger count so collapse returns to what was + // first visible. + counts[workspaceId] = Math.max(page.items.length, SESSIONS_INITIAL_PAGE_SIZE); } rawState.sessionsHasMoreByWorkspace = hasMore; rawState.sessionsCursorByWorkspace = cursors; diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts index 79e2d29e1..522562f59 100644 --- a/apps/kimi-web/src/composables/useKimiWebClient.ts +++ b/apps/kimi-web/src/composables/useKimiWebClient.ts @@ -334,8 +334,8 @@ export interface ExtendedState extends KimiClientState { * the end of the last fetched page so a deep-linked older session appended * out of band does not shift the cursor and skip intervening sessions. */ sessionsCursorByWorkspace: Record; - /** Number of sessions loaded on first paint per workspace (the "first page"). - * Drives the sidebar's in-group show-less collapse target. */ + /** First-page capacity per workspace (sessions loaded on first paint, floored + * at one full page). Drives the sidebar's in-group show-less collapse target. */ sessionsInitialCountByWorkspace: Record; /** True once every session has been loaded (after a search-triggered full drain). */ sessionsFullyLoaded: boolean; diff --git a/apps/kimi-web/src/types.ts b/apps/kimi-web/src/types.ts index 9c5d95fe8..52b1f4253 100644 --- a/apps/kimi-web/src/types.ts +++ b/apps/kimi-web/src/types.ts @@ -78,8 +78,9 @@ export interface WorkspaceGroup { hasMore: boolean; /** True while the next page of sessions is being fetched for this workspace. */ loadingMore: boolean; - /** Number of sessions loaded on first paint (the "first page"); the collapse - * target for the in-group show-less control. */ + /** First-page capacity for the in-group show-less collapse target: the number + * of sessions loaded on first paint, floored at one full page so a workspace + * that was empty or sparse does not hide sessions created later. */ initialCount: number; } From 2867814b2ebd57d7fb136ef7890945f3f5237498 Mon Sep 17 00:00:00 2001 From: qer Date: Thu, 2 Jul 2026 21:43:33 +0800 Subject: [PATCH 3/3] fix(web): keep the active session visible in a collapsed group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A collapsed workspace only rendered its first page, so an older session selected from outside the pagination flow — Cmd/Ctrl-K search (loadAllSessions) or a URL deep link (fetchSessionIntoList) — was marked active but had no visible row in the sidebar until the user manually clicked Show all. Include the active session in the collapsed view (appended in newest-first order) on both the desktop sidebar and the mobile switcher, so selection and search never navigate to a hidden row. --- .../src/components/WorkspaceGroup.vue | 19 +++++++++++++------ .../components/mobile/MobileSwitcherSheet.vue | 10 +++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/kimi-web/src/components/WorkspaceGroup.vue b/apps/kimi-web/src/components/WorkspaceGroup.vue index 07d854a33..d29e7356b 100644 --- a/apps/kimi-web/src/components/WorkspaceGroup.vue +++ b/apps/kimi-web/src/components/WorkspaceGroup.vue @@ -63,12 +63,19 @@ const renameValueModel = computed({ // Sessions to render: all when expanded, otherwise only the first page. The // collapse is a pure view-layer trim — data, cursor and hasMore stay intact, so -// re-expanding never refetches. -const visibleSessions = computed(() => - props.isExpanded(props.group.workspace.id) - ? props.group.sessions - : props.group.sessions.slice(0, props.group.initialCount), -); +// re-expanding never refetches. When collapsed, the active session is always +// kept visible: an older session selected via Cmd/Ctrl-K search or a URL deep +// link would otherwise be hidden past the first page, so navigation would land +// on a missing row. It appends in newest-first order (older than the head). +const visibleSessions = computed(() => { + if (props.isExpanded(props.group.workspace.id)) return props.group.sessions; + const head = props.group.sessions.slice(0, props.group.initialCount); + if (props.activeId && !head.some((s) => s.id === props.activeId)) { + const active = props.group.sessions.find((s) => s.id === props.activeId); + if (active) return [...head, active]; + } + return head; +}); // True once more than the first page is loaded — gates the show-less/show-all toggle. const canToggleExpand = computed( () => props.group.sessions.length > props.group.initialCount, diff --git a/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue b/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue index 6f6b3a267..da5d73cbd 100644 --- a/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue +++ b/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue @@ -111,7 +111,15 @@ function toggleExpand(id: string): void { } function visibleSessions(g: WorkspaceGroup): Session[] { - return isExpanded(g.workspace.id) ? g.sessions : g.sessions.slice(0, g.initialCount); + if (isExpanded(g.workspace.id)) return g.sessions; + const head = g.sessions.slice(0, g.initialCount); + // Keep the active session visible when it's beyond the first page (e.g. + // selected via search or a deep link), mirroring the desktop sidebar. + if (props.activeId && !head.some((s) => s.id === props.activeId)) { + const active = g.sessions.find((s) => s.id === props.activeId); + if (active) return [...head, active]; + } + return head; } function onLoadMore(id: string): void {