diff --git a/Cargo.lock b/Cargo.lock index 5e3fb490a..d9a94facc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2239,7 +2239,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2258,7 +2258,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2300,7 +2300,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2323,7 +2323,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2367,11 +2367,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -2387,7 +2387,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2489,7 +2489,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2518,7 +2518,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2547,7 +2547,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2562,7 +2562,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2574,7 +2574,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2593,7 +2593,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2607,7 +2607,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2666,7 +2666,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -2689,7 +2689,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2699,7 +2699,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2718,7 +2718,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2733,7 +2733,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2774,7 +2774,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2785,7 +2785,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "fabro-api", @@ -2803,7 +2803,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2823,7 +2823,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2850,7 +2850,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2868,7 +2868,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2884,7 +2884,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2906,7 +2906,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2914,7 +2914,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "cc", "libc", @@ -2923,7 +2923,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2939,7 +2939,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2984,7 +2984,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3076,7 +3076,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3098,18 +3098,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" [[package]] name = "fabro-store" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3139,7 +3139,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3165,7 +3165,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3179,7 +3179,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3204,7 +3204,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3225,7 +3225,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3239,7 +3239,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3261,7 +3261,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3295,7 +3295,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3312,7 +3312,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3331,7 +3331,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -8495,7 +8495,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "axum", "base64", @@ -8514,7 +8514,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index e9f63c9d7..c2368507d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.303.0-nightly.4" +version = "0.304.0-nightly.0" license = "MIT" [workspace.dependencies] diff --git a/apps/fabro-web/app/components/runs-list/row-actions-menu.tsx b/apps/fabro-web/app/components/runs-list/row-actions-menu.tsx index b1582dbef..8d543e24c 100644 --- a/apps/fabro-web/app/components/runs-list/row-actions-menu.tsx +++ b/apps/fabro-web/app/components/runs-list/row-actions-menu.tsx @@ -12,9 +12,13 @@ import { canCancel, canDelete, canUnarchive, + cancellationActionLabel, + cancellationSuccessMessage, cancelRun, + deleteErrorMessage, deleteRun, denyRun, + isCancellationPendingState, mapError, retryRun, unarchiveRun, @@ -32,7 +36,8 @@ const MENU_ITEM_DANGER_CLASS = export function RowActionsMenu({ run }: { run: RunWithStatus }) { const { mutate } = useSWRConfig(); const { push } = useToast(); - const [pending, setPending] = useState(false); + const [pendingAction, setPendingAction] = useState(null); + const [optimisticallyCancelled, setOptimisticallyCancelled] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [idCopied, setIdCopied] = useState(false); @@ -44,6 +49,12 @@ export function RowActionsMenu({ run }: { run: RunWithStatus }) { const showUnarchive = canUnarchive(status); const showCancel = canCancel(status); const showDelete = canDelete(status); + const cancellationPending = isCancellationPendingState( + status, + run.pendingControl, + pendingAction === "cancel" || optimisticallyCancelled, + ); + const pending = pendingAction !== null || cancellationPending; const hasLifecycle = showRetry || showArchive || showUnarchive; const hasDestructive = showDeny || showCancel || showDelete; @@ -51,17 +62,25 @@ export function RowActionsMenu({ run }: { run: RunWithStatus }) { async function runAction( label: LifecycleAction, action: () => Promise, - successMessage: string, + successMessage: string | ((result: T) => string), ) { if (pending) return; - setPending(true); + setPendingAction(label); try { - await action(); - push({ message: successMessage }); + const result = await action(); + if (label === "cancel") { + setOptimisticallyCancelled(true); + } + push({ + message: + typeof successMessage === "function" + ? successMessage(result) + : successMessage, + }); } catch (error) { push({ message: mapError(error, label), tone: "error" }); } finally { - setPending(false); + setPendingAction(null); mutateRunListCaches(mutate); } } @@ -81,15 +100,14 @@ export function RowActionsMenu({ run }: { run: RunWithStatus }) { async function handleDeleteConfirm() { if (pending) return; - setPending(true); + setPendingAction("delete"); try { await deleteRun(run.id); push({ message: "Deleted run." }); } catch (error) { - // deleteRun throws LifecycleActionError shapes via lifecycleActionErrorFromError - push({ message: mapError(error, "archive"), tone: "error" }); + push({ message: deleteErrorMessage(error), tone: "error" }); } finally { - setPending(false); + setPendingAction(null); setDeleteDialogOpen(false); mutateRunListCaches(mutate); } @@ -208,12 +226,12 @@ export function RowActionsMenu({ run }: { run: RunWithStatus }) { )} diff --git a/apps/fabro-web/app/components/steer-bar.test.tsx b/apps/fabro-web/app/components/steer-bar.test.tsx new file mode 100644 index 000000000..33fefa556 --- /dev/null +++ b/apps/fabro-web/app/components/steer-bar.test.tsx @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { + isInterruptDisabled, + SteerWaitingStatus, +} from "./steer-bar"; + +describe("SteerBar", () => { + test("shows durable waiting state and prevents a second interrupt", () => { + expect(isInterruptDisabled(true, false)).toBe(true); + expect(isInterruptDisabled(false, true)).toBe(true); + expect(isInterruptDisabled(false, false)).toBe(false); + + const html = renderToStaticMarkup( + createElement(SteerWaitingStatus, { waitingForSteer: true }), + ); + expect(html).toContain('role="status"'); + expect(html).toContain("Interrupted — waiting for steering"); + }); +}); diff --git a/apps/fabro-web/app/components/steer-bar.tsx b/apps/fabro-web/app/components/steer-bar.tsx index 6c169dc9a..b78a62f92 100644 --- a/apps/fabro-web/app/components/steer-bar.tsx +++ b/apps/fabro-web/app/components/steer-bar.tsx @@ -13,6 +13,7 @@ import { ErrorMessage } from "./ui"; export interface SteerBarProps { runId: string; + waitingForSteer?: boolean; ref?: Ref; } @@ -20,13 +21,38 @@ export interface SteerBarHandle { focus(): void; } -export function SteerBar({ runId, ref }: SteerBarProps) { +export function isInterruptDisabled( + waitingForSteer: boolean, + mutationPending: boolean, +): boolean { + return waitingForSteer || mutationPending; +} + +export function SteerWaitingStatus({ + waitingForSteer, +}: { + waitingForSteer: boolean; +}) { + if (!waitingForSteer) return null; + return ( +

+ Interrupted — waiting for steering +

+ ); +} + +export function SteerBar({ + runId, + waitingForSteer = false, + ref, +}: SteerBarProps) { const [text, setText] = useState(""); const [errorMessage, setErrorMessage] = useState(null); const textareaRef = useRef(null); const steer = useSteerRun(runId); const interrupt = useInterruptRun(runId); const pending = steer.isMutating || interrupt.isMutating; + const interruptDisabled = isInterruptDisabled(waitingForSteer, pending); useImperativeHandle(ref, () => ({ focus() { @@ -49,7 +75,7 @@ export function SteerBar({ runId, ref }: SteerBarProps) { } async function fireInterrupt() { - if (pending) return; + if (interruptDisabled) return; setErrorMessage(null); try { await interrupt.trigger(); @@ -91,7 +117,7 @@ export function SteerBar({ runId, ref }: SteerBarProps) {