Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/web-session-list-paging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Page the web session list per workspace so the first screen no longer fetches every session up front.
3 changes: 3 additions & 0 deletions apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,8 @@ function openPr(url: string): void {
@rename-workspace="(id, name) => client.renameWorkspace(id, name)"
@delete-workspace="(id) => client.deleteWorkspace(id)"
@reorder-workspaces="client.reorderWorkspaces($event)"
@load-more-sessions="(id) => void client.loadMoreSessions(id)"
@load-all-sessions="void client.loadAllSessions()"
@select-workspaces="handleSelectWorkspaces"
@open-settings="showSettings = true"
@collapse="toggleSidebarCollapse"
Expand Down Expand Up @@ -899,6 +901,7 @@ function openPr(url: string): void {
@rename="(id, title) => client.renameSession(id, title)"
@archive="(id) => client.archiveSession(id)"
@delete-workspace="(id) => client.deleteWorkspace(id)"
@load-more="(id) => void client.loadMoreSessions(id)"
/>

<!-- Mobile settings bottom-sheet: session controls + app prefs + auth -->
Expand Down
4 changes: 2 additions & 2 deletions apps/kimi-web/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,8 +658,8 @@ export interface KimiWebApi {
openInApp(sessionId: string, appId: string, path: string, line?: number): Promise<void>;
connectEvents(handlers: KimiEventHandlers): KimiEventConnection;

// Workspaces + daemon folder browser
// PRESUMED — falls back until the daemon ships /workspaces, /fs:browse, /fs:home.
// Workspaces + daemon folder browser. /workspaces now ships and includes
// derived workspaces (cwds with sessions that were never explicitly registered).
listWorkspaces(): Promise<AppWorkspace[]>;
addWorkspace(input: { root: string; name?: string }): Promise<AppWorkspace>;
deleteWorkspace(id: string): Promise<void>;
Expand Down
56 changes: 12 additions & 44 deletions apps/kimi-web/src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The old workspace rail and workspace tabs have been removed;
workspace switching, folding and renaming all live in the group header. -->
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { serverEndpointLabel } from '../api/config';
import { copyTextToClipboard } from '../lib/clipboard';
Expand Down Expand Up @@ -58,6 +58,8 @@ const emit = defineEmits<{
renameWorkspace: [id: string, name: string];
deleteWorkspace: [id: string];
reorderWorkspaces: [ids: string[]];
loadMoreSessions: [workspaceId: string];
loadAllSessions: [];
openSettings: [];
collapse: [];
}>();
Expand Down Expand Up @@ -89,6 +91,12 @@ function onSelectResult(sessionId: string): void {
onSelectSession(sessionId);
}

// Sessions are loaded per-workspace (first page only). The first time the user
// searches, lazily drain the rest so the client-side filter covers everything.
watch(isSearching, (active) => {
if (active) emit('loadAllSessions');
});

// ---------------------------------------------------------------------------
// Collapse groups
// ---------------------------------------------------------------------------
Expand All @@ -100,15 +108,8 @@ function isCollapsed(id: string): boolean {

function toggleCollapse(id: string): void {
const next = new Set(collapsedIds.value);
if (next.has(id)) {
next.delete(id);
// Reset session expansion when workspace is expanded
const expandedNext = new Set(expandedWsIds.value);
expandedNext.delete(id);
expandedWsIds.value = expandedNext;
} else {
next.add(id);
}
if (next.has(id)) next.delete(id);
else next.add(id);
collapsedIds.value = next;
saveCollapsedWorkspaces(next);
}
Expand Down Expand Up @@ -161,37 +162,6 @@ function onGroupDrop(targetId: string): void {
emit('reorderWorkspaces', next);
}

// ---------------------------------------------------------------------------
// Session list truncation per workspace
// ---------------------------------------------------------------------------
const DEFAULT_VISIBLE_COUNT = 10;

/** workspace id → true = show all sessions */
const expandedWsIds = ref<Set<string>>(new Set());

function isExpanded(wsId: string): boolean {
return expandedWsIds.value.has(wsId);
}

function toggleExpand(wsId: string): void {
const next = new Set(expandedWsIds.value);
if (next.has(wsId)) next.delete(wsId);
else next.add(wsId);
expandedWsIds.value = next;
}

/** Show the most recent N sessions. If the active session is older than N,
replace the last slot with it so the highlight never disappears. */
function visibleSessions(sessions: Session[], expanded: boolean, activeId?: string): Session[] {
if (expanded || sessions.length <= DEFAULT_VISIBLE_COUNT) return sessions;
const visible = sessions.slice(0, DEFAULT_VISIBLE_COUNT);
if (activeId && !visible.some((s) => s.id === activeId)) {
const active = sessions.find((s) => s.id === activeId);
if (active) visible[DEFAULT_VISIBLE_COUNT - 1] = active;
}
return visible;
}

// ---------------------------------------------------------------------------
// Shift-multi-select workspaces
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -603,8 +573,6 @@ function blinkOnce(): void {
:ws-menu-open-id="wsMenuOpenId"
:dragging="draggingWsId === g.workspace.id"
:is-collapsed="isCollapsed"
:is-expanded="isExpanded"
:visible-sessions="visibleSessions"
@group-click="handleGhClick"
@group-contextmenu="openGhMenu"
@toggle-ws-menu="toggleWsMenu"
Expand All @@ -613,7 +581,7 @@ function blinkOnce(): void {
@rename-session="(id, title) => emit('rename', id, title)"
@archive-session="(id) => emit('archive', id)"
@fork-session="(id) => emit('fork', id)"
@toggle-expand="toggleExpand"
@load-more="(id) => emit('loadMoreSessions', id)"
@confirm-rename="confirmRenameWorkspace"
@cancel-rename="cancelRenameWorkspace"
@update-rename-value="onUpdateRenameValue"
Expand Down
19 changes: 11 additions & 8 deletions apps/kimi-web/src/components/WorkspaceGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<script setup lang="ts">
import { computed, type ComponentPublicInstance, type Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { Session, WorkspaceGroup, WorkspaceView } from '../types';
import type { WorkspaceGroup, WorkspaceView } from '../types';
import SessionRow from './SessionRow.vue';

const { t } = useI18n();
Expand All @@ -26,8 +26,6 @@ const props = defineProps<{
/** True while this group is the active drag source (drag-to-reorder). */
dragging: boolean;
isCollapsed: (id: string) => boolean;
isExpanded: (id: string) => boolean;
visibleSessions: (sessions: Session[], expanded: boolean, activeId?: string) => Session[];
}>();

const emit = defineEmits<{
Expand All @@ -39,7 +37,7 @@ const emit = defineEmits<{
renameSession: [id: string, title: string];
archiveSession: [id: string];
forkSession: [id: string];
toggleExpand: [workspaceId: string];
loadMore: [workspaceId: string];
confirmRename: [];
cancelRename: [];
updateRenameValue: [value: string];
Expand Down Expand Up @@ -157,7 +155,7 @@ function onHeaderDragStart(event: DragEvent): void {
</div>
<div v-show="!isCollapsed(group.workspace.id)" class="group-sessions">
<SessionRow
v-for="s in visibleSessions(group.sessions, isExpanded(group.workspace.id), activeId)"
v-for="s in group.sessions"
:key="s.id"
:session="s"
:active="s.id === activeId"
Expand All @@ -170,11 +168,16 @@ function onHeaderDragStart(event: DragEvent): void {
@fork="emit('forkSession', $event)"
/>
<button
v-if="!isExpanded(group.workspace.id) && visibleSessions(group.sessions, false, activeId).length < group.sessions.length"
v-if="group.hasMore || group.loadingMore"
class="show-more"
@click.stop="emit('toggleExpand', group.workspace.id)"
:disabled="group.loadingMore"
@click.stop="emit('loadMore', group.workspace.id)"
>
{{ t('sidebar.showMore', { count: group.sessions.length - visibleSessions(group.sessions, false, activeId).length }) }}
{{
group.loadingMore
? t('sidebar.loadingMore')
: t('sidebar.showMore', { count: Math.max(0, group.workspace.sessionCount - group.sessions.length) })
}}
</button>
<div v-if="group.sessions.length === 0" class="group-empty">{{ t('sidebar.noSessions') }}</div>
</div>
Expand Down
66 changes: 12 additions & 54 deletions apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const emit = defineEmits<{
archive: [id: string];
/** NOTE: needs `@delete-workspace="client.deleteWorkspace($event)"` wiring in App.vue. */
deleteWorkspace: [workspaceId: string];
loadMore: [workspaceId: string];
}>();

function close(): void {
Expand Down Expand Up @@ -77,15 +78,8 @@ function isCollapsed(id: string): boolean {

function toggleCollapse(id: string): void {
const next = new Set(collapsedIds.value);
if (next.has(id)) {
next.delete(id);
// Reset session expansion when the workspace is expanded (desktop parity)
const expandedNext = new Set(expandedWsIds.value);
expandedNext.delete(id);
expandedWsIds.value = expandedNext;
} else {
next.add(id);
}
if (next.has(id)) next.delete(id);
else next.add(id);
collapsedIds.value = next;
// Tapping a header also dismisses any open row/workspace menu.
menuFor.value = null;
Expand All @@ -96,47 +90,6 @@ function wsAttention(id: string): number {
return props.attentionByWorkspace[id] ?? 0;
}

// ---------------------------------------------------------------------------
// Session list truncation per workspace (desktop sidebar parity):
// default visible = union of (first 5) and (updated within 5 days), and the
// active session is always kept visible.
// ---------------------------------------------------------------------------
const DEFAULT_VISIBLE_COUNT = 5;
const FIVE_DAYS_MS = 5 * 24 * 60 * 60 * 1000;

/** workspace id → true = show all sessions */
const expandedWsIds = ref<Set<string>>(new Set());

function isExpanded(wsId: string): boolean {
return expandedWsIds.value.has(wsId);
}

function toggleExpand(wsId: string): void {
const next = new Set(expandedWsIds.value);
if (next.has(wsId)) next.delete(wsId);
else next.add(wsId);
expandedWsIds.value = next;
}

function visibleSessions(sessions: Session[], expanded: boolean, activeId?: string): Session[] {
if (expanded || sessions.length <= DEFAULT_VISIBLE_COUNT) return sessions;
const now = Date.now();
const cutoff = now - FIVE_DAYS_MS;
const recent5 = sessions.slice(0, DEFAULT_VISIBLE_COUNT);
const recent5Ids = new Set(recent5.map((s) => s.id));
const within5Days = sessions.filter((s) => {
if (recent5Ids.has(s.id)) return false;
const ts = s.updatedAt ? Date.parse(s.updatedAt) : 0;
return ts > cutoff;
});
const visible = [...recent5, ...within5Days];
if (activeId && !visible.some((s) => s.id === activeId)) {
const active = sessions.find((s) => s.id === activeId);
if (active) visible.push(active);
}
return visible;
}

// ---------------------------------------------------------------------------
// Per-row kebab menu (rename / archive) — opened from the ⋯ button.
// Archiving is two-step: the first tap arms the item ("Archive session?"),
Expand Down Expand Up @@ -312,7 +265,7 @@ onUnmounted(() => {
<div v-show="!isCollapsed(g.workspace.id)">
<div v-if="g.sessions.length === 0" class="mempty small">{{ t('sidebar.noSessions') }}</div>
<div
v-for="s in visibleSessions(g.sessions, isExpanded(g.workspace.id), activeId)"
v-for="s in g.sessions"
:key="s.id"
class="srow"
:class="{ cur: s.id === activeId }"
Expand Down Expand Up @@ -345,12 +298,17 @@ onUnmounted(() => {
</div>
</div>
<button
v-if="!isExpanded(g.workspace.id) && visibleSessions(g.sessions, false, activeId).length < g.sessions.length"
v-if="g.hasMore || g.loadingMore"
type="button"
class="mshow-more"
@click.stop="toggleExpand(g.workspace.id)"
:disabled="g.loadingMore"
@click.stop="emit('loadMore', g.workspace.id)"
>
{{ t('sidebar.showMore', { count: g.sessions.length - visibleSessions(g.sessions, false, activeId).length }) }}
{{
g.loadingMore
? t('sidebar.loadingMore')
: t('sidebar.showMore', { count: Math.max(0, g.workspace.sessionCount - g.sessions.length) })
}}
</button>
</div>
</div>
Expand Down
Loading
Loading