diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 10e7dbba..69db8362 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -13439,6 +13439,14 @@ fn browser_worker_spawn_policy( } } +fn browser_worker_console_href(workspace_id: &str, resource_key: &str) -> String { + format!( + "/w/{}/workers/{}/console", + encode_path_segment(workspace_id), + encode_path_segment(resource_key) + ) +} + async fn create_workspace_worker( State(api): State, headers: HeaderMap, @@ -13750,18 +13758,22 @@ fn browser_worker_response_from_summary( let runtime_id = worker.worker.runtime_id.clone(); let worker_id = worker.worker.worker_id.clone(); let workspace_id = api.workspace_id().to_string(); - let console_href = format!( - "/w/{}/runtimes/{}/workers/{}/console", - encode_path_segment(&workspace_id), - encode_path_segment(&runtime_id), - encode_path_segment(&worker_id) - ); + let resource_key = api + .store + .resource_key(&workspace_id, WorkspaceResourceKind::Worker, &worker_id)? + .ok_or_else(|| { + Error::Store(format!( + "Workspace Worker `{worker_id}` has no resource key after registration" + )) + })?; + let worker = worker_launch_worker_summary(worker); + let console_href = browser_worker_console_href(&workspace_id, &resource_key); Ok(BrowserCreateWorkerResponse { workspace_id, runtime_id, worker_id, console_href, - worker: worker_launch_worker_summary(worker), + worker, diagnostics: diagnostics .into_iter() .map(workspace_api::Diagnostic::from) @@ -17193,6 +17205,13 @@ mod tests { SqliteWorkspaceStore, UserRecord, WorkspaceRecord, WorkspaceRuntimeBinding, }; + #[test] + fn browser_worker_console_href_uses_logical_worker_route() { + let href = browser_worker_console_href("workspace/one", "W-7"); + assert_eq!(href, "/w/workspace%2Fone/workers/W-7/console"); + assert!(!href.contains("/runtimes/")); + } + #[tokio::test] async fn workspace_mutation_gate_serializes_deletion_with_active_mutations() { let locks = Arc::new(AsyncMutex::new(HashMap::new())); diff --git a/docs/report/2026-08-03-lan-http-randomuuid-blocks-websocket.md b/docs/report/2026-08-03-lan-http-randomuuid-blocks-websocket.md index 038179f4..31484ac3 100644 --- a/docs/report/2026-08-03-lan-http-randomuuid-blocks-websocket.md +++ b/docs/report/2026-08-03-lan-http-randomuuid-blocks-websocket.md @@ -17,7 +17,7 @@ context ではない。`crypto.randomUUID()` は secure context 限定なので Worker Console の `connectProtocolTransport` は、最初に `protocolState` を `"connecting"` に設定してから `WorkspaceMultiplexer.subscribe()` を呼ぶ。 -- `web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte` +- `web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte` - `protocolState = "connecting"` - 直後に `workspaceMultiplexer(...).subscribe(...)` - `web/workspace/src/lib/workspace/multiplexer.ts` diff --git a/web/workspace/src/lib/workspace/console/model.test.ts b/web/workspace/src/lib/workspace/console/model.test.ts index 8be2bb77..4303bec2 100644 --- a/web/workspace/src/lib/workspace/console/model.test.ts +++ b/web/workspace/src/lib/workspace/console/model.test.ts @@ -12,8 +12,8 @@ import { resolveConsoleWorkerView, segmentsToText, selectConsoleTimelineLines, - workerConsoleHref, } from "./model.ts"; +import { workerConsoleHref } from "../resource-links.ts"; declare const Deno: { test(name: string, fn: () => void): void; @@ -461,14 +461,13 @@ Deno.test("reload snapshot projects provenance-annotated history entries", () => ); }); -Deno.test("workerConsoleHref encodes runtime and worker target authority", () => { +Deno.test("workerConsoleHref uses logical Worker resource authority", () => { assert( - workerConsoleHref({ - runtime_id: "local runtime", - worker_id: "worker/one", - }, "workspace-1") === - "/w/workspace-1/runtimes/local%20runtime/workers/worker%2Fone/console", - "href should contain encoded runtime_id and worker_id segments", + workerConsoleHref("workspace-1", { + resource_key: "W-123", + display_name: "Worker One", + }) === "/w/workspace-1/workers/W-123-worker-one/console", + "href should contain the canonical logical Worker reference", ); }); diff --git a/web/workspace/src/lib/workspace/console/model.ts b/web/workspace/src/lib/workspace/console/model.ts index 481c5313..890db3ed 100644 --- a/web/workspace/src/lib/workspace/console/model.ts +++ b/web/workspace/src/lib/workspace/console/model.ts @@ -15,7 +15,6 @@ import type { WorkerStatus, } from "$lib/generated/protocol"; import { stringify as stringifyYaml } from "yaml"; -import { workspaceRoute } from "$lib/workspace/api/http"; import { applyRunActivityEvent, emptyRunActivityStats, @@ -220,31 +219,6 @@ export type WorkerTarget = { worker_id: string; }; -export function workerConsoleHref( - target: WorkerTarget, - workspaceId: string, -): string { - return workspaceRoute( - workspaceId, - `/runtimes/${encodeURIComponent(target.runtime_id)}/workers/${ - encodeURIComponent( - target.worker_id, - ) - }/console`, - ); -} - -export function workerConsolePath( - workspaceId: string, - runtimeId: string, - workerId: string, -): string { - return workerConsoleHref( - { runtime_id: runtimeId, worker_id: workerId }, - workspaceId, - ); -} - export type ConsoleEventInput = { eventId: string; event: ProtocolEvent; diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 8b45f90c..ae35bd0f 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -371,7 +371,7 @@ Deno.test("root layout keeps Workspace selection explicit", async () => { Deno.test("Worker Console uses protocol observation events without transcript fetch", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -395,7 +395,7 @@ Deno.test("Worker Console uses protocol observation events without transcript fe Deno.test("Worker Console owns its narrower centered shell width", async () => { const page = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -494,7 +494,7 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps" Deno.test("Worker Console exposes a foldable timeline beside the scroll body", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -529,7 +529,7 @@ Deno.test("Worker Console exposes a foldable timeline beside the scroll body", a Deno.test("Worker Console removes redundant chrome and uses shared alerts", async () => { const page = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -558,7 +558,7 @@ Deno.test("Worker Console removes redundant chrome and uses shared alerts", asyn Deno.test("Worker Console composer keeps a compact bounded chip editor", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -592,7 +592,7 @@ Deno.test("Worker Console composer keeps a compact bounded chip editor", async ( Deno.test("Worker Console paste chips preserve typed draft and target authority", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -724,24 +724,28 @@ Deno.test("workspace Worker sidebar links New to the dedicated create page", asy ); }); -Deno.test("Worker Console page is routed by runtime_id and worker_id through backend APIs", async () => { +Deno.test("Worker Console route resolves logical Worker authority before Runtime APIs", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); const routeLoad = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.ts", import.meta.url, ), ); assert( - routeLoad.includes("workspaceId") && - routeLoad.includes("runtimeId") && routeLoad.includes("workerId"), - "route load should expose workspace and target ids", + routeLoad.includes("params.workerRef") && + routeLoad.includes("resourceKey(params.workerRef)") && + routeLoad.includes("/workers/${encodeURIComponent(reference)}") && + routeLoad.includes("canonicalResourceReference") && + routeLoad.includes("runtimeId: result.data?.runtime_id") && + routeLoad.includes("workerId: result.data?.worker_id"), + "route load should resolve the logical Worker reference before exposing its execution target", ); assert( consolePage.includes("workspaceApiPath(workspaceId, path)") && @@ -951,7 +955,7 @@ Deno.test("Account UI owns browser passkey session state without workspace autho Deno.test("Workspace Worker list and Console share the multiplexed connection", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -988,7 +992,7 @@ Deno.test("Workspace Worker list and Console share the multiplexed connection", Deno.test("Web Console renders the client-projected Worker task store", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -1027,7 +1031,7 @@ Deno.test("Web Console renders the client-projected Worker task store", async () Deno.test("Web Console switches main and direct SubWorker views from the Tasks row", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); @@ -1071,7 +1075,7 @@ Deno.test("Web Console switches main and direct SubWorker views from the Tasks r Deno.test("Web Console uses Notify while running and exposes durable pending controls", async () => { const consolePage = await Deno.readTextFile( new URL( - "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url, ), ); diff --git a/web/workspace/src/lib/workspace/header/breadcrumb-model.test.ts b/web/workspace/src/lib/workspace/header/breadcrumb-model.test.ts index f4a12bfe..5a61fcd8 100644 --- a/web/workspace/src/lib/workspace/header/breadcrumb-model.test.ts +++ b/web/workspace/src/lib/workspace/header/breadcrumb-model.test.ts @@ -25,7 +25,7 @@ Deno.test("Ticket detail breadcrumbs expose the Ticket list and current id", () Deno.test("Worker console breadcrumbs use the logical Workers route and display name", () => { assertEquals( buildWorkspaceBreadcrumbs( - "/w/workspace/runtimes/runtime-a/workers/worker-7/console", + "/w/workspace/workers/W-7-review-worker/console", "workspace", { workerName: "Review Worker" }, ), @@ -39,12 +39,12 @@ Deno.test("Worker console breadcrumbs use the logical Workers route and display Deno.test("Worker console breadcrumbs fall back to Worker id", () => { assertEquals( buildWorkspaceBreadcrumbs( - "/w/workspace/runtimes/runtime-a/workers/worker-7/console", + "/w/workspace/workers/W-7-review-worker/console", "workspace", ), [ { label: "workers", href: "/w/workspace/workers" }, - { label: "worker-7" }, + { label: "W-7-review-worker" }, ], ); }); diff --git a/web/workspace/src/lib/workspace/header/breadcrumb-model.ts b/web/workspace/src/lib/workspace/header/breadcrumb-model.ts index d9f1d2ec..97228b0f 100644 --- a/web/workspace/src/lib/workspace/header/breadcrumb-model.ts +++ b/web/workspace/src/lib/workspace/header/breadcrumb-model.ts @@ -24,13 +24,13 @@ export function buildWorkspaceBreadcrumbs( .map(decodeURIComponent); if ( - segments[0] === "runtimes" && - segments[2] === "workers" && - segments[3] + segments[0] === "workers" && + segments[1] && + segments[2] === "console" ) { return [ { label: "workers", href: `${workspaceRoot}/workers` }, - { label: context.workerName?.trim() || segments[3] }, + { label: context.workerName?.trim() || segments[1] }, ]; } diff --git a/web/workspace/src/lib/workspace/resource-links.ts b/web/workspace/src/lib/workspace/resource-links.ts index 67268544..509a663d 100644 --- a/web/workspace/src/lib/workspace/resource-links.ts +++ b/web/workspace/src/lib/workspace/resource-links.ts @@ -45,3 +45,10 @@ export function workerHref( const reference = canonicalResourceReference(worker.resource_key, worker.display_name); return `/w/${encodeURIComponent(workspaceId)}/workers/${encodeURIComponent(reference)}`; } + +export function workerConsoleHref( + workspaceId: string, + worker: { resource_key: string; display_name: string }, +): string { + return `${workerHref(workspaceId, worker)}/console`; +} diff --git a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte index c50a4220..e527f0ed 100644 --- a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte +++ b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte @@ -1,6 +1,6 @@