From 54e823e14045ca357405ef69371f25635ae40ede Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Jul 2026 10:45:09 +0200 Subject: [PATCH] fix: expose native session title summaries --- internal/workbench/http.ts | 15 ++++++++++++++- internal/workbench/workbench.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/internal/workbench/http.ts b/internal/workbench/http.ts index d28f85c4..b3f23624 100644 --- a/internal/workbench/http.ts +++ b/internal/workbench/http.ts @@ -76,7 +76,10 @@ function requireAuthorization(request: Request, options: { snapshot?: unknown; a async function nativeSessionList(options: { snapshot?: () => Promise; mode?: WorkbenchMode }, url: URL) { const state = await requiredSnapshot(options); const owner = text(url.searchParams.get("ownerUserId")); - const sessions = Object.values(state.sessions).filter((session: any) => !owner || session.ownerUserId === owner).sort((left: any, right: any) => String(right.updatedAt).localeCompare(String(left.updatedAt))); + const sessions = Object.values(state.sessions) + .filter((session: any) => !owner || session.ownerUserId === owner) + .map((session: any) => ({ ...session, firstUserMessagePreview: firstUserMessagePreview(session.messages) ?? (text(session.firstUserMessagePreview) || null) })) + .sort((left: any, right: any) => String(right.updatedAt).localeCompare(String(left.updatedAt))); return json(200, { ok: true, status: "ready", sessions, count: sessions.length, mode: options.mode ?? "native-test" }); } async function nativeSessionDetail(options: { snapshot?: () => Promise; mode?: WorkbenchMode }, sessionId: string, messagesOnly: boolean) { @@ -244,6 +247,16 @@ function nativeEnvelopeMatches(envelope: any, sessionId: string, traceId: string return (!sessionId || envelopeSessionId === sessionId) && (!traceId || envelopeTraceId === traceId); } +function firstUserMessagePreview(messages: unknown): string | null { + if (!Array.isArray(messages)) return null; + for (const message of messages) { + if (!message || typeof message !== "object" || text((message as any).role) !== "user") continue; + const preview = text((message as any).text ?? (message as any).content ?? (message as any).message); + if (preview) return preview.slice(0, 240); + } + return null; +} + function actorFrom(request: Request, nativeTest: boolean) { const id = text(request.headers.get("x-hwlab-actor-id")); if (!id && nativeTest) return { id: "usr_native", role: "user" }; diff --git a/internal/workbench/workbench.test.ts b/internal/workbench/workbench.test.ts index 97931278..0a81b02c 100644 --- a/internal/workbench/workbench.test.ts +++ b/internal/workbench/workbench.test.ts @@ -75,6 +75,34 @@ describe("Workbench Cloud application adapter", () => { }); describe("Workbench native HTTP adapter", () => { + test("native session list exposes the first user preview for summary-only rail loading", async () => { + const app = createWorkbenchHttpApp({ + async dispatch() { return { ok: true }; }, + async snapshot() { + return { + sessions: { + ses_title: { + sessionId: "ses_title", + updatedAt: "2026-07-20T01:00:00.000Z", + messages: [{ role: "user", content: "persisted native title", createdAt: "2026-07-20T00:59:00.000Z" }] + }, + ses_empty: { sessionId: "ses_empty", updatedAt: "2026-07-20T00:00:00.000Z", messages: [] } + }, + turns: {} + }; + } + }); + + const response = await app.fetch(new Request("http://native.test/v1/workbench/sessions")); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + sessions: [ + { sessionId: "ses_title", firstUserMessagePreview: "persisted native title" }, + { sessionId: "ses_empty", firstUserMessagePreview: null } + ] + }); + }); + test("authorizes only the Workbench navigation entry", async () => { const app = createWorkbenchHttpApp({ async dispatch() { return { ok: true }; },