From 54bb7209a47239f7854ff0e0b5d4b67e818338fb Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 24 Jul 2026 17:54:16 +0900 Subject: [PATCH] style: format web workspace --- web/workspace/README.md | 15 +- web/workspace/src/app.html | 2 +- web/workspace/src/lib/generated/protocol.ts | 356 +++++++++---- .../src/lib/workspace/alerts/store.ts | 11 +- web/workspace/src/lib/workspace/auth/api.ts | 30 +- .../src/lib/workspace/auth/model.test.ts | 30 +- web/workspace/src/lib/workspace/auth/model.ts | 37 +- .../lib/workspace/console/chat-submit.test.ts | 22 +- .../src/lib/workspace/console/chat-submit.ts | 21 +- .../console/composer-command.test.ts | 5 +- .../lib/workspace/console/composer-command.ts | 31 +- .../src/lib/workspace/console/model.test.ts | 44 +- .../src/lib/workspace/console/model.ts | 62 ++- .../console/worker-console.ui.test.ts | 107 ++-- .../src/lib/workspace/settings/model.test.ts | 23 +- .../src/lib/workspace/settings/profile-api.ts | 21 +- .../lib/workspace/settings/profile-routes.ts | 13 +- .../workspace/sidebar/repository-nav.test.ts | 28 +- .../lib/workspace/sidebar/repository-nav.ts | 5 +- .../src/lib/workspace/sidebar/sidebar.css | 488 +++++++++--------- .../src/lib/workspace/sidebar/types.ts | 18 +- .../lib/workspace/sidebar/worker-launch.ts | 37 +- .../src/routes/w/[workspaceId]/+layout.ts | 5 +- .../w/[workspaceId]/memory/staging/+page.ts | 8 +- .../objectives/[objectiveId]/+page.ts | 2 +- .../workers/[workerId]/console/+page.ts | 4 +- .../profiles/trees/[sourceTreeId]/+page.ts | 2 +- .../runtimes/[runtimeId]/workdirs/+page.ts | 10 +- .../[workspaceId]/tickets/[ticketId]/+page.ts | 5 +- .../routes/w/[workspaceId]/workers/+page.ts | 15 +- web/workspace/svelte.config.js | 16 +- web/workspace/vite.config.ts | 30 +- 32 files changed, 974 insertions(+), 529 deletions(-) diff --git a/web/workspace/README.md b/web/workspace/README.md index 87d7d88b..613364d8 100644 --- a/web/workspace/README.md +++ b/web/workspace/README.md @@ -2,7 +2,8 @@ SvelteKit static SPA for the Yoi workspace control plane. -The frontend is intentionally static. Workspace authority, validation, and API behavior live in the Rust `yoi-workspace-server` backend. +The frontend is intentionally static. Workspace authority, validation, and API +behavior live in the Rust `yoi-workspace-server` backend. ## Development @@ -22,7 +23,9 @@ cd web/workspace deno task dev ``` -The Vite dev server proxies `/api/*` to `http://127.0.0.1:8787`, so frontend hot reload works while the Rust backend serves the workspace API. Open the Vite URL printed by `deno task dev`. +The Vite dev server proxies `/api/*` to `http://127.0.0.1:8787`, so frontend hot +reload works while the Rust backend serves the workspace API. Open the Vite URL +printed by `deno task dev`. If you want to run the backend from the repository root instead: @@ -30,7 +33,10 @@ If you want to run the backend from the repository root instead: cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787 ``` -The backend reads Workspace records from the Yoi server DB at `/server/server.db`. Run `cargo run -p yoi-workspace-server -- init --workspace .` first when the server DB has not been initialized. +The backend reads Workspace records from the Yoi server DB at +`/server/server.db`. Run +`cargo run -p yoi-workspace-server -- init --workspace .` first when the server +DB has not been initialized. ## Static build @@ -40,7 +46,8 @@ Build the SPA: deno task build ``` -Static asset packaging is a deployment concern. Local development normally uses the Vite dev server proxy plus the Rust backend command above. +Static asset packaging is a deployment concern. Local development normally uses +the Vite dev server proxy plus the Rust backend command above. ## Checks diff --git a/web/workspace/src/app.html b/web/workspace/src/app.html index adf8bd87..179941ae 100644 --- a/web/workspace/src/app.html +++ b/web/workspace/src/app.html @@ -1,4 +1,4 @@ - + diff --git a/web/workspace/src/lib/generated/protocol.ts b/web/workspace/src/lib/generated/protocol.ts index b3da98cd..b639802a 100644 --- a/web/workspace/src/lib/generated/protocol.ts +++ b/web/workspace/src/lib/generated/protocol.ts @@ -12,112 +12,294 @@ export type WorkerStatus = "idle" | "running" | "paused"; export type TurnResult = "finished" | "paused"; -export type InvokeKind = "user_send" | "notify" | "worker_event" | "system_reminder" | "wakeup"; +export type InvokeKind = + | "user_send" + | "notify" + | "worker_event" + | "system_reminder" + | "wakeup"; export type RunResult = "finished" | "paused" | "limit_reached" | "rolled_back"; -export type ErrorCode = "already_running" | "not_running" | "not_paused" | "provider_error" | "tool_error" | "invalid_request" | "internal"; +export type ErrorCode = + | "already_running" + | "not_running" + | "not_paused" + | "provider_error" + | "tool_error" + | "invalid_request" + | "internal"; export type Permission = "read" | "write"; export type InFlightToolCallState = "pending" | "streaming_args" | "done"; export type ScopeRule = { -/** - * Target path. Must be absolute by the time a `Scope` is built from - * this rule — relative paths are resolved per-layer against the - * manifest file's directory (cwd for overlay layers) before cascade - * merge. - */ -target: string, -/** - * Permission level this rule grants (allow) or caps strictly below - * (deny). - */ -permission: Permission, -/** - * When `false`, the rule only matches the target itself and its - * direct children. Defaults to `true`. - */ -recursive: boolean, }; + /** + * Target path. Must be absolute by the time a `Scope` is built from + * this rule — relative paths are resolved per-layer against the + * manifest file's directory (cwd for overlay layers) before cascade + * merge. + */ + target: string; + /** + * Permission level this rule grants (allow) or caps strictly below + * (deny). + */ + permission: Permission; + /** + * When `false`, the rule only matches the target itself and its + * direct children. Defaults to `true`. + */ + recursive: boolean; +}; -export type CompletionEntry = { value: string, is_dir: boolean, }; +export type CompletionEntry = { value: string; is_dir: boolean }; -export type RewindTargetId = { segment_id: string, user_input_entry_index: number, }; +export type RewindTargetId = { + segment_id: string; + user_input_entry_index: number; +}; -export type RewindTarget = { id: RewindTargetId, expected_head_entries: number, truncate_entries: number, turn_index: number, timestamp_ms: number | null, preview: string, eligible: boolean, disabled_reason: string | null, warning: string | null, }; +export type RewindTarget = { + id: RewindTargetId; + expected_head_entries: number; + truncate_entries: number; + turn_index: number; + timestamp_ms: number | null; + preview: string; + eligible: boolean; + disabled_reason: string | null; + warning: string | null; +}; -export type RewindSummary = { truncated_to_entries: number, discarded_entries: number, tool_side_effect_warning: boolean, }; +export type RewindSummary = { + truncated_to_entries: number; + discarded_entries: number; + tool_side_effect_warning: boolean; +}; -export type InFlightBlock = { "kind": "text", text: string, finished?: boolean, } | { "kind": "thinking", text: string, finished?: boolean, } | { "kind": "tool_call", id: string, name: string, args: string, state?: InFlightToolCallState, }; +export type InFlightBlock = + | { "kind": "text"; text: string; finished?: boolean } + | { "kind": "thinking"; text: string; finished?: boolean } + | { + "kind": "tool_call"; + id: string; + name: string; + args: string; + state?: InFlightToolCallState; + }; -export type InFlightSnapshot = { blocks?: Array, }; +export type InFlightSnapshot = { blocks?: Array }; -export type Greeting = { worker_name: string, cwd: string, provider: string, model: string, scope_summary: string, tools: Array, -/** - * Model context window in tokens. Always filled by the Worker greeting. - */ -context_window: number, -/** - * Estimated current session context tokens at connect time. - */ -context_tokens: number, }; +export type Greeting = { + worker_name: string; + cwd: string; + provider: string; + model: string; + scope_summary: string; + tools: Array; + /** + * Model context window in tokens. Always filled by the Worker greeting. + */ + context_window: number; + /** + * Estimated current session context tokens at connect time. + */ + context_tokens: number; +}; -export type Alert = { level: AlertLevel, source: AlertSource, message: string, -/** - * Milliseconds since the Unix epoch. - */ -timestamp_ms: number, }; +export type Alert = { + level: AlertLevel; + source: AlertSource; + message: string; + /** + * Milliseconds since the Unix epoch. + */ + timestamp_ms: number; +}; -export type MemoryWorkerEvent = { worker: string, status: string, run_id: string, trigger: string, reason: string, -/** - * Human-readable compact form for actionbar rendering. - */ -message: string, -/** - * Milliseconds since the Unix epoch. - */ -timestamp_ms: number, }; +export type MemoryWorkerEvent = { + worker: string; + status: string; + run_id: string; + trigger: string; + reason: string; + /** + * Human-readable compact form for actionbar rendering. + */ + message: string; + /** + * Milliseconds since the Unix epoch. + */ + timestamp_ms: number; +}; -export type Segment = { "kind": "text", content: string, } | { "kind": "paste", id: number, chars: number, lines: number, content: string, } | { "kind": "file_ref", path: string, } | { "kind": "unknown" }; +export type Segment = + | { "kind": "text"; content: string } + | { + "kind": "paste"; + id: number; + chars: number; + lines: number; + content: string; + } + | { "kind": "file_ref"; path: string } + | { "kind": "unknown" }; -export type WorkerEvent = { "kind": "turn_ended", worker_name: string, } | { "kind": "errored", worker_name: string, message: string, } | { "kind": "shut_down", worker_name: string, } | { "kind": "scope_sub_delegated", -/** - * Sub-delegating Worker (= the sender itself). - */ -parent_worker: string, -/** - * Name of the grandchild Worker. - */ -sub_worker: string, -/** - * Unix-socket path where the grandchild is reachable. - */ -sub_socket: string, -/** - * Scope delegated to the grandchild. - */ -scope: Array, }; +export type WorkerEvent = + | { "kind": "turn_ended"; worker_name: string } + | { "kind": "errored"; worker_name: string; message: string } + | { "kind": "shut_down"; worker_name: string } + | { + "kind": "scope_sub_delegated"; + /** + * Sub-delegating Worker (= the sender itself). + */ + parent_worker: string; + /** + * Name of the grandchild Worker. + */ + sub_worker: string; + /** + * Unix-socket path where the grandchild is reachable. + */ + sub_socket: string; + /** + * Scope delegated to the grandchild. + */ + scope: Array; + }; -export type Method = { "method": "run", "params": { input: Array, } } | { "method": "notify", "params": { message: string, auto_run?: boolean, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "resume" } | { "method": "cancel" } | { "method": "pause" } | { "method": "compact" } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown" } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string, } } | { "method": "list_workers" } | { "method": "restore_worker", "params": { name: string, } } | { "method": "register_peer", "params": { name: string, } }; +export type Method = + | { "method": "run"; "params": { input: Array } } + | { "method": "notify"; "params": { message: string; auto_run?: boolean } } + | { "method": "worker_event"; "params": WorkerEvent } + | { "method": "resume" } + | { "method": "cancel" } + | { "method": "pause" } + | { "method": "compact" } + | { "method": "list_rewind_targets" } + | { + "method": "rewind_to"; + "params": { target: RewindTargetId; expected_head_entries: number }; + } + | { "method": "shutdown" } + | { + "method": "list_completions"; + "params": { kind: CompletionKind; prefix: string }; + } + | { "method": "list_workers" } + | { "method": "restore_worker"; "params": { name: string } } + | { "method": "register_peer"; "params": { name: string } }; -export type Event = { "event": "user_message", "data": { segments: Array, } } | { "event": "system_item", "data": { item: unknown, } } | { "event": "invoke_start", "data": { kind: InvokeKind, } } | { "event": "turn_start", "data": { turn: number, } } | { "event": "turn_end", "data": { turn: number, result: TurnResult, } } | { "event": "llm_call_start", "data": { llm_call: number, } } | { "event": "llm_call_end", "data": { llm_call: number, } } | { "event": "llm_retry", "data": { llm_call: number, -/** - * The attempt that just failed. 1 origin. - */ -failed_attempt: number, max_attempts: number, wait_ms: number, elapsed_ms: number, status?: number | null, error: string, } } | { "event": "llm_continuation", "data": { llm_call: number, attempt: number, max_attempts: number, reason: string, } } | { "event": "text_delta", "data": { text: string, } } | { "event": "text_done", "data": { text: string, } } | { "event": "thinking_start" } | { "event": "thinking_delta", "data": { text: string, } } | { "event": "thinking_done", "data": { text: string, } } | { "event": "tool_call_start", "data": { id: string, name: string, } } | { "event": "tool_call_args_delta", "data": { id: string, json: string, } } | { "event": "tool_call_done", "data": { id: string, name: string, arguments: string, } } | { "event": "tool_result", "data": { id: string, -/** - * Short human-readable summary. Always present; used by clients - * that only want a 1-line rendering (e.g. collapsed views). - */ -summary: string, -/** - * Full tool output. Absent when the tool chose to return - * summary-only, or when the result was pruned. - */ -output?: string | null, is_error: boolean, } } | { "event": "usage", "data": { input_tokens: number | null, output_tokens: number | null, cache_read_input_tokens?: number | null, } } | { "event": "run_end", "data": { result: RunResult, } } | { "event": "error", "data": { code: ErrorCode, message: string, } } | { "event": "snapshot", "data": { entries: Array, greeting: Greeting, status: WorkerStatus, -/** - * Unfinished model output that has already streamed in the current - * run but is not yet represented by committed snapshot entries. - */ -in_flight?: InFlightSnapshot, } } | { "event": "segment_rotated", "data": { entry: unknown, } } | { "event": "status", "data": { status: WorkerStatus, } } | { "event": "completions", "data": { kind: CompletionKind, entries: Array, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array, } } | { "event": "rewind_applied", "data": { entries: Array, input: Array, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compact_start" } | { "event": "compact_done", "data": { new_segment_id: string, } } | { "event": "compact_failed", "data": { error: string, } } | { "event": "shutdown" }; +export type Event = + | { "event": "user_message"; "data": { segments: Array } } + | { "event": "system_item"; "data": { item: unknown } } + | { "event": "invoke_start"; "data": { kind: InvokeKind } } + | { "event": "turn_start"; "data": { turn: number } } + | { "event": "turn_end"; "data": { turn: number; result: TurnResult } } + | { "event": "llm_call_start"; "data": { llm_call: number } } + | { "event": "llm_call_end"; "data": { llm_call: number } } + | { + "event": "llm_retry"; + "data": { + llm_call: number; + /** + * The attempt that just failed. 1 origin. + */ + failed_attempt: number; + max_attempts: number; + wait_ms: number; + elapsed_ms: number; + status?: number | null; + error: string; + }; + } + | { + "event": "llm_continuation"; + "data": { + llm_call: number; + attempt: number; + max_attempts: number; + reason: string; + }; + } + | { "event": "text_delta"; "data": { text: string } } + | { "event": "text_done"; "data": { text: string } } + | { "event": "thinking_start" } + | { "event": "thinking_delta"; "data": { text: string } } + | { "event": "thinking_done"; "data": { text: string } } + | { "event": "tool_call_start"; "data": { id: string; name: string } } + | { "event": "tool_call_args_delta"; "data": { id: string; json: string } } + | { + "event": "tool_call_done"; + "data": { id: string; name: string; arguments: string }; + } + | { + "event": "tool_result"; + "data": { + id: string; + /** + * Short human-readable summary. Always present; used by clients + * that only want a 1-line rendering (e.g. collapsed views). + */ + summary: string; + /** + * Full tool output. Absent when the tool chose to return + * summary-only, or when the result was pruned. + */ + output?: string | null; + is_error: boolean; + }; + } + | { + "event": "usage"; + "data": { + input_tokens: number | null; + output_tokens: number | null; + cache_read_input_tokens?: number | null; + }; + } + | { "event": "run_end"; "data": { result: RunResult } } + | { "event": "error"; "data": { code: ErrorCode; message: string } } + | { + "event": "snapshot"; + "data": { + entries: Array; + greeting: Greeting; + status: WorkerStatus; + /** + * Unfinished model output that has already streamed in the current + * run but is not yet represented by committed snapshot entries. + */ + in_flight?: InFlightSnapshot; + }; + } + | { "event": "segment_rotated"; "data": { entry: unknown } } + | { "event": "status"; "data": { status: WorkerStatus } } + | { + "event": "completions"; + "data": { kind: CompletionKind; entries: Array }; + } + | { + "event": "rewind_targets"; + "data": { head_entries: number; targets: Array }; + } + | { + "event": "rewind_applied"; + "data": { + entries: Array; + input: Array; + summary: RewindSummary; + }; + } + | { "event": "workers_listed"; "data": { workers: unknown } } + | { "event": "worker_restored"; "data": { result: unknown } } + | { "event": "peer_registered"; "data": { result: unknown } } + | { "event": "alert"; "data": Alert } + | { "event": "memory_worker"; "data": MemoryWorkerEvent } + | { "event": "compact_start" } + | { "event": "compact_done"; "data": { new_segment_id: string } } + | { "event": "compact_failed"; "data": { error: string } } + | { "event": "shutdown" }; diff --git a/web/workspace/src/lib/workspace/alerts/store.ts b/web/workspace/src/lib/workspace/alerts/store.ts index 5e7916f8..a0538431 100644 --- a/web/workspace/src/lib/workspace/alerts/store.ts +++ b/web/workspace/src/lib/workspace/alerts/store.ts @@ -1,4 +1,10 @@ -export type WorkspaceAlertLevel = "success" | "info" | "warning" | "error" | "system" | "debug"; +export type WorkspaceAlertLevel = + | "success" + | "info" + | "warning" + | "error" + | "system" + | "debug"; export type WorkspaceAlert = { id: string; @@ -32,7 +38,8 @@ export function pushWorkspaceAlert( message: string, options: { title?: string; id?: string } = {}, ): string { - const id = options.id ?? `${Date.now().toString(36)}-${(sequence++).toString(36)}`; + const id = options.id ?? + `${Date.now().toString(36)}-${(sequence++).toString(36)}`; alerts = [ ...alerts.filter((alert) => alert.id !== id), { diff --git a/web/workspace/src/lib/workspace/auth/api.ts b/web/workspace/src/lib/workspace/auth/api.ts index 08210e6b..94bf2531 100644 --- a/web/workspace/src/lib/workspace/auth/api.ts +++ b/web/workspace/src/lib/workspace/auth/api.ts @@ -1,20 +1,22 @@ import { authenticationCredentialToJson, - isPublicKeyCredential, - prepareLoginOptions, - prepareRegistrationOptions, - registrationCredentialToJson, type DeviceApprovalResponse, + isPublicKeyCredential, type PasskeyLoginOptionsResponse, type PasskeyRegistrationOptionsResponse, type PasskeyUserResponse, + prepareLoginOptions, + prepareRegistrationOptions, + registrationCredentialToJson, type WhoamiResponse, } from "./model"; async function jsonOrThrow(response: Response): Promise { const text = await response.text(); if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}${text ? `: ${text}` : ""}`); + throw new Error( + `${response.status} ${response.statusText}${text ? `: ${text}` : ""}`, + ); } return text ? JSON.parse(text) as T : (null as T); } @@ -23,8 +25,12 @@ function browserOrigin(): string | null { return globalThis.location?.origin ?? null; } -export async function loadWhoami(fetcher: typeof fetch = fetch): Promise { - return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then(jsonOrThrow); +export async function loadWhoami( + fetcher: typeof fetch = fetch, +): Promise { + return await fetcher("/api/auth/whoami", { credentials: "same-origin" }).then( + jsonOrThrow, + ); } export async function registerPasskey( @@ -36,14 +42,20 @@ export async function registerPasskey( method: "POST", headers: { "content-type": "application/json" }, credentials: "same-origin", - body: JSON.stringify({ handle, display_name: displayName, browser_origin: browserOrigin() }), + body: JSON.stringify({ + handle, + display_name: displayName, + browser_origin: browserOrigin(), + }), }).then(jsonOrThrow); const credential = await navigator.credentials.create({ publicKey: prepareRegistrationOptions(options), }); if (!isPublicKeyCredential(credential)) { - throw new Error("Passkey registration did not return a public-key credential."); + throw new Error( + "Passkey registration did not return a public-key credential.", + ); } return await fetcher("/api/auth/passkeys/registration/complete", { diff --git a/web/workspace/src/lib/workspace/auth/model.test.ts b/web/workspace/src/lib/workspace/auth/model.test.ts index 443443aa..d014086c 100644 --- a/web/workspace/src/lib/workspace/auth/model.test.ts +++ b/web/workspace/src/lib/workspace/auth/model.test.ts @@ -11,7 +11,9 @@ declare const Deno: { function assertEquals(actual: T, expected: T): void { if (JSON.stringify(actual) !== JSON.stringify(expected)) { - throw new Error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + throw new Error( + `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ); } } @@ -19,7 +21,9 @@ function bytes(buffer: BufferSource): number[] { if (buffer instanceof ArrayBuffer) { return [...new Uint8Array(buffer)]; } - return [...new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)]; + return [ + ...new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength), + ]; } Deno.test("base64url helpers round-trip binary data", () => { @@ -43,13 +47,20 @@ Deno.test("prepareRegistrationOptions decodes binary public key fields", () => { displayName: "Local User", }, pubKeyCredParams: [{ type: "public-key", alg: -7 }], - excludeCredentials: [{ type: "public-key", id: "BwgJ" as unknown as BufferSource }], + excludeCredentials: [{ + type: "public-key", + id: "BwgJ" as unknown as BufferSource, + }], }, }); assertEquals(bytes(options.challenge), [1, 2, 3]); assertEquals(bytes(options.user.id), [4, 5, 6]); - assertEquals(bytes(options.excludeCredentials?.[0].id as BufferSource), [7, 8, 9]); + assertEquals(bytes(options.excludeCredentials?.[0].id as BufferSource), [ + 7, + 8, + 9, + ]); }); Deno.test("prepareLoginOptions decodes challenge and allowed credential ids", () => { @@ -57,10 +68,17 @@ Deno.test("prepareLoginOptions decodes challenge and allowed credential ids", () challenge_id: "challenge-1", public_key: { challenge: "AQID" as unknown as BufferSource, - allowCredentials: [{ type: "public-key", id: "BwgJ" as unknown as BufferSource }], + allowCredentials: [{ + type: "public-key", + id: "BwgJ" as unknown as BufferSource, + }], }, }); assertEquals(bytes(options.challenge), [1, 2, 3]); - assertEquals(bytes(options.allowCredentials?.[0].id as BufferSource), [7, 8, 9]); + assertEquals(bytes(options.allowCredentials?.[0].id as BufferSource), [ + 7, + 8, + 9, + ]); }); diff --git a/web/workspace/src/lib/workspace/auth/model.ts b/web/workspace/src/lib/workspace/auth/model.ts index 56899616..5d6711d2 100644 --- a/web/workspace/src/lib/workspace/auth/model.ts +++ b/web/workspace/src/lib/workspace/auth/model.ts @@ -15,12 +15,16 @@ export type WhoamiResponse = { export type PasskeyRegistrationOptionsResponse = { challenge_id: string; - public_key: PublicKeyCredentialCreationOptions | { publicKey: PublicKeyCredentialCreationOptions }; + public_key: PublicKeyCredentialCreationOptions | { + publicKey: PublicKeyCredentialCreationOptions; + }; }; export type PasskeyLoginOptionsResponse = { challenge_id: string; - public_key: PublicKeyCredentialRequestOptions | { publicKey: PublicKeyCredentialRequestOptions }; + public_key: PublicKeyCredentialRequestOptions | { + publicKey: PublicKeyCredentialRequestOptions; + }; }; export type PasskeyUserResponse = { @@ -65,7 +69,10 @@ export function bufferToBase64Url(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); - return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace( + /=+$/g, + "", + ); } function unwrapPublicKey(value: T | { publicKey: T }): T { @@ -79,12 +86,16 @@ export function prepareRegistrationOptions( options: PasskeyRegistrationOptionsResponse, ): PublicKeyCredentialCreationOptions { const publicKey = structuredClone(unwrapPublicKey(options.public_key)); - publicKey.challenge = base64UrlToBuffer(publicKey.challenge as unknown as string); + publicKey.challenge = base64UrlToBuffer( + publicKey.challenge as unknown as string, + ); publicKey.user = { ...publicKey.user, id: base64UrlToBuffer(publicKey.user.id as unknown as string), }; - publicKey.excludeCredentials = publicKey.excludeCredentials?.map((credential) => ({ + publicKey.excludeCredentials = publicKey.excludeCredentials?.map(( + credential, + ) => ({ ...credential, id: base64UrlToBuffer(credential.id as unknown as string), })); @@ -95,8 +106,12 @@ export function prepareLoginOptions( options: PasskeyLoginOptionsResponse, ): PublicKeyCredentialRequestOptions { const publicKey = structuredClone(unwrapPublicKey(options.public_key)); - publicKey.challenge = base64UrlToBuffer(publicKey.challenge as unknown as string); - publicKey.allowCredentials = publicKey.allowCredentials?.map((credential) => ({ + publicKey.challenge = base64UrlToBuffer( + publicKey.challenge as unknown as string, + ); + publicKey.allowCredentials = publicKey.allowCredentials?.map(( + credential, + ) => ({ ...credential, id: base64UrlToBuffer(credential.id as unknown as string), })); @@ -131,11 +146,15 @@ export function authenticationCredentialToJson( clientDataJSON: bufferToBase64Url(response.clientDataJSON), authenticatorData: bufferToBase64Url(response.authenticatorData), signature: bufferToBase64Url(response.signature), - userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null, + userHandle: response.userHandle + ? bufferToBase64Url(response.userHandle) + : null, }, }; } -export function isPublicKeyCredential(credential: Credential | null): credential is PublicKeyCredential { +export function isPublicKeyCredential( + credential: Credential | null, +): credential is PublicKeyCredential { return credential != null && credential.type === "public-key"; } diff --git a/web/workspace/src/lib/workspace/console/chat-submit.test.ts b/web/workspace/src/lib/workspace/console/chat-submit.test.ts index 87aa7455..851f871b 100644 --- a/web/workspace/src/lib/workspace/console/chat-submit.test.ts +++ b/web/workspace/src/lib/workspace/console/chat-submit.test.ts @@ -53,9 +53,16 @@ Deno.test("shouldSubmitChatKey still supports explicit Cmd+Enter behavior", () = }); Deno.test("shouldSubmitChatKey ignores IME composition and repeated Enter", () => { - const options = { mode: "mod-enter" as const, modKey: "meta" as const, enabled: true }; + const options = { + mode: "mod-enter" as const, + modKey: "meta" as const, + enabled: true, + }; assertEquals( - shouldSubmitChatKey({ key: "Enter", metaKey: true, isComposing: true }, options), + shouldSubmitChatKey( + { key: "Enter", metaKey: true, isComposing: true }, + options, + ), false, ); assertEquals( @@ -69,7 +76,14 @@ Deno.test("shouldSubmitChatKey ignores IME composition and repeated Enter", () = }); Deno.test("shouldSubmitChatKey supports enter submit mode", () => { - const options = { mode: "enter" as const, modKey: "meta" as const, enabled: true }; + const options = { + mode: "enter" as const, + modKey: "meta" as const, + enabled: true, + }; assert(shouldSubmitChatKey({ key: "Enter" }, options), "Enter should submit"); - assertEquals(shouldSubmitChatKey({ key: "Enter", shiftKey: true }, options), false); + assertEquals( + shouldSubmitChatKey({ key: "Enter", shiftKey: true }, options), + false, + ); }); diff --git a/web/workspace/src/lib/workspace/console/chat-submit.ts b/web/workspace/src/lib/workspace/console/chat-submit.ts index 88c4a1d9..709e59ce 100644 --- a/web/workspace/src/lib/workspace/console/chat-submit.ts +++ b/web/workspace/src/lib/workspace/console/chat-submit.ts @@ -23,7 +23,9 @@ export type ChatSubmitKeyEventLike = { which?: number; }; -function normalizeOptions(options: ChatSubmitOptions): NormalizedChatSubmitOptions { +function normalizeOptions( + options: ChatSubmitOptions, +): NormalizedChatSubmitOptions { return { mode: "mod-enter", modKey: "auto", @@ -47,7 +49,8 @@ function isApplePlatform(): boolean { } const platform = navigator.platform || ""; const userAgent = navigator.userAgent || ""; - return /Mac|iPhone|iPad|iPod/.test(platform) || /Mac|iPhone|iPad|iPod/.test(userAgent); + return /Mac|iPhone|iPad|iPod/.test(platform) || + /Mac|iPhone|iPad|iPod/.test(userAgent); } function resolveModKey(modKey: ChatSubmitModKey): "meta" | "ctrl" { @@ -57,8 +60,13 @@ function resolveModKey(modKey: ChatSubmitModKey): "meta" | "ctrl" { return modKey; } -function isModPressed(event: ChatSubmitKeyEventLike, modKey: ChatSubmitModKey): boolean { - return resolveModKey(modKey) === "meta" ? event.metaKey === true : event.ctrlKey === true; +function isModPressed( + event: ChatSubmitKeyEventLike, + modKey: ChatSubmitModKey, +): boolean { + return resolveModKey(modKey) === "meta" + ? event.metaKey === true + : event.ctrlKey === true; } export function shouldSubmitChatKey( @@ -79,7 +87,10 @@ export function shouldSubmitChatKey( return isModPressed(event, options.modKey); } -export function chatSubmit(node: HTMLTextAreaElement, options: ChatSubmitOptions) { +export function chatSubmit( + node: HTMLTextAreaElement, + options: ChatSubmitOptions, +) { let current = normalizeOptions(options); let isComposing = false; diff --git a/web/workspace/src/lib/workspace/console/composer-command.test.ts b/web/workspace/src/lib/workspace/console/composer-command.test.ts index baffa920..7cdaa7cd 100644 --- a/web/workspace/src/lib/workspace/console/composer-command.test.ts +++ b/web/workspace/src/lib/workspace/console/composer-command.test.ts @@ -16,5 +16,8 @@ Deno.test("parseSigilSegments turns file sigils into file refs", () => { }); Deno.test("parseSigilSegments leaves hash sigils as plain text", () => { - assertEquals(parseSigilSegments("ask #memory"), [{ kind: "text", content: "ask #memory" }]); + assertEquals(parseSigilSegments("ask #memory"), [{ + kind: "text", + content: "ask #memory", + }]); }); diff --git a/web/workspace/src/lib/workspace/console/composer-command.ts b/web/workspace/src/lib/workspace/console/composer-command.ts index 34681b3a..ddc68c8d 100644 --- a/web/workspace/src/lib/workspace/console/composer-command.ts +++ b/web/workspace/src/lib/workspace/console/composer-command.ts @@ -25,7 +25,8 @@ type CommandSpec = { const COMMANDS: Record = { help: { usage: ":help [command]", - description: "Show available Web Console commands or details for one command.", + description: + "Show available Web Console commands or details for one command.", }, "?": { usage: ":? [command]", @@ -49,7 +50,8 @@ const COMMANDS: Record = { }, peer: { usage: ":peer ", - description: "Register another existing Worker as a reciprocal metadata peer.", + description: + "Register another existing Worker as a reciprocal metadata peer.", }, system: { usage: ":system ", @@ -71,7 +73,9 @@ export function buildComposerRequest(value: string): ComposerCommandResult { request: { kind: "user", content, - segments: segments.some((segment) => segment.kind !== "text") ? segments : undefined, + segments: segments.some((segment) => segment.kind !== "text") + ? segments + : undefined, }, }; } @@ -97,13 +101,21 @@ function buildColonCommand(commandLine: string): ComposerCommandResult { if (argv.length > 0) { return invalidUsage("compact"); } - return { ok: true, request: { kind: "compact", content: "" }, notice: "compact requested" }; + return { + ok: true, + request: { kind: "compact", content: "" }, + notice: "compact requested", + }; case "rewind": case "rollback": if (argv.length > 0) { return invalidUsage("rewind"); } - return { ok: true, request: { kind: "list_rewind_targets", content: "" }, notice: "rewind targets requested" }; + return { + ok: true, + request: { kind: "list_rewind_targets", content: "" }, + notice: "rewind targets requested", + }; case "peer": if (argv.length !== 1) { return invalidUsage("peer"); @@ -156,7 +168,10 @@ function helpCommand(argv: string[]): ComposerCommandResult { } function invalidUsage(name: string): ComposerCommandResult { - return { ok: false, message: `Invalid arguments. Usage: ${COMMANDS[name].usage}` }; + return { + ok: false, + message: `Invalid arguments. Usage: ${COMMANDS[name].usage}`, + }; } export function parseSigilSegments(input: string): Segment[] { @@ -178,7 +193,9 @@ export function parseSigilSegments(input: string): Segment[] { if (cursor < input.length) { segments.push({ kind: "text", content: input.slice(cursor) }); } - return coalesceTextSegments(segments.length > 0 ? segments : [{ kind: "text", content: input }]); + return coalesceTextSegments( + segments.length > 0 ? segments : [{ kind: "text", content: input }], + ); } function sigilSegment(sigil: string, value: string): Segment { diff --git a/web/workspace/src/lib/workspace/console/model.test.ts b/web/workspace/src/lib/workspace/console/model.test.ts index bb15afd3..9b031f29 100644 --- a/web/workspace/src/lib/workspace/console/model.test.ts +++ b/web/workspace/src/lib/workspace/console/model.test.ts @@ -1,11 +1,11 @@ import type { Event } from "$lib/generated/protocol"; import { + type ConsoleLine, createConsoleProjector, projectConsole, segmentsToText, selectConsoleTimelineLines, workerConsoleHref, - type ConsoleLine, } from "./model.ts"; declare const Deno: { @@ -68,8 +68,6 @@ Deno.test("workerConsoleHref encodes runtime and worker target authority", () => ); }); - - Deno.test("projectConsole projects visible protocol rows", () => { const projection = projectConsole([ { @@ -192,7 +190,8 @@ Deno.test("projectConsole groups tool call lifecycle into one Call block", () => data: { id: "call-1", summary: "command completed", - output: "/repo\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12", + output: + "/repo\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12", is_error: false, }, } satisfies Event, @@ -269,7 +268,10 @@ Deno.test("projectConsole caps default tool request and result previews", () => assertEquals(line.title, "Call · CustomTool"); assertEquals(line.body.split("\n").length, 7); assert(line.body.includes("CustomTool — done"), "tool state should be shown"); - assert(line.body.includes('"first": "one"'), "request preview should be shown"); + assert( + line.body.includes('"first": "one"'), + "request preview should be shown", + ); assert(line.body.includes("out1"), "result preview should be shown"); assert(!line.body.includes("third"), "request preview should be capped"); assert(!line.body.includes("out3"), "result preview should be capped"); @@ -305,7 +307,10 @@ Deno.test("projectConsole shows Grep query and caps result preview to five entri const [line] = projection.lines.filter((line) => line.kind === "tool"); assertEquals(line.title, "Call · Grep"); - assert(line.body.includes("Grep — 6 matches"), "Grep summary should be shown"); + assert( + line.body.includes("Grep — 6 matches"), + "Grep summary should be shown", + ); assert(line.body.includes("query: needle"), "Grep query should be shown"); assert(line.body.includes("hit1"), "first result should be shown"); assert(line.body.includes("hit5"), "fifth result should be shown"); @@ -840,7 +845,9 @@ Deno.test("projectConsole renders snapshot entries and in-flight output", () => assertEquals(projection.status, "running"); assertEquals( - projection.lines.map((line) => `${line.kind}:${line.body}:${line.streaming}`), + projection.lines.map((line) => + `${line.kind}:${line.body}:${line.streaming}` + ), [ "user:seed user:false", "user:new user:false", @@ -903,7 +910,9 @@ Deno.test("selectConsoleTimelineLines keeps all users and only last assistant pe ]; assertEquals( - selectConsoleTimelineLines(items).map(({ item, index }) => `${index}:${item.id}`), + selectConsoleTimelineLines(items).map(({ item, index }) => + `${index}:${item.id}` + ), ["0:u1", "3:a2", "4:u2", "6:a4"], ); }); @@ -940,7 +949,10 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd data: { id: "write-rel", name: "Write", - arguments: JSON.stringify({ file_path: "/repo/out.txt", content: "ok" }), + arguments: JSON.stringify({ + file_path: "/repo/out.txt", + content: "ok", + }), }, } satisfies Event, }, @@ -1001,7 +1013,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd data: { id: "glob-rel", summary: "Found 2 file(s) matching **/*.rs", - output: "Found 2 file(s) matching **/*.rs\n/repo/src/main.rs\n/outside/lib.rs", + output: + "Found 2 file(s) matching **/*.rs\n/repo/src/main.rs\n/outside/lib.rs", is_error: false, }, } satisfies Event, @@ -1031,14 +1044,18 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd }, ]); - const bodies = projection.lines.filter((line) => line.kind === "tool").map((line) => line.body); + const bodies = projection.lines.filter((line) => line.kind === "tool").map(( + line, + ) => line.body); assertEquals(bodies[0], "Read — 1 file read\n src/main.rs"); assert( projection.lines[0].detail?.includes("from src/main.rs"), "Read summary detail path should be relative", ); assert( - bodies.some((body) => body.includes("Write — out.txt") && body.includes("Wrote out.txt")), + bodies.some((body) => + body.includes("Write — out.txt") && body.includes("Wrote out.txt") + ), "Write header and known result path should be relative", ); assert( @@ -1055,7 +1072,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd ); assert( bodies.some((body) => - body.includes("src/main.rs:12:needle") && body.includes("/outside/lib.rs:1:needle") + body.includes("src/main.rs:12:needle") && + body.includes("/outside/lib.rs:1:needle") ), "Grep should relativize line-start cwd paths and keep outside paths absolute", ); diff --git a/web/workspace/src/lib/workspace/console/model.ts b/web/workspace/src/lib/workspace/console/model.ts index 4be61683..abfa3d52 100644 --- a/web/workspace/src/lib/workspace/console/model.ts +++ b/web/workspace/src/lib/workspace/console/model.ts @@ -1,6 +1,6 @@ import type { - Event as ProtocolEvent, Alert, + Event as ProtocolEvent, InFlightBlock, InFlightToolCallState, Segment, @@ -148,7 +148,10 @@ export function emptyConsoleProjection(): ConsoleProjection { export function projectConsole( events: ConsoleEventInput[] = [], ): ConsoleProjection { - const projection = events.reduce(applyProtocolEvent, emptyConsoleProjection()); + const projection = events.reduce( + applyProtocolEvent, + emptyConsoleProjection(), + ); return projectVisibleConsole(projection); } @@ -171,7 +174,9 @@ export function createConsoleProjector() { }; } -function projectVisibleConsole(projection: ConsoleProjection): ConsoleProjection { +function projectVisibleConsole( + projection: ConsoleProjection, +): ConsoleProjection { return { ...projection, lines: aggregateReadToolLines(projection.lines), @@ -302,7 +307,9 @@ export function applyProtocolEvent( next.status = event.data.status; break; case "segment_rotated": - next.lines = snapshotLinesFromEntries(envelope.eventId, [event.data.entry], next.cwd); + next.lines = snapshotLinesFromEntries(envelope.eventId, [ + event.data.entry, + ], next.cwd); break; case "invoke_start": case "turn_start": @@ -730,7 +737,9 @@ function readDetail(toolCall: ToolCallView): string { `state: ${stateSuffix(toolCall.state)}`, `path: ${readPath(toolCall)}`, toolCall.summary - ? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}` + ? `summary: ${ + normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd) + }` : undefined, ]); } @@ -902,7 +911,9 @@ function toolCallDetail(toolCall: ToolCallView): string { `id: ${toolCall.id}`, `state: ${stateSuffix(toolCall.state)}`, toolCall.summary - ? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}` + ? `summary: ${ + normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd) + }` : undefined, argsText(toolCall) ? `arguments:\n${argsText(toolCall)}` : undefined, ]); @@ -1155,7 +1166,8 @@ function applyExtensionEntry( } const blockId = stringField(payload, "block_id") || "compact"; const state = stringField(payload, "state") || "running"; - const message = stringField(payload, "message") || compactMessageForState(state, payload); + const message = stringField(payload, "message") || + compactMessageForState(state, payload); upsertStatusLine( projection, blockId, @@ -1204,26 +1216,38 @@ function applyLoggedItem( break; } case "reasoning": { - const text = stringField(item, "text") ?? arrayField(item, "summary").filter((value) => typeof value === "string").join("\n"); + const text = stringField(item, "text") ?? + arrayField(item, "summary").filter((value) => typeof value === "string") + .join("\n"); if (text) { projection.lines.push(line(eventId, "thinking", "Thought", text)); } break; } case "tool_call": - upsertToolCall(projection, eventId, stringField(item, "call_id") ?? eventId, { - name: stringField(item, "name") ?? "Tool", - arguments: stringField(item, "arguments") ?? "", - argsStream: stringField(item, "arguments") ?? "", - state: "running", - }); + upsertToolCall( + projection, + eventId, + stringField(item, "call_id") ?? eventId, + { + name: stringField(item, "name") ?? "Tool", + arguments: stringField(item, "arguments") ?? "", + argsStream: stringField(item, "arguments") ?? "", + state: "running", + }, + ); break; case "tool_result": - attachToolResult(projection, eventId, stringField(item, "call_id") ?? eventId, { - summary: stringField(item, "summary") ?? "", - output: stringField(item, "content"), - isError: item["is_error"] === true, - }); + attachToolResult( + projection, + eventId, + stringField(item, "call_id") ?? eventId, + { + summary: stringField(item, "summary") ?? "", + output: stringField(item, "content"), + isError: item["is_error"] === true, + }, + ); break; default: break; 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 4a0e2c9f..4a5e5759 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 @@ -10,7 +10,9 @@ function assert(condition: unknown, message: string): asserts condition { } Deno.test("workspace app css uses bundled UI fonts", async () => { - const appCss = await Deno.readTextFile(new URL("./../../../app.css", import.meta.url)); + const appCss = await Deno.readTextFile( + new URL("./../../../app.css", import.meta.url), + ); assert( appCss.includes("gen-interface-jp/400.css") && appCss.includes("gen-interface-jp/500.css") && @@ -34,12 +36,20 @@ Deno.test("workspace app css uses bundled UI fonts", async () => { }); Deno.test("workspace feature css is owned outside app css", async () => { - const appCss = await Deno.readTextFile(new URL("./../../../app.css", import.meta.url)); + const appCss = await Deno.readTextFile( + new URL("./../../../app.css", import.meta.url), + ); const workspaceLayout = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url), + new URL( + "./../../../routes/w/[workspaceId]/+layout.svelte", + import.meta.url, + ), ); const settingsLayout = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/settings/+layout.svelte", import.meta.url), + new URL( + "./../../../routes/w/[workspaceId]/settings/+layout.svelte", + import.meta.url, + ), ); const accountPage = await Deno.readTextFile( new URL("./../../../routes/account/+page.svelte", import.meta.url), @@ -86,7 +96,9 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async () workspacePage.includes("runtimeSettingsHref") && workspacePage.includes("workersHref") && workspacePage.includes("workspaceRoute(workspaceId, '/tickets')") && - workspacePage.includes("workspaceRoute(workspaceId, '/settings/runtimes')") && + workspacePage.includes( + "workspaceRoute(workspaceId, '/settings/runtimes')", + ) && workspacePage.includes("workspaceRoute(workspaceId, '/workers')"), "top workspace page should link to Tickets, Runtime Inventory under Settings, and the Workers page", ); @@ -99,7 +111,7 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async () workersPage.includes("workerConsoleHref(worker, data.workspaceId)") && workersPage.includes('') && workersPage.includes('class="icon-action"') && - workersPage.includes('Delete ${worker.label}'), + workersPage.includes("Delete ${worker.label}"), "dedicated Workers page should expose a table, console link target, and icon actions per Worker", ); assert( @@ -122,10 +134,16 @@ Deno.test("workspace Tickets surface uses read-only Backend Ticket APIs", async new URL("../sidebar/TicketsNavSection.svelte", import.meta.url), ); const ticketsLoad = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/tickets/+page.ts", import.meta.url), + new URL( + "./../../../routes/w/[workspaceId]/tickets/+page.ts", + import.meta.url, + ), ); const ticketsPage = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/tickets/+page.svelte", import.meta.url), + new URL( + "./../../../routes/w/[workspaceId]/tickets/+page.svelte", + import.meta.url, + ), ); const ticketDetailLoad = await Deno.readTextFile( new URL( @@ -146,14 +164,18 @@ Deno.test("workspace Tickets surface uses read-only Backend Ticket APIs", async "Tickets sidebar section should link to the workspace Tickets surface", ); assert( - ticketsLoad.includes('`${workspaceApiPath(params.workspaceId, "/tickets")}?limit=1000`') && + ticketsLoad.includes( + '`${workspaceApiPath(params.workspaceId, "/tickets")}?limit=1000`', + ) && ticketsPage.includes("Notion-style filtering and sorting") && ticketsPage.includes("toggleSort('updated_at')") && ticketsPage.includes("bind:value={visibilityFilter}") && ticketsPage.includes("sortKey = $state('panel')") && ticketsPage.includes("workspace_action_priority") && ticketsPage.includes("bind:value={stateFilter}") && - ticketsPage.includes("workspaceRoute(data.workspaceId, `/tickets/${ticket.id}`)"), + ticketsPage.includes( + "workspaceRoute(data.workspaceId, `/tickets/${ticket.id}`)", + ), "Tickets list should read the workspace-scoped Ticket API and expose sortable/filterable table links", ); assert( @@ -170,10 +192,16 @@ Deno.test("workspace Memory Staging surface uses read-only scoped memory API", a new URL("../sidebar/MemoryNavSection.svelte", import.meta.url), ); const memoryLoad = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/memory/staging/+page.ts", import.meta.url), + new URL( + "./../../../routes/w/[workspaceId]/memory/staging/+page.ts", + import.meta.url, + ), ); const memoryPage = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/memory/staging/+page.svelte", import.meta.url), + new URL( + "./../../../routes/w/[workspaceId]/memory/staging/+page.svelte", + import.meta.url, + ), ); assert( @@ -182,7 +210,8 @@ Deno.test("workspace Memory Staging surface uses read-only scoped memory API", a "Memory sidebar section should link to the workspace Memory Staging surface", ); assert( - memoryLoad.includes("workspaceApiPath(params.workspaceId, '/memory/staging')") && + memoryLoad.includes("workspaceApiPath(params.workspaceId") && + memoryLoad.includes('"/memory/staging"') && memoryPage.includes("Memory Staging") && memoryPage.includes("Workspace Server memory authority") && memoryPage.includes("data.staging.data.invalid_count") && @@ -236,7 +265,9 @@ Deno.test("Worker Console renders markdown only for message rows", async () => { assert( consoleLine.includes("function shouldRenderMarkdown") && consoleLine.includes("item.kind === 'tool'") && - consoleLine.includes('

{bodyTextAfterToolSummary(item)}

') && + consoleLine.includes( + '

{bodyTextAfterToolSummary(item)}

', + ) && consoleLine.includes("{:else if shouldRenderMarkdown(item)}") && consoleLine.includes(""), "Console should keep markdown rendering to user/assistant/system message bodies and render tool text literally", @@ -249,7 +280,9 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps" ); assert( - consoleLine.includes('
') && + consoleLine.includes( + '
', + ) && consoleLine.includes("{#each item.diff as diffLine}") && consoleLine.includes("class={`diff-line ${diffLine.kind}`}") && !consoleLine.includes('
") &&
+      consolePage.includes('
') && !consolePage.includes("handleComposerShellClick") && consolePage.includes("bind:this={composerTextareaElement}") && - consolePage.includes('event.key === "PageUp" || event.key === "PageDown"') && + consolePage.includes( + 'event.key === "PageUp" || event.key === "PageDown"', + ) && consolePage.includes("scrollConsoleByPage") && - consolePage.includes("class=\"composer-input-footer\"") && + consolePage.includes('class="composer-input-footer"') && consolePage.includes("pointer-events: none") && - consolePage.includes("class=\"composer-footer-slot\"") && - consolePage.includes("class=\"composer-send-button\"") && + consolePage.includes('class="composer-footer-slot"') && + consolePage.includes('class="composer-send-button"') && consolePage.includes("pointer-events: auto") && - consolePage.includes("class=\"composer-send-icon\"") && + consolePage.includes('class="composer-send-icon"') && consolePage.includes('d="M8 6L12 2L16 6"') && consolePage.includes(".console-composer textarea") && consolePage.includes("resize: none") && @@ -366,14 +401,16 @@ Deno.test("workspace Runtime inventory lives under Settings admin routes", async assert( !sidebar.includes("RuntimesNavSection") && settingsModel.includes('id: "runtime-inventory"') && - settingsModel.includes('return `${SETTINGS_ROUTE}/runtimes`;'), + settingsModel.includes("return `${SETTINGS_ROUTE}/runtimes`;"), "Runtime inventory should be admin Settings navigation, not primary workspace sidebar navigation", ); assert( runtimesPage.includes("Runtime Inventory") && runtimesPage.includes("Open workdirs") && runtimesPage.includes("runtimes-table") && - runtimesPage.includes("/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs"), + runtimesPage.includes( + "/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs", + ), "Settings Runtime Inventory page should table Runtimes and link to each Runtime's workdirs", ); assert( @@ -431,7 +468,9 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac assert( consolePage.includes("workspaceApiPath(workspaceId, path)") && consolePage.includes("workerApiPath(") && - consolePage.includes("`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(") && + consolePage.includes( + "`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(", + ) && consolePage.includes("target.workerId"), "Worker detail should use the scoped backend Worker detail API", ); @@ -460,14 +499,16 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac "target-change effect should load data without depending on manual refresh state reads", ); assert( - consolePage.includes('const workerRunning = $derived(workerState === "running");') && + consolePage.includes( + 'const workerRunning = $derived(workerState === "running");', + ) && consolePage.includes( 'const composerEditable = $derived(protocolState === "open" && !sending);', ) && consolePage.includes('sendControl({ method: "cancel" }, "Stop")') && consolePage.includes("enabled: canSubmitDraft") && consolePage.includes("disabled={!composerEditable}") && - consolePage.includes('class:stop={workerRunning}') && + consolePage.includes("class:stop={workerRunning}") && consolePage.includes('"Stop Worker"') && consolePage.includes("disabled={composerSubmitDisabled}") && !consolePage.includes("disabled={!inputReady || sending}") && @@ -502,7 +543,10 @@ Deno.test("Account UI owns browser passkey session state without workspace autho new URL("../sidebar/sidebar.css", import.meta.url), ); const workspaceLayout = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url), + new URL( + "./../../../routes/w/[workspaceId]/+layout.svelte", + import.meta.url, + ), ); const workspaceLayoutLoad = await Deno.readTextFile( new URL("./../../../routes/w/[workspaceId]/+layout.ts", import.meta.url), @@ -562,7 +606,9 @@ Deno.test("Account UI owns browser passkey session state without workspace autho assert( workspaceLayout.includes("{#snippet workspaceSidebar()}") && workspaceLayout.includes("WorkspaceSidebar") && - workspaceLayout.includes("") && + workspaceLayout.includes( + "", + ) && workspaceLayoutLoad.includes("params.workspaceId") && workspaceLayoutLoad.includes("workspaceApiPath(workspaceId"), "Workspace layout should load workspace data and register a WorkspaceSidebar snippet", @@ -592,7 +638,8 @@ Deno.test("Account UI owns browser passkey session state without workspace autho "SidebarOverride should register and clean up the child-provided sidebar snippet", ); assert( - rootLayoutLoad.includes('"/account"') && rootLayoutLoad.includes('"/login/device"'), + rootLayoutLoad.includes('"/account"') && + rootLayoutLoad.includes('"/login/device"'), "Root layout should not redirect account and device-login public routes to a workspace", ); }); diff --git a/web/workspace/src/lib/workspace/settings/model.test.ts b/web/workspace/src/lib/workspace/settings/model.test.ts index 4e528e74..c4b80221 100644 --- a/web/workspace/src/lib/workspace/settings/model.test.ts +++ b/web/workspace/src/lib/workspace/settings/model.test.ts @@ -6,7 +6,10 @@ import { SETTINGS_SECTIONS, settingsSectionHref, } from "./model.ts"; -import { profileSourceTreeSettingsHref, virtualProfilePathForCreate } from "./profile-routes.ts"; +import { + profileSourceTreeSettingsHref, + virtualProfilePathForCreate, +} from "./profile-routes.ts"; declare const Deno: { test(name: string, fn: () => void): void; @@ -104,7 +107,6 @@ Deno.test("diagnostic labels preserve severity and code", () => { ); }); - Deno.test("profile source tree routes use encoded scoped ids", () => { const href = profileSourceTreeSettingsHref("workspace a", "project/tree"); assert( @@ -115,7 +117,18 @@ Deno.test("profile source tree routes use encoded scoped ids", () => { }); Deno.test("profile source create paths are normalized to virtual profile paths", () => { - assert(virtualProfilePathForCreate("alpha.dcdl") === "profiles/alpha.dcdl", "bare file names are scoped"); - assert(virtualProfilePathForCreate("profiles/alpha.dcdl") === "profiles/alpha.dcdl", "virtual paths are preserved"); - assert(virtualProfilePathForCreate("project:profiles/alpha.dcdl") === "project:profiles/alpha.dcdl", "safe virtual namespaces are preserved"); + assert( + virtualProfilePathForCreate("alpha.dcdl") === "profiles/alpha.dcdl", + "bare file names are scoped", + ); + assert( + virtualProfilePathForCreate("profiles/alpha.dcdl") === + "profiles/alpha.dcdl", + "virtual paths are preserved", + ); + assert( + virtualProfilePathForCreate("project:profiles/alpha.dcdl") === + "project:profiles/alpha.dcdl", + "safe virtual namespaces are preserved", + ); }); diff --git a/web/workspace/src/lib/workspace/settings/profile-api.ts b/web/workspace/src/lib/workspace/settings/profile-api.ts index 7eae1c9c..b23fc3f6 100644 --- a/web/workspace/src/lib/workspace/settings/profile-api.ts +++ b/web/workspace/src/lib/workspace/settings/profile-api.ts @@ -1,7 +1,4 @@ -import { - workspaceApiJson, - workspaceApiJsonWithBody, -} from "../api/http"; +import { workspaceApiJson, workspaceApiJsonWithBody } from "../api/http"; import type { ProfileSettingsMutationResponse, ProfileSettingsResponse, @@ -124,7 +121,9 @@ export function fetchProfileSourceTree( sourceTreeId: string, ): Promise { return workspaceApiJson( - `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}`, + `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${ + encodeURIComponent(sourceTreeId) + }`, ); } @@ -134,7 +133,9 @@ export function fetchProfileTreeFile( path: string, ): Promise { return workspaceApiJson( - `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}/file?path=${encodeURIComponent(path)}`, + `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${ + encodeURIComponent(sourceTreeId) + }/file?path=${encodeURIComponent(path)}`, ); } @@ -144,7 +145,9 @@ export function writeProfileTreeFile( request: { path: string; content: string; revision?: string | null }, ): Promise { return workspaceApiJsonWithBody( - `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}/file`, + `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${ + encodeURIComponent(sourceTreeId) + }/file`, { method: "PUT", body: JSON.stringify(request) }, ); } @@ -155,7 +158,9 @@ export function deleteProfileTreeFile( request: { path: string; revision: string }, ): Promise { return workspaceApiJsonWithBody( - `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${encodeURIComponent(sourceTreeId)}/file`, + `/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${ + encodeURIComponent(sourceTreeId) + }/file`, { method: "DELETE", body: JSON.stringify(request) }, ); } diff --git a/web/workspace/src/lib/workspace/settings/profile-routes.ts b/web/workspace/src/lib/workspace/settings/profile-routes.ts index d6dbfb7e..d217bc35 100644 --- a/web/workspace/src/lib/workspace/settings/profile-routes.ts +++ b/web/workspace/src/lib/workspace/settings/profile-routes.ts @@ -2,14 +2,21 @@ export function profileSettingsHref(workspaceId: string): string { return `/w/${encodeURIComponent(workspaceId)}/settings/profiles`; } -export function profileSourceTreeSettingsHref(workspaceId: string, sourceTreeId: string): string { - return `${profileSettingsHref(workspaceId)}/trees/${encodeURIComponent(sourceTreeId)}`; +export function profileSourceTreeSettingsHref( + workspaceId: string, + sourceTreeId: string, +): string { + return `${profileSettingsHref(workspaceId)}/trees/${ + encodeURIComponent(sourceTreeId) + }`; } export function virtualProfilePathForCreate(input: string): string { const trimmed = input.trim(); if (!trimmed) return ""; - if (trimmed.startsWith("project:") || trimmed.startsWith("workspace:")) return trimmed; + if (trimmed.startsWith("project:") || trimmed.startsWith("workspace:")) { + return trimmed; + } if (trimmed.startsWith("profiles/")) return trimmed; return `profiles/${trimmed}`; } diff --git a/web/workspace/src/lib/workspace/sidebar/repository-nav.test.ts b/web/workspace/src/lib/workspace/sidebar/repository-nav.test.ts index e87d86df..11237a91 100644 --- a/web/workspace/src/lib/workspace/sidebar/repository-nav.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/repository-nav.test.ts @@ -25,18 +25,22 @@ function repositories( } Deno.test("repository nav does not invent main for an empty registry", () => { - const projection = projectRepositoryNav({ - workspace_id: "workspace-1", - items: [], - source: "workspace_backend_config", - diagnostics: [ - { - code: "repository_config_empty", - severity: "warning", - message: "No repositories configured", - }, - ], - }, "/w/workspace-1", "workspace-1"); + const projection = projectRepositoryNav( + { + workspace_id: "workspace-1", + items: [], + source: "workspace_backend_config", + diagnostics: [ + { + code: "repository_config_empty", + severity: "warning", + message: "No repositories configured", + }, + ], + }, + "/w/workspace-1", + "workspace-1", + ); assertEquals(projection.count, 0); assertEquals(projection.items, []); diff --git a/web/workspace/src/lib/workspace/sidebar/repository-nav.ts b/web/workspace/src/lib/workspace/sidebar/repository-nav.ts index df46470e..4a7abeb0 100644 --- a/web/workspace/src/lib/workspace/sidebar/repository-nav.ts +++ b/web/workspace/src/lib/workspace/sidebar/repository-nav.ts @@ -25,7 +25,10 @@ export function projectRepositoryNav( count: summaries.length, diagnostics: repositories?.diagnostics ?? [], items: summaries.map((repository) => { - const href = workspaceRoute(workspaceId, `/repositories/${encodeURIComponent(repository.id)}`); + const href = workspaceRoute( + workspaceId, + `/repositories/${encodeURIComponent(repository.id)}`, + ); return { id: repository.id, title: repository.display_name || repository.id, diff --git a/web/workspace/src/lib/workspace/sidebar/sidebar.css b/web/workspace/src/lib/workspace/sidebar/sidebar.css index ff6733b4..2ee5c574 100644 --- a/web/workspace/src/lib/workspace/sidebar/sidebar.css +++ b/web/workspace/src/lib/workspace/sidebar/sidebar.css @@ -1,249 +1,249 @@ @layer reset, tokens, base, layout, components; @layer components { - .sidebar-frame { - grid-column: 1; - grid-row: 1 / 3; - width: clamp(220px, 20vw, 280px); - min-width: 0; - min-height: 0; - overflow-y: auto; - padding: var(--space-4) var(--space-3); - border-right: 1px solid var(--line); - } - .sidebar-frame.folded { - width: max-content; - overflow: hidden; - padding-inline: var(--space-2); - } - .sidebar-frame-content, - .global-sidebar, - .workspace-sidebar { - min-width: 0; - } - .global-sidebar, - .global-sidebar-section, - .workspace-sidebar { - display: grid; - gap: var(--space-2); - } - .sidebar-header { - display: grid; - gap: var(--space-2); - margin-bottom: var(--space-2); - min-width: 0; - } - .sidebar-control-row, - .sidebar-actions-row { - display: flex; - justify-content: flex-end; - gap: var(--space-1); - } - .sidebar-control-row { - margin-bottom: var(--space-2); - } - .sidebar-frame.folded .sidebar-control-row { - justify-content: center; - margin-bottom: 0; - } - .sidebar-title-row { - display: flex; - align-items: center; - gap: var(--space-2); - min-width: 0; - } - .workspace-label { - flex: 1 1 auto; - min-width: 0; - } - .workspace-name { - display: block; - min-width: 0; - overflow: hidden; - border-radius: var(--radius-soft); - padding: 0.45rem 0.6rem; - color: var(--text-strong); - font-size: 1.05rem; - font-weight: 800; - line-height: 1.25; - text-overflow: ellipsis; - white-space: nowrap; - } - .workspace-status { - margin: var(--space-1) 0 0; - color: var(--text-muted); - font-size: 0.78rem; - line-height: 1.35; - } - .workspace-status.error, - .section-state.error, - .error { - color: var(--danger); - } - .sidebar-icon-button, - .sidebar-fold-button { - flex: 0 0 auto; - display: grid; - place-items: center; - width: 32px; - height: 32px; - border-radius: var(--radius-soft); - color: var(--text-muted); - text-decoration: none; - } - .sidebar-fold-button { - border: 1px solid var(--line); - background: var(--bg-raised); - box-shadow: var(--shadow-soft); - color: var(--text-strong); - cursor: pointer; - } - .sidebar-icon { - width: 18px; - height: 18px; - fill: none; - stroke: currentColor; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - } - .sidebar-icon-button:hover, - .sidebar-icon-button:focus-visible, - .sidebar-fold-button:hover, - .sidebar-fold-button:focus-visible { - background: var(--interactive-hover); - color: var(--text-muted); - } - .sidebar-sections { - display: grid; - gap: var(--space-5); - min-width: 0; - } - .nav-section { - display: grid; - gap: var(--space-2); - } - .section-heading-row, - .section-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - } - .section-heading-row h2, - .section-header, - .sidebar-section-label { - color: var(--text-faint); - font-size: 0.72rem; - font-weight: 750; - letter-spacing: 0.11em; - text-transform: uppercase; - } - .section-heading-row h2, - .sidebar-section-label { - margin: 0; - } - .section-heading-link { - color: inherit; - text-decoration: none; - } - .section-heading-link:hover, - .section-heading-link:focus-visible, - .section-heading-link.active { - color: var(--accent); - } - .section-count { - color: var(--text-muted); - font-size: 0.72rem; - line-height: 1; - } - .nav-list, - .sidebar-list { - display: grid; - gap: var(--space-1); - margin: 0; - padding: 0; - list-style: none; - } - .nav-item, - .objective-link, - .sidebar-link { - display: grid; - gap: 3px; - min-width: 0; - margin-inline: calc(-1 * var(--space-2)); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-soft); - color: inherit; - text-align: left; - text-decoration: none; - transition: background-color 140ms ease, color 140ms ease; - } - a.nav-item:hover, - a.nav-item:focus-visible, - a.objective-link:hover, - a.objective-link:focus-visible, - a.sidebar-link:hover, - a.sidebar-link:focus-visible { - background: var(--interactive-hover); - } - a.nav-item.active, - a.objective-link.active, - a.sidebar-link.active { - background: var(--interactive-selected); - } - a.nav-item.active .item-title, - a.objective-link.active .item-title, - a.sidebar-link.active { - color: var(--accent); - } - .item-title, - .item-meta { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .item-title { - color: var(--text-strong); - font-weight: 650; - } - .item-meta { - color: var(--text-muted); - font-size: 0.82rem; - } - .worker-nav-item { - gap: 2px; - } - .worker-nav-item.disabled { - cursor: default; - opacity: 0.62; - } - .worker-nav-item.disabled .item-title { - color: var(--text-muted); - } - .worker-title-row { - display: grid; - grid-template-columns: minmax(0, max-content) minmax(0, 1fr); - align-items: baseline; - gap: var(--space-2); - min-width: 0; - } - .worker-task-title { - overflow: hidden; - color: var(--text-muted); - font-size: 0.82rem; - text-overflow: ellipsis; - white-space: nowrap; - } + .sidebar-frame { + grid-column: 1; + grid-row: 1 / 3; + width: clamp(220px, 20vw, 280px); + min-width: 0; + min-height: 0; + overflow-y: auto; + padding: var(--space-4) var(--space-3); + border-right: 1px solid var(--line); + } + .sidebar-frame.folded { + width: max-content; + overflow: hidden; + padding-inline: var(--space-2); + } + .sidebar-frame-content, + .global-sidebar, + .workspace-sidebar { + min-width: 0; + } + .global-sidebar, + .global-sidebar-section, + .workspace-sidebar { + display: grid; + gap: var(--space-2); + } + .sidebar-header { + display: grid; + gap: var(--space-2); + margin-bottom: var(--space-2); + min-width: 0; + } + .sidebar-control-row, + .sidebar-actions-row { + display: flex; + justify-content: flex-end; + gap: var(--space-1); + } + .sidebar-control-row { + margin-bottom: var(--space-2); + } + .sidebar-frame.folded .sidebar-control-row { + justify-content: center; + margin-bottom: 0; + } + .sidebar-title-row { + display: flex; + align-items: center; + gap: var(--space-2); + min-width: 0; + } + .workspace-label { + flex: 1 1 auto; + min-width: 0; + } + .workspace-name { + display: block; + min-width: 0; + overflow: hidden; + border-radius: var(--radius-soft); + padding: 0.45rem 0.6rem; + color: var(--text-strong); + font-size: 1.05rem; + font-weight: 800; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; + } + .workspace-status { + margin: var(--space-1) 0 0; + color: var(--text-muted); + font-size: 0.78rem; + line-height: 1.35; + } + .workspace-status.error, + .section-state.error, + .error { + color: var(--danger); + } + .sidebar-icon-button, + .sidebar-fold-button { + flex: 0 0 auto; + display: grid; + place-items: center; + width: 32px; + height: 32px; + border-radius: var(--radius-soft); + color: var(--text-muted); + text-decoration: none; + } + .sidebar-fold-button { + border: 1px solid var(--line); + background: var(--bg-raised); + box-shadow: var(--shadow-soft); + color: var(--text-strong); + cursor: pointer; + } + .sidebar-icon { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + } + .sidebar-icon-button:hover, + .sidebar-icon-button:focus-visible, + .sidebar-fold-button:hover, + .sidebar-fold-button:focus-visible { + background: var(--interactive-hover); + color: var(--text-muted); + } + .sidebar-sections { + display: grid; + gap: var(--space-5); + min-width: 0; + } + .nav-section { + display: grid; + gap: var(--space-2); + } + .section-heading-row, + .section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + } + .section-heading-row h2, + .section-header, + .sidebar-section-label { + color: var(--text-faint); + font-size: 0.72rem; + font-weight: 750; + letter-spacing: 0.11em; + text-transform: uppercase; + } + .section-heading-row h2, + .sidebar-section-label { + margin: 0; + } + .section-heading-link { + color: inherit; + text-decoration: none; + } + .section-heading-link:hover, + .section-heading-link:focus-visible, + .section-heading-link.active { + color: var(--accent); + } + .section-count { + color: var(--text-muted); + font-size: 0.72rem; + line-height: 1; + } + .nav-list, + .sidebar-list { + display: grid; + gap: var(--space-1); + margin: 0; + padding: 0; + list-style: none; + } + .nav-item, + .objective-link, + .sidebar-link { + display: grid; + gap: 3px; + min-width: 0; + margin-inline: calc(-1 * var(--space-2)); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-soft); + color: inherit; + text-align: left; + text-decoration: none; + transition: background-color 140ms ease, color 140ms ease; + } + a.nav-item:hover, + a.nav-item:focus-visible, + a.objective-link:hover, + a.objective-link:focus-visible, + a.sidebar-link:hover, + a.sidebar-link:focus-visible { + background: var(--interactive-hover); + } + a.nav-item.active, + a.objective-link.active, + a.sidebar-link.active { + background: var(--interactive-selected); + } + a.nav-item.active .item-title, + a.objective-link.active .item-title, + a.sidebar-link.active { + color: var(--accent); + } + .item-title, + .item-meta { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .item-title { + color: var(--text-strong); + font-weight: 650; + } + .item-meta { + color: var(--text-muted); + font-size: 0.82rem; + } + .worker-nav-item { + gap: 2px; + } + .worker-nav-item.disabled { + cursor: default; + opacity: 0.62; + } + .worker-nav-item.disabled .item-title { + color: var(--text-muted); + } + .worker-title-row { + display: grid; + grid-template-columns: minmax(0, max-content) minmax(0, 1fr); + align-items: baseline; + gap: var(--space-2); + min-width: 0; + } + .worker-task-title { + overflow: hidden; + color: var(--text-muted); + font-size: 0.82rem; + text-overflow: ellipsis; + white-space: nowrap; + } - @media (max-width: 760px) { - .sidebar-frame, - .sidebar-frame.folded { - grid-column: 1; - grid-row: 1; - width: auto; - border-right: 0; - border-bottom: 1px solid var(--line); - } - } + @media (max-width: 760px) { + .sidebar-frame, + .sidebar-frame.folded { + grid-column: 1; + grid-row: 1; + width: auto; + border-right: 0; + border-bottom: 1px solid var(--line); + } + } } diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts index 4c3affaf..1e440ba8 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -319,12 +319,12 @@ export type RepositoryLogResponse = { }; export type MemoryCandidateKind = - | 'preference' - | 'working_assumption' - | 'constraint' - | 'decision' - | 'open_question' - | 'lesson'; + | "preference" + | "working_assumption" + | "constraint" + | "decision" + | "open_question" + | "lesson"; export type MemorySourceRef = { segment_id: string; @@ -387,7 +387,11 @@ export type TicketSummary = { updated_at?: string | null; queued_by?: string | null; queued_at?: string | null; - workspace_action_priority?: 'ready_for_queue' | 'active_work' | 'background' | null; + workspace_action_priority?: + | "ready_for_queue" + | "active_work" + | "background" + | null; record_source?: string; }; diff --git a/web/workspace/src/lib/workspace/sidebar/worker-launch.ts b/web/workspace/src/lib/workspace/sidebar/worker-launch.ts index 6742dc43..3049d9d8 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-launch.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-launch.ts @@ -35,17 +35,23 @@ export function defaultWorkerLaunchForm( const preferredProfile = options?.profiles.find((candidate) => candidate.id === "builtin:coder") ?? options?.profiles[0]; - const availableWorkingDirectories = options?.working_directories.filter((directory) => - directory.status === "active" && - directory.cleanliness === "clean" && - directory.primary_worker_id == null && - directory.occupied_by == null - ) ?? []; + const availableWorkingDirectories = + options?.working_directories.filter((directory) => + directory.status === "active" && + directory.cleanliness === "clean" && + directory.primary_worker_id == null && + directory.occupied_by == null + ) ?? []; const selectedRuntime = current.runtime_id - ? options?.runtimes.find((runtime) => runtime.runtime_id === current.runtime_id) + ? options?.runtimes.find((runtime) => + runtime.runtime_id === current.runtime_id + ) : preferredRuntime; - const workdirlessRuntime = selectedRuntime?.working_directory_required === false; - const preferredWorkingDirectory = workdirlessRuntime ? undefined : availableWorkingDirectories[0]; + const workdirlessRuntime = + selectedRuntime?.working_directory_required === false; + const preferredWorkingDirectory = workdirlessRuntime + ? undefined + : availableWorkingDirectories[0]; const preferredRepository = options?.repositories.find((repository) => repository.id === current.working_directory_repository_id @@ -60,12 +66,13 @@ export function defaultWorkerLaunchForm( ? current.profile : preferredProfile?.id || "", initial_text: current.initial_text, - working_directory_id: !workdirlessRuntime && availableWorkingDirectories.some( - (directory) => - directory.working_directory_id === current.working_directory_id, - ) - ? current.working_directory_id - : preferredWorkingDirectory?.working_directory_id || "", + working_directory_id: + !workdirlessRuntime && availableWorkingDirectories.some( + (directory) => + directory.working_directory_id === current.working_directory_id, + ) + ? current.working_directory_id + : preferredWorkingDirectory?.working_directory_id || "", working_directory_repository_id: current.working_directory_repository_id || preferredRepository?.id || "", working_directory_selector: current.working_directory_selector || diff --git a/web/workspace/src/routes/w/[workspaceId]/+layout.ts b/web/workspace/src/routes/w/[workspaceId]/+layout.ts index a90efd91..134f0b05 100644 --- a/web/workspace/src/routes/w/[workspaceId]/+layout.ts +++ b/web/workspace/src/routes/w/[workspaceId]/+layout.ts @@ -1,5 +1,8 @@ import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; -import type { RepositoryListResponse, WorkspaceResponse } from "$lib/workspace/sidebar/types"; +import type { + RepositoryListResponse, + WorkspaceResponse, +} from "$lib/workspace/sidebar/types"; import type { LayoutLoad } from "./$types"; export const load: LayoutLoad = async ({ fetch, params }) => { diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.ts b/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.ts index 584db1c0..1ee379bd 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.ts @@ -1,13 +1,13 @@ -import { loadJson, workspaceApiPath } from '$lib/workspace/api/http'; -import type { MemoryStagingListResponse } from '$lib/workspace/sidebar/types'; -import type { PageLoad } from './$types'; +import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import type { MemoryStagingListResponse } from "$lib/workspace/sidebar/types"; +import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch, params }) => { return { workspaceId: params.workspaceId, staging: await loadJson( fetch, - `${workspaceApiPath(params.workspaceId, '/memory/staging')}?limit=200`, + `${workspaceApiPath(params.workspaceId, "/memory/staging")}?limit=200`, ), }; }; diff --git a/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts index 262e5d20..d09c3473 100644 --- a/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts @@ -9,7 +9,7 @@ export const load: PageLoad = async ({ fetch, params }) => { const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path); const objectiveId = params.objectiveId; const [objectives, objective] = await Promise.all([ - loadJson(fetch, apiPath('/objectives')), + loadJson(fetch, apiPath("/objectives")), loadJson( fetch, apiPath(`/objectives/${encodeURIComponent(objectiveId)}`), diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts index 4ea9fe95..c6d5cbeb 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts @@ -1,5 +1,7 @@ export function load( - { params }: { params: { workspaceId: string; runtimeId: string; workerId: string } }, + { params }: { + params: { workspaceId: string; runtimeId: string; workerId: string }; + }, ) { return { workspaceId: params.workspaceId, diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/profiles/trees/[sourceTreeId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/settings/profiles/trees/[sourceTreeId]/+page.ts index c8d0ef85..b96d2552 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/profiles/trees/[sourceTreeId]/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/settings/profiles/trees/[sourceTreeId]/+page.ts @@ -1,4 +1,4 @@ -import type { PageLoad } from './$types'; +import type { PageLoad } from "./$types"; export const load: PageLoad = ({ params }) => ({ sourceTreeId: params.sourceTreeId, diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.ts b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.ts index cc595850..4543ff7a 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.ts @@ -10,7 +10,10 @@ import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch, params }) => { const runtimeId = params.runtimeId; const [runtimes, workdirs, cleanupPlan] = await Promise.all([ - loadJson>(fetch, workspaceApiPath(params.workspaceId, "/runtimes")), + loadJson>( + fetch, + workspaceApiPath(params.workspaceId, "/runtimes"), + ), loadJson( fetch, workspaceApiPath( @@ -20,7 +23,10 @@ export const load: PageLoad = async ({ fetch, params }) => { ), loadJson( fetch, - workspaceApiPath(params.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`), + workspaceApiPath( + params.workspaceId, + `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`, + ), ), ]); diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts index 77e87077..6277d9a7 100644 --- a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts @@ -6,7 +6,10 @@ export const load = (async ({ fetch, params }) => { const ticketId = params.ticketId; const ticket = await loadJson( fetch, - workspaceApiPath(params.workspaceId, `/tickets/${encodeURIComponent(ticketId)}`), + workspaceApiPath( + params.workspaceId, + `/tickets/${encodeURIComponent(ticketId)}`, + ), ); return { diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts b/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts index b027cee2..7508733c 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/workers/+page.ts @@ -1,5 +1,9 @@ import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; -import type { ListResponse, RuntimeCleanupPlanResponse, Worker } from "$lib/workspace/sidebar/types"; +import type { + ListResponse, + RuntimeCleanupPlanResponse, + Worker, +} from "$lib/workspace/sidebar/types"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch, params }) => { @@ -7,12 +11,17 @@ export const load: PageLoad = async ({ fetch, params }) => { fetch, workspaceApiPath(params.workspaceId, "/workers"), ); - const runtimeIds = Array.from(new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? [])); + const runtimeIds = Array.from( + new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []), + ); const cleanupPlanEntries = await Promise.all( runtimeIds.map(async (runtimeId) => { const cleanupPlan = await loadJson( fetch, - workspaceApiPath(params.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`), + workspaceApiPath( + params.workspaceId, + `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`, + ), ); return [runtimeId, cleanupPlan] as const; }), diff --git a/web/workspace/svelte.config.js b/web/workspace/svelte.config.js index cb64985a..2bdefd81 100644 --- a/web/workspace/svelte.config.js +++ b/web/workspace/svelte.config.js @@ -1,18 +1,18 @@ -import adapter from '@sveltejs/adapter-static'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ - pages: 'build', - assets: 'build', - fallback: 'index.html', + pages: "build", + assets: "build", + fallback: "index.html", precompress: false, - strict: true - }) - } + strict: true, + }), + }, }; export default config; diff --git a/web/workspace/vite.config.ts b/web/workspace/vite.config.ts index 047e1d45..71c07e50 100644 --- a/web/workspace/vite.config.ts +++ b/web/workspace/vite.config.ts @@ -1,20 +1,20 @@ -import tailwindcss from '@tailwindcss/vite'; -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import tailwindcss from "@tailwindcss/vite"; +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], + plugins: [tailwindcss(), sveltekit()], - server: { - allowedHosts: ['develop.hareworks.net'], - watch: { ignored: ['**/.tmp*', '**/.tmp*/**'] }, + server: { + allowedHosts: ["develop.hareworks.net"], + watch: { ignored: ["**/.tmp*", "**/.tmp*/**"] }, - proxy: { - '/api': { - target: 'http://127.0.0.1:8787', - changeOrigin: true, - ws: true - } - } - } + proxy: { + "/api": { + target: "http://127.0.0.1:8787", + changeOrigin: true, + ws: true, + }, + }, + }, });