From 98f8aa5740a759f43be2cc538e7c224765eb8377 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:36:29 +0530 Subject: [PATCH 1/4] feat(inspector): add browser transport adapter --- src/ui/inspector/http-transport.ts | 70 ++++++++++++++++++++++++ src/ui/inspector/transport.ts | 24 ++++---- src/ui/inspector/workspace-inspector.tsx | 8 ++- 3 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 src/ui/inspector/http-transport.ts diff --git a/src/ui/inspector/http-transport.ts b/src/ui/inspector/http-transport.ts new file mode 100644 index 00000000..7bc493a5 --- /dev/null +++ b/src/ui/inspector/http-transport.ts @@ -0,0 +1,70 @@ +import { + workspaceActivityDataSchema, + workspaceDiffDataSchema, + workspaceRefsDataSchema, + workspaceToolCallDataSchema, + type WorkspaceDiffScopeInput, + type WorkspaceInspectorTransport, +} from "./transport.js"; + +export function createHttpInspectorTransport(): WorkspaceInspectorTransport { + return { + getActivity(workspaceId, reviewRef) { + const search = reviewRef ? `?review=${encodeURIComponent(reviewRef)}` : ""; + return getJson( + `/api/workspaces/${encodeURIComponent(workspaceId)}/activity${search}`, + workspaceActivityDataSchema, + ); + }, + async getToolCall(workspaceId, callId) { + const result = await getJson( + `/api/workspaces/${encodeURIComponent(workspaceId)}/tool-calls/${callId}`, + workspaceToolCallDataSchema, + ); + return result.call; + }, + getDiff(workspaceId, scope) { + return getJson( + `/api/workspaces/${encodeURIComponent(workspaceId)}/diff?${diffScopeSearch(scope)}`, + workspaceDiffDataSchema, + ); + }, + getRefs(workspaceId) { + return getJson( + `/api/workspaces/${encodeURIComponent(workspaceId)}/refs`, + workspaceRefsDataSchema, + ); + }, + }; +} + +async function getJson( + url: string, + schema: { parse(value: unknown): T }, +): Promise { + const response = await fetch(url, { headers: { accept: "application/json" } }); + if (!response.ok) { + const body = await response.json().catch(() => null) as { error?: string } | null; + throw new Error(body?.error ?? `Inspector request failed (${response.status}).`); + } + return schema.parse(await response.json()); +} + +function diffScopeSearch(scope: WorkspaceDiffScopeInput): string { + const search = new URLSearchParams({ scope: scope.kind }); + switch (scope.kind) { + case "review": + search.set("review", scope.review_ref); + break; + case "working-tree": + break; + case "branch": + if (scope.base_ref) search.set("base", scope.base_ref); + break; + case "compare": + search.set("from", scope.from_ref); + search.set("to", scope.to_ref); + break; + } + return search.toString(); +} diff --git a/src/ui/inspector/transport.ts b/src/ui/inspector/transport.ts index cd2a9296..8042861c 100644 --- a/src/ui/inspector/transport.ts +++ b/src/ui/inspector/transport.ts @@ -18,11 +18,11 @@ const activityGroupSchema = z.object({ calls: z.array(activityCallSchema), }); -const activitySchema = z.object({ +export const workspaceActivityDataSchema = z.object({ groups: z.array(activityGroupSchema), }); -const toolCallSchema = z.object({ +export const workspaceToolCallDataSchema = z.object({ call: z.object({ id: z.number().int(), tool_name: z.string(), @@ -36,7 +36,7 @@ const toolCallSchema = z.object({ }), }); -const diffSchema = z.object({ +export const workspaceDiffDataSchema = z.object({ scope: z.unknown(), summary: z.object({ files: z.number().int(), @@ -53,16 +53,16 @@ const diffSchema = z.object({ patch: z.string(), }); -const refsSchema = z.object({ +export const workspaceRefsDataSchema = z.object({ current_ref: z.string().optional(), default_base_ref: z.string().optional(), refs: z.array(z.string()), }); -export type WorkspaceActivityData = z.infer; -export type WorkspaceToolCallData = z.infer["call"]; -export type WorkspaceDiffData = z.infer; -export type WorkspaceRefsData = z.infer; +export type WorkspaceActivityData = z.infer; +export type WorkspaceToolCallData = z.infer["call"]; +export type WorkspaceDiffData = z.infer; +export type WorkspaceRefsData = z.infer; export type WorkspaceDiffScopeInput = | { kind: "review"; review_ref: string } @@ -88,7 +88,7 @@ export function createMcpInspectorTransport(app: App): WorkspaceInspectorTranspo ...(reviewRef ? { review_ref: reviewRef } : {}), }, }), - activitySchema, + workspaceActivityDataSchema, ); }, async getToolCall(workspaceId, callId) { @@ -97,7 +97,7 @@ export function createMcpInspectorTransport(app: App): WorkspaceInspectorTranspo name: "get_workspace_tool_call", arguments: { workspace_id: workspaceId, call_id: callId }, }), - toolCallSchema, + workspaceToolCallDataSchema, ).call; }, async getDiff(workspaceId, scope) { @@ -106,7 +106,7 @@ export function createMcpInspectorTransport(app: App): WorkspaceInspectorTranspo name: "get_workspace_diff", arguments: { workspace_id: workspaceId, scope }, }), - diffSchema, + workspaceDiffDataSchema, ); }, async getRefs(workspaceId) { @@ -115,7 +115,7 @@ export function createMcpInspectorTransport(app: App): WorkspaceInspectorTranspo name: "get_workspace_refs", arguments: { workspace_id: workspaceId }, }), - refsSchema, + workspaceRefsDataSchema, ); }, }; diff --git a/src/ui/inspector/workspace-inspector.tsx b/src/ui/inspector/workspace-inspector.tsx index 669fb146..b41051e6 100644 --- a/src/ui/inspector/workspace-inspector.tsx +++ b/src/ui/inspector/workspace-inspector.tsx @@ -4,6 +4,7 @@ import { Outlet, RouterProvider, createMemoryHistory, + createBrowserHistory, createRootRoute, createRoute, createRouter, @@ -34,6 +35,7 @@ interface InspectorOptions { transport: WorkspaceInspectorTransport; initialReviewRef?: string; onExitFullscreen?: () => void; + browserBasepath?: string; } interface MountedInspector { @@ -84,9 +86,13 @@ export function mountWorkspaceInspector( const initialPath = options.initialReviewRef ? `/activity?review=${encodeURIComponent(options.initialReviewRef)}` : "/activity"; + const history = options.browserBasepath + ? createBrowserHistory() + : createMemoryHistory({ initialEntries: [initialPath] }); const router = createRouter({ routeTree, - history: createMemoryHistory({ initialEntries: [initialPath] }), + history, + ...(options.browserBasepath ? { basepath: options.browserBasepath } : {}), }); root.render( From a11936736ad62454b31de9575f7f8e9f81d576c7 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:36:29 +0530 Subject: [PATCH 2/4] feat(ui): add standalone workspace inspector entry --- src/ui/standalone-workspace-inspector.tsx | 24 +++++++++++++++++++++++ src/ui/workspace-inspector.html | 13 ++++++++++++ vite.config.ts | 5 ++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/ui/standalone-workspace-inspector.tsx create mode 100644 src/ui/workspace-inspector.html diff --git a/src/ui/standalone-workspace-inspector.tsx b/src/ui/standalone-workspace-inspector.tsx new file mode 100644 index 00000000..3f82296b --- /dev/null +++ b/src/ui/standalone-workspace-inspector.tsx @@ -0,0 +1,24 @@ +import { createHttpInspectorTransport } from "./inspector/http-transport.js"; +import { mountWorkspaceInspector } from "./inspector/workspace-inspector.js"; +import "./workspace-app.css"; + +interface WorkspaceInspectorBootstrap { + workspaceId: string; + root: string; + mode?: "checkout" | "worktree"; +} + +const root = document.querySelector("#app"); +if (!root) throw new Error("Missing #app root element."); + +const bootstrapElement = document.querySelector("#devspace-workspace-bootstrap"); +if (!bootstrapElement?.textContent) throw new Error("Missing workspace inspector bootstrap data."); + +const bootstrap = JSON.parse(bootstrapElement.textContent) as WorkspaceInspectorBootstrap; +mountWorkspaceInspector(root, { + workspaceId: bootstrap.workspaceId, + root: bootstrap.root, + mode: bootstrap.mode, + transport: createHttpInspectorTransport(), + browserBasepath: `/ws/${encodeURIComponent(bootstrap.workspaceId)}`, +}); diff --git a/src/ui/workspace-inspector.html b/src/ui/workspace-inspector.html new file mode 100644 index 00000000..86d73ff5 --- /dev/null +++ b/src/ui/workspace-inspector.html @@ -0,0 +1,13 @@ + + + + + + DevSpace Workspace Inspector + + +
+ + + + diff --git a/vite.config.ts b/vite.config.ts index 9774d1c2..b6f28da6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,10 @@ export default defineConfig({ emptyOutDir: true, manifest: true, rollupOptions: { - input: resolve(__dirname, "src/ui/workspace-app.html"), + input: { + "workspace-app": resolve(__dirname, "src/ui/workspace-app.html"), + "workspace-inspector": resolve(__dirname, "src/ui/workspace-inspector.html"), + }, output: { entryFileNames: "assets/[name]-[hash].js", chunkFileNames: "assets/[name]-[hash].js", From 66b1071bbdbcfd126f7df161cfd154a221a497d1 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:41:00 +0530 Subject: [PATCH 3/4] feat(server): serve local workspace inspector --- src/server.ts | 346 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 296 insertions(+), 50 deletions(-) diff --git a/src/server.ts b/src/server.ts index 4783db70..732de151 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,7 +15,7 @@ import { RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; import express from "express"; -import type { Request, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; import * as z from "zod/v4"; import { isArtifactDownloadSupportedPlatform, @@ -81,6 +81,7 @@ import { } from "./tool-surfaces/types.js"; const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html"; +const WORKSPACE_INSPECTOR_MANIFEST_ENTRY = "workspace-inspector.html"; function mcpServerInfo() { return { @@ -122,6 +123,7 @@ class ToolActivityTracker { interface WorkspaceAppManifestEntry { file: string; css?: string[]; + imports?: string[]; isEntry?: boolean; } @@ -237,12 +239,14 @@ function readWorkspaceAppManifest(): WorkspaceAppManifest { return JSON.parse(readFileSync(uiManifestUrl(), "utf8")) as WorkspaceAppManifest; } -function getWorkspaceAppManifestEntry(): WorkspaceAppManifestEntry { +function getWorkspaceAppManifestEntry( + entryName = WORKSPACE_APP_MANIFEST_ENTRY, +): WorkspaceAppManifestEntry { const manifest = readWorkspaceAppManifest(); - const entry = manifest[WORKSPACE_APP_MANIFEST_ENTRY]; + const entry = manifest[entryName]; if (!entry?.file) { - throw new Error(`Missing ${WORKSPACE_APP_MANIFEST_ENTRY} in UI manifest.`); + throw new Error(`Missing ${entryName} in UI manifest.`); } return entry; @@ -252,10 +256,28 @@ function assetUrl(baseUrl: string, assetPath: string): string { return `${baseUrl}/${assetPath.replace(/^\/+/, "")}`; } +function workspaceAppStylesheets(entryName: string): string[] { + const manifest = readWorkspaceAppManifest(); + const stylesheets = new Set(); + const visited = new Set(); + + const visit = (name: string) => { + if (visited.has(name)) return; + visited.add(name); + const entry = manifest[name]; + if (!entry) return; + for (const stylesheet of entry.css ?? []) stylesheets.add(stylesheet); + for (const imported of entry.imports ?? []) visit(imported); + }; + + visit(entryName); + return [...stylesheets]; +} + function workspaceAppHtml(config: ServerConfig): string { const baseUrl = assetBaseUrl(config); const entry = getWorkspaceAppManifestEntry(); - const stylesheets = (entry.css ?? []) + const stylesheets = workspaceAppStylesheets(WORKSPACE_APP_MANIFEST_ENTRY) .map( (stylesheet) => ` `, @@ -279,6 +301,37 @@ ${stylesheets} `; } +function workspaceInspectorHtml(input: { + workspaceId: string; + root: string; + mode: "checkout" | "worktree"; +}): string { + const entry = getWorkspaceAppManifestEntry(WORKSPACE_INSPECTOR_MANIFEST_ENTRY); + const baseUrl = "/mcp-app-assets"; + const stylesheets = workspaceAppStylesheets(WORKSPACE_INSPECTOR_MANIFEST_ENTRY) + .map( + (stylesheet) => + ` `, + ) + .join("\n"); + const bootstrap = JSON.stringify(input).replace(/ + + + + + DevSpace Workspace Inspector + +${stylesheets} + + +
+ + +`; +} + function appCsp(config: ServerConfig): { resourceDomains: string[]; connectDomains: string[]; @@ -303,7 +356,10 @@ function setAssetHeaders(res: Response): void { async function assertWorkspaceAppAssets(): Promise { const entry = getWorkspaceAppManifestEntry(); - const candidates = [entry.file, ...(entry.css ?? [])].map( + const candidates = [ + entry.file, + ...workspaceAppStylesheets(WORKSPACE_APP_MANIFEST_ENTRY), + ].map( (assetPath) => new URL(`../dist/ui/${assetPath}`, import.meta.url), ); @@ -842,26 +898,10 @@ function registerWorkspaceInspectorTools( }, async ({ workspace_id, review_ref }) => { await workspaces.getWorkspace(workspace_id); - const groups = review_ref - ? [activity.findReviewGroup(workspace_id, review_ref)].filter(Boolean) - : activity.listActivity(workspace_id).groups; - const outputGroups = groups.map((group) => ({ - id: group!.id, - kind: group!.kind, - started_at: group!.startedAt, - completed_at: group!.completedAt, - review_ref: group!.reviewRef, - calls: group!.calls.map((call) => ({ - id: call.id, - tool_name: call.toolName, - started_at: call.startedAt, - completed_at: call.completedAt, - duration_ms: call.durationMs, - })), - })); + const output = workspaceActivityOutput(activity, workspace_id, review_ref); return { - content: [textBlock(`Loaded ${outputGroups.length} activity groups.`)], - structuredContent: { groups: outputGroups }, + content: [textBlock(`Loaded ${output.groups.length} activity groups.`)], + structuredContent: output, }; }, ); @@ -898,19 +938,7 @@ function registerWorkspaceInspectorTools( if (!call) throw new Error(`Unknown tool call ${call_id} for workspace ${workspace_id}.`); return { content: [textBlock(`Loaded ${call.toolName} tool call.`)], - structuredContent: { - call: { - id: call.id, - tool_name: call.toolName, - arguments: call.arguments, - result: call.result, - error: call.error, - started_at: call.startedAt, - completed_at: call.completedAt, - duration_ms: call.durationMs, - review_ref: call.reviewRef, - }, - }, + structuredContent: { call: workspaceToolCallOutput(call) }, }; }, ); @@ -949,12 +977,7 @@ function registerWorkspaceInspectorTools( const diff = await readWorkspaceDiff(workspace, reviewCheckpoints, internalScope); return { content: [textBlock(`Loaded workspace diff for ${diff.scope.kind}.`)], - structuredContent: { - scope: workspaceDiffScopeToOutput(diff.scope), - summary: diff.summary, - files: diff.files, - patch: diff.patch, - }, + structuredContent: workspaceDiffOutput(diff), }; }, ); @@ -979,16 +1002,105 @@ function registerWorkspaceInspectorTools( const refs = await listWorkspaceRefs(workspace); return { content: [textBlock(`Loaded ${refs.refs.length} Git refs.`)], - structuredContent: { - current_ref: refs.currentRef, - default_base_ref: refs.defaultBaseRef, - refs: refs.refs, - }, + structuredContent: workspaceRefsOutput(refs), }; }, ); } +function workspaceActivityOutput( + activity: WorkspaceActivityService, + workspaceId: string, + reviewRef?: string, +): { + groups: Array<{ + id: string; + kind: "review" | "inferred"; + started_at: string; + completed_at?: string; + review_ref?: string; + calls: Array<{ + id: number; + tool_name: string; + started_at: string; + completed_at?: string; + duration_ms?: number; + }>; + }>; +} { + const groups = reviewRef + ? [activity.findReviewGroup(workspaceId, reviewRef)].filter( + (group): group is NonNullable => Boolean(group), + ) + : activity.listActivity(workspaceId).groups; + return { + groups: groups.map((group) => ({ + id: group.id, + kind: group.kind, + started_at: group.startedAt, + ...(group.completedAt ? { completed_at: group.completedAt } : {}), + ...(group.reviewRef ? { review_ref: group.reviewRef } : {}), + calls: group.calls.map((call) => ({ + id: call.id, + tool_name: call.toolName, + started_at: call.startedAt, + ...(call.completedAt ? { completed_at: call.completedAt } : {}), + ...(call.durationMs !== undefined ? { duration_ms: call.durationMs } : {}), + })), + })), + }; +} + +function workspaceToolCallOutput(call: NonNullable>): { + id: number; + tool_name: string; + arguments: unknown; + result?: unknown; + error?: unknown; + started_at: string; + completed_at?: string; + duration_ms?: number; + review_ref?: string; +} { + return { + id: call.id, + tool_name: call.toolName, + arguments: call.arguments, + ...(call.result !== undefined ? { result: call.result } : {}), + ...(call.error !== undefined ? { error: call.error } : {}), + started_at: call.startedAt, + ...(call.completedAt ? { completed_at: call.completedAt } : {}), + ...(call.durationMs !== undefined ? { duration_ms: call.durationMs } : {}), + ...(call.reviewRef ? { review_ref: call.reviewRef } : {}), + }; +} + +function workspaceDiffOutput(diff: Awaited>): { + scope: Record; + summary: typeof diff.summary; + files: typeof diff.files; + patch: string; +} { + return { + scope: workspaceDiffScopeToOutput(diff.scope), + summary: diff.summary, + files: diff.files, + patch: diff.patch, + }; +} + +function workspaceRefsOutput(refs: Awaited>): { + current_ref?: string; + default_base_ref?: string; + refs: string[]; +} { + return { + ...(refs.currentRef ? { current_ref: refs.currentRef } : {}), + ...(refs.defaultBaseRef ? { default_base_ref: refs.defaultBaseRef } : {}), + refs: refs.refs, + }; +} + function workspaceDiffScopeFromInput(input: { kind: "review" | "working-tree" | "branch" | "compare"; review_ref?: string; @@ -1023,6 +1135,64 @@ function workspaceDiffScopeToOutput(scope: WorkspaceDiffScope): Record { + const workspaceId = requiredRouteParam(req, "workspaceId"); + const queryIndex = req.originalUrl.indexOf("?"); + const search = queryIndex >= 0 ? req.originalUrl.slice(queryIndex) : ""; + res.redirect(302, `/ws/${encodeURIComponent(workspaceId)}/activity${search}`); + }); + + app.get(["/ws/:workspaceId/activity", "/ws/:workspaceId/changes"], async (req, res) => { + try { + const workspace = await workspaces.getWorkspace(requiredRouteParam(req, "workspaceId")); + res.type("html").send(workspaceInspectorHtml({ + workspaceId: workspace.id, + root: workspace.root, + mode: workspace.mode, + })); + } catch (error) { + sendInspectorHttpError(res, error, 404); + } + }); + + app.get("/api/workspaces/:workspaceId/activity", async (req, res) => { + try { + const workspaceId = requiredRouteParam(req, "workspaceId"); + await workspaces.getWorkspace(workspaceId); + const reviewRef = typeof req.query.review === "string" ? req.query.review : undefined; + res.json(workspaceActivityOutput(workspaceActivityService, workspaceId, reviewRef)); + } catch (error) { + sendInspectorHttpError(res, error, 404); + } + }); + + app.get("/api/workspaces/:workspaceId/tool-calls/:callId", async (req, res) => { + try { + const workspaceId = requiredRouteParam(req, "workspaceId"); + await workspaces.getWorkspace(workspaceId); + const callId = Number(requiredRouteParam(req, "callId")); + if (!Number.isSafeInteger(callId) || callId <= 0) { + res.status(400).json({ error: "callId must be a positive integer." }); + return; + } + const call = workspaceActivityService.getToolCall(workspaceId, callId); + if (!call) { + res.status(404).json({ error: `Unknown tool call ${callId} for workspace ${workspaceId}.` }); + return; + } + res.json({ call: workspaceToolCallOutput(call) }); + } catch (error) { + sendInspectorHttpError(res, error, 404); + } + }); + + app.get("/api/workspaces/:workspaceId/diff", async (req, res) => { + try { + const workspace = await workspaces.getWorkspace(requiredRouteParam(req, "workspaceId")); + const scope = workspaceDiffScopeFromQuery(req.query); + const diff = await readWorkspaceDiff(workspace, reviewCheckpoints, scope); + res.json(workspaceDiffOutput(diff)); + } catch (error) { + sendInspectorHttpError(res, error, 400); + } + }); + + app.get("/api/workspaces/:workspaceId/refs", async (req, res) => { + try { + const workspace = await workspaces.getWorkspace(requiredRouteParam(req, "workspaceId")); + res.json(workspaceRefsOutput(await listWorkspaceRefs(workspace))); + } catch (error) { + sendInspectorHttpError(res, error, 404); + } + }); + } + app.get("/healthz", (_req, res) => { res.json({ ok: true, name: "devspace" }); }); From 65f6fdbb2638d8693442c5ee4cf456b210bdbda4 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:41:00 +0530 Subject: [PATCH 4/4] test(server): cover local workspace inspector APIs --- src/server.test.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/server.test.ts b/src/server.test.ts index 467b85ad..203fe19c 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -14,7 +14,7 @@ import { buildLocalAgentProviderStatuses } from "./local-agent-catalog.js"; import type { SubagentsConfig } from "./local-agent-config.js"; import { createReviewCheckpointManager } from "./review-checkpoints.js"; import { ProcessSessionManager } from "./process-sessions.js"; -import { createMcpServer, createServer } from "./server.js"; +import { createMcpServer, createServer, isLocalInspectorHost } from "./server.js"; import { SqliteWorkspaceStore } from "./workspace-store.js"; import { WorkspaceRegistry } from "./workspaces.js"; import { writeTestDevspaceConfig } from "./test-support/config.test.js"; @@ -591,6 +591,76 @@ test("HTTP endpoint serves modern MCP and stateless legacy clients", async (t) = assert.match(await legacyTools.text(), /"open_workspace"/); }); +test("local workspace inspector exposes activity and diff APIs", async (t) => { + assert.equal(isLocalInspectorHost("localhost:3210"), true); + assert.equal(isLocalInspectorHost("127.0.0.1:3210"), true); + assert.equal(isLocalInspectorHost("[::1]:3210"), true); + assert.equal(isLocalInspectorHost("devspace.example"), false); + + const { root, localBaseUrl, accessToken } = await httpServerFixture( + t, + "devspace-inspector-http-test-", + ); + await execFileAsync("git", ["init", "-b", "main"], { cwd: root }); + await execFileAsync("git", ["config", "user.email", "devspace@example.com"], { cwd: root }); + await execFileAsync("git", ["config", "user.name", "DevSpace Test"], { cwd: root }); + await writeFile(join(root, "README.md"), "base\n"); + await execFileAsync("git", ["add", "README.md"], { cwd: root }); + await execFileAsync("git", ["commit", "-m", "base"], { cwd: root }); + + const opened = await postModernMcp( + localBaseUrl, + accessToken, + "tools/call", + { + name: "open_workspace", + arguments: { path: root }, + _meta: { "openai/session": "inspector-http-test" }, + }, + ); + const openBody = await opened.json() as { + result?: { structuredContent?: { workspace_id?: string } }; + }; + const workspaceId = openBody.result?.structuredContent?.workspace_id; + assert.ok(typeof workspaceId === "string"); + + await postModernMcp( + localBaseUrl, + accessToken, + "tools/call", + { name: "read", arguments: { workspace_id: workspaceId, path: "README.md" } }, + ); + + const activity = await fetch( + `${localBaseUrl}/api/workspaces/${encodeURIComponent(workspaceId)}/activity`, + ); + assert.equal(activity.status, 200, await activity.clone().text()); + const activityBody = await activity.json() as { + groups?: Array<{ calls?: Array<{ tool_name?: string }> }>; + }; + assert.ok(activityBody.groups?.some((group) => + group.calls?.some((call) => call.tool_name === "read"), + )); + + await writeFile(join(root, "README.md"), "changed\n"); + const diff = await fetch( + `${localBaseUrl}/api/workspaces/${encodeURIComponent(workspaceId)}/diff?scope=working-tree`, + ); + assert.equal(diff.status, 200, await diff.clone().text()); + const diffBody = await diff.json() as { patch?: string }; + assert.match(diffBody.patch ?? "", /-base\n\+changed/); + + const deepLink = await fetch( + `${localBaseUrl}/ws/${encodeURIComponent(workspaceId)}?review=abc123`, + { redirect: "manual" }, + ); + assert.equal(deepLink.status, 302); + assert.equal( + deepLink.headers.get("location"), + `/ws/${encodeURIComponent(workspaceId)}/activity?review=abc123`, + ); +}); + test("server shutdown waits for an active MCP tool call", async (t) => { const { root, localBaseUrl, accessToken, running } = await httpServerFixture( t,