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:
2026-05-31 02:14:42 +00:00
parent e857815d79
commit d05f73be26
5 changed files with 226 additions and 17 deletions

View File

@@ -2,6 +2,7 @@ import type { Agent, Session, ToolCall } from '../../types/api.js';
import * as modelContext from '../model-context.js';
import { PathScopeError } from '../path_guard.js';
import { TOOLS_BY_NAME } from '../tools.js';
import type { ToolExecCtx } from '../tools.js';
import { matchToolGlob } from '../agents.js';
import { maybeFlagForCompaction } from './payload.js';
import { insertParts, partsFromAssistantMessage, partsFromToolMessage } from './parts.js';
@@ -31,6 +32,7 @@ async function executeToolCall(
projectRoot: string,
toolCall: ToolCall,
extraRoots: readonly string[],
toolCtx?: ToolExecCtx,
): Promise<{ output: unknown; truncated: boolean; error?: string }> {
const tool = TOOLS_BY_NAME[toolCall.name];
if (!tool) {
@@ -65,7 +67,7 @@ async function executeToolCall(
};
}
try {
const output = await tool.execute(parsed.data, projectRoot, extraRoots);
const output = await tool.execute(parsed.data, projectRoot, extraRoots, toolCtx);
const truncated =
typeof output === 'object' && output !== null && 'truncated' in output
? Boolean((output as { truncated: unknown }).truncated)
@@ -289,7 +291,10 @@ export async function executeToolPhase(
});
return;
}
const tres = await executeToolCall(projectRoot, tc, session.allowed_read_paths);
const tres = await executeToolCall(projectRoot, tc, session.allowed_read_paths, {
sql: ctx.sql,
sessionId,
});
if (SYNTHESIS_TOOLS.has(tc.name)) {
synthEntries.push({ tc, output: tres.output, ...(tres.error ? { error: tres.error } : {}) });
}