diff --git a/docs/plans/state.json b/docs/plans/state.json index 7accf2a..e720e6f 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -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": { diff --git a/src/gateway/ui/pages/chat.js b/src/gateway/ui/pages/chat.js index b1757e1..70d5256 100644 --- a/src/gateway/ui/pages/chat.js +++ b/src/gateway/ui/pages/chat.js @@ -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 = {
+
@@ -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 = []; },