mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
## Summary This branch improves several run-management surfaces that agents and users rely on: archived runs now stay visible and ordered correctly in the board view, pair-session messages appear in the stage Thread tab, and the `fabro_run_create` MCP tool accepts the workflow-string shorthand it advertises. ## Changes - Updates the web board cache invalidation and archived-column handling so archive/unarchive actions refresh both active and archived board queries and keep archived runs in a predictable column position. - Adds pair user/system message events to stage activity parsing, Thread rendering, search, details, and DNA timeline items. - Aligns `fabro_run_create` MCP runtime deserialization and `tools/list` schema so each run entry may be either a workflow string or a full create spec object. ## Test Plan - `cargo nextest run -p fabro-tool -p fabro-mcp-server` - `cargo nextest run -p fabro-cli stdio_server_initializes_and_lists_run_tools mcp_create_string_shorthand_deserializes_before_auth mcp_create_validation_errors_happen_before_auth_or_network mcp_create_and_search_manage_real_runs_with_cli_auth` - `cargo +nightly-2026-04-14 fmt --check --all` --- [](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 { mutateBoardRunCaches } from "./board-cache";
|
|
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));
|
|
mutateBoardRunCaches(mutate);
|
|
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 });
|
|
mutateBoardRunCaches(mutate);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
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,
|
|
});
|
|
},
|
|
);
|
|
}
|