fix: enable running composer delivery

This commit is contained in:
2026-09-06 03:32:23 +09:00
parent 9d55ce0a87
commit ddb4c1454d
5 changed files with 253 additions and 26 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev", "dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts", "test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts",
"build": "deno run -A npm:vite@7.2.7 build", "build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview" "preview": "deno run -A npm:vite@7.2.7 preview"
}, },
@@ -0,0 +1,136 @@
declare const Deno: {
test(name: string, fn: () => void): void;
};
import {
canDeliverComposerDraft,
sendComposerDelivery,
} from "./composer-delivery.ts";
function assertEquals(actual: unknown, expected: unknown): void {
if (actual !== expected) {
throw new Error(`Expected ${String(expected)}, got ${String(actual)}`);
}
}
const base = {
protocolOpen: true,
sending: false,
hasText: true,
hasAttachments: false,
};
Deno.test("running Composer enables Queue Submit and Notify but not immediate Submit", () => {
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "queue",
workerState: "running",
}),
true,
);
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "notify",
workerState: "running",
}),
true,
);
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "submit",
workerState: "running",
}),
false,
);
});
Deno.test("running Queue Submit and Notify dispatch their protocol methods", () => {
const sent: string[] = [];
assertEquals(
sendComposerDelivery(
{ ...base, delivery: "queue", workerState: "running" },
"submit",
(method) => sent.push(method),
),
true,
);
assertEquals(
sendComposerDelivery(
{ ...base, delivery: "notify", workerState: "running" },
"notify",
(method) => sent.push(method),
),
true,
);
assertEquals(sent.join(","), "submit,notify");
});
Deno.test("idle Composer enables only immediate Submit", () => {
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "submit",
workerState: "idle",
}),
true,
);
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "queue",
workerState: "idle",
}),
false,
);
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "notify",
workerState: "idle",
}),
false,
);
});
Deno.test("running delivery remains fenced by protocol, send state, and payload kind", () => {
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "queue",
workerState: "running",
protocolOpen: false,
}),
false,
);
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "notify",
workerState: "running",
sending: true,
}),
false,
);
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "notify",
workerState: "running",
hasAttachments: true,
}),
false,
);
assertEquals(
canDeliverComposerDraft({
...base,
delivery: "queue",
workerState: "running",
hasText: false,
hasAttachments: true,
}),
true,
);
});
@@ -0,0 +1,39 @@
export type ComposerDelivery = "submit" | "queue" | "notify";
export type ComposerDeliveryState = {
delivery: ComposerDelivery;
workerState: string;
protocolOpen: boolean;
sending: boolean;
hasText: boolean;
hasAttachments: boolean;
};
/**
* Resolve whether the current Composer draft can use one delivery action.
* Immediate Submit is idle-only; Queue and Notify are running-only.
*/
export function canDeliverComposerDraft(state: ComposerDeliveryState): boolean {
if (!state.protocolOpen || state.sending) return false;
const hasInput = state.hasText || state.hasAttachments;
switch (state.delivery) {
case "submit":
return state.workerState === "idle" && hasInput;
case "queue":
return state.workerState === "running" && hasInput;
case "notify":
return state.workerState === "running" && state.hasText &&
!state.hasAttachments;
}
}
export function sendComposerDelivery<T>(
state: ComposerDeliveryState,
method: T,
send: (method: T) => void,
): boolean {
if (!canDeliverComposerDraft(state)) return false;
send(method);
return true;
}
@@ -1068,7 +1068,8 @@ Deno.test("Web Console uses Notify while running and exposes durable pending con
), ),
); );
for (const token of [ for (
const token of [
'method: "submit"', 'method: "submit"',
'method: "notify"', 'method: "notify"',
"notification_request_id: crypto.randomUUID()", "notification_request_id: crypto.randomUUID()",
@@ -1079,10 +1080,17 @@ Deno.test("Web Console uses Notify while running and exposes durable pending con
'method: "continue_pending"', 'method: "continue_pending"',
"handleQueueSubmit", "handleQueueSubmit",
"handleNotifySubmit", "handleNotifySubmit",
'submitDraft(composerInputElement?.snapshot() ?? draft, "queue")',
"disabled={!canQueueDraft}",
"disabled={!canNotifyDraft}",
">Queue Submit</button>", ">Queue Submit</button>",
">Notify</button>", ">Notify</button>",
]) { ]
assert(consolePage.includes(token), `missing durable pending control token: ${token}`); ) {
assert(
consolePage.includes(token),
`missing durable pending control token: ${token}`,
);
} }
const userCase = consolePage.slice( const userCase = consolePage.slice(
@@ -5,6 +5,11 @@
import ConsoleTimeline from "$lib/workspace/console/ConsoleTimeline.svelte"; import ConsoleTimeline from "$lib/workspace/console/ConsoleTimeline.svelte";
import ComposerInput from "$lib/workspace/console/ComposerInput.svelte"; import ComposerInput from "$lib/workspace/console/ComposerInput.svelte";
import type { ComposerDraftSnapshot } from "$lib/workspace/console/composer-draft"; import type { ComposerDraftSnapshot } from "$lib/workspace/console/composer-draft";
import {
canDeliverComposerDraft,
sendComposerDelivery,
type ComposerDelivery,
} from "$lib/workspace/console/composer-delivery";
import { import {
buildComposerSegmentsRequest, buildComposerSegmentsRequest,
type WorkerConsoleInputRequest, type WorkerConsoleInputRequest,
@@ -247,13 +252,42 @@
const workerState = $derived(liveWorkerState ?? worker?.state ?? "loading"); const workerState = $derived(liveWorkerState ?? worker?.state ?? "loading");
const workerRunning = $derived(workerState === "running"); const workerRunning = $derived(workerState === "running");
const workerPaused = $derived(workerState === "paused"); const workerPaused = $derived(workerState === "paused");
const inputReady = $derived(workerState === "idle");
const composerEditable = $derived(protocolState === "open" && !sending); const composerEditable = $derived(protocolState === "open" && !sending);
const canSubmitDraft = $derived(inputReady && composerEditable); const draftHasText = $derived(draft.content.trim().length > 0);
const canSend = $derived(canSubmitDraft && draft.content.trim().length > 0); const draftHasAttachments = $derived(attachments.length > 0);
const canSubmitDraft = $derived(
canDeliverComposerDraft({
delivery: "submit",
workerState,
protocolOpen: protocolState === "open",
sending,
hasText: draftHasText,
hasAttachments: draftHasAttachments,
}),
);
const canQueueDraft = $derived(
canDeliverComposerDraft({
delivery: "queue",
workerState,
protocolOpen: protocolState === "open",
sending,
hasText: draftHasText,
hasAttachments: draftHasAttachments,
}),
);
const canNotifyDraft = $derived(
canDeliverComposerDraft({
delivery: "notify",
workerState,
protocolOpen: protocolState === "open",
sending,
hasText: draftHasText,
hasAttachments: draftHasAttachments,
}),
);
const canStopFromComposer = $derived(workerRunning && composerEditable); const canStopFromComposer = $derived(workerRunning && composerEditable);
const composerSubmitDisabled = $derived( const composerSubmitDisabled = $derived(
workerRunning ? !canStopFromComposer : !canSend, workerRunning ? !canStopFromComposer : !canSubmitDraft,
); );
async function getJson<T>(path: string): Promise<T> { async function getJson<T>(path: string): Promise<T> {
@@ -664,7 +698,7 @@
} }
function handleQueueSubmit() { function handleQueueSubmit() {
void submitDraft(composerInputElement?.snapshot() ?? draft); void submitDraft(composerInputElement?.snapshot() ?? draft, "queue");
} }
function handleNotifySubmit() { function handleNotifySubmit() {
@@ -772,7 +806,7 @@
async function submitDraft( async function submitDraft(
value: ComposerDraftSnapshot, value: ComposerDraftSnapshot,
delivery: "submit" | "notify" = "submit", delivery: ComposerDelivery = "submit",
) { ) {
if (delivery === "notify" && attachments.length > 0) { if (delivery === "notify" && attachments.length > 0) {
composerNotice = null; composerNotice = null;
@@ -808,7 +842,15 @@
composerInputElement?.clear(); composerInputElement?.clear();
return; return;
} }
if (sending || !inputReady) { const deliveryState = {
delivery,
workerState,
protocolOpen: protocolState === "open",
sending,
hasText: value.content.trim().length > 0,
hasAttachments: attachments.length > 0,
};
if (!canDeliverComposerDraft(deliveryState)) {
return; return;
} }
@@ -825,7 +867,9 @@
sendError = null; sendError = null;
try { try {
const method = composerRequestToProtocolMethod(request); const method = composerRequestToProtocolMethod(request);
sendProtocolMethod(method); if (!sendComposerDelivery(deliveryState, method, sendProtocolMethod)) {
return;
}
composerInputElement?.recordHistory(value); composerInputElement?.recordHistory(value);
composerInputElement?.clear(); composerInputElement?.clear();
attachments = []; attachments = [];
@@ -1963,12 +2007,12 @@
{#if workerRunning} {#if workerRunning}
<button <button
type="button" type="button"
disabled={sending || !inputReady} disabled={!canQueueDraft}
onclick={handleQueueSubmit} onclick={handleQueueSubmit}
>Queue Submit</button> >Queue Submit</button>
<button <button
type="button" type="button"
disabled={sending || !inputReady} disabled={!canNotifyDraft}
onclick={handleNotifySubmit} onclick={handleNotifySubmit}
>Notify</button> >Notify</button>
{/if} {/if}