Long chats stay fast; search in a chat; media overview

Performance (measured with 5,900 messages: open 9 s -> 0.7 s, typing 440 ms -> 25 ms)
- GET /sessions/{id}?limit=N loads the end of a chat (from the last summary
  on, at most N); /sessions/{id}/messages?before|after|around loads more
  (scrolling up stops at a summary unless asked). Without parameters the
  full history as before (terminal client).
- Chat view: memoised message list (typing, streaming and status changes no
  longer re-render the history), finished turns fold into "N steps · M files"
  with the answer visible, tool card bodies and summaries are built only when
  opened, scroll-to-bottom only when content changes.

Search: /sessions/{id}/search over the whole chat; a hit loads its part
(around) and marks the message, "back to the latest messages" returns.

Media: /sessions/{id}/media lists images, videos and audio of a chat and its
subagents (generated images, tool output, files written, media in answers,
attachments); ⋯ -> Media shows them with filters, a large view and a jump to
the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-11 07:32:18 +01:00
1 parent 6e9eda28b5
commit 3b0764450a
14 files changed
+1278 -99

No files matched your search

+39 -2
View File
@@ -32,6 +32,8 @@ export interface State {
/** Includes subagent sessions loaded on demand. */
sessionById: Record<string, Session>;
messages: Record<string, Message[]>;
/** Long chats are loaded in parts: are there older / newer messages than the loaded ones? */
windows: Record<string, { older: boolean; newer: boolean } | undefined>;
/** Sessions the current user may only read (shared with them). */
readOnly: Record<string, boolean>;
drafts: Record<string, Draft | undefined>;
@@ -58,6 +60,7 @@ let state: State = {
sessions: [],
sessionById: {},
messages: {},
windows: {},
readOnly: {},
drafts: {},
busy: {},
@@ -130,7 +133,8 @@ export function applyEvent(ev: ServerEvent) {
setState((s) => {
const list = s.messages[ev.session_id];
const drafts = s.drafts[ev.session_id]?.id === ev.message.id ? { ...s.drafts, [ev.session_id]: undefined } : s.drafts;
if (!list) return { drafts };
// Viewing an older part of the chat: the new message comes with "back to the end".
if (!list || s.windows[ev.session_id]?.newer) return { drafts };
if (list.some((m) => m.id === ev.message.id)) return { drafts };
return { messages: { ...s.messages, [ev.session_id]: [...list, ev.message] }, drafts };
});
@@ -231,9 +235,17 @@ export async function loadSessions() {
}));
}
/** Messages loaded when a chat opens (from the last summary on, at most this many). */
export const PAGE = 200;
type Page = { messages: Message[]; has_older: boolean; has_newer: boolean };
export async function loadSession(id: string) {
const data = await get<{ session: Session; messages: Message[]; busy: boolean; can_write: boolean; queue: QueueItem[]; step?: any }>(`/sessions/${id}`);
const data = await get<{ session: Session; messages: Message[]; busy: boolean; can_write: boolean; queue: QueueItem[]; step?: any; has_older?: boolean; has_newer?: boolean }>(
`/sessions/${id}?limit=${PAGE}`,
);
setState((s) => ({
windows: { ...s.windows, [id]: { older: !!data.has_older, newer: !!data.has_newer } },
...upsertSession(s, data.session),
sessionById: { ...s.sessionById, [id]: data.session },
messages: { ...s.messages, [id]: data.messages },
@@ -247,6 +259,31 @@ export async function loadSession(id: string) {
}));
}
/** Older messages before the loaded ones (scrolling up). Returns how many came. */
export async function loadOlder(id: string, stopAtSummary = true): Promise<number> {
const first = getState().messages[id]?.[0];
if (!first) return 0;
const page = await get<Page>(`/sessions/${id}/messages?before=${encodeURIComponent(first.id)}&limit=${PAGE}${stopAtSummary ? "&stop_at_summary=true" : ""}`);
setState((s) => {
const cur = s.messages[id] ?? [];
if (cur[0]?.id !== first.id) return {}; // changed meanwhile
return {
messages: { ...s.messages, [id]: [...page.messages, ...cur] },
windows: { ...s.windows, [id]: { older: page.has_older, newer: s.windows[id]?.newer ?? false } },
};
});
return page.messages.length;
}
/** Show the part of the chat around a message (search hit, media). */
export async function loadAround(id: string, messageId: string) {
const page = await get<Page>(`/sessions/${id}/messages?around=${encodeURIComponent(messageId)}&limit=${PAGE}`);
setState((s) => ({
messages: { ...s.messages, [id]: page.messages },
windows: { ...s.windows, [id]: { older: page.has_older, newer: page.has_newer } },
}));
}
/** After reconnects: reload everything that could have changed. */
export async function resync() {
const isAdmin = getState().me?.role === "admin";