Skip to content

Commit 57befcd

Browse files
fix: count conversation messages in web sidebar (#782)
1 parent 13fe6b9 commit 57befcd

2 files changed

Lines changed: 84 additions & 5 deletions

File tree

internal/serveui/static/app-render.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ let sidebarRenderKey = '';
144144

145145
const sidebarScrollContainer = () => elements.sidebarContent || elements.sessionGroups?.parentElement || null;
146146

147+
const conversationMessageCount = (session) => {
148+
if (typeof session?.messageCount === 'number' && Number.isFinite(session.messageCount)) {
149+
return Math.max(session.messageCount, 0);
150+
}
151+
const messages = Array.isArray(session?.messages) ? session.messages : [];
152+
return messages.filter((message) => message?.role === 'user' || message?.role === 'assistant').length;
153+
};
154+
155+
const formatSessionMessageCount = (session) => {
156+
const count = conversationMessageCount(session);
157+
return `${count} message${count === 1 ? '' : 's'}`;
158+
};
159+
147160
const restoreSidebarScroll = (scroller, scrollTop) => {
148161
if (!scroller || !Number.isFinite(scrollTop)) return;
149162
if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
@@ -170,7 +183,7 @@ const computeSidebarKey = (sorted) =>
170183
[
171184
s.id, s.title, s.longTitle || '', s.searchSnippet || '',
172185
s.pinned ? 1 : 0, s.archived ? 1 : 0,
173-
s.messageCount || s.messages.length || 0,
186+
conversationMessageCount(s),
174187
s.lastMessageAt || s.created,
175188
sessionHasInProgressState(s) ? 1 : 0,
176189
s.id === state.activeSessionId ? 1 : 0
@@ -212,8 +225,7 @@ const buildCachedSessionRow = (session) => {
212225
metaEl.textContent = session.searchSnippet;
213226
metaEl.title = session.searchSnippet;
214227
} else {
215-
const msgCount = session.messageCount || session.messages.filter(m => m.role !== 'tool-group').length || 0;
216-
const metaParts = [`${msgCount} message${msgCount === 1 ? '' : 's'}`];
228+
const metaParts = [formatSessionMessageCount(session)];
217229
if (session.archived) metaParts.push('hidden');
218230
const activityAt = session.lastMessageAt || session.created;
219231
metaParts.push(relativeTime(activityAt));
@@ -323,8 +335,7 @@ const updateCachedSessionRow = (session, cached) => {
323335
newMeta = session.searchSnippet;
324336
newMetaTitle = session.searchSnippet;
325337
} else {
326-
const msgCount = session.messageCount || session.messages.filter(m => m.role !== 'tool-group').length || 0;
327-
const metaParts = [`${msgCount} message${msgCount === 1 ? '' : 's'}`];
338+
const metaParts = [formatSessionMessageCount(session)];
328339
if (session.archived) metaParts.push('hidden');
329340
const activityAt = session.lastMessageAt || session.created;
330341
metaParts.push(relativeTime(activityAt));

internal/serveui/static/app_render_test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,74 @@ async function run(name, fn) {
10171017
assertEqual(rows[1].querySelector('.session-title').textContent, 'Beta', 'second row title');
10181018
});
10191019

1020+
await run('renderSidebar trusts server conversation message count over loaded client rows', () => {
1021+
const sessions = [
1022+
{ id: 'a', title: 'Alpha', created: 2000, pinned: false, archived: false, lastMessageAt: 2000, messageCount: 2, messages: [
1023+
{ id: 'u1', role: 'user', content: 'run tools' },
1024+
{ id: 't1', role: 'tool', name: 'grep' },
1025+
{ id: 't2', role: 'tool', name: 'read_file' },
1026+
{ id: 't3', role: 'tool', name: 'shell' },
1027+
{ id: 't4', role: 'tool', name: 'edit_file' },
1028+
{ id: 'a1', role: 'assistant', content: 'done' },
1029+
] },
1030+
];
1031+
const { app } = createHarness({ visibleSessions: () => sessions });
1032+
1033+
app.renderSidebar();
1034+
1035+
const metaEl = app.elements.sessionGroups.querySelector('.session-meta');
1036+
assert(metaEl.textContent.startsWith('2 messages'), 'meta uses server conversation count');
1037+
});
1038+
1039+
await run('renderSidebar counts only user and assistant messages for unsynced local sessions', () => {
1040+
const sessions = [
1041+
{ id: 'a', title: 'Alpha', created: 2000, pinned: false, archived: false, lastMessageAt: 2000, messages: [
1042+
{ id: 'u1', role: 'user', content: 'run tools' },
1043+
{ id: 't1', role: 'tool', name: 'grep' },
1044+
{ id: 'tg1', role: 'tool-group', tools: [{ id: 't2' }, { id: 't3' }, { id: 't4' }] },
1045+
{ id: 'a1', role: 'assistant', content: 'done' },
1046+
] },
1047+
];
1048+
const { app } = createHarness({ visibleSessions: () => sessions });
1049+
1050+
app.renderSidebar();
1051+
1052+
const metaEl = app.elements.sessionGroups.querySelector('.session-meta');
1053+
assert(metaEl.textContent.startsWith('2 messages'), 'meta counts user/assistant only');
1054+
assert(!metaEl.textContent.includes('/'), 'meta does not show raw/tool count');
1055+
});
1056+
1057+
await run('renderSidebar trusts explicit zero server conversation message count', () => {
1058+
const sessions = [
1059+
{ id: 'a', title: 'Alpha', created: 2000, pinned: false, archived: false, lastMessageAt: 2000, messageCount: 0, messages: [
1060+
{ id: 'u1', role: 'user', content: 'locally loaded but server says zero' },
1061+
{ id: 'a1', role: 'assistant', content: 'locally loaded but server says zero' },
1062+
] },
1063+
];
1064+
const { app } = createHarness({ visibleSessions: () => sessions });
1065+
1066+
app.renderSidebar();
1067+
1068+
const metaEl = app.elements.sessionGroups.querySelector('.session-meta');
1069+
assert(metaEl.textContent.startsWith('0 messages'), 'explicit server zero is authoritative');
1070+
});
1071+
1072+
await run('renderSidebar falls back to local count when server count is absent', () => {
1073+
const sessions = [
1074+
{ id: 'a', title: 'Alpha', created: 2000, pinned: false, archived: false, lastMessageAt: 2000, messageCount: null, messages: [
1075+
{ id: 'u1', role: 'user', content: 'run tools' },
1076+
{ id: 'tg1', role: 'tool-group', tools: [{ id: 't1' }, { id: 't2' }, { id: 't3' }, { id: 't4' }] },
1077+
{ id: 'a1', role: 'assistant', content: 'done' },
1078+
] },
1079+
];
1080+
const { app } = createHarness({ visibleSessions: () => sessions });
1081+
1082+
app.renderSidebar();
1083+
1084+
const metaEl = app.elements.sessionGroups.querySelector('.session-meta');
1085+
assert(metaEl.textContent.startsWith('2 messages'), 'null server count falls back to local conversation count');
1086+
});
1087+
10201088
await run('renderSidebar skips re-render when nothing changed', () => {
10211089
const session = { id: 'a', title: 'Alpha', created: 1000, messages: [], pinned: false, archived: false, messageCount: 2, lastMessageAt: 1000 };
10221090
const { app } = createHarness({ visibleSessions: () => [session] });

0 commit comments

Comments
 (0)