From 8de58eacbc1fef45358b8ecb799555790625b6e7 Mon Sep 17 00:00:00 2001 From: qer Date: Wed, 17 Jun 2026 01:59:24 +0800 Subject: [PATCH] feat(web): show session content summaries --- apps/kimi-web/src/components/SessionRow.vue | 18 +++++ .../src/components/SessionsDialog.vue | 15 +++- .../src/composables/useKimiWebClient.ts | 74 +++++++++++++------ apps/kimi-web/src/types.ts | 2 + apps/kimi-web/test/session-row.test.ts | 8 ++ .../useKimiWebClient-session-cache.test.ts | 15 ++++ 6 files changed, 106 insertions(+), 26 deletions(-) diff --git a/apps/kimi-web/src/components/SessionRow.vue b/apps/kimi-web/src/components/SessionRow.vue index 51e3e2786..b32f212fa 100644 --- a/apps/kimi-web/src/components/SessionRow.vue +++ b/apps/kimi-web/src/components/SessionRow.vue @@ -220,6 +220,10 @@ defineExpose({ closeMenu, cancelArchive }); + +
+ {{ session.summary }} +
@@ -289,6 +293,20 @@ defineExpose({ closeMenu, cancelArchive }); .ts { color: var(--muted); font-size: max(9px, calc(var(--ui-font-size) - 3.5px)); flex: none; } .se:hover .ts { display: none; } +.session-summary { + margin-top: 2px; + padding-left: calc(var(--sb-gutter, 16px) + var(--sb-gap, 6px)); + color: var(--muted); + font-size: max(9px, calc(var(--ui-font-size) - 3.5px)); + line-height: 1.35; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.se.on .session-summary { + color: var(--dim); +} + /* Pending tags — small coloured pills, one per kind. "Ask" reuses the Kimi-blue accent; "Approve" uses the warn tone so the two read as distinct at a glance. Fixed height + matching line-height keeps the text truly vertically centred diff --git a/apps/kimi-web/src/components/SessionsDialog.vue b/apps/kimi-web/src/components/SessionsDialog.vue index 07ab673ee..95ffbf5ab 100644 --- a/apps/kimi-web/src/components/SessionsDialog.vue +++ b/apps/kimi-web/src/components/SessionsDialog.vue @@ -40,7 +40,7 @@ const workspaceNameBySession = computed>(() => { }); // --------------------------------------------------------------------------- -// Search (filters by title, case-insensitive) +// Search (filters by title + local content summary, case-insensitive) // --------------------------------------------------------------------------- const query = ref(''); const searchRef = ref(null); @@ -48,7 +48,10 @@ const searchRef = ref(null); const filtered = computed(() => { 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([]);