mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(web): show session content summaries
This commit is contained in:
parent
cf722fafbb
commit
8de58eacbc
6 changed files with 106 additions and 26 deletions
|
|
@ -220,6 +220,10 @@ defineExpose({ closeMenu, cancelArchive });
|
|||
<button class="menu-item" @click.stop="forkRow">{{ t('sidebar.fork') }}</button>
|
||||
<button class="menu-item archive" @click.stop="startArchive">{{ t('sidebar.archive') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="session.summary && !confirming" class="session-summary" :title="session.summary">
|
||||
{{ session.summary }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const workspaceNameBySession = computed<Record<string, string>>(() => {
|
|||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search (filters by title, case-insensitive)
|
||||
// Search (filters by title + local content summary, case-insensitive)
|
||||
// ---------------------------------------------------------------------------
|
||||
const query = ref('');
|
||||
const searchRef = ref<HTMLInputElement | null>(null);
|
||||
|
|
@ -48,7 +48,10 @@ const searchRef = ref<HTMLInputElement | null>(null);
|
|||
const filtered = computed<Session[]>(() => {
|
||||
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(() => {
|
|||
<span class="dot" :class="dotClass(s)" />
|
||||
<div class="meta">
|
||||
<span class="title">{{ s.title }}</span>
|
||||
<span v-if="s.summary" class="summary" :title="s.summary">{{ s.summary }}</span>
|
||||
<span class="sub">
|
||||
<span class="ws">{{ workspaceNameBySession[s.id] ?? t('sessions.noWorkspace') }}</span>
|
||||
<span class="sep">·</span>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Workspace>(() => {
|
|||
|
||||
const sessions = computed<Session[]>(() => {
|
||||
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<string>(() => rawState.activeSessionId ?? '');
|
||||
|
|
@ -2393,15 +2434,7 @@ const sessionsForView = computed<Session[]>(() => {
|
|||
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<WorkspaceGroup[]>(() => {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue