load chat transcripts as a tail window and lazy-load older on scroll-up

extremely long transcripts made bottom-anchoring the virtualized list unreliable
(thousands of unmeasured variable-height items = a huge estimate the scroll never
lands on). now GET /chat/sessions/:id takes limit+before and returns a windowed
slice plus total+offset. the chat opens on the last 20 messages, anchors to the
bottom instantly, and scrolling near the top pages in the next older window,
prepending it and pinning the previously-top message so the view stays put.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 03:49:13 +00:00
co-authored by Claude Opus 4.8
parent 3682269936
commit b27dd7512b
7 changed files with 173 additions and 18 deletions
+17 -2
View File
@@ -46,7 +46,12 @@ chatRouter.get('/sessions', async (ctx) => {
return ctx.json({ sessions });
});
// GET /chat/sessions/:id[?cwd=] — one conversation's transcript. Routes by harness (ses_ = OpenCode).
// GET /chat/sessions/:id[?cwd=][&limit=&before=] — one conversation's transcript. Routes by harness
// (ses_ = OpenCode). Transcripts get extremely long, so the client opens at the tail and pages upward:
// `limit` caps how many messages come back and `before` (absolute index into the full transcript,
// exclusive) selects the window's upper bound. Absent params return the whole transcript (legacy). The
// response carries `total` (full length) and `offset` (absolute index of messages[0]) so the client knows
// where the window sits and whether older messages remain above it.
chatRouter.get('/sessions/:id', async (ctx) => {
const email = ctx.get('user').email;
const id = ctx.req.param('id');
@@ -57,7 +62,17 @@ chatRouter.get('/sessions/:id', async (ctx) => {
? await loadOpenCodeSession(id)
: (loadClaudeSession(email, cwd, id) ?? loadClaudeSessionById(email, id));
if (!detail) return ctx.text('Not found', 404);
return ctx.json(detail);
const total = detail.messages.length;
const limitRaw = ctx.req.query('limit');
const beforeRaw = ctx.req.query('before');
const limit = limitRaw != null ? Math.max(0, parseInt(limitRaw, 10) || 0) : null;
const end = beforeRaw != null ? Math.min(total, Math.max(0, parseInt(beforeRaw, 10) || 0)) : total;
const start = limit != null ? Math.max(0, end - limit) : 0;
const windowed = limit != null || beforeRaw != null;
const messages = windowed ? detail.messages.slice(start, end) : detail.messages;
return ctx.json({ ...detail, messages, total, offset: windowed ? start : 0 });
});
// DELETE /chat/sessions/:id[?cwd=] — remove a conversation from the owning harness's store.