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:
1 parent
6e9eda28b5
commit
3b0764450a
14 files changed
+1278
-99
No files matched your search
+39
-2
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user