fix: confirm queue closures across clients

This commit is contained in:
2026-08-25 11:51:40 +09:00
parent cabe38db1d
commit a41147916b
9 changed files with 315 additions and 97 deletions
@@ -21,7 +21,7 @@ export type TicketRoleAssignmentSummary = { assignment_id: string, role: string,
export type TicketAssignmentPrincipalSummary = { "kind": "user", account_id: string, } | { "kind": "worker", runtime_id: string, worker_id: string, } | { "kind": "workspace_agent", agent_key: string, };
export type TicketActionEligibility = { can_assign_orchestrator: boolean, can_unassign_orchestrator: boolean, can_queue: boolean, can_start_manual_coder: boolean, blockers: Array<string>, };
export type TicketActionEligibility = { can_assign_orchestrator: boolean, can_unassign_orchestrator: boolean, can_queue: boolean, can_start_manual_coder: boolean, queue_tickets: Array<string>, blockers: Array<string>, };
export type TicketMergeRequestSummary = { merge_request_id: string, repository_id: string, state: string, review_status: string, selector_from: string | null, selector_to: string, updated_at: string, current_subject_ref: string | null, review_subject_ref: string | null, review_requested_at: string | null, review_submitted_at: string | null, review_excerpt: string | null, };
@@ -120,10 +120,9 @@ Deno.test("ticket detail uses server-derived role assignment actions", async ()
assertEquals(source.includes("ticket.action_eligibility.can_queue"), true);
assertEquals(source.includes("ticket.relations.blockers.length > 0"), true);
assertEquals(
source.includes("Queue records orchestration demand. Dependency relations remain visible"),
true,
);
assertEquals(source.includes("ticket.action_eligibility.queue_tickets"), true);
assertEquals(source.includes("This operation queues:"), true);
assertEquals(source.includes("outcome.queued_tickets.join"), true);
assertEquals(
source.includes("resolve the listed blockers before Queue"),
false,
@@ -38,6 +38,11 @@
if (!loadedTicket) throw new Error(initialData.ticket.error ?? "ticket load failed");
const loadedRepositories = initialData.repositories.data;
type QueueOutcome = {
requested_ticket: string;
queued_tickets: string[];
};
let ticket = $state<TicketDetail>(loadedTicket);
const mergeRequest = $derived(ticket.merge_request);
let editing = $state(false);
@@ -52,6 +57,7 @@
let resolution = $state("");
let busy = $state<string | null>(null);
let errorMessage = $state<string | null>(null);
let queueMessage = $state<string | null>(null);
let readyOperationKey = $state<string | null>(null);
let manualRuntimeId = $state("");
let manualWorkerId = $state("");
@@ -117,6 +123,25 @@
}
}
async function queueTicket(): Promise<void> {
if (busy) return;
busy = "queue";
errorMessage = null;
queueMessage = null;
try {
const outcome = await workspaceApiJsonWithBody<QueueOutcome>(
`${ticketPath}/queue`,
{ method: "POST", body: JSON.stringify({}) },
);
queueMessage = `Queued ${outcome.queued_tickets.length} Ticket(s): ${outcome.queued_tickets.join(", ")}`;
applyTicket(await workspaceApiJson<TicketDetail>(ticketPath));
} catch (error) {
errorMessage = error instanceof Error ? error.message : String(error);
} finally {
busy = null;
}
}
async function mutateAssignment(
action: string,
role: "orchestrator" | "coder",
@@ -272,6 +297,10 @@
<div class="workspace-callout is-error" role="alert">{errorMessage}</div>
{/if}
{#if queueMessage}
<div class="workspace-callout" role="status">{queueMessage}</div>
{/if}
{#if editing}
<form class="ticket-editor" onsubmit={saveEdit}>
<label>Title<input bind:value={editTitle} required /></label>
@@ -462,13 +491,16 @@
<p class="workspace-empty-copy">Choose a healthy repository and an effective ref selector before marking ready.</p>
{/if}
{:else if ticket.state === "ready"}
<button class="workspace-primary-button ticket-queue-button" type="button" disabled={busy === "queue" || !ticket.action_eligibility.can_queue} onclick={() => mutate("queue", "/queue", {})}>
{busy === "queue" ? "Queueing…" : "Queue ticket"}
<button class="workspace-primary-button ticket-queue-button" type="button" disabled={busy === "queue" || !ticket.action_eligibility.can_queue} onclick={() => void queueTicket()}>
{busy === "queue" ? "Queueing…" : `Queue ${ticket.action_eligibility.queue_tickets.length} Ticket(s)`}
</button>
{#if !ticket.action_eligibility.can_queue}
<p class="workspace-empty-copy">Queue requires a valid target, an active Orchestrator assignment, no active Coder assignment, and no dependency still in planning.</p>
{:else if ticket.relations.blockers.length > 0}
<p class="workspace-empty-copy">Ready dependencies are queued atomically. Queued or in-progress dependencies remain unchanged for the Orchestrator to schedule.</p>
{:else if ticket.action_eligibility.queue_tickets.length > 0}
<p class="workspace-empty-copy">This operation queues: {ticket.action_eligibility.queue_tickets.join(", ")}.</p>
{#if ticket.relations.blockers.length > 0}
<p class="workspace-empty-copy">Ready dependencies are queued atomically. Queued or in-progress dependencies remain unchanged for the Orchestrator to schedule.</p>
{/if}
{/if}
{/if}
</section>