diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 632ee393..e10c04ae 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -694,67 +694,12 @@ color: var(--danger); } -.transcript-card, +.console-card, .composer-card { display: grid; gap: 1rem; } -.transcript-list { - display: grid; - gap: 0.85rem; - list-style: none; - margin: 0; - padding: 0; -} - -.transcript-item { - display: grid; - gap: 0.45rem; - padding: 0.85rem 1rem; - border-radius: 16px; - border: 1px solid var(--line); - background: var(--bg-raised); -} - -.transcript-item.user { - margin-left: min(8vw, 4rem); - background: var(--bg-raised); - border-color: var(--line-strong); -} - -.transcript-item.assistant { - margin-right: min(8vw, 4rem); - background: var(--bg-raised); - border-color: var(--line-strong); -} - -.message-meta { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.55rem; - color: var(--text-muted); - font-size: 0.78rem; -} - -.message-meta strong { - color: var(--text-strong); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.message-meta time { - margin-left: auto; -} - -.transcript-item p { - margin: 0; - white-space: pre-wrap; - line-height: 1.55; - color: var(--code); -} - .empty-state { margin: 0; padding: 1rem; @@ -843,7 +788,20 @@ } .worker-console-shell { - grid-template-rows: auto minmax(0, 1fr) auto; + min-height: 100dvh; + padding-bottom: 0; +} + +.worker-console-shell > .console-body { + flex: 1 1 auto; + min-height: 0; +} + +.console-header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-2); } .console-status-pill { @@ -864,45 +822,45 @@ color: var(--warning); } -.console-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(18rem, 26rem); +.console-body { + display: flex; + flex-direction: column; gap: var(--space-4); min-height: 0; } -.transcript-toolbar { +.console-toolbar { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); } -.worker-transcript { +.console-log { display: grid; - gap: var(--space-3); - max-height: min(68dvh, 50rem); + align-content: start; + gap: var(--space-2); + max-height: none; + min-height: 0; margin: 0; padding: 0; overflow-y: auto; list-style: none; } -.worker-transcript li { +.console-log li { display: grid; - gap: var(--space-2); - border: 1px solid var(--line); - border-radius: 14px; - padding: 0.75rem 0.85rem; - background: var(--bg); + gap: var(--space-1); + padding: 0.2rem 0; + background: transparent; } -.worker-transcript li.error-line { - border-color: var(--danger); +.console-log li.error-line { + color: var(--danger); } .message-heading { @@ -910,8 +868,13 @@ align-items: center; justify-content: space-between; gap: var(--space-2); - color: var(--text-strong); - font-weight: 800; + color: var(--text-muted); + font-size: 0.78rem; + font-weight: 750; +} + +.message-heading.streaming-heading { + justify-content: flex-start; } .message-heading small { @@ -922,7 +885,7 @@ text-transform: uppercase; } -.worker-transcript pre { +.console-log pre { max-width: 100%; margin: 0; overflow-x: auto; @@ -942,15 +905,31 @@ font-weight: 800; } -.console-side-card { - align-content: start; +.console-side-panel { + position: fixed; + top: 0; + right: 0; + bottom: 0; + z-index: 5; display: grid; + align-content: start; gap: var(--space-4); - min-width: 0; + width: min(32rem, 100vw); + padding: var(--space-6); + overflow-y: auto; + border-left: 1px solid var(--line); + background: var(--bg); } -.console-side-card dl, -.console-side-card ul { +.side-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); +} + +.console-side-panel dl, +.console-side-panel ul { display: grid; gap: var(--space-2); margin: 0; @@ -958,7 +937,7 @@ list-style: none; } -.console-side-card dt { +.console-side-panel dt { color: var(--text-muted); font-size: 0.72rem; font-weight: 800; @@ -966,20 +945,21 @@ text-transform: uppercase; } -.console-side-card dd { +.console-side-panel dd { margin: 0; color: var(--text-strong); font-weight: 700; } .console-composer { + position: sticky; + bottom: 0; + z-index: 2; display: grid; gap: var(--space-3); -} - -.console-composer label { - color: var(--text-strong); - font-weight: 800; + margin-inline: calc(-1 * var(--space-6)); + padding: var(--space-3) var(--space-6) var(--space-4); + background: var(--bg); } .console-composer textarea { @@ -999,10 +979,6 @@ } @media (max-width: 960px) { - .console-grid { - grid-template-columns: 1fr; - } - .console-header { flex-direction: column; } diff --git a/web/workspace/src/lib/workspace-console/model.test.ts b/web/workspace/src/lib/workspace-console/model.test.ts index 0a7dbd88..eeaec30b 100644 --- a/web/workspace/src/lib/workspace-console/model.test.ts +++ b/web/workspace/src/lib/workspace-console/model.test.ts @@ -45,7 +45,7 @@ Deno.test("segmentsToText preserves protocol segment semantics", () => { ); }); -Deno.test("projectConsole keeps transcript and protocol-derived event rows distinct", () => { +Deno.test("projectConsole projects initial console output and live protocol rows", () => { const projection = projectConsole( [ { @@ -101,15 +101,15 @@ Deno.test("projectConsole keeps transcript and protocol-derived event rows disti assert( projection.lines.some((line) => - line.source === "transcript" && line.kind === "user" + line.source === "initial" && line.kind === "user" ), - "transcript user row expected", + "initial user row expected", ); assert( projection.lines.some((line) => - line.source === "event" && line.kind === "assistant" + line.source === "live" && line.kind === "assistant" ), - "assistant event row expected", + "assistant live row expected", ); assert( projection.lines.some((line) => line.kind === "thinking"), @@ -133,7 +133,7 @@ Deno.test("projectConsole keeps transcript and protocol-derived event rows disti ); }); -Deno.test("projectConsole displays snapshot and in-flight state", () => { +Deno.test("projectConsole uses snapshot for state without rendering it as console output", () => { const projection = projectConsole([], [ { cursor: "20", @@ -171,8 +171,8 @@ Deno.test("projectConsole displays snapshot and in-flight state", () => { assert(projection.status === "running", "snapshot should update status"); assert( - projection.lines.some((line) => line.kind === "snapshot"), - "snapshot row expected", + !projection.lines.some((line) => line.title.includes("snapshot")), + "snapshot should not render as a console row", ); assert( projection.lines.filter((line) => line.kind === "in_flight").length === 2, diff --git a/web/workspace/src/lib/workspace-console/model.ts b/web/workspace/src/lib/workspace-console/model.ts index 526e1707..26a45e11 100644 --- a/web/workspace/src/lib/workspace-console/model.ts +++ b/web/workspace/src/lib/workspace-console/model.ts @@ -13,7 +13,6 @@ export type ConsoleLineKind = | "status" | "error" | "usage" - | "snapshot" | "in_flight" | "system"; @@ -24,7 +23,7 @@ export type ConsoleLine = { body: string; detail?: string; cursor?: string | null; - source: "transcript" | "event"; + source: "initial" | "live"; streaming?: boolean; error?: boolean; }; @@ -53,23 +52,22 @@ export function workerConsolePath(runtimeId: string, workerId: string): string { return workerConsoleHref({ runtime_id: runtimeId, worker_id: workerId }); } -export function transcriptLines(items: WorkerTranscriptItem[]): ConsoleLine[] { +export function initialConsoleLines(items: WorkerTranscriptItem[]): ConsoleLine[] { return items.map((item) => ({ - id: `transcript-${item.event_id}-${item.sequence}`, - kind: transcriptRoleKind(item.role), - title: `${item.role} · transcript #${item.sequence}`, + id: `initial-${item.event_id}-${item.sequence}`, + kind: initialRoleKind(item.role), + title: item.role, body: item.content, - detail: `event ${item.event_id}`, - source: "transcript", + source: "initial", })); } export function projectConsole( - transcript: WorkerTranscriptItem[], + initialItems: WorkerTranscriptItem[], events: Array<{ cursor: string; event: ProtocolEvent }> = [], ): ConsoleProjection { return events.reduce(applyProtocolEvent, { - lines: transcriptLines(transcript), + lines: initialConsoleLines(initialItems), status: null, usage: null, lastCursor: null, @@ -208,15 +206,6 @@ export function applyProtocolEvent( break; case "snapshot": next.status = event.data.status; - next.lines.push( - line( - envelope.cursor, - "snapshot", - `snapshot · ${event.data.status}`, - `${event.data.entries.length} entries · ${event.data.greeting.provider} / ${event.data.greeting.model}`, - `${event.data.greeting.worker_name} · context ${event.data.greeting.context_tokens}/${event.data.greeting.context_window}`, - ), - ); for (const block of event.data.in_flight?.blocks ?? []) { next.lines.push(inFlightLine(envelope.cursor, block)); } @@ -443,7 +432,7 @@ export function segmentsToText(segments: Segment[]): string { .join("\n"); } -function transcriptRoleKind(role: string): ConsoleLineKind { +function initialRoleKind(role: string): ConsoleLineKind { if (role === "user" || role === "assistant" || role === "system") { return role; } @@ -466,7 +455,7 @@ function line( body, detail, cursor, - source: "event", + source: "live", streaming, error, }; diff --git a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte b/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte index 0a743e1e..8b909b5d 100644 --- a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte +++ b/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte @@ -38,6 +38,7 @@ let sendError = $state(null); let streamState = $state<'connecting' | 'open' | 'closed' | 'error'>('connecting'); let streamDiagnostics = $state([]); + let workerDetailsOpen = $state(false); let observedEvents = $state>([]); let nextReloadToken = 0; let reloadToken = $state(0); @@ -138,11 +139,6 @@ return nextReloadToken; } - async function refreshConsole() { - advanceReloadToken(); - await loadConsoleData(consoleTarget); - } - async function sendMessage(event: SubmitEvent) { event.preventDefault(); const content = draft.trim(); @@ -283,33 +279,27 @@
-

Worker attach Console

{worker?.label ?? workerId}

-

- Target authority is runtime_id + worker_id. Browser traffic uses Workspace Backend Worker APIs only; - Runtime endpoints, credentials, socket paths, and session paths are not exposed. -

-
- {worker?.state ?? 'unknown'} · {worker?.status ?? 'loading'} · stream {streamState} +
+
+ {worker?.state ?? 'unknown'} · {worker?.status ?? 'loading'} · stream {streamState} +
+
-
-
-
-
-

Transcript and protocol events

- {#if projection.status || projection.usage} -

- {#if projection.status}status: {projection.status}{/if} - {#if projection.status && projection.usage} · {/if} - {#if projection.usage}usage: {projection.usage}{/if} -

- {/if} -
- -
+
+
+ {#if projection.status || projection.usage} +

+ {#if projection.status}status: {projection.status}{/if} + {#if projection.status && projection.usage} · {/if} + {#if projection.usage}usage: {projection.usage}{/if} +

+ {/if} {#if workerError}

{workerError}

@@ -319,21 +309,26 @@ {/if} {#if lines.length === 0} -

No transcript items or observation events are available for this Worker yet.

+

No console output is available for this Worker yet.

{:else} -
    +
      {#each lines as item}
    1. -
      - {item.title} - {item.source}{item.streaming ? ' · streaming' : ''} -
      + {#if lineClass(item) !== 'assistant' && lineClass(item) !== 'user'} +
      + {item.title} + {#if item.streaming}streaming{/if} +
      + {:else if item.streaming} +
      + streaming +
      + {/if}
      {item.body || '—'}
      - {#if item.detail || item.cursor} + {#if item.detail}
      - metadata - {#if item.detail}

      {item.detail}

      {/if} - {#if item.cursor}{item.cursor}{/if} + detail +

      {item.detail}

      {/if}
    2. @@ -342,8 +337,14 @@ {/if}
-
+ + {#if workerDetailsOpen} + -
+ {/if}
-
diff --git a/web/workspace/vite.config.ts b/web/workspace/vite.config.ts index ef9074fe..ebb3b63e 100644 --- a/web/workspace/vite.config.ts +++ b/web/workspace/vite.config.ts @@ -7,7 +7,8 @@ export default defineConfig({ proxy: { '/api': { target: 'http://127.0.0.1:8787', - changeOrigin: true + changeOrigin: true, + ws: true } } }