Merge branch 'develop' into work/T-595-runtime-trust-key-management
# Conflicts: # web/workspace/deno.json
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"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",
|
||||
"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 tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.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 tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.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",
|
||||
"preview": "deno run -A npm:vite@7.2.7 preview"
|
||||
},
|
||||
|
||||
@@ -103,7 +103,13 @@ entry_id: string,
|
||||
*/
|
||||
timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array<string>, } & ({ "kind": "user_input", segments: Array<Segment>, } | { "kind": "message", role: SessionMessageRole, content: Array<SessionContentPart>, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array<SessionToolAttachment>, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_error", message: string, });
|
||||
|
||||
export type SessionSnapshot = { entries: Array<SessionSnapshotEntry>, };
|
||||
export type PendingSubmissionSummary = { submission_id: string, accepted_at_ms: number, segment_count: number, byte_len: number, };
|
||||
|
||||
export type PendingSubmissionsSnapshot = { revision: number, notification_count: number, head_id: string | null, submissions: Array<PendingSubmissionSummary>, };
|
||||
|
||||
export type SubmissionDisposition = "started" | "queued";
|
||||
|
||||
export type SessionSnapshot = { pending_submissions: PendingSubmissionsSnapshot, entries: Array<SessionSnapshotEntry>, };
|
||||
|
||||
export type InternalWorkerKind = "sub_worker" | { "service": { kind: string, } };
|
||||
|
||||
@@ -225,9 +231,9 @@ export type SubscriptionFramePayload = { "frame": "request", "message": Subscrip
|
||||
|
||||
export type SubscriptionFrame = { protocol_version: number, } & ({ "frame": "request", "message": SubscriptionRequest } | { "frame": "response", "message": SubscriptionResponse } | { "frame": "event", "message": SubscriptionEvent } | { "frame": "worker_protocol", "message": SubscriptionWorkerProtocolMethod });
|
||||
|
||||
export type Method = { "method": "run", "params": { input: Array<Segment>, } } | { "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": "submit", "params": { submission_request_id: string, input: Array<Segment>, } } | { "method": "notify", "params": { notification_request_id: string, message: string, auto_run?: boolean, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "list_pending_submissions" } | { "method": "cancel_pending_submission", "params": { submission_id: string, expected_revision: number, } } | { "method": "clear_pending_submissions", "params": { expected_revision: number, } } | { "method": "continue_pending", "params": { expected_revision: number, expected_head_id: string, } } | { "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<Segment>, } } | { "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,
|
||||
export type Event = { "event": "submission_accepted", "data": { submission_request_id: string, submission_id: string, disposition: SubmissionDisposition, } } | { "event": "submission_rejected", "data": { submission_request_id: string, message: string, } } | { "event": "pending_submissions_changed", "data": { pending: PendingSubmissionsSnapshot, } } | { "event": "user_message", "data": { segments: Array<Segment>, } } | { "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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2150,6 +2150,12 @@ Deno.test("snapshot restores TaskStore state from system history", () => {
|
||||
const event = snapshotEvent("/repo");
|
||||
if (event.event !== "snapshot") throw new Error("snapshot fixture expected");
|
||||
event.data.session = {
|
||||
pending_submissions: {
|
||||
revision: 0,
|
||||
notification_count: 0,
|
||||
head_id: null,
|
||||
submissions: [],
|
||||
},
|
||||
entries: [{
|
||||
entry_id: "task-reminder-1",
|
||||
timestamp: 1,
|
||||
|
||||
@@ -1064,3 +1064,46 @@ Deno.test("Web Console switches main and direct SubWorker views from the Tasks r
|
||||
"Worker view selection should expose only direct SubWorker session identities with main fallback",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Web Console uses Notify while running and exposes durable pending controls", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
for (
|
||||
const token of [
|
||||
'method: "submit"',
|
||||
'method: "notify"',
|
||||
"notification_request_id: crypto.randomUUID()",
|
||||
"submission_request_id: crypto.randomUUID()",
|
||||
'payload.event === "pending_submissions_changed"',
|
||||
'method: "cancel_pending_submission"',
|
||||
'method: "clear_pending_submissions"',
|
||||
'method: "continue_pending"',
|
||||
"handleQueueSubmit",
|
||||
"handleNotifySubmit",
|
||||
'submitDraft(composerInputElement?.snapshot() ?? draft, "queue")',
|
||||
"disabled={!canQueueDraft}",
|
||||
"disabled={!canNotifyDraft}",
|
||||
">Queue Submit</button>",
|
||||
">Notify</button>",
|
||||
]
|
||||
) {
|
||||
assert(
|
||||
consolePage.includes(token),
|
||||
`missing durable pending control token: ${token}`,
|
||||
);
|
||||
}
|
||||
|
||||
const userCase = consolePage.slice(
|
||||
consolePage.indexOf('case "user":'),
|
||||
consolePage.indexOf('case "compact":'),
|
||||
);
|
||||
assert(
|
||||
!userCase.includes("workerRunning"),
|
||||
"ordinary text must remain Submit instead of being implicitly converted to Notify",
|
||||
);
|
||||
});
|
||||
|
||||
+199
-12
@@ -5,6 +5,11 @@
|
||||
import ConsoleTimeline from "$lib/workspace/console/ConsoleTimeline.svelte";
|
||||
import ComposerInput from "$lib/workspace/console/ComposerInput.svelte";
|
||||
import type { ComposerDraftSnapshot } from "$lib/workspace/console/composer-draft";
|
||||
import {
|
||||
canDeliverComposerDraft,
|
||||
sendComposerDelivery,
|
||||
type ComposerDelivery,
|
||||
} from "$lib/workspace/console/composer-delivery";
|
||||
import {
|
||||
buildComposerSegmentsRequest,
|
||||
type WorkerConsoleInputRequest,
|
||||
@@ -31,7 +36,13 @@
|
||||
type ConsoleViewMode,
|
||||
type ConsoleViewScroll,
|
||||
} from "$lib/workspace/console/model";
|
||||
import type { Event as ProtocolEvent, Method as ProtocolMethod, RewindTarget, Segment } from "$lib/generated/protocol";
|
||||
import type {
|
||||
Event as ProtocolEvent,
|
||||
Method as ProtocolMethod,
|
||||
PendingSubmissionsSnapshot,
|
||||
RewindTarget,
|
||||
Segment,
|
||||
} from "$lib/generated/protocol";
|
||||
import {
|
||||
MAX_FILES_PER_SUBMISSION,
|
||||
uploadAttachment,
|
||||
@@ -152,6 +163,13 @@
|
||||
"connecting",
|
||||
);
|
||||
let protocolSubscription: WorkspaceMultiplexerSubscription | null = null;
|
||||
let pendingSubmissions = $state<PendingSubmissionsSnapshot>({
|
||||
revision: 0,
|
||||
notification_count: 0,
|
||||
head_id: null,
|
||||
submissions: [],
|
||||
});
|
||||
let pendingSubmissionItems = $derived(pendingSubmissions.submissions ?? []);
|
||||
let pendingCompletionRequest: {
|
||||
resolve: (entries: ComposerCompletionEntry[]) => void;
|
||||
reject: (error: Error) => void;
|
||||
@@ -234,13 +252,42 @@
|
||||
const workerState = $derived(liveWorkerState ?? worker?.state ?? "loading");
|
||||
const workerRunning = $derived(workerState === "running");
|
||||
const workerPaused = $derived(workerState === "paused");
|
||||
const inputReady = $derived(workerState === "idle");
|
||||
const composerEditable = $derived(protocolState === "open" && !sending);
|
||||
const canSubmitDraft = $derived(inputReady && composerEditable);
|
||||
const canSend = $derived(canSubmitDraft && draft.content.trim().length > 0);
|
||||
const draftHasText = $derived(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 composerSubmitDisabled = $derived(
|
||||
workerRunning ? !canStopFromComposer : !canSend,
|
||||
workerRunning ? !canStopFromComposer : !canSubmitDraft,
|
||||
);
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
@@ -334,6 +381,13 @@
|
||||
|
||||
function handleIncomingProtocolEvent(payload: ProtocolEvent) {
|
||||
handleProtocolCommandEvent(payload);
|
||||
if (payload.event === "snapshot") {
|
||||
pendingSubmissions = payload.data.session.pending_submissions;
|
||||
} else if (payload.event === "segment_rotated") {
|
||||
pendingSubmissions = payload.data.session.pending_submissions;
|
||||
} else if (payload.event === "pending_submissions_changed") {
|
||||
pendingSubmissions = payload.data.pending;
|
||||
}
|
||||
if (payload.event === "error") {
|
||||
queueObservationDiagnostic({
|
||||
code: payload.data.code,
|
||||
@@ -556,8 +610,9 @@
|
||||
switch (request.kind) {
|
||||
case "user":
|
||||
return {
|
||||
method: "run",
|
||||
method: "submit",
|
||||
params: {
|
||||
submission_request_id: crypto.randomUUID(),
|
||||
input: request.segments ?? [
|
||||
{ kind: "text", content: request.content },
|
||||
],
|
||||
@@ -566,7 +621,11 @@
|
||||
case "notify":
|
||||
return {
|
||||
method: "notify",
|
||||
params: { message: request.content, auto_run: true },
|
||||
params: {
|
||||
notification_request_id: crypto.randomUUID(),
|
||||
message: request.content,
|
||||
auto_run: true,
|
||||
},
|
||||
};
|
||||
case "compact":
|
||||
return { method: "compact" };
|
||||
@@ -638,6 +697,14 @@
|
||||
void submitDraft(composerInputElement?.snapshot() ?? draft);
|
||||
}
|
||||
|
||||
function handleQueueSubmit() {
|
||||
void submitDraft(composerInputElement?.snapshot() ?? draft, "queue");
|
||||
}
|
||||
|
||||
function handleNotifySubmit() {
|
||||
void submitDraft(composerInputElement?.snapshot() ?? draft, "notify");
|
||||
}
|
||||
|
||||
function attachmentPath(): string {
|
||||
return `/api/w/${encodeURIComponent(workspaceId)}/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}`;
|
||||
}
|
||||
@@ -737,7 +804,15 @@
|
||||
if (event.dataTransfer?.files) addAttachmentFiles(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
async function submitDraft(value: ComposerDraftSnapshot) {
|
||||
async function submitDraft(
|
||||
value: ComposerDraftSnapshot,
|
||||
delivery: ComposerDelivery = "submit",
|
||||
) {
|
||||
if (delivery === "notify" && attachments.length > 0) {
|
||||
composerNotice = null;
|
||||
sendError = "Notify accepts text only; remove attachments or queue a Submit.";
|
||||
return;
|
||||
}
|
||||
const incompleteAttachment = attachments.find((attachment) =>
|
||||
attachment.state !== "uploaded" || !attachment.reference
|
||||
);
|
||||
@@ -767,19 +842,38 @@
|
||||
composerInputElement?.clear();
|
||||
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;
|
||||
}
|
||||
|
||||
let request: WorkerConsoleInputRequest = command.request;
|
||||
if (delivery === "notify") {
|
||||
if (request.kind !== "user") {
|
||||
composerNotice = null;
|
||||
sendError = "Notify accepts ordinary text, not a Composer command.";
|
||||
return;
|
||||
}
|
||||
request = { kind: "notify", content: request.content };
|
||||
}
|
||||
sending = true;
|
||||
sendError = null;
|
||||
try {
|
||||
const method = composerRequestToProtocolMethod(command.request);
|
||||
sendProtocolMethod(method);
|
||||
const method = composerRequestToProtocolMethod(request);
|
||||
if (!sendComposerDelivery(deliveryState, method, sendProtocolMethod)) {
|
||||
return;
|
||||
}
|
||||
composerInputElement?.recordHistory(value);
|
||||
composerInputElement?.clear();
|
||||
attachments = [];
|
||||
if (method.method === "run" || method.method === "notify") {
|
||||
if (method.method === "submit" || method.method === "notify") {
|
||||
liveWorkerState = "running";
|
||||
}
|
||||
composerNotice = "Sent through Worker protocol.";
|
||||
@@ -1722,6 +1816,62 @@
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
{#if pendingSubmissionItems.length > 0 || pendingSubmissions.notification_count > 0}
|
||||
<details class="pending-submissions">
|
||||
<summary>
|
||||
Pending activations ({pendingSubmissionItems.length} submissions · {pendingSubmissions.notification_count} notifications)
|
||||
</summary>
|
||||
<ol>
|
||||
{#each pendingSubmissionItems as submission (submission.submission_id)}
|
||||
<li>
|
||||
<code>{submission.submission_id}</code>
|
||||
<span>{submission.segment_count} segments · {submission.byte_len} bytes</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() =>
|
||||
sendControl(
|
||||
{
|
||||
method: "cancel_pending_submission",
|
||||
params: {
|
||||
submission_id: submission.submission_id,
|
||||
expected_revision: pendingSubmissions.revision,
|
||||
},
|
||||
},
|
||||
"Pending submission cancellation",
|
||||
)}
|
||||
>Cancel</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
<button
|
||||
type="button"
|
||||
disabled={workerRunning || pendingSubmissions.head_id === null}
|
||||
onclick={() =>
|
||||
sendControl(
|
||||
{
|
||||
method: "continue_pending",
|
||||
params: {
|
||||
expected_revision: pendingSubmissions.revision,
|
||||
expected_head_id: pendingSubmissions.head_id ?? "",
|
||||
},
|
||||
},
|
||||
"Pending activation continue",
|
||||
)}
|
||||
>Continue next</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() =>
|
||||
sendControl(
|
||||
{
|
||||
method: "clear_pending_submissions",
|
||||
params: { expected_revision: pendingSubmissions.revision },
|
||||
},
|
||||
"Pending submissions clear",
|
||||
)}
|
||||
>Clear all</button>
|
||||
</details>
|
||||
{/if}
|
||||
|
||||
{#if workerRunning}
|
||||
<WorkerRunStatus
|
||||
startedAtMs={consoleProjection.runActivity.startedAtMs}
|
||||
@@ -1854,6 +2004,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-actions">
|
||||
{#if workerRunning}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canQueueDraft}
|
||||
onclick={handleQueueSubmit}
|
||||
>Queue Submit</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canNotifyDraft}
|
||||
onclick={handleNotifySubmit}
|
||||
>Notify</button>
|
||||
{/if}
|
||||
{#if composerNotice}
|
||||
<span class="composer-notice">{composerNotice}</span>
|
||||
{/if}
|
||||
@@ -2035,6 +2197,31 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pending-submissions {
|
||||
margin: 0 var(--space-3);
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.pending-submissions ol {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
margin: var(--space-2) 0;
|
||||
padding-left: var(--space-5);
|
||||
}
|
||||
|
||||
.pending-submissions li {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pending-submissions code {
|
||||
max-width: 16rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.console-log {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
|
||||
Reference in New Issue
Block a user