(() => {
const q = query.value.toLowerCase().trim();
if (!q) return props.sessions;
- return props.sessions.filter((s) => s.title.toLowerCase().includes(q));
+ return props.sessions.filter((s) =>
+ s.title.toLowerCase().includes(q) ||
+ (s.summary?.toLowerCase().includes(q) ?? false),
+ );
});
const selectedIdx = ref(0);
@@ -152,6 +155,7 @@ onUnmounted(() => {
{{ s.title }}
+ {{ s.summary }}
{{ workspaceNameBySession[s.id] ?? t('sessions.noWorkspace') }}
·
@@ -311,6 +315,13 @@ onUnmounted(() => {
text-overflow: ellipsis;
white-space: nowrap;
}
+.summary {
+ color: var(--dim);
+ font-size: max(9px, calc(var(--ui-font-size) - 3px));
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
.sub {
display: flex;
align-items: center;
diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts
index 31db5fc72..9906bae16 100644
--- a/apps/kimi-web/src/composables/useKimiWebClient.ts
+++ b/apps/kimi-web/src/composables/useKimiWebClient.ts
@@ -1762,6 +1762,53 @@ function toUiTask(task: AppTask): TaskItem {
};
}
+function compactSessionSummary(text: string): string | undefined {
+ const normalized = text
+ .replace(/```[\s\S]*?```/g, ' code block ')
+ .replace(/`([^`]+)`/g, '$1')
+ .replace(/\s+/g, ' ')
+ .trim();
+ if (!normalized) return undefined;
+ return normalized.length > 110 ? `${normalized.slice(0, 107).trimEnd()}...` : normalized;
+}
+
+function messageTextForSessionSummary(message: AppMessage): string | undefined {
+ const text = message.content
+ .filter((part) => part.type === 'text')
+ .map((part) => part.text)
+ .join(' ')
+ .trim();
+ return compactSessionSummary(text);
+}
+
+function sessionContentSummary(sessionId: string): string | undefined {
+ const hiddenIds = new Set(rawState.sideChatUserMessageIdsBySession[sessionId] ?? []);
+ const messages = (rawState.messagesBySession[sessionId] ?? []).filter((m) => !hiddenIds.has(m.id));
+ for (const message of messages) {
+ if (message.role !== 'user') continue;
+ const text = messageTextForSessionSummary(message);
+ if (text) return text;
+ }
+ for (const message of messages.toReversed()) {
+ if (message.role !== 'assistant') continue;
+ const text = messageTextForSessionSummary(message);
+ if (text) return text;
+ }
+ return undefined;
+}
+
+function toSessionView(session: AppSession): Session {
+ return {
+ id: session.id,
+ title: session.title,
+ summary: sessionContentSummary(session.id),
+ time: formatTime(session.updatedAt, session.status),
+ status: session.status,
+ busy: isSessionEffectivelyRunning(session.id),
+ updatedAt: session.updatedAt,
+ };
+}
+
// ---------------------------------------------------------------------------
// Computed view props
// ---------------------------------------------------------------------------
@@ -1777,13 +1824,7 @@ const workspace = computed(() => {
const sessions = computed(() => {
void sessionTimeClock.value;
- return rawState.sessions.map((s) => ({
- id: s.id,
- title: s.title,
- time: formatTime(s.updatedAt, s.status),
- status: s.status,
- busy: isSessionEffectivelyRunning(s.id),
- }));
+ return rawState.sessions.map(toSessionView);
});
const activeSessionId = computed(() => rawState.activeSessionId ?? '');
@@ -2393,15 +2434,7 @@ const sessionsForView = computed(() => {
void sessionTimeClock.value;
// Child ("side chat") sessions never appear in the main list — they live in
// the side-chat panel only.
- return rawState.sessions
- .filter((s) => !s.parentSessionId)
- .map((s) => ({
- id: s.id,
- title: s.title,
- time: formatTime(s.updatedAt, s.status),
- status: s.status,
- busy: isSessionEffectivelyRunning(s.id),
- }));
+ return rawState.sessions.filter((s) => !s.parentSessionId).map(toSessionView);
});
/** Per-workspace groups for the 'all workspaces' scope. */
@@ -2411,14 +2444,7 @@ const workspaceGroups = computed(() => {
for (const s of rawState.sessions) {
if (s.parentSessionId) continue; // child sessions stay out of the list
const wid = workspaceIdForSession(s);
- const view: Session = {
- id: s.id,
- title: s.title,
- time: formatTime(s.updatedAt, s.status),
- status: s.status,
- busy: isSessionEffectivelyRunning(s.id),
- updatedAt: s.updatedAt,
- };
+ const view = toSessionView(s);
const list = byId.get(wid) ?? [];
list.push(view);
byId.set(wid, list);
diff --git a/apps/kimi-web/src/types.ts b/apps/kimi-web/src/types.ts
index ee67d414c..045ba2d20 100644
--- a/apps/kimi-web/src/types.ts
+++ b/apps/kimi-web/src/types.ts
@@ -8,6 +8,8 @@ export type SessionStatus = AppSessionStatus;
export interface Session {
id: string;
title: string;
+ /** One-line local preview from already-loaded conversation content. */
+ summary?: string;
time: string;
status: SessionStatus;
/** True only when the session should show a "working" spinner: it is
diff --git a/apps/kimi-web/test/session-row.test.ts b/apps/kimi-web/test/session-row.test.ts
index 70a00b70f..3622f74db 100644
--- a/apps/kimi-web/test/session-row.test.ts
+++ b/apps/kimi-web/test/session-row.test.ts
@@ -56,4 +56,12 @@ describe('SessionRow status / busy', () => {
expect(w.find('.tag-ask').exists()).toBe(false);
expect(w.find('.tag-aborted').exists()).toBe(false);
});
+
+ it('shows a one-line content summary when available', () => {
+ const w = row({ summary: 'Fix dark-mode markdown code rendering' });
+ const summary = w.find('.session-summary');
+ expect(summary.exists()).toBe(true);
+ expect(summary.text()).toBe('Fix dark-mode markdown code rendering');
+ expect(summary.attributes('title')).toBe('Fix dark-mode markdown code rendering');
+ });
});
diff --git a/apps/kimi-web/test/useKimiWebClient-session-cache.test.ts b/apps/kimi-web/test/useKimiWebClient-session-cache.test.ts
index ea9d5b2d9..38e5070fc 100644
--- a/apps/kimi-web/test/useKimiWebClient-session-cache.test.ts
+++ b/apps/kimi-web/test/useKimiWebClient-session-cache.test.ts
@@ -150,6 +150,21 @@ describe('useKimiWebClient session memory cache', () => {
});
});
+ it('adds a local content summary to loaded session rows', async () => {
+ const initial: AppMessage = {
+ id: 'msg_1',
+ sessionId: 'sess_1',
+ role: 'user',
+ content: [{ type: 'text', text: 'Please review this\n\n```ts\nconst value = 1;\n```\n\nand explain the risk.' }],
+ createdAt: now,
+ };
+ const { client } = await setup([initial]);
+
+ await client.createSession('/repo');
+
+ expect(client.sessionsForView.value[0]?.summary).toBe('Please review this code block and explain the risk.');
+ });
+
it('does not raise the loading state for a locally created session', async () => {
const { client } = await setup([]);