feat(webchat): add session header sorting controls
This commit is contained in:
@@ -5636,6 +5636,17 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/tools/builtin/system-info.test.ts + pnpm typecheck passing"
|
"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": {
|
"overall_progress": {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ let _searchMode = false;
|
|||||||
let _slashPopupIndex = -1;
|
let _slashPopupIndex = -1;
|
||||||
let _elements = {};
|
let _elements = {};
|
||||||
let _pendingAttachments = [];
|
let _pendingAttachments = [];
|
||||||
|
let _sessionSort = 'recent';
|
||||||
|
|
||||||
// ── Slash Command Definitions ───────────────────────────────
|
// ── Slash Command Definitions ───────────────────────────────
|
||||||
|
|
||||||
@@ -80,6 +81,62 @@ function createTimestampEl(role, timestamp) {
|
|||||||
return ts;
|
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()) {
|
function createMessageEl(role, content, timestamp = Date.now()) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
|
|
||||||
@@ -549,7 +606,7 @@ async function loadSessions(client) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await client.call('sessions.list');
|
const result = await client.call('sessions.list');
|
||||||
const sessions = result.sessions ?? [];
|
const sessions = sortSessions(result.sessions ?? [], _sessionSort);
|
||||||
|
|
||||||
// Preserve current selection
|
// Preserve current selection
|
||||||
const current = _currentSession;
|
const current = _currentSession;
|
||||||
@@ -564,7 +621,9 @@ async function loadSessions(client) {
|
|||||||
for (const s of sessions) {
|
for (const s of sessions) {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = s.id;
|
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;}
|
if (s.id === current) {opt.selected = true;}
|
||||||
select.appendChild(opt);
|
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 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">
|
<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-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-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>
|
<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>
|
</div>
|
||||||
@@ -741,6 +805,7 @@ export const ChatPage = {
|
|||||||
|
|
||||||
_elements = {
|
_elements = {
|
||||||
sessionSelect: el.querySelector('#chat-session-select'),
|
sessionSelect: el.querySelector('#chat-session-select'),
|
||||||
|
sessionSort: el.querySelector('#chat-session-sort'),
|
||||||
messages: el.querySelector('#chat-messages'),
|
messages: el.querySelector('#chat-messages'),
|
||||||
input: el.querySelector('#chat-input'),
|
input: el.querySelector('#chat-input'),
|
||||||
sendBtn: el.querySelector('#chat-send'),
|
sendBtn: el.querySelector('#chat-send'),
|
||||||
@@ -759,6 +824,11 @@ export const ChatPage = {
|
|||||||
_currentSession = _elements.sessionSelect.value || null;
|
_currentSession = _elements.sessionSelect.value || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_elements.sessionSort.addEventListener('change', () => {
|
||||||
|
_sessionSort = _elements.sessionSort.value || 'recent';
|
||||||
|
loadSessions(client);
|
||||||
|
});
|
||||||
|
|
||||||
// Event: new session
|
// Event: new session
|
||||||
el.querySelector('#chat-new-session').addEventListener('click', async () => {
|
el.querySelector('#chat-new-session').addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
@@ -887,6 +957,7 @@ export const ChatPage = {
|
|||||||
_sending = false;
|
_sending = false;
|
||||||
_searchMode = false;
|
_searchMode = false;
|
||||||
_slashPopupIndex = -1;
|
_slashPopupIndex = -1;
|
||||||
|
_sessionSort = 'recent';
|
||||||
_elements = {};
|
_elements = {};
|
||||||
_pendingAttachments = [];
|
_pendingAttachments = [];
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user