feat(web): add Children tab to Run detail page (#294)

## Summary

- Surfaces parent/child run relationships in the web UI as a new
**Children** tab between Files Changed and Sandbox on `/runs/:id`.
- Backend exposes a new `children_count` field on the `Run` summary,
computed on read from the existing
`RunProjectionCacheState.children_by_parent` index — accurate without an
extra query.
- Frontend reuses the compact-table `RunRow` from `/runs` (now exported)
so the children list matches the existing list-view at a glance.
- Tab always shows, with a zero state when there are no children.
Refresh button (icon-only, matching the Files Changed pattern)
re-fetches both the list and the parent detail so the count badge
updates with the list.

## Screenshots

Captured live from the running fabro server.

**Populated — `Children · 2` tab active, two succeeded child rows:**

![Children tab
populated](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/populated.png)

**Zero state — visiting a run that has no children:**

![Children tab zero
state](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/zero-state.png)

## API verification

```sh
# parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP5DJJ4EV6T7QSB081Z1N \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "parent_id": null,
  "children_count": 2
}

# child
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP7VAS2J2AG73GQSAKF4G \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP7VAS2J2AG73GQSAKF4G",
  "parent_id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "children_count": 0
}

# list-by-parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    "http://127.0.0.1:32276/api/v1/runs?parent_id=01KRTKP5DJJ4EV6T7QSB081Z1N" \
    | jq '{count: (.data | length), has_more: .meta.has_more}'
{ "count": 2, "has_more": false }
```

## What's in each commit

| Commit | What |
| --- | --- |
| `2f5f4296` | `chore(api-client)`: regenerate TS client from current
OpenAPI spec — catches up drift from #292's source-aware diagnostics and
the session/turn shape updates that hadn't been re-run yet. Pure
generator output. |
| `ba16d2b3` | `feat(web)`: the actual Children tab feature. Backend
`children_count` field + cache wiring, new `useChildRuns` SWR hook,
exported `RunRow`/`RUNS_LIST_GRID_TEMPLATE` from `runs.tsx`, new
`run-children.tsx` route, `Run.children_count` on the generated TS type.
|
| `de0c32c9` | `docs`: live UI screenshots for this PR. Safe to revert
before merge if reviewers prefer a screenshot-free repo. |

## Reproducing the screenshots

1. `cargo build -p fabro-cli && ./target/debug/fabro server start`
2. `cd apps/fabro-web && bun run build`
3. ```sh
PARENT=$(./target/debug/fabro run hello --dry-run --detach --sandbox
local --json | jq -r .run_id)
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
   ```
4. Open `http://127.0.0.1:<port>/runs/$PARENT/children` (populated) and
a child's children tab (zero state).

## Test plan

- [x] `cargo nextest run -p fabro-store -p fabro-types -p fabro-api -p
fabro-server -p fabro-mcp-server` — 900+ tests pass, including new
`run_summary_includes_children_count` in `fabro-store`
- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `cd apps/fabro-web && bun test` — 383/383 pass
- [x] OpenAPI ↔ Rust parity (the `fabro-api` `run_summary_round_trip`
test covers the new field both directions)
- [x] Manual API verification via curl (above)
- [x] Live UI verification (screenshots above)

## Out of scope (v1)

- Real-time SSE updates of the children list (refresh button covers
this).
- Multi-page pagination UI (shows first page with a "more exist" footer
when `has_more`).
- Parent breadcrumb on the child run page (separate small change).
- Tree/nesting view (flat list only).
- Empty-state CTA.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-17 07:21:40 -07:00 committed by GitHub
parent 19b84777c7
commit ac32963538
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 418 additions and 117 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

View file

@ -136,6 +136,16 @@ export function columnForRun(run: Run): BoardColumn | null {
return columnForStatus(run.lifecycle.status);
}
export function toRunWithStatus(run: Run): RunWithStatus {
const item = mapRunListItem(run);
const column = columnForRun(run) ?? "queued";
return {
...item,
status: column,
statusLabel: columnStatusDisplay[column].label,
};
}
export function deriveCiStatus(checks: CheckRun[]): CiStatus {
if (checks.some((c) => c.status === "failure")) return "failing";
if (checks.some((c) => c.status === "pending" || c.status === "queued")) return "pending";

View file

@ -143,6 +143,16 @@ export function useRunFiles(
);
}
export function useChildRuns(parentId: string | undefined) {
return useSWR<PaginatedRunList | null>(
parentId ? queryKeys.runs.children(parentId) : null,
() =>
apiNullableData(() =>
runsApi.listRuns(undefined, undefined, false, parentId!),
),
);
}
export function useRunCommits(id: string | undefined) {
return useSWR<PaginatedRunCommitList | null>(
id ? queryKeys.runs.commits(id) : null,

View file

@ -50,6 +50,7 @@ export const queryKeys = {
queryKeys.runs.files(id, runFileScopeSelection(scope)),
),
commits: (id: string) => ["runs", "commits", id] as const,
children: (parentId: string) => ["runs", "children", parentId] as const,
stages: (id: string) => ["runs", "stages", id] as const,
graph: (id: string, direction?: RunGraphDirection) =>
["runs", "graph", id, direction ?? null] as const,

View file

@ -23,6 +23,7 @@ import * as RunSource from "./routes/run-source";
import * as RunLogs from "./routes/run-logs";
import * as RunEvents from "./routes/run-events";
import * as RunArtifacts from "./routes/run-artifacts";
import * as RunChildren from "./routes/run-children";
import * as RunFiles from "./routes/run-files";
import * as RunSandbox from "./routes/run-sandbox";
import * as RunTerminal from "./routes/run-terminal";
@ -116,6 +117,7 @@ export const routes: RouteObject[] = [
route("events", RunEvents),
route("artifacts", RunArtifacts),
route("files", RunFiles),
route("children", RunChildren),
route("sandbox", RunSandbox),
route("billing", RunBilling),
],

View file

@ -0,0 +1,124 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router";
import { ArrowPathIcon } from "@heroicons/react/20/solid";
import { EmptyState, ErrorState, LoadingState } from "../components/state";
import { SECONDARY_BUTTON_CLASS } from "../components/ui";
import { toRunWithStatus } from "../data/runs";
import { ApiError } from "../lib/api-client";
import { formatRelativeTime } from "../lib/format";
import { useChildRuns, useRun } from "../lib/queries";
import { RUNS_LIST_GRID_TEMPLATE, RunRow } from "./runs";
export default function RunChildren() {
const { id } = useParams();
const runQuery = useRun(id);
const childRunsQuery = useChildRuns(id);
const lastFetchedAtRef = useRef<number | null>(null);
const [now, setNow] = useState<number>(() => Date.now());
useEffect(() => {
if (childRunsQuery.data) {
lastFetchedAtRef.current = Date.now();
setNow(Date.now());
}
}, [childRunsQuery.data]);
useEffect(() => {
const interval = window.setInterval(() => setNow(Date.now()), 15_000);
return () => window.clearInterval(interval);
}, []);
const handleRefresh = useCallback(() => {
void childRunsQuery.mutate();
void runQuery.mutate();
}, [childRunsQuery, runQuery]);
if (childRunsQuery.isLoading && !childRunsQuery.data) {
return <LoadingState label="Loading child runs…" />;
}
const apiError =
childRunsQuery.error instanceof ApiError ? childRunsQuery.error : null;
if (apiError && !childRunsQuery.data) {
return (
<ErrorState
title="Couldn't load child runs"
description={`Server returned ${apiError.status}.`}
onRetry={handleRefresh}
/>
);
}
const data = childRunsQuery.data;
const children = data?.data ?? [];
const hasMore = data?.meta.has_more ?? false;
const updatedAt = lastFetchedAtRef.current;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-fg-3">Runs spawned from this run.</p>
<div className="flex items-center gap-3">
{updatedAt != null ? (
<span className="font-mono text-xs text-fg-muted">
Updated{" "}
{formatRelativeTime(new Date(updatedAt).toISOString(), now)}
</span>
) : null}
<button
type="button"
onClick={handleRefresh}
disabled={childRunsQuery.isValidating}
aria-label={
childRunsQuery.isValidating
? "Refreshing child runs"
: "Refresh child runs"
}
title="Refresh"
className="inline-flex size-7 items-center justify-center rounded-md border border-line bg-panel text-fg-3 transition-colors hover:bg-overlay hover:text-fg disabled:cursor-default disabled:opacity-60 disabled:hover:bg-panel disabled:hover:text-fg-3"
>
<ArrowPathIcon
className={`size-3.5 ${childRunsQuery.isValidating ? "animate-spin [animation-duration:450ms]" : ""}`}
aria-hidden="true"
/>
</button>
</div>
</div>
{children.length === 0 ? (
<EmptyState
title="No child runs"
description="When you launch another run with this run as its parent, it will appear here."
action={
<a
href="https://docs.fabro.sh/reference/cli#fabro-parent-link"
target="_blank"
rel="noopener noreferrer"
className={SECONDARY_BUTTON_CLASS}
>
Learn about parent links
</a>
}
/>
) : (
<>
<div
className="grid gap-2"
style={{ gridTemplateColumns: RUNS_LIST_GRID_TEMPLATE }}
>
{children.map((child) => (
<RunRow key={child.id} run={toRunWithStatus(child)} />
))}
</div>
{hasMore ? (
<p className="text-xs text-fg-muted">
Showing the first {children.length} child runs more exist.
</p>
) : null}
</>
)}
</div>
);
}

View file

@ -62,6 +62,7 @@ const allTabs = [
{ name: "Overview", path: "", count: null, demoOnly: false },
{ name: "Stages", path: "/stages", count: null, demoOnly: false },
{ name: "Files Changed", path: "/files", count: null, demoOnly: false },
{ name: "Children", path: "/children", count: null, demoOnly: false },
{ name: "Sandbox", path: "/sandbox", count: null, demoOnly: false, requiresSandbox: true },
{ name: "Billing", path: "/billing", count: null, demoOnly: false },
];
@ -189,11 +190,14 @@ export default function RunDetail({ params }: { params: { id: string } }) {
const [deletePending, setDeletePending] = useState(false);
const { push, dismiss } = useToast();
const filesCount = runQuery.data?.diff?.files_changed ?? null;
const childrenCount = runQuery.data?.children_count ?? null;
const hasSandbox = runHasSandbox(runStateQuery.data);
const tabs = allTabs
.map((tab) =>
tab.name === "Files Changed" ? { ...tab, count: filesCount } : tab,
)
.map((tab) => {
if (tab.name === "Files Changed") return { ...tab, count: filesCount };
if (tab.name === "Children") return { ...tab, count: childrenCount };
return tab;
})
.filter((t) => (!t.demoOnly || demoMode) && (!t.requiresSandbox || hasSandbox));
const lifecycleToastStateRef = useRef<LifecycleToastState>(INITIAL_LIFECYCLE_TOAST_STATE);
const steerBarRef = useRef<SteerBarHandle | null>(null);

View file

@ -581,7 +581,9 @@ function createdCutoffMsFor(filter: CreatedFilter): number | null {
}
}
function RunRow({ run }: { run: RunWithStatus }) {
export const RUNS_LIST_GRID_TEMPLATE = "auto 5rem auto 1fr auto auto 8rem auto";
export function RunRow({ run }: { run: RunWithStatus }) {
const lifecycleLabel = listLifecycleStatusLabel(run);
const statusDisplay = columnStatusDisplay[run.status];
@ -995,7 +997,7 @@ export default function Runs() {
) : (
<>
{filteredRuns > 0 && (
<div className="grid gap-2" style={{ gridTemplateColumns: "auto 5rem auto 1fr auto auto 8rem auto" }}>
<div className="grid gap-2" style={{ gridTemplateColumns: RUNS_LIST_GRID_TEMPLATE }}>
{visibleColumns.flatMap((col) =>
col.items.map((item) => (
<RunRow key={item.id} run={{ ...item, status: col.id, statusLabel: col.name }} />

View file

@ -7302,12 +7302,18 @@ components:
- current_question
- superseded_by
- links
- children_count
properties:
id:
type: string
parent_id:
type: ["string", "null"]
description: Current orchestration parent run ID, if linked.
children_count:
type: integer
format: uint64
minimum: 0
description: Number of runs currently linked to this run as their orchestration parent.
title:
type: string
goal:

View file

@ -25,6 +25,7 @@ fn run_summary_json_matches_openapi_shape() {
let summary = Run {
id: run_id,
parent_id: None,
children_count: 2,
title: "API title".to_string(),
goal: String::new(),
workflow: WorkflowRef {
@ -83,6 +84,7 @@ fn run_summary_json_matches_openapi_shape() {
serde_json::to_value(&summary).unwrap(),
json!({
"id": run_id.to_string(),
"children_count": 2,
"title": "API title",
"goal": "",
"workflow": {
@ -188,6 +190,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {
.unwrap();
assert_eq!(summary.id, run_id);
assert_eq!(summary.children_count, 0);
assert_eq!(summary.workflow.name, "unnamed");
assert_eq!(summary.workflow.slug, None);
assert_eq!(summary.goal, "ship it");

View file

@ -338,6 +338,7 @@ mod tests {
Run {
id: id.parse().expect("test run id should parse"),
parent_id: None,
children_count: 0,
title: "test".to_string(),
goal: "test".to_string(),
workflow: WorkflowRef {

View file

@ -1057,6 +1057,7 @@ mod runs {
Run {
id: run_id,
parent_id: None,
children_count: 0,
title: fabro_types::infer_run_title(goal),
goal: goal.into(),
workflow: WorkflowRef {

View file

@ -679,6 +679,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {
Run {
id: *run_id,
parent_id: state.parent_id,
children_count: 0,
title: state.title().into_owned(),
goal,
workflow: WorkflowRef {

View file

@ -438,6 +438,12 @@ mod tests {
.cast_unsigned(),
3,
),
"run-4" => (
dt("2026-03-27T12:00:30Z")
.timestamp_millis()
.cast_unsigned(),
4,
),
_ => panic!("unknown test run id: {label}"),
};
RunId::from(ulid::Ulid::from_parts(timestamp_ms, random))
@ -836,6 +842,51 @@ mod tests {
assert_eq!(summaries[0].parent_id, Some(test_run_id("run-1")));
}
#[tokio::test]
async fn run_summary_includes_children_count() {
let (_object_store, store) = make_store();
let parent = store.create_run(&test_run_id("run-1")).await.unwrap();
let child_a = store.create_run(&test_run_id("run-2")).await.unwrap();
let child_b = store.create_run(&test_run_id("run-3")).await.unwrap();
let unrelated = store.create_run(&test_run_id("run-4")).await.unwrap();
append_created(&parent, "run-1", dt("2026-03-27T12:00:00Z")).await;
append_created_with_parent(
&child_a,
"run-2",
dt("2026-03-27T12:00:10Z"),
test_run_id("run-1"),
)
.await;
append_created_with_parent(
&child_b,
"run-3",
dt("2026-03-27T12:00:20Z"),
test_run_id("run-1"),
)
.await;
append_created(&unrelated, "run-4", dt("2026-03-27T12:00:30Z")).await;
let summaries = store.list_runs(&ListRunsQuery::default()).await.unwrap();
let parent_summary = summaries
.iter()
.find(|r| r.id == test_run_id("run-1"))
.expect("parent summary should be present");
assert_eq!(parent_summary.children_count, 2);
let child_summary = summaries
.iter()
.find(|r| r.id == test_run_id("run-2"))
.expect("child summary should be present");
assert_eq!(child_summary.children_count, 0);
let unrelated_summary = summaries
.iter()
.find(|r| r.id == test_run_id("run-4"))
.expect("unrelated summary should be present");
assert_eq!(unrelated_summary.children_count, 0);
}
#[tokio::test]
async fn control_effect_events_clear_pending_control_and_update_status() {
let (_object_store, store) = make_store();

View file

@ -79,6 +79,17 @@ impl RunProjectionCacheState {
self.children_by_parent.remove(&parent_id);
}
}
fn count_children(&self, run_id: &RunId) -> u64 {
self.children_by_parent
.get(run_id)
.map_or(0, |children| children.len() as u64)
}
fn with_children_count(&self, mut entry: CachedRunProjection) -> CachedRunProjection {
entry.summary.children_count = self.count_children(&entry.run_id);
entry
}
}
impl RunProjectionCache {
@ -93,7 +104,7 @@ impl RunProjectionCache {
pub(crate) async fn list(&self, query: &ListRunsQuery) -> Vec<CachedRunProjection> {
let entries = {
let state = self.state.lock().await;
match query.parent_id {
let raw = match query.parent_id {
Some(parent_id) => state
.children_by_parent
.get(&parent_id)
@ -102,7 +113,10 @@ impl RunProjectionCache {
.filter_map(|run_id| state.entries.get(run_id).cloned())
.collect::<Vec<_>>(),
None => state.entries.values().cloned().collect::<Vec<_>>(),
}
};
raw.into_iter()
.map(|entry| state.with_children_count(entry))
.collect::<Vec<_>>()
};
let mut entries = entries
.into_iter()
@ -128,16 +142,21 @@ impl RunProjectionCache {
}
pub(crate) async fn get(&self, run_id: &RunId) -> Option<CachedRunProjection> {
self.state.lock().await.entries.get(run_id).cloned()
let state = self.state.lock().await;
state
.entries
.get(run_id)
.cloned()
.map(|entry| state.with_children_count(entry))
}
pub(crate) async fn get_summary(&self, run_id: &RunId) -> Option<Run> {
self.state
.lock()
.await
.entries
.get(run_id)
.map(|entry| entry.summary.clone())
let state = self.state.lock().await;
state.entries.get(run_id).map(|entry| {
let mut summary = entry.summary.clone();
summary.children_count = state.count_children(run_id);
summary
})
}
pub(crate) async fn apply_event(&self, run_id: &RunId, event: &EventEnvelope) -> Result<()> {

View file

@ -13,6 +13,8 @@ pub struct Run {
pub id: RunId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<RunId>,
#[serde(default)]
pub children_count: u64,
pub title: String,
pub goal: String,
pub workflow: WorkflowRef,

View file

@ -235,6 +235,7 @@ models/pull-request-user.ts
models/pull-request.ts
models/question-type.ts
models/reasoning-effort-feature.ts
models/related-workflow-diagnostic.ts
models/render-workflow-graph-direction.ts
models/render-workflow-graph-format.ts
models/render-workflow-graph-request.ts

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -32,3 +32,4 @@ export * from './api/sessions-api';
export * from './api/settings-api';
export * from './api/system-api';
export * from './api/workflows-api';

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -47,9 +47,9 @@ import type { UpdateSessionRequest } from '../models';
export const SessionsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* @param {CreateSessionRequest} createSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -88,9 +88,9 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
*
* @summary Delete session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -127,9 +127,9 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -167,10 +167,10 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -211,10 +211,10 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -257,8 +257,8 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
* @param {string} id
* @param {number} [sinceSeq]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -300,9 +300,9 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
*
* @summary List session turns
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -340,7 +340,7 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -378,8 +378,8 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -421,10 +421,10 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -475,9 +475,9 @@ export const SessionsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SessionsApiAxiosParamCreator(configuration)
return {
/**
*
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* @param {CreateSessionRequest} createSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -488,9 +488,9 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary Delete session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -501,9 +501,9 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -514,10 +514,10 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -528,10 +528,10 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -544,8 +544,8 @@ export const SessionsApiFp = function(configuration?: Configuration) {
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
* @param {string} id
* @param {number} [sinceSeq]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -556,9 +556,9 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary List session turns
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -569,7 +569,7 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -583,8 +583,8 @@ export const SessionsApiFp = function(configuration?: Configuration) {
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -595,10 +595,10 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -618,9 +618,9 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
const localVarFp = SessionsApiFp(configuration)
return {
/**
*
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* @param {CreateSessionRequest} createSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -628,9 +628,9 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.createSession(createSessionRequest, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary Delete session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -638,9 +638,9 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.deleteSession(id, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -648,10 +648,10 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.getSession(id, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -659,10 +659,10 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.getSessionTurn(id, turnId, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -672,8 +672,8 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
* @param {string} id
* @param {number} [sinceSeq]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -681,9 +681,9 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.listSessionEvents(id, sinceSeq, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary List session turns
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -691,7 +691,7 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.listSessionTurns(id, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -702,8 +702,8 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -711,10 +711,10 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.submitSessionTurn(id, submitTurnRequest, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -729,9 +729,9 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
*/
export class SessionsApi extends BaseAPI {
/**
*
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* @param {CreateSessionRequest} createSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -740,9 +740,9 @@ export class SessionsApi extends BaseAPI {
}
/**
*
*
* @summary Delete session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -751,9 +751,9 @@ export class SessionsApi extends BaseAPI {
}
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -762,10 +762,10 @@ export class SessionsApi extends BaseAPI {
}
/**
*
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -774,10 +774,10 @@ export class SessionsApi extends BaseAPI {
}
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -788,8 +788,8 @@ export class SessionsApi extends BaseAPI {
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
* @param {string} id
* @param {number} [sinceSeq]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -798,9 +798,9 @@ export class SessionsApi extends BaseAPI {
}
/**
*
*
* @summary List session turns
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -809,7 +809,7 @@ export class SessionsApi extends BaseAPI {
}
/**
*
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -821,8 +821,8 @@ export class SessionsApi extends BaseAPI {
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -831,10 +831,10 @@ export class SessionsApi extends BaseAPI {
}
/**
*
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -842,3 +842,4 @@ export class SessionsApi extends BaseAPI {
return SessionsApiFp(this.configuration).updateSession(id, updateSessionRequest, options).then((request) => request(this.axios, this.basePath));
}
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -21,3 +21,4 @@ export interface CreateSessionRequest {
'model'?: string;
'permissions'?: string;
}

View file

@ -211,6 +211,7 @@ export * from './pull-request-settings';
export * from './pull-request-user';
export * from './question-type';
export * from './reasoning-effort-feature';
export * from './related-workflow-diagnostic';
export * from './render-workflow-graph-direction';
export * from './render-workflow-graph-format';
export * from './render-workflow-graph-request';

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -27,3 +27,4 @@ export interface PaginatedSessionEventList {
'data': Array<SessionEventEnvelope>;
'meta': PaginationMeta;
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -27,3 +27,4 @@ export interface PaginatedSessionList {
'data': Array<SessionSummary>;
'meta': PaginationMeta;
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -27,3 +27,4 @@ export interface PaginatedTurnList {
'data': Array<TurnRecord>;
'meta': PaginationMeta;
}

View file

@ -0,0 +1,23 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface RelatedWorkflowDiagnostic {
'message': string;
'source_path'?: string | null;
'line'?: number | null;
'column'?: number | null;
}

View file

@ -65,6 +65,10 @@ export interface Run {
* Current orchestration parent run ID, if linked.
*/
'parent_id'?: string | null;
/**
* Number of runs currently linked to this run as their orchestration parent.
*/
'children_count': number;
'title': string;
'goal': string;
'workflow': WorkflowRef;

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -31,3 +31,4 @@ export interface SessionEventEnvelope {
'properties': any;
'ts': string;
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -37,3 +37,5 @@ export const SessionMessageKindEnum = {
} as const;
export type SessionMessageKindEnum = typeof SessionMessageKindEnum[keyof typeof SessionMessageKindEnum];

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -39,3 +39,6 @@ export interface SessionRecord {
'deleted_at'?: string | null;
'runtime_context': Array<SessionMessage>;
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -24,3 +24,6 @@ export const SessionStatus = {
} as const;
export type SessionStatus = typeof SessionStatus[keyof typeof SessionStatus];

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -33,3 +33,6 @@ export interface SessionSummary {
'created_at': string;
'updated_at': string;
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -17,3 +17,4 @@
export interface SubmitTurnRequest {
'input': string;
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -37,3 +37,6 @@ export interface TurnRecord {
'updated_at': string;
'completed_at'?: string | null;
}

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -23,3 +23,6 @@ export const TurnStatus = {
} as const;
export type TurnStatus = typeof TurnStatus[keyof typeof TurnStatus];

View file

@ -5,7 +5,7 @@
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
@ -17,3 +17,4 @@
export interface UpdateSessionRequest {
'title'?: string;
}

View file

@ -13,6 +13,9 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { RelatedWorkflowDiagnostic } from './related-workflow-diagnostic';
export interface WorkflowDiagnostic {
'rule': string;
@ -21,6 +24,12 @@ export interface WorkflowDiagnostic {
'node_id'?: string | null;
'edge'?: Array<string> | null;
'fix'?: string | null;
'source_path'?: string | null;
'line'?: number | null;
'column'?: number | null;
'span_start'?: number | null;
'span_len'?: number | null;
'related'?: Array<RelatedWorkflowDiagnostic>;
}
export const WorkflowDiagnosticSeverityEnum = {