mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
## 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` --- [](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>
209 lines
5.3 KiB
TypeScript
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,
|
|
});
|
|
},
|
|
);
|
|
}
|