feat(webchat): add session header sorting controls

This commit is contained in:
William Valentin
2026-02-18 18:10:18 -08:00
parent b0ae5f638b
commit 3647198295
2 changed files with 84 additions and 2 deletions
+11
View File
@@ -5636,6 +5636,17 @@
"docs/plans/state.json"
],
"test_status": "pnpm test:run src/tools/builtin/system-info.test.ts + pnpm typecheck passing"
},
"webchat-session-header-sorting": {
"status": "completed",
"date": "2026-02-19",
"updated": "2026-02-19",
"summary": "Added session sorting controls in WebChat header with modes for Recent activity, Most messages, and Name. Session titles in the selector were upgraded to include message count and last-active timestamp for faster scanning.",
"files_modified": [
"src/gateway/ui/pages/chat.js",
"docs/plans/state.json"
],
"test_status": "pnpm typecheck passing"
}
},
"overall_progress": {
+73 -2
View File
@@ -14,6 +14,7 @@ let _searchMode = false;
let _slashPopupIndex = -1;
let _elements = {};
let _pendingAttachments = [];
let _sessionSort = 'recent';
// ── Slash Command Definitions ───────────────────────────────
@@ -80,6 +81,62 @@ function createTimestampEl(role, timestamp) {
return ts;
}
function formatSessionLastActive(timestamp) {
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) {
return 'no activity';
}
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) {
return 'no activity';
}
const now = new Date();
const sameDay = now.getFullYear() === date.getFullYear()
&& now.getMonth() === date.getMonth()
&& now.getDate() === date.getDate();
if (sameDay) {
return `today ${new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date)}`;
}
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
}).format(date);
}
function sortSessions(sessions, mode) {
const copy = [...sessions];
if (mode === 'messages') {
copy.sort((a, b) => {
const byMessages = (b.messageCount ?? 0) - (a.messageCount ?? 0);
if (byMessages !== 0) {return byMessages;}
return String(a.id ?? '').localeCompare(String(b.id ?? ''));
});
return copy;
}
if (mode === 'name') {
copy.sort((a, b) => String(a.id ?? '').localeCompare(String(b.id ?? '')));
return copy;
}
// Default: most recent activity first, then message count, then name.
copy.sort((a, b) => {
const aTs = typeof a.lastMessageAt === 'number' ? a.lastMessageAt : 0;
const bTs = typeof b.lastMessageAt === 'number' ? b.lastMessageAt : 0;
const byTime = bTs - aTs;
if (byTime !== 0) {return byTime;}
const byMessages = (b.messageCount ?? 0) - (a.messageCount ?? 0);
if (byMessages !== 0) {return byMessages;}
return String(a.id ?? '').localeCompare(String(b.id ?? ''));
});
return copy;
}
function createMessageEl(role, content, timestamp = Date.now()) {
const wrapper = document.createElement('div');
@@ -549,7 +606,7 @@ async function loadSessions(client) {
try {
const result = await client.call('sessions.list');
const sessions = result.sessions ?? [];
const sessions = sortSessions(result.sessions ?? [], _sessionSort);
// Preserve current selection
const current = _currentSession;
@@ -564,7 +621,9 @@ async function loadSessions(client) {
for (const s of sessions) {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = `${s.id} (${s.messageCount} msgs)`;
const msgCount = s.messageCount ?? 0;
const msgLabel = msgCount === 1 ? '1 msg' : `${msgCount} msgs`;
opt.textContent = `${s.id} · ${msgLabel} · ${formatSessionLastActive(s.lastMessageAt)}`;
if (s.id === current) {opt.selected = true;}
select.appendChild(opt);
}
@@ -713,6 +772,11 @@ export const ChatPage = {
<div class="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-3rem)] max-w-3xl">
<div class="flex items-center gap-3 pb-3 border-b border-zinc-800 mb-3 flex-wrap">
<select id="chat-session-select" class="bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm outline-none focus:border-blue-500"></select>
<select id="chat-session-sort" class="bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm outline-none focus:border-blue-500" title="Sort sessions">
<option value="recent">Sort: Recent</option>
<option value="messages">Sort: Most messages</option>
<option value="name">Sort: Name</option>
</select>
<button id="chat-new-session" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors">+ New</button>
<button id="chat-load-history" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors">History</button>
</div>
@@ -741,6 +805,7 @@ export const ChatPage = {
_elements = {
sessionSelect: el.querySelector('#chat-session-select'),
sessionSort: el.querySelector('#chat-session-sort'),
messages: el.querySelector('#chat-messages'),
input: el.querySelector('#chat-input'),
sendBtn: el.querySelector('#chat-send'),
@@ -759,6 +824,11 @@ export const ChatPage = {
_currentSession = _elements.sessionSelect.value || null;
});
_elements.sessionSort.addEventListener('change', () => {
_sessionSort = _elements.sessionSort.value || 'recent';
loadSessions(client);
});
// Event: new session
el.querySelector('#chat-new-session').addEventListener('click', async () => {
try {
@@ -887,6 +957,7 @@ export const ChatPage = {
_sending = false;
_searchMode = false;
_slashPopupIndex = -1;
_sessionSort = 'recent';
_elements = {};
_pendingAttachments = [];
},