Skip to content

Commit 2052d3e

Browse files
d-csclaude
andcommitted
fix(webapp): dismiss cancel dialog on submit; reflect cancelled state in synthetic SpanRun
The cancel dialog stayed open after a successful submit because it was uncontrolled Radix state and the action redirects to the same URL — revalidation didn't trigger a re-mount. Wrap the submit button in DialogClose so the click closes the dialog at the same time the form posts. The SyntheticRun synthesised for the run-detail page hardcoded status PENDING regardless of whether the buffer snapshot had cancelledAt set. Customers cancelling a buffered run saw their run still labelled Queued until the drainer materialised it. Surface cancelledAt + cancelReason on SyntheticRun, switch the synthesised SpanRun status to CANCELED, and mirror the cancelled flag onto the single-span trace so the timeline matches PG behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 213185b commit 2052d3e

4 files changed

Lines changed: 43 additions & 20 deletions

File tree

apps/webapp/app/components/runs/v3/CancelRunDialog.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,19 @@ export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialog
2828
<FormButtons
2929
confirmButton={
3030
<Form action={`/resources/taskruns/${runFriendlyId}/cancel`} method="post">
31-
<Button
32-
type="submit"
33-
name="redirectUrl"
34-
value={redirectPath}
35-
variant="danger/medium"
36-
LeadingIcon={isLoading ? SpinnerWhite : NoSymbolIcon}
37-
disabled={isLoading}
38-
shortcut={{ modifiers: ["mod"], key: "enter" }}
39-
>
40-
{isLoading ? "Canceling..." : "Cancel run"}
41-
</Button>
31+
<DialogClose asChild>
32+
<Button
33+
type="submit"
34+
name="redirectUrl"
35+
value={redirectPath}
36+
variant="danger/medium"
37+
LeadingIcon={isLoading ? SpinnerWhite : NoSymbolIcon}
38+
disabled={isLoading}
39+
shortcut={{ modifiers: ["mod"], key: "enter" }}
40+
>
41+
{isLoading ? "Canceling..." : "Cancel run"}
42+
</Button>
43+
</DialogClose>
4244
</Form>
4345
}
4446
cancelButton={

apps/webapp/app/v3/mollifier/readFallback.server.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ export type SyntheticRun = {
1616
// expected (cast). Derived deterministically from `friendlyId`.
1717
id: string;
1818
friendlyId: string;
19-
status: "QUEUED" | "FAILED";
19+
status: "QUEUED" | "FAILED" | "CANCELED";
20+
// Set when the customer cancelled the run via the dashboard or API
21+
// while it was buffered. The drainer's cancel bifurcation reads this
22+
// on next pop and writes a CANCELED PG row directly (skipping
23+
// materialisation). Reflected back into the UI by the synthesised
24+
// SpanRun so the run-detail page shows the cancelled state even before
25+
// the drainer materialises it.
26+
cancelledAt: Date | undefined;
27+
cancelReason: string | undefined;
2028
taskIdentifier: string | undefined;
2129
createdAt: Date;
2230

@@ -122,10 +130,21 @@ export async function findRunByIdWithMollifierFallback(
122130
? (snapshot.environment as Record<string, unknown>)
123131
: undefined;
124132

133+
const cancelledAtRaw = asString(snapshot.cancelledAt);
134+
const cancelledAt = cancelledAtRaw ? new Date(cancelledAtRaw) : undefined;
135+
const cancelReason = asString(snapshot.cancelReason);
136+
const status: SyntheticRun["status"] = cancelledAt
137+
? "CANCELED"
138+
: entry.status === "FAILED"
139+
? "FAILED"
140+
: "QUEUED";
141+
125142
return {
126143
id: RunId.fromFriendlyId(entry.runId),
127144
friendlyId: entry.runId,
128-
status: entry.status === "FAILED" ? "FAILED" : "QUEUED",
145+
status,
146+
cancelledAt,
147+
cancelReason,
129148
taskIdentifier: asString(snapshot.taskIdentifier),
130149
createdAt: entry.createdAt,
131150

apps/webapp/app/v3/mollifier/syntheticSpanRun.server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,19 @@ export async function buildSyntheticSpanRun(args: {
5252
const isAgentRun = taskKind === "AGENT";
5353

5454
const queueName = run.queue ?? "task/";
55+
const isCancelled = run.status === "CANCELED";
5556
return {
5657
id: run.id,
5758
friendlyId: run.friendlyId,
58-
status: "PENDING",
59-
statusReason: undefined,
59+
status: isCancelled ? "CANCELED" : "PENDING",
60+
statusReason: isCancelled ? run.cancelReason ?? undefined : undefined,
6061
createdAt: run.createdAt,
6162
startedAt: null,
6263
executedAt: null,
63-
updatedAt: run.createdAt,
64+
updatedAt: run.cancelledAt ?? run.createdAt,
6465
delayUntil: null,
6566
expiredAt: null,
66-
completedAt: null,
67+
completedAt: run.cancelledAt ?? null,
6768
logsDeletedAt: null,
6869
ttl: run.ttl ?? null,
6970
taskIdentifier: run.taskIdentifier ?? "",

apps/webapp/app/v3/mollifier/syntheticTrace.server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { SyntheticRun } from "./readFallback.server";
1212
// emitted any events yet.
1313
export function buildSyntheticTraceForBufferedRun(run: SyntheticRun) {
1414
const spanId = run.spanId ?? "";
15+
const isCancelled = run.status === "CANCELED";
1516
const span: SpanSummary = {
1617
id: spanId,
1718
parentId: run.parentSpanId,
@@ -23,8 +24,8 @@ export function buildSyntheticTraceForBufferedRun(run: SyntheticRun) {
2324
startTime: run.createdAt,
2425
duration: 0,
2526
isError: false,
26-
isPartial: true,
27-
isCancelled: false,
27+
isPartial: !isCancelled,
28+
isCancelled,
2829
isDebug: false,
2930
level: "TRACE",
3031
},
@@ -53,7 +54,7 @@ export function buildSyntheticTraceForBufferedRun(run: SyntheticRun) {
5354
: [];
5455

5556
return {
56-
rootSpanStatus: "executing" as const,
57+
rootSpanStatus: (isCancelled ? "completed" : "executing") as "executing" | "completed" | "failed",
5758
events,
5859
duration: totalDuration,
5960
rootStartedAt: tree?.data.startTime,

0 commit comments

Comments
 (0)