From 862f8f7f9858e8aac30ac7aab0d3a0bfa7c14193 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 24 Jul 2026 15:53:02 +0900 Subject: [PATCH] web: make running composer stoppable --- .../console/worker-console.ui.test.ts | 15 +++++ .../workers/[workerId]/console/+page.svelte | 67 ++++++++++++++----- 2 files changed, 66 insertions(+), 16 deletions(-) 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 23d409ee..34fc3d64 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 @@ -416,6 +416,21 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac !consolePage.includes("void refreshConsole();\n });\n\n $effect"), "target-change effect should load data without depending on manual refresh state reads", ); + assert( + 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('"Stop Worker"') && + consolePage.includes("disabled={composerSubmitDisabled}") && + !consolePage.includes("disabled={!inputReady || sending}") && + !consolePage.includes("enabled: inputReady && !sending"), + "Worker Console composer should stay editable during runs and turn the submit button into a Stop control", + ); }); Deno.test("Account UI owns browser passkey session state without workspace authorization", async () => { diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte index deff4139..af09081e 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte @@ -156,8 +156,15 @@ mergeDiagnostics(worker?.diagnostics ?? [], streamDiagnostics), ); const workerState = $derived(liveWorkerState ?? worker?.state ?? "loading"); + const workerRunning = $derived(workerState === "running"); const inputReady = $derived(workerState === "idle"); - const canSend = $derived(inputReady && draft.trim().length > 0 && !sending); + const composerEditable = $derived(protocolState === "open" && !sending); + const canSubmitDraft = $derived(inputReady && composerEditable); + const canSend = $derived(canSubmitDraft && draft.trim().length > 0); + const canStopFromComposer = $derived(workerRunning && composerEditable); + const composerSubmitDisabled = $derived( + workerRunning ? !canStopFromComposer : !canSend, + ); async function getJson(path: string): Promise { const response = await fetch(path); @@ -433,6 +440,14 @@ } } + function handleComposerSubmit(value = draft) { + if (workerRunning) { + sendControl({ method: "cancel" }, "Stop"); + return; + } + void submitDraft(value); + } + async function submitDraft(value = draft) { const command = buildComposerRequest(value); if (!command.ok) { @@ -466,9 +481,9 @@ } } - async function sendMessage(event: SubmitEvent) { + function sendMessage(event: SubmitEvent) { event.preventDefault(); - await submitDraft(); + handleComposerSubmit(); } function workerStateFromProtocolEvent( @@ -1314,12 +1329,12 @@ bind:this={composerTextareaElement} bind:value={draft} use:chatSubmit={{ - enabled: inputReady && !sending, - onSubmit: (value) => void submitDraft(value), + enabled: canSubmitDraft, + onSubmit: (value) => handleComposerSubmit(value), }} use:fitTextarea={{ value: draft, maxRows: 10 }} onkeydown={handleComposerKeydown} - disabled={!inputReady || sending}> + disabled={!composerEditable}> @@ -1657,6 +1687,11 @@ pointer-events: auto; } + .composer-send-button.stop { + background: var(--danger); + color: var(--bg); + } + .composer-send-button:disabled { cursor: not-allowed; opacity: 0.55;