feat(server): workspace_panes envelope + read_tab_by_number tool
Widen the sessions.workspace_panes JSONB from a bare WorkspacePane[] to a
WorkspaceState envelope { panes, tabNumbers, nextTabNumber, closedPaneStack }.
The PATCH validator accepts either the legacy array or the envelope (zod union)
and normalizes to a full envelope before storing, so existing array-shaped rows
migrate transparently on next write. The session_workspace_updated WS frame
schema is widened to match (kept byte-identical to the web copy; parity test
passes).
Adds read_tab_by_number, a read-only tool that resolves a session-scoped tab
number to its chat via the persisted tabNumbers map and returns that chat's
transcript (oldest-first, sentinels skipped, capped at 20k chars). Tools gain an
optional ToolExecCtx ({ sql, sessionId }) 4th param on ToolDef.execute, threaded
through executeToolCall from executeToolPhase; the param is optional so existing
filesystem tools and the apps/coder consumer stay compatible. Registered in
ALL_TOOLS + READ_ONLY_TOOL_NAMES.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,18 +28,20 @@ const HtmlArtifactStateZ = z.object({
|
||||
title: z.string().max(500),
|
||||
});
|
||||
|
||||
const PaneKindZ = z.enum([
|
||||
'chat',
|
||||
'terminal',
|
||||
'coder',
|
||||
'agent', // legacy alias — normalized to coder on write
|
||||
'empty',
|
||||
'settings',
|
||||
'markdown_artifact',
|
||||
'html_artifact',
|
||||
]);
|
||||
|
||||
const WorkspacePaneZ = z.object({
|
||||
id: z.string().min(1).max(200),
|
||||
kind: z.enum([
|
||||
'chat',
|
||||
'terminal',
|
||||
'coder',
|
||||
'agent', // legacy alias — normalized to coder on write
|
||||
'empty',
|
||||
'settings',
|
||||
'markdown_artifact',
|
||||
'html_artifact',
|
||||
]),
|
||||
kind: PaneKindZ,
|
||||
chatId: z.string().min(1).max(200).optional(),
|
||||
chatIds: z.array(z.string().min(1).max(200)).max(50),
|
||||
activeChatIdx: z.number().int(),
|
||||
@@ -47,8 +49,27 @@ const WorkspacePaneZ = z.object({
|
||||
html_artifact_state: HtmlArtifactStateZ.optional(),
|
||||
});
|
||||
|
||||
// v2.6.x: workspace_panes column widened from a bare WorkspacePane[] to a
|
||||
// WorkspaceState envelope (panes + stable session-scoped tab numbering +
|
||||
// reopen stack). closedPaneStack entries are lighter than full panes — just
|
||||
// the kind + chat ids needed to recreate a closed pane on reopen.
|
||||
const ClosedPaneEntryZ = z.object({
|
||||
kind: PaneKindZ,
|
||||
chatIds: z.array(z.string().min(1).max(200)).max(50),
|
||||
activeChatIdx: z.number().int(),
|
||||
});
|
||||
|
||||
const WorkspaceStateZ = z.object({
|
||||
panes: z.array(WorkspacePaneZ).max(10),
|
||||
tabNumbers: z.record(z.string(), z.number().int()).default({}),
|
||||
nextTabNumber: z.number().int().default(1),
|
||||
closedPaneStack: z.array(ClosedPaneEntryZ).max(10).default([]),
|
||||
});
|
||||
|
||||
// Accept either the legacy bare array OR the envelope. The handler normalizes
|
||||
// to a full envelope before storing (see MIGRATION rule in the PATCH handler).
|
||||
const WorkspacePanesBody = z.object({
|
||||
workspace_panes: z.array(WorkspacePaneZ).max(10),
|
||||
workspace_panes: z.union([z.array(WorkspacePaneZ).max(10), WorkspaceStateZ]),
|
||||
});
|
||||
|
||||
const PatchBody = z.object({
|
||||
@@ -308,12 +329,20 @@ export function registerSessionRoutes(
|
||||
reply.code(400);
|
||||
return { error: 'invalid body', details: parsed.error.flatten() };
|
||||
}
|
||||
const workspacePanes = parsed.data.workspace_panes.map((pane) =>
|
||||
// v2.6.x MIGRATION: the body is either a legacy bare WorkspacePane[] or
|
||||
// the WorkspaceState envelope. Normalize to a full envelope so the column
|
||||
// always stores the envelope shape going forward.
|
||||
const body = parsed.data.workspace_panes;
|
||||
const envelope = Array.isArray(body)
|
||||
? { panes: body, tabNumbers: {}, nextTabNumber: 1, closedPaneStack: [] }
|
||||
: body;
|
||||
// agent → coder normalization on the panes array (unchanged write rule).
|
||||
envelope.panes = envelope.panes.map((pane) =>
|
||||
pane.kind === 'agent' ? { ...pane, kind: 'coder' as const } : pane,
|
||||
);
|
||||
const rows = await sql<Session[]>`
|
||||
UPDATE sessions
|
||||
SET workspace_panes = ${sql.json(workspacePanes as never)},
|
||||
SET workspace_panes = ${sql.json(envelope as never)},
|
||||
updated_at = clock_timestamp()
|
||||
WHERE id = ${req.params.id}
|
||||
RETURNING id, project_id, name, model, system_prompt, status, created_at, updated_at,
|
||||
|
||||
Reference in New Issue
Block a user