fabro/apps/fabro-web/app/lib/mutations.ts
Bryan Helmkamp 4f5e3b78f8
refactor: remove compatibility shims (#281)
## Summary
Simplifies the greenfield PR/run schema surface by collapsing alias-only
type shims and removing legacy compatibility paths that kept old wire
shapes and workflow names alive.

## Changes
- Use canonical `Run`, `PullRequestLink`, `PullRequestResponse`,
`BoardColumn`, `WorkflowSettings`, SWR `Key`, and `SteerRunRequest`
names directly across Rust and web code.
- Remove legacy PR/event deserialization compatibility for old PR
records and command output fields, with tests updated to reject stale
wire shapes.
- Drop obsolete workflow aliases for `agent_loop`, `one_shot`,
`codergen_mode`, and `stack.child_dotfile`, then update docs and tests
to the current names.

## Verification
- `git diff --check`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-types -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-workflow -p fabro-cli`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:29:25 -04:00

209 lines
5.3 KiB
TypeScript

import useSWRMutation from "swr/mutation";
import { useSWRConfig } from "swr";
import type {
PreviewUrlResponse,
Run,
SteerRunRequest,
SubmitAnswerRequest,
UpdateRunRequest,
} from "@qltysh/fabro-api-client";
import {
apiData,
authApi,
humanInTheLoopApi,
runsApi,
} from "./api-client";
import { queryKeys } from "./query-keys";
import type { LifecycleAction, LifecycleActionError } from "./run-actions";
import {
archiveRun,
cancelRun,
isLifecycleActionError,
unarchiveRun,
} from "./run-actions";
export type PreviewRunArg = {
port: number;
expires_in_secs: number;
signed?: boolean;
};
export type PreviewMutationResult = {
intent: "preview";
url: string;
};
export type LifecycleMutationResult =
| {
intent: LifecycleAction;
ok: true;
run: Run;
}
| {
intent: LifecycleAction;
ok: false;
error: LifecycleActionError | null;
};
export function usePreviewRun(id: string | undefined) {
return useSWRMutation(
id ? queryKeys.runs.preview(id) : null,
async (_key, { arg }: { arg: PreviewRunArg }): Promise<PreviewMutationResult> => {
const result = await apiData<PreviewUrlResponse>(() =>
humanInTheLoopApi.generatePreviewUrl(id!, arg),
);
return { intent: "preview", url: result.url };
},
);
}
export function useCancelRun(id: string | undefined) {
return useLifecycleMutation(id, "cancel", cancelRun);
}
export function useArchiveRun(id: string | undefined) {
return useLifecycleMutation(id, "archive", archiveRun);
}
export function useUnarchiveRun(id: string | undefined) {
return useLifecycleMutation(id, "unarchive", unarchiveRun);
}
function useLifecycleMutation(
id: string | undefined,
intent: LifecycleAction,
action: (id: string) => Promise<Run>,
) {
const { mutate } = useSWRConfig();
const key = id ? queryKeys.runs[intent](id) : null;
return useSWRMutation(
key,
async (): Promise<LifecycleMutationResult> => {
if (!id) {
return { intent, ok: false, error: null };
}
try {
return { intent, ok: true, run: await action(id) };
} catch (error) {
return {
intent,
ok: false,
error: isLifecycleActionError(error) ? error : null,
};
}
},
{
onSuccess: (result) => {
if (!id || !result.ok) return;
void mutate(queryKeys.runs.detail(id));
void mutate(queryKeys.boards.runs());
void mutate(queryKeys.runs.billing(id));
},
},
);
}
export function useUpdateRunTitle(id: string | undefined) {
const { mutate } = useSWRConfig();
return useSWRMutation(
id ? queryKeys.runs.updateTitle(id) : null,
async (_key, { arg }: { arg: UpdateRunRequest }): Promise<Run> => {
if (!id) throw new Error("id is required");
return apiData(() => runsApi.updateRun(id, arg));
},
{
onSuccess: (run) => {
if (!id) return;
void mutate(queryKeys.runs.detail(id), run, { revalidate: false });
void mutate(queryKeys.boards.runs());
void mutate(queryKeys.boards.runs(true));
},
},
);
}
export type SubmitInterviewAnswerArg = {
questionId: string;
answer: SubmitAnswerRequest;
};
export function useSubmitInterviewAnswer(runId: string | undefined) {
const { mutate } = useSWRConfig();
return useSWRMutation(
runId ? `interview-answer:${runId}` : null,
async (_key: string, { arg }: { arg: SubmitInterviewAnswerArg }) => {
if (!runId) throw new Error("runId is required");
await apiData(() =>
humanInTheLoopApi.submitRunAnswer(runId, arg.questionId, arg.answer),
);
},
{
onSuccess: () => {
if (!runId) return;
void mutate(queryKeys.runs.questions(runId, 25, 0));
void mutate(queryKeys.runs.detail(runId));
},
},
);
}
export function useInterruptRun(runId: string | undefined) {
const { mutate } = useSWRConfig();
return useSWRMutation(
runId ? `interrupt-run:${runId}` : null,
async (_key: string) => {
if (!runId) throw new Error("runId is required");
await apiData(() => humanInTheLoopApi.interruptRun(runId));
},
{
onSuccess: () => {
if (!runId) return;
void mutate(queryKeys.runs.detail(runId));
},
},
);
}
export function useSteerRun(runId: string | undefined) {
const { mutate } = useSWRConfig();
return useSWRMutation(
runId ? `steer-run:${runId}` : null,
async (_key: string, { arg }: { arg: SteerRunRequest }) => {
if (!runId) throw new Error("runId is required");
await apiData(() => humanInTheLoopApi.steerRun(runId, arg));
},
{
onSuccess: () => {
if (!runId) return;
void mutate(queryKeys.runs.detail(runId));
},
},
);
}
export function useToggleDemoMode() {
const { mutate } = useSWRConfig();
return useSWRMutation(
queryKeys.demo.toggle(),
async (_key, { arg }: { arg: { enabled: boolean } }) => {
await apiData(() => authApi.toggleDemo(arg));
},
{
onSuccess: () => {
void mutate(queryKeys.auth.me());
},
},
);
}
export function useLoginDevToken() {
return useSWRMutation(
queryKeys.auth.loginDevToken(),
async (_key, { arg }: { arg: { token: string } }) => {
return apiData(() => authApi.loginDevToken(arg), {
redirectOnUnauthorized: false,
});
},
);
}