mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
Collapsible run stage sidebar (#352)
## What Makes the run-detail stage sidebar (shown on the Overview and Stages tabs) collapsible with a slide animation. - A toggle button slides the panel between full width (`w-56`) and an icon-only rail (`w-12`), animating `width` over 300ms with the same easing as the Ask Fabro panel. - When collapsed, **stage status icons stay visible** — green check / red X / spinning teal for running — so run progress is still scannable at a glance. Workflow links (Graph Source, Run Logs, etc.) collapse to icons too so they remain reachable. - Labels and durations become `sr-only` with `title` tooltips for hover. - The open/closed choice persists to `localStorage` (`fabro:stage-sidebar-collapsed`), carrying across the Overview and Stages tabs and reloads. ## Layout - The collapse toggle is inline with the `STAGES` heading row (or `WORKFLOW` when a run has no stages yet), so it doesn't push the stage list down. - The stage sidebar's top padding on the Stages tab was reduced (`pt-6` → `pt-3`) so the heading aligns with the adjacent content column and sits closer to the tab nav. ## Notes Self-contained in `StageSidebar` — `run-overview.tsx` and `run-stages.tsx` render it inside flex layouts that already track its width, so the slide works in both with no parent changes (aside from the padding tweak). Verified: `tsc` typecheck passes; `stage-sidebar` lib tests pass (10/10). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Co-authored-by: Fabro <noreply@fabro.sh>
This commit is contained in:
parent
199cf0822e
commit
4190e13a20
49 changed files with 262 additions and 500 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { type ComponentType } from "react";
|
||||
import { type ComponentType, type ReactNode, useCallback, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import type { StageHandler, StageState } from "@qltysh/fabro-api-client";
|
||||
import {
|
||||
|
|
@ -12,6 +12,8 @@ import {
|
|||
import {
|
||||
Bars3BottomLeftIcon,
|
||||
BoltIcon,
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronDoubleRightIcon,
|
||||
DocumentTextIcon,
|
||||
MapIcon,
|
||||
PaperClipIcon,
|
||||
|
|
@ -42,6 +44,90 @@ export const statusConfig: Record<StageState, { icon: ComponentType<{ className?
|
|||
cancelled: { icon: NoSymbolIcon, color: "text-fg-muted" },
|
||||
};
|
||||
|
||||
const COLLAPSED_STORAGE_KEY = "fabro:stage-sidebar-collapsed";
|
||||
|
||||
/** Read the persisted collapsed preference; defaults to expanded. */
|
||||
function loadStoredCollapsed(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
return window.localStorage.getItem(COLLAPSED_STORAGE_KEY) === "1";
|
||||
} catch {
|
||||
// localStorage not available (e.g. sandboxed iframe)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function persistCollapsed(collapsed: boolean) {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(COLLAPSED_STORAGE_KEY, collapsed ? "1" : "0");
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
interface SidebarRowProps {
|
||||
to: string;
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
iconClass: string;
|
||||
label: string;
|
||||
/** Right-aligned text (e.g. stage duration); hidden when collapsed. */
|
||||
trailing?: string;
|
||||
active: boolean;
|
||||
collapsed: boolean;
|
||||
/** Spin the icon to signal an in-flight stage. */
|
||||
spin?: boolean;
|
||||
}
|
||||
|
||||
/** A single sidebar link. The icon stays visible when collapsed; the label
|
||||
* becomes screen-reader-only and a `title` tooltip stands in for sighted users. */
|
||||
function SidebarRow({ to, icon: Icon, iconClass, label, trailing, active, collapsed, spin }: SidebarRowProps) {
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
to={to}
|
||||
title={collapsed ? label : undefined}
|
||||
className={`flex items-center rounded-md py-1.5 text-sm transition-colors ${
|
||||
collapsed ? "mx-1 justify-center" : "gap-2 px-2"
|
||||
} ${active ? "bg-overlay text-fg" : "text-fg-3 hover:bg-overlay hover:text-fg"}`}
|
||||
>
|
||||
<Icon className={`size-4 shrink-0 ${iconClass} ${spin ? "animate-spin" : ""}`} />
|
||||
<span className={collapsed ? "sr-only" : "flex-1 truncate"}>{label}</span>
|
||||
{trailing != null && !collapsed && (
|
||||
<span className="shrink-0 font-mono text-xs tabular-nums text-fg-muted">{trailing}</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** Section heading. When a `toggle` is given it sits inline with the heading
|
||||
* (or stands alone, centered, when collapsed) rather than on its own row. */
|
||||
function SectionHeading({
|
||||
title,
|
||||
collapsed,
|
||||
toggle,
|
||||
}: {
|
||||
title: string;
|
||||
collapsed: boolean;
|
||||
toggle?: ReactNode;
|
||||
}) {
|
||||
if (collapsed) {
|
||||
return (
|
||||
<>
|
||||
<h3 className="sr-only">{title}</h3>
|
||||
{toggle && <div className="flex h-7 items-center justify-center">{toggle}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex h-7 items-center justify-between">
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">{title}</h3>
|
||||
{toggle}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StageSidebarProps {
|
||||
stages: Stage[];
|
||||
runId: string;
|
||||
|
|
@ -49,7 +135,30 @@ interface StageSidebarProps {
|
|||
activeLink?: "settings" | "source" | "logs" | "artifacts" | "events";
|
||||
}
|
||||
|
||||
const WORKFLOW_LINKS: ReadonlyArray<{
|
||||
key: NonNullable<StageSidebarProps["activeLink"]>;
|
||||
path: string;
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: "source", path: "source", icon: MapIcon, label: "Graph Source" },
|
||||
{ key: "logs", path: "logs", icon: Bars3BottomLeftIcon, label: "Run Logs" },
|
||||
{ key: "events", path: "events", icon: BoltIcon, label: "Run Events" },
|
||||
{ key: "artifacts", path: "artifacts", icon: PaperClipIcon, label: "Artifacts" },
|
||||
{ key: "settings", path: "settings", icon: DocumentTextIcon, label: "Run Settings" },
|
||||
];
|
||||
|
||||
export function StageSidebar({ stages, runId, selectedStageId, activeLink }: StageSidebarProps) {
|
||||
// Persisted so the choice carries across the Overview and Stages tabs.
|
||||
const [collapsed, setCollapsed] = useState(loadStoredCollapsed);
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
persistCollapsed(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Tick every second while any stage is running so the elapsed clock keeps up.
|
||||
const hasActive = stages.some((s) => ACTIVE_STAGE_STATES.has(s.status));
|
||||
const now = useTickingNow(hasActive);
|
||||
|
|
@ -62,108 +171,75 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta
|
|||
return stage.duration;
|
||||
}
|
||||
|
||||
const linkBase = "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors";
|
||||
const toggleButton = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleCollapsed}
|
||||
aria-expanded={!collapsed}
|
||||
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
className="-mr-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-fg-3 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500"
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronDoubleRightIcon className="size-4" />
|
||||
) : (
|
||||
<ChevronDoubleLeftIcon className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className="w-56 shrink-0 space-y-6">
|
||||
{stages.length > 0 && (
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Stages</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
{stages.map((stage) => {
|
||||
const config = statusConfig[stage.status];
|
||||
const Icon = config.icon;
|
||||
const isSelected = selectedStageId === stage.id;
|
||||
return (
|
||||
<li key={stage.id}>
|
||||
<Link
|
||||
<nav
|
||||
className={`${collapsed ? "w-12" : "w-56"} shrink-0 transition-[width] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]`}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{stages.length > 0 && (
|
||||
<div>
|
||||
<SectionHeading title="Stages" collapsed={collapsed} toggle={toggleButton} />
|
||||
<ul className="mt-2 space-y-0.5 overflow-hidden">
|
||||
{stages.map((stage) => {
|
||||
const config = statusConfig[stage.status];
|
||||
return (
|
||||
<SidebarRow
|
||||
key={stage.id}
|
||||
to={`/runs/${runId}/stages/${stage.id}`}
|
||||
className={`${linkBase} ${
|
||||
isSelected
|
||||
? "bg-overlay text-fg"
|
||||
: "text-fg-3 hover:bg-overlay hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
<Icon className={`size-4 shrink-0 ${config.color} ${ACTIVE_STAGE_STATES.has(stage.status) ? "animate-spin" : ""}`} />
|
||||
<span className="flex-1 truncate">{formatStageLabel(stage)}</span>
|
||||
<span className="font-mono text-xs tabular-nums text-fg-muted">{stageDuration(stage)}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
icon={config.icon}
|
||||
iconClass={config.color}
|
||||
label={formatStageLabel(stage)}
|
||||
trailing={stageDuration(stage)}
|
||||
active={selectedStageId === stage.id}
|
||||
collapsed={collapsed}
|
||||
spin={ACTIVE_STAGE_STATES.has(stage.status)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<SectionHeading
|
||||
title="Workflow"
|
||||
collapsed={collapsed}
|
||||
toggle={stages.length === 0 ? toggleButton : undefined}
|
||||
/>
|
||||
{collapsed && stages.length > 0 && (
|
||||
<div aria-hidden="true" className="mx-3 border-t border-line" />
|
||||
)}
|
||||
<ul className="mt-2 space-y-0.5 overflow-hidden">
|
||||
{WORKFLOW_LINKS.map((link) => (
|
||||
<SidebarRow
|
||||
key={link.key}
|
||||
to={`/runs/${runId}/${link.path}`}
|
||||
icon={link.icon}
|
||||
iconClass="text-fg-muted"
|
||||
label={link.label}
|
||||
active={activeLink === link.key}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Workflow</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${runId}/source`}
|
||||
className={`${linkBase} ${
|
||||
activeLink === "source"
|
||||
? "bg-overlay text-fg"
|
||||
: "text-fg-3 hover:bg-overlay hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
<MapIcon className="size-4 shrink-0 text-fg-muted" />
|
||||
Graph Source
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${runId}/logs`}
|
||||
className={`${linkBase} ${
|
||||
activeLink === "logs"
|
||||
? "bg-overlay text-fg"
|
||||
: "text-fg-3 hover:bg-overlay hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
<Bars3BottomLeftIcon className="size-4 shrink-0 text-fg-muted" />
|
||||
Run Logs
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${runId}/events`}
|
||||
className={`${linkBase} ${
|
||||
activeLink === "events"
|
||||
? "bg-overlay text-fg"
|
||||
: "text-fg-3 hover:bg-overlay hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
<BoltIcon className="size-4 shrink-0 text-fg-muted" />
|
||||
Run Events
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${runId}/artifacts`}
|
||||
className={`${linkBase} ${
|
||||
activeLink === "artifacts"
|
||||
? "bg-overlay text-fg"
|
||||
: "text-fg-3 hover:bg-overlay hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
<PaperClipIcon className="size-4 shrink-0 text-fg-muted" />
|
||||
Artifacts
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${runId}/settings`}
|
||||
className={`${linkBase} ${
|
||||
activeLink === "settings"
|
||||
? "bg-overlay text-fg"
|
||||
: "text-fg-3 hover:bg-overlay hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
<DocumentTextIcon className="size-4 shrink-0 text-fg-muted" />
|
||||
Run Settings
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@ describe("RunDetail full-height child routes", () => {
|
|||
const outletWrappers = renderer.root.findAll(
|
||||
(node) =>
|
||||
node.type === "div" &&
|
||||
hasClasses(node.props.className, ["mt-6", "min-h-0", "flex-1"]),
|
||||
hasClasses(node.props.className, ["pt-3", "min-h-0", "flex-1"]),
|
||||
);
|
||||
expect(outletWrappers).toHaveLength(1);
|
||||
});
|
||||
|
|
@ -541,7 +541,7 @@ describe("RunDetail full-height child routes", () => {
|
|||
(node) =>
|
||||
node.type === "div" &&
|
||||
hasClasses(node.props.className, [
|
||||
"mt-6",
|
||||
"pt-3",
|
||||
"pb-[var(--fabro-interview-dock-clearance)]",
|
||||
]),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -700,8 +700,8 @@ export default function RunDetail({ params }: { params: { id: string } }) {
|
|||
<div
|
||||
className={
|
||||
fullHeight
|
||||
? "mt-6 flex min-h-0 flex-1 flex-col"
|
||||
: "mt-6 pb-[var(--fabro-interview-dock-clearance)]"
|
||||
? "pt-3 flex min-h-0 flex-1 flex-col"
|
||||
: "pt-3 pb-[var(--fabro-interview-dock-clearance)]"
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
|
|
@ -744,10 +744,9 @@ const ASK_FABRO_UNAVAILABLE_TOOLTIPS: Record<
|
|||
AskFabroUnavailableReasonEnum,
|
||||
string
|
||||
> = {
|
||||
[AskFabroUnavailableReasonEnum.FEATURE_DISABLED]: "Ask Fabro is disabled",
|
||||
[AskFabroUnavailableReasonEnum.NO_SANDBOX]: "Run sandbox isn't ready",
|
||||
[AskFabroUnavailableReasonEnum.SANDBOX_NOT_READY]:"Run sandbox isn't ready",
|
||||
[AskFabroUnavailableReasonEnum.LLM_UNCONFIGURED]: "No LLM configured",
|
||||
[AskFabroUnavailableReasonEnum.NO_SANDBOX]: "Run sandbox isn't ready",
|
||||
[AskFabroUnavailableReasonEnum.SANDBOX_NOT_READY]: "Run sandbox isn't ready",
|
||||
[AskFabroUnavailableReasonEnum.LLM_UNCONFIGURED]: "No LLM configured",
|
||||
};
|
||||
|
||||
function AskFabroTriggerButton({
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ export default function RunEvents() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="-mr-4 -mt-6 flex min-h-0 flex-1 sm:-mr-6 lg:-mr-8">
|
||||
<div className="shrink-0 pb-6 pr-3 pt-6">
|
||||
<div className="-mr-4 -mt-3 flex min-h-0 flex-1 sm:-mr-6 lg:-mr-8">
|
||||
<div className="shrink-0 pb-6 pr-3 pt-3">
|
||||
<StageSidebar stages={stages} runId={id!} activeLink="events" />
|
||||
</div>
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ function EventsView({
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-w-0 flex-1 pt-6">
|
||||
<div className="min-w-0 flex-1 pt-3">
|
||||
<ErrorState
|
||||
title="Couldn't load events"
|
||||
description={errorMessage(error)}
|
||||
|
|
@ -121,7 +121,7 @@ function EventsView({
|
|||
}
|
||||
if (events === undefined) {
|
||||
return (
|
||||
<div className="min-w-0 flex-1 pt-6">
|
||||
<div className="min-w-0 flex-1 pt-3">
|
||||
<LoadingState label="Loading events…" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -287,14 +287,13 @@ export default function RunSandbox({ params }: { params: { id: string } }) {
|
|||
};
|
||||
|
||||
// The outer flex spans from the tab bar's bottom border down to the
|
||||
// steer bar — `-mt-6` cancels the outlet wrapper's top gap, and we
|
||||
// intentionally omit `pb-[clearance]` here so the column divider can
|
||||
// run the full height. Each column adds its own `pt-6` and dock
|
||||
// clearance to its content instead.
|
||||
// steer bar — `-mt-3` cancels the outlet wrapper's top padding so the
|
||||
// column divider runs the full height, and we omit `pb-[clearance]`
|
||||
// here. Each column adds its own `pt-3` and dock clearance instead.
|
||||
return (
|
||||
<div className="-mt-6 flex min-h-0 flex-1">
|
||||
<div className="-mt-3 flex min-h-0 flex-1">
|
||||
<aside
|
||||
className={`w-80 shrink-0 min-h-0 overflow-y-auto pt-6 pr-6 ${TERMINAL_DOCK_CLEARANCE_CLASS}`}
|
||||
className={`w-80 shrink-0 min-h-0 overflow-y-auto pt-3 pr-6 ${TERMINAL_DOCK_CLEARANCE_CLASS}`}
|
||||
>
|
||||
{sandboxQuery.error ? (
|
||||
<ErrorState
|
||||
|
|
@ -311,7 +310,7 @@ export default function RunSandbox({ params }: { params: { id: string } }) {
|
|||
</aside>
|
||||
<div className="flex min-w-0 min-h-0 flex-1 flex-col border-l border-line">
|
||||
<div
|
||||
className={`flex min-h-0 flex-1 flex-col pt-6 pl-6 ${TERMINAL_DOCK_CLEARANCE_CLASS}`}
|
||||
className={`flex min-h-0 flex-1 flex-col pt-3 pl-6 ${TERMINAL_DOCK_CLEARANCE_CLASS}`}
|
||||
>
|
||||
{(() => {
|
||||
const modeToggle = (
|
||||
|
|
|
|||
|
|
@ -1480,8 +1480,8 @@ export default function RunStages() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="-mr-4 -mt-6 flex min-h-0 flex-1 sm:-mr-6 lg:-mr-8">
|
||||
<div className="shrink-0 pb-6 pr-3 pt-6">
|
||||
<div className="-mr-4 -mt-3 flex min-h-0 flex-1 sm:-mr-6 lg:-mr-8">
|
||||
<div className="shrink-0 pb-6 pr-3 pt-3">
|
||||
<StageSidebar stages={stages} runId={id} selectedStageId={selectedStage.id} />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -97,10 +97,9 @@ function sampleResources(overrides: ResourceOverrides = {}): SystemResourcesResp
|
|||
|
||||
function sampleServerSettings(maxConcurrentRuns = 8): ServerSettings {
|
||||
return {
|
||||
server: {
|
||||
server: {
|
||||
scheduler: { max_concurrent_runs: maxConcurrentRuns },
|
||||
},
|
||||
features: { session_sandboxes: false },
|
||||
} as unknown as ServerSettings;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
MagnifyingGlassIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useSystemInfo } from "../lib/queries";
|
||||
|
||||
export const handle = { hideHeader: true, wide: true };
|
||||
|
||||
|
|
@ -45,10 +44,6 @@ function BranchIcon({ className }: { className?: string }) {
|
|||
}
|
||||
|
||||
export default function Start() {
|
||||
const systemInfo = useSystemInfo();
|
||||
const features = systemInfo.data?.features ?? {
|
||||
session_sandboxes: false,
|
||||
};
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [project, setProject] = useState(projects[0]);
|
||||
const [branch, setBranch] = useState(branches[0]);
|
||||
|
|
@ -105,22 +100,20 @@ export default function Start() {
|
|||
/>
|
||||
|
||||
<div className="absolute bottom-3 inset-x-3 flex items-center justify-between">
|
||||
{features.session_sandboxes && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Picker
|
||||
value={project}
|
||||
onChange={setProject}
|
||||
options={projects}
|
||||
icon={<FolderIcon className="size-3.5 text-fg-muted" />}
|
||||
/>
|
||||
<Picker
|
||||
value={branch}
|
||||
onChange={setBranch}
|
||||
options={branches}
|
||||
icon={<BranchIcon className="size-3.5 text-fg-muted" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Picker
|
||||
value={project}
|
||||
onChange={setProject}
|
||||
options={projects}
|
||||
icon={<FolderIcon className="size-3.5 text-fg-muted" />}
|
||||
/>
|
||||
<Picker
|
||||
value={branch}
|
||||
onChange={setBranch}
|
||||
options={branches}
|
||||
icon={<BranchIcon className="size-3.5 text-fg-muted" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<span className="text-xs text-fg-muted select-none">
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Fabro only reads `settings.toml`. Older `server.toml`, `user.toml`, and `cli.tom
|
|||
|
||||
| Scope | Examples |
|
||||
|---|---|
|
||||
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]`, `[features]` |
|
||||
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
|
||||
| Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.sandbox]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |
|
||||
|
||||
The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing.
|
||||
|
|
@ -106,9 +106,6 @@ default_branch = "main"
|
|||
[run.git.author]
|
||||
name = "fabro-bot"
|
||||
email = "fabro-bot@company.com"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
```
|
||||
|
||||
### CLI overrides
|
||||
|
|
@ -228,7 +225,7 @@ The `[run.*]` sections in `settings.toml` act as defaults for every run.
|
|||
|
||||
On a same-machine setup, `settings.toml` is the shared machine-default layer under `workflow.toml` and `.fabro/project.toml`.
|
||||
|
||||
On a remote setup, the client bundles workflow, project, and user config into the run manifest. The server then layers those bundled client configs over its own local defaults for run-shaped fields. Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, `[features]`, and `[server.scheduler]` always come from the server machine's own `settings.toml` or `fabro server start` flags.
|
||||
On a remote setup, the client bundles workflow, project, and user config into the run manifest. The server then layers those bundled client configs over its own local defaults for run-shaped fields. Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, and `[server.scheduler]` always come from the server machine's own `settings.toml` or `fabro server start` flags.
|
||||
|
||||
Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, CLI `-I` / `--input` values replayed from run manifests merge per key at highest precedence, `[run.sandbox.env]` and `[run.sandbox.daytona.labels]` merge by key, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging.
|
||||
|
||||
|
|
@ -299,16 +296,6 @@ Configure checkpoint behavior for all runs.
|
|||
|
||||
`exclude_globs` replaces across layers — the highest-precedence layer wins wholesale. See [Run Configuration — Checkpoint](/execution/run-configuration#runcheckpoint) for per-run configuration.
|
||||
|
||||
### `[features]` section
|
||||
|
||||
Toggle experimental or opt-in features. All features default to `false`.
|
||||
|
||||
| Key | Description |
|
||||
|---|---|
|
||||
| `session_sandboxes` | Enable session sandboxes in the web UI |
|
||||
|
||||
The same `[features]` section can be set in `.fabro/project.toml` (project-level) to enable features per-project.
|
||||
|
||||
## Secrets and environment variables
|
||||
|
||||
Fabro splits secrets into two scopes:
|
||||
|
|
|
|||
|
|
@ -8156,7 +8156,6 @@ components:
|
|||
unavailable_reason:
|
||||
type: ["string", "null"]
|
||||
enum:
|
||||
- feature_disabled
|
||||
- no_sandbox
|
||||
- sandbox_not_ready
|
||||
- llm_unconfigured
|
||||
|
|
@ -10026,12 +10025,10 @@ components:
|
|||
ServerSettings:
|
||||
description: Current in-memory server settings view.
|
||||
type: object
|
||||
required: [server, features]
|
||||
required: [server]
|
||||
properties:
|
||||
server:
|
||||
$ref: "#/components/schemas/ServerNamespace"
|
||||
features:
|
||||
$ref: "#/components/schemas/FeaturesNamespace"
|
||||
|
||||
ServerNamespace:
|
||||
type: object
|
||||
|
|
@ -10071,13 +10068,6 @@ components:
|
|||
integrations:
|
||||
$ref: "#/components/schemas/ServerIntegrationsSettings"
|
||||
|
||||
FeaturesNamespace:
|
||||
type: object
|
||||
required: [session_sandboxes]
|
||||
properties:
|
||||
session_sandboxes:
|
||||
type: boolean
|
||||
|
||||
ServerListenSettings:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/ServerListenTcpSettings"
|
||||
|
|
@ -10989,16 +10979,6 @@ components:
|
|||
sandbox_provider:
|
||||
type: string
|
||||
description: Effective sandbox provider for launched runs.
|
||||
features:
|
||||
$ref: "#/components/schemas/SystemFeatures"
|
||||
|
||||
SystemFeatures:
|
||||
description: Server-level capability flags.
|
||||
type: object
|
||||
properties:
|
||||
session_sandboxes:
|
||||
type: boolean
|
||||
description: Whether session sandboxes are enabled.
|
||||
|
||||
SystemRunCounts:
|
||||
description: Counts of known runs in the active server process.
|
||||
|
|
|
|||
|
|
@ -31,14 +31,6 @@ fabro ps # active runs
|
|||
fabro ps -a # all runs including completed
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**`[feature_flags]` renamed to `[features]`.** If your `fabro.toml` uses `[feature_flags]`, rename the section to `[features]`.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**Retros are now experimental and disabled by default.** If you use retros, enable them explicitly with `retros = true` under `[features]` in your `fabro.toml`.
|
||||
</Warning>
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="CLI">
|
||||
|
|
|
|||
|
|
@ -551,7 +551,7 @@ When running locally, the machine defaults at `~/.fabro/settings.toml` can set r
|
|||
Fabro validates the run config when it loads:
|
||||
|
||||
- **`_version` check** — Only `_version = 1` (or missing, which defaults to `1`) is accepted. The legacy top-level `version` key is rejected with a rename hint.
|
||||
- **Unknown keys** — Any top-level key not in `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, `[features]`, or `_version` is rejected with a targeted rename hint pointing at the v2 replacement path.
|
||||
- **Unknown keys** — Any top-level key not in `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, or `_version` is rejected with a targeted rename hint pointing at the v2 replacement path.
|
||||
- **Variable check** — Undefined workflow or prompt template variables produce diagnostics. `fabro validate` reports them as warnings; run-style commands treat them as errors before creating or starting a run.
|
||||
|
||||
Use `fabro preflight` to validate a run config without executing it:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ Files that omit `_version` are treated as version `1`. The legacy top-level `ver
|
|||
|
||||
## Who reads what
|
||||
|
||||
`settings.toml` uses the same schema as `.fabro/project.toml` and `workflow.toml`, but each process only reads the fields it understands. The top-level schema is strictly namespaced — the only allowed domains are `[project]`, `[workflow]`, `[run]`, `[llm]`, `[cli]`, `[server]`, and `[features]`.
|
||||
`settings.toml` uses the same schema as `.fabro/project.toml` and `workflow.toml`, but each process only reads the fields it understands. The top-level schema is strictly namespaced — the only allowed domains are `[project]`, `[workflow]`, `[run]`, `[llm]`, `[cli]`, and `[server]`.
|
||||
|
||||
| Scope | Examples |
|
||||
|---|---|
|
||||
|
|
@ -44,7 +44,7 @@ See [Server Configuration](/administration/server-configuration) for the server-
|
|||
|
||||
## Precedence
|
||||
|
||||
Shared layered domains (`[project]`, `[workflow]`, `[run]`, `[features]`) use this override order:
|
||||
Shared layered domains (`[project]`, `[workflow]`, `[run]`) use this override order:
|
||||
|
||||
1. **CLI flags** — always win
|
||||
2. **Environment overrides** — Fabro-defined override channels
|
||||
|
|
|
|||
|
|
@ -209,11 +209,6 @@ fn main() {
|
|||
"fabro_types::settings::ServerNamespace",
|
||||
&[],
|
||||
),
|
||||
(
|
||||
"FeaturesNamespace",
|
||||
"fabro_types::settings::FeaturesNamespace",
|
||||
&[],
|
||||
),
|
||||
(
|
||||
"ServerListenSettings",
|
||||
"fabro_types::settings::server::ServerListenSettings",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ pub mod types {
|
|||
Model, ModelCosts, ModelFeatures, ModelLimits, ModelRef as BillingModelRef, ModelTestMode,
|
||||
Provider, ReasoningEffortFeature, Speed as BillingSpeed,
|
||||
};
|
||||
pub use fabro_types::settings::ServerNamespace;
|
||||
pub use fabro_types::settings::server::{
|
||||
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
|
||||
IpAllowEntry, LogDestination, ObjectStoreSettings, ServerApiSettings,
|
||||
|
|
@ -27,7 +28,6 @@ pub mod types {
|
|||
ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
|
||||
WebhookStrategy,
|
||||
};
|
||||
pub use fabro_types::settings::{FeaturesNamespace, ServerNamespace};
|
||||
pub use fabro_types::status::{
|
||||
BlockedReason, FailureReason, RunControlAction, RunStatus, SuccessReason,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::{
|
||||
FeaturesNamespace as ApiFeaturesNamespace, LogDestination as ApiLogDestination,
|
||||
ObjectStoreSettings as ApiObjectStoreSettings, ServerNamespace as ApiServerNamespace,
|
||||
ServerSettings as ApiServerSettings,
|
||||
LogDestination as ApiLogDestination, ObjectStoreSettings as ApiObjectStoreSettings,
|
||||
ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings,
|
||||
};
|
||||
use fabro_config::ServerSettingsBuilder;
|
||||
use fabro_types::ServerSettings;
|
||||
use fabro_types::settings::ServerNamespace;
|
||||
use fabro_types::settings::server::{LogDestination, ObjectStoreSettings};
|
||||
use fabro_types::settings::{FeaturesNamespace, ServerNamespace};
|
||||
|
||||
#[test]
|
||||
fn server_settings_family_reuses_domain_types() {
|
||||
assert_same_type::<ApiServerSettings, ServerSettings>();
|
||||
assert_same_type::<ApiServerNamespace, ServerNamespace>();
|
||||
assert_same_type::<ApiFeaturesNamespace, FeaturesNamespace>();
|
||||
assert_same_type::<ApiObjectStoreSettings, ObjectStoreSettings>();
|
||||
assert_same_type::<ApiLogDestination, LogDestination>();
|
||||
}
|
||||
|
|
@ -54,9 +52,6 @@ strategy = "app"
|
|||
app_id = "12345"
|
||||
client_id = "Iv1.abcdef"
|
||||
slug = "fabro-dev"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#,
|
||||
)
|
||||
.expect("settings should resolve");
|
||||
|
|
@ -66,7 +61,7 @@ session_sandboxes = true
|
|||
assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276");
|
||||
assert_eq!(json["server"]["storage"]["root"], "/srv/fabro");
|
||||
assert_eq!(json["server"]["logging"]["destination"], "stdout");
|
||||
assert_eq!(json["features"]["session_sandboxes"], true);
|
||||
assert!(json.get("features").is_none());
|
||||
|
||||
let round_trip: ApiServerSettings =
|
||||
serde_json::from_value(json).expect("server settings should deserialize");
|
||||
|
|
|
|||
|
|
@ -468,10 +468,7 @@ shared = "cli"
|
|||
cfg["user"]["cli"]["output"]["verbosity"].as_str(),
|
||||
Some("verbose")
|
||||
);
|
||||
assert_eq!(
|
||||
cfg["user"]["features"]["session_sandboxes"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert!(cfg["user"].get("features").is_none());
|
||||
assert_eq!(
|
||||
cfg["server"]["server"]["auth"]["methods"][0].as_str(),
|
||||
Some("dev-token")
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ use fabro_util::error::SharedError;
|
|||
use crate::defaults::DEFAULTS_LAYER;
|
||||
use crate::load::load_settings_path;
|
||||
use crate::resolve::{
|
||||
ResolveError, resolve_cli, resolve_features, resolve_project, resolve_run, resolve_server,
|
||||
resolve_workflow,
|
||||
ResolveError, resolve_cli, resolve_project, resolve_run, resolve_server, resolve_workflow,
|
||||
};
|
||||
use crate::user::load_settings_config;
|
||||
use crate::{
|
||||
|
|
@ -98,9 +97,8 @@ impl ServerSettingsBuilder {
|
|||
let layer = layer.clone().combine(DEFAULTS_LAYER.clone());
|
||||
let mut errors = Vec::new();
|
||||
let server = resolve_server(&layer.server.clone().unwrap_or_default(), &mut errors);
|
||||
let features = resolve_features(&layer.features.clone().unwrap_or_default(), &mut errors);
|
||||
finish_result(
|
||||
ServerSettings { server, features },
|
||||
ServerSettings { server },
|
||||
"failed to resolve server settings",
|
||||
errors,
|
||||
)
|
||||
|
|
@ -148,9 +146,8 @@ impl UserSettingsBuilder {
|
|||
let layer = layer.clone().combine(DEFAULTS_LAYER.clone());
|
||||
let mut errors = Vec::new();
|
||||
let cli = resolve_cli(&layer.cli.clone().unwrap_or_default(), &mut errors);
|
||||
let features = resolve_features(&layer.features.clone().unwrap_or_default(), &mut errors);
|
||||
finish_result(
|
||||
UserSettings { cli, features },
|
||||
UserSettings { cli },
|
||||
"failed to resolve user settings",
|
||||
errors,
|
||||
)
|
||||
|
|
@ -552,7 +549,6 @@ impl WorkflowSettingsBuilder {
|
|||
layer = layer.combine(DEFAULTS_LAYER.clone());
|
||||
layer.server = None;
|
||||
layer.cli = None;
|
||||
layer.features = None;
|
||||
layer
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,3 @@ provider = "local"
|
|||
prefix = ""
|
||||
flush_interval = "1ms"
|
||||
disk_cache = false
|
||||
|
||||
[features]
|
||||
session_sandboxes = false
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ use fabro_types::settings::{Duration, InterpString, Size};
|
|||
|
||||
use super::LogFilter;
|
||||
use super::cli::{CliAuthLayer, CliLoggingLayer, CliTargetLayer};
|
||||
use super::features::FeaturesLayer;
|
||||
use super::llm::{CostRates, CredentialRef, HeaderValueRef, ReasoningEffortFeature};
|
||||
use super::run::{
|
||||
DaytonaSnapshotLayer, DaytonaVolumeLayer, HookAgentMarker, HookEntry, HookTlsMode,
|
||||
|
|
@ -148,7 +147,6 @@ impl_combine_self!(
|
|||
CliAuthLayer,
|
||||
CliLoggingLayer,
|
||||
CliTargetLayer,
|
||||
FeaturesLayer,
|
||||
DaytonaNetworkLayer,
|
||||
DaytonaSnapshotLayer,
|
||||
InterviewProviderLayer,
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
//! Sparse `[features]` settings layer definitions.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A sparse `[features]` layer as it appears in a single settings file.
|
||||
///
|
||||
/// Every field is an `Option<bool>` so layers can independently set or
|
||||
/// override a flag without forcing a default that hides an unset value.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FeaturesLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub session_sandboxes: Option<bool>,
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
mod cli;
|
||||
mod combine;
|
||||
mod features;
|
||||
mod llm;
|
||||
mod log_filter;
|
||||
mod maps;
|
||||
|
|
@ -16,7 +15,6 @@ pub use cli::{
|
|||
CliOutputLayer, CliTargetLayer, CliUpdatesLayer,
|
||||
};
|
||||
pub(crate) use combine::Combine;
|
||||
pub use features::FeaturesLayer;
|
||||
pub use llm::{
|
||||
CostRates, CredentialRef, CredentialRefParseError, HeaderValueRef, LlmLayer, ModelControls,
|
||||
ModelCostTable, ModelFeatures as LlmModelFeatures, ModelLimits as LlmModelLimits,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ use std::str::FromStr;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::cli::CliLayer;
|
||||
use super::features::FeaturesLayer;
|
||||
use super::llm::LlmLayer;
|
||||
use super::project::ProjectLayer;
|
||||
use super::run::RunLayer;
|
||||
|
|
@ -34,8 +33,6 @@ pub(crate) struct SettingsLayer {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub server: Option<ServerLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub features: Option<FeaturesLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub llm: Option<LlmLayer>,
|
||||
}
|
||||
|
||||
|
|
@ -56,15 +53,6 @@ impl From<CliLayer> for SettingsLayer {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<FeaturesLayer> for SettingsLayer {
|
||||
fn from(features: FeaturesLayer) -> Self {
|
||||
Self {
|
||||
features: Some(features),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LlmLayer> for SettingsLayer {
|
||||
fn from(llm: LlmLayer) -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ pub use layers::{
|
|||
CliAuthLayer, CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliLoggingLayer,
|
||||
CliOutputLayer, CliTargetLayer, CliUpdatesLayer, CostRates, CredentialRef,
|
||||
CredentialRefParseError, DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer,
|
||||
DaytonaVolumeLayer, DockerSandboxLayer, FeaturesLayer, GitAuthorLayer, GithubIntegrationLayer,
|
||||
HeaderValueRef, HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer,
|
||||
InterviewProviderLayer, InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter,
|
||||
McpEntryLayer, MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings,
|
||||
DaytonaVolumeLayer, DockerSandboxLayer, GitAuthorLayer, GithubIntegrationLayer, HeaderValueRef,
|
||||
HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer, InterviewProviderLayer,
|
||||
InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter, McpEntryLayer,
|
||||
MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings,
|
||||
NotificationProviderLayer, NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
|
||||
PrepareStep, ProjectLayer, ProviderSettings, ReasoningEffortFeature, ReplaceMap, RunAgentLayer,
|
||||
RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer,
|
||||
|
|
@ -61,8 +61,7 @@ pub(crate) use layers::{Combine, SettingsLayer};
|
|||
pub use logging::{resolve_log_destination, resolve_log_destination_with_env};
|
||||
pub use parse::ParseError;
|
||||
pub use resolve::{
|
||||
ResolveError, resolve_cli, resolve_features, resolve_project, resolve_run, resolve_server,
|
||||
resolve_workflow,
|
||||
ResolveError, resolve_cli, resolve_project, resolve_run, resolve_server, resolve_workflow,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use storage::{RunScratch, RuntimeDirectory, Storage};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::SettingsLayer;
|
|||
const CURRENT_VERSION: u32 = 1;
|
||||
|
||||
const ALLOWED_TOP_LEVEL_KEYS: &[&str] = &[
|
||||
"_version", "project", "workflow", "run", "cli", "server", "features", "llm",
|
||||
"_version", "project", "workflow", "run", "cli", "server", "llm",
|
||||
];
|
||||
|
||||
/// Legacy `[llm]` keys that pre-date the settings-driven catalog plan and
|
||||
|
|
@ -38,7 +38,7 @@ impl fmt::Display for ParseError {
|
|||
} else {
|
||||
write!(
|
||||
f,
|
||||
"unknown top-level settings key `{key}`: expected one of `_version`, `project`, `workflow`, `run`, `cli`, `server`, `features`, `llm`"
|
||||
"unknown top-level settings key `{key}`: expected one of `_version`, `project`, `workflow`, `run`, `cli`, `server`, `llm`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -190,6 +190,18 @@ mod tests {
|
|||
assert!(matches!(err, ParseError::UnknownTopLevelKey { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_legacy_features_section() {
|
||||
let namespace = "features";
|
||||
let err = format!("[{namespace}]\nremoved_flag = true\n")
|
||||
.parse::<SettingsLayer>()
|
||||
.unwrap_err();
|
||||
match err {
|
||||
ParseError::UnknownTopLevelKey { key, .. } => assert_eq!(key, "features"),
|
||||
other => panic!("expected UnknownTopLevelKey, got: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_version_rejected_with_upgrade_hint() {
|
||||
let err = "_version = 99".parse::<SettingsLayer>().unwrap_err();
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
use fabro_types::settings::FeaturesNamespace;
|
||||
|
||||
use super::ResolveError;
|
||||
use crate::FeaturesLayer;
|
||||
|
||||
pub fn resolve_features(
|
||||
layer: &FeaturesLayer,
|
||||
_errors: &mut Vec<ResolveError>,
|
||||
) -> FeaturesNamespace {
|
||||
FeaturesNamespace {
|
||||
session_sandboxes: layer
|
||||
.session_sandboxes
|
||||
.expect("defaults.toml should provide features.session_sandboxes"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
mod cli;
|
||||
mod error;
|
||||
mod features;
|
||||
mod project;
|
||||
mod run;
|
||||
mod server;
|
||||
|
|
@ -9,7 +8,6 @@ mod workflow;
|
|||
pub use cli::resolve_cli;
|
||||
pub use error::ResolveError;
|
||||
use fabro_types::settings::InterpString;
|
||||
pub use features::resolve_features;
|
||||
pub use project::resolve_project;
|
||||
pub use run::resolve_run;
|
||||
pub use server::resolve_server;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ mod combine;
|
|||
mod defaults;
|
||||
mod log_filter;
|
||||
mod resolve_cli;
|
||||
mod resolve_features;
|
||||
mod resolve_project;
|
||||
mod resolve_root;
|
||||
mod resolve_run;
|
||||
|
|
|
|||
|
|
@ -35,9 +35,6 @@ _version = 1
|
|||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://config.example.com"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#,
|
||||
)
|
||||
.expect("user settings should resolve");
|
||||
|
|
@ -48,7 +45,6 @@ session_sandboxes = true
|
|||
url: InterpString::parse("https://config.example.com"),
|
||||
})
|
||||
);
|
||||
assert!(user_settings.features.session_sandboxes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -61,9 +57,6 @@ _version = 1
|
|||
|
||||
[cli.output]
|
||||
verbosity = "verbose"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -72,7 +65,6 @@ session_sandboxes = true
|
|||
let user_settings = fabro_config::UserSettingsBuilder::load_default()
|
||||
.expect("user settings should resolve");
|
||||
assert_eq!(user_settings.cli.output.verbosity, OutputVerbosity::Verbose);
|
||||
assert!(user_settings.features.session_sandboxes);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +77,6 @@ fn user_settings_resolve_returns_defaults_when_default_settings_file_is_missing(
|
|||
.expect("user settings should resolve");
|
||||
assert_eq!(user_settings.cli.output.format, OutputFormat::Text);
|
||||
assert_eq!(user_settings.cli.output.verbosity, OutputVerbosity::Normal);
|
||||
assert!(!user_settings.features.session_sandboxes);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
use crate::{SettingsLayer, UserSettingsBuilder};
|
||||
|
||||
#[test]
|
||||
fn resolves_features_defaults_from_empty_settings() {
|
||||
let settings = SettingsLayer::default();
|
||||
|
||||
let features = UserSettingsBuilder::from_layer(&settings)
|
||||
.expect("empty settings should resolve")
|
||||
.features;
|
||||
|
||||
assert!(!features.session_sandboxes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_session_sandboxes_flag() {
|
||||
let features = UserSettingsBuilder::from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
",
|
||||
)
|
||||
.expect("features should resolve")
|
||||
.features;
|
||||
|
||||
assert!(features.session_sandboxes);
|
||||
}
|
||||
|
|
@ -198,20 +198,13 @@ methods = ["dev-token"]
|
|||
|
||||
[server.storage]
|
||||
root = "/srv/fabro"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let context = fabro_config::ServerSettingsBuilder::from_layer(&settings)
|
||||
.expect("settings should resolve");
|
||||
let user_settings = fabro_config::UserSettingsBuilder::from_layer(&settings)
|
||||
.expect("user settings should resolve");
|
||||
|
||||
assert_eq!(context.server.storage.root.as_source(), "/srv/fabro");
|
||||
assert!(context.features.session_sandboxes);
|
||||
assert_eq!(context.features, user_settings.features);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -227,9 +220,6 @@ methods = ["dev-token"]
|
|||
|
||||
[server.storage]
|
||||
root = "/srv/from-home"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -238,7 +228,6 @@ session_sandboxes = true
|
|||
let settings =
|
||||
fabro_config::ServerSettingsBuilder::load_default().expect("settings should resolve");
|
||||
assert_eq!(settings.server.storage.root.as_source(), "/srv/from-home");
|
||||
assert!(settings.features.session_sandboxes);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -875,8 +875,7 @@ pub(crate) async fn get_system_info(
|
|||
"storage_dir": "/demo/fabro/storage",
|
||||
"uptime_secs": 42,
|
||||
"runs": { "total": 3, "active": 1 },
|
||||
"sandbox_provider": "local",
|
||||
"features": { "session_sandboxes": false }
|
||||
"sandbox_provider": "local"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
|
|
@ -2123,9 +2122,6 @@ strategy = "app"
|
|||
app_id = "12345"
|
||||
client_id = "Iv1.abc123"
|
||||
slug = "fabro-dev"
|
||||
|
||||
[features]
|
||||
session_sandboxes = false
|
||||
"#,
|
||||
)
|
||||
.expect("demo settings fixture should resolve"),
|
||||
|
|
|
|||
|
|
@ -37,10 +37,9 @@ pub use fabro_api::types::{
|
|||
RunStage, SandboxDetails, SandboxFileEntry, SandboxFileListResponse, SandboxService,
|
||||
SandboxServiceListResponse, SshAccessRequest, SshAccessResponse, StageHandler, StageState,
|
||||
StartRunRequest, SubmitAnswerRequest, SystemCpuResourceScope, SystemCpuResources,
|
||||
SystemDiskResourceScope, SystemDiskResources, SystemFeatures, SystemInfoResponse,
|
||||
SystemMemoryResourceScope, SystemMemoryResources, SystemRepairRunIssue,
|
||||
SystemRepairRunsResponse, SystemResourcesResponse, SystemRunCounts, TimelineEntryResponse,
|
||||
VncPreviewResponse, WriteBlobResponse,
|
||||
SystemDiskResourceScope, SystemDiskResources, SystemInfoResponse, SystemMemoryResourceScope,
|
||||
SystemMemoryResources, SystemRepairRunIssue, SystemRepairRunsResponse, SystemResourcesResponse,
|
||||
SystemRunCounts, TimelineEntryResponse, VncPreviewResponse, WriteBlobResponse,
|
||||
};
|
||||
use fabro_auth::{CredentialSource, VaultCredentialSource, auth_issue_message};
|
||||
#[cfg(test)]
|
||||
|
|
@ -641,8 +640,7 @@ pub struct AppState {
|
|||
type PullRequestCreateLocks = Arc<Mutex<HashMap<RunId, Arc<AsyncMutex<()>>>>>;
|
||||
|
||||
struct AskFabroReadiness {
|
||||
feature_enabled: bool,
|
||||
default_model: Option<String>,
|
||||
default_model: Option<String>,
|
||||
}
|
||||
|
||||
impl AskFabroReadiness {
|
||||
|
|
@ -652,9 +650,7 @@ impl AskFabroReadiness {
|
|||
}
|
||||
|
||||
fn ask_fabro_for(&self, run: &fabro_types::Run) -> AskFabro {
|
||||
let unavailable_reason = if !self.feature_enabled {
|
||||
Some(AskFabroUnavailableReason::FeatureDisabled)
|
||||
} else if run.sandbox.is_none() {
|
||||
let unavailable_reason = if run.sandbox.is_none() {
|
||||
Some(AskFabroUnavailableReason::NoSandbox)
|
||||
} else if run
|
||||
.sandbox
|
||||
|
|
@ -871,14 +867,6 @@ impl AppState {
|
|||
}
|
||||
|
||||
async fn ask_fabro_readiness(&self) -> AskFabroReadiness {
|
||||
let feature_enabled = self.server_settings().features.session_sandboxes;
|
||||
if !feature_enabled {
|
||||
return AskFabroReadiness {
|
||||
feature_enabled,
|
||||
default_model: None,
|
||||
};
|
||||
}
|
||||
|
||||
let provider_ids = self.ready_llm_provider_ids().await;
|
||||
let default_model = if provider_ids.is_empty() {
|
||||
None
|
||||
|
|
@ -890,10 +878,7 @@ impl AppState {
|
|||
.clone(),
|
||||
)
|
||||
};
|
||||
AskFabroReadiness {
|
||||
feature_enabled,
|
||||
default_model,
|
||||
}
|
||||
AskFabroReadiness { default_model }
|
||||
}
|
||||
|
||||
pub(crate) fn vault_or_env(&self, name: &str) -> Option<String> {
|
||||
|
|
@ -1503,16 +1488,6 @@ async fn github_webhook(
|
|||
StatusCode::OK
|
||||
}
|
||||
|
||||
fn system_features(
|
||||
server_settings: &ServerSettings,
|
||||
_manifest_run_settings: &std::result::Result<RunNamespace, SharedError>,
|
||||
) -> SystemFeatures {
|
||||
let session_sandboxes = server_settings.features.session_sandboxes;
|
||||
SystemFeatures {
|
||||
session_sandboxes: Some(session_sandboxes),
|
||||
}
|
||||
}
|
||||
|
||||
struct PrunePlan {
|
||||
run_ids: Vec<RunId>,
|
||||
rows: Vec<PruneRunEntry>,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ use super::super::{
|
|||
PruneRunsRequest, PruneRunsResponse, Query, RequiredUser, Response, Router, RunStatus, State,
|
||||
StatusCode, SystemInfoResponse, SystemRepairRunIssue, SystemRepairRunsResponse,
|
||||
SystemRunCounts, build_disk_usage_response, build_prune_plan, delete_run_internal, diagnostics,
|
||||
get, post, resolve_interp_string, resource_sampler, spawn_blocking, system_features,
|
||||
system_sandbox_provider, to_i64,
|
||||
get, post, resolve_interp_string, resource_sampler, spawn_blocking, system_sandbox_provider,
|
||||
to_i64,
|
||||
};
|
||||
|
||||
pub(super) fn routes() -> Router<Arc<AppState>> {
|
||||
|
|
@ -76,10 +76,6 @@ async fn get_system_info(_auth: RequiredUser, State(state): State<Arc<AppState>>
|
|||
active: Some(to_i64(active_runs)),
|
||||
}),
|
||||
sandbox_provider: Some(system_sandbox_provider(&manifest_run_settings)),
|
||||
features: Some(system_features(
|
||||
server_settings.as_ref(),
|
||||
&manifest_run_settings,
|
||||
)),
|
||||
};
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -870,49 +870,6 @@ provider = "invalid-provider"
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_features_use_dense_server_and_manifest_defaults() {
|
||||
let source = r#"
|
||||
_version = 1
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#;
|
||||
let server_settings = server_settings_from_toml(source);
|
||||
let manifest_run_settings = resolve_manifest_run_settings(
|
||||
&run_manifest::manifest_run_defaults(Some(&manifest_run_defaults_from_toml(source))),
|
||||
);
|
||||
let features = system_features(&server_settings, &manifest_run_settings);
|
||||
|
||||
assert_eq!(features.session_sandboxes, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_features_ignore_manifest_run_settings_resolution() {
|
||||
let source = r#"
|
||||
_version = 1
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
|
||||
[run.sandbox]
|
||||
provider = "invalid-provider"
|
||||
"#;
|
||||
let server_settings = server_settings_from_toml(source);
|
||||
let manifest_run_settings = resolve_manifest_run_settings(
|
||||
&run_manifest::manifest_run_defaults(Some(&manifest_run_defaults_from_toml(source))),
|
||||
);
|
||||
let features = system_features(&server_settings, &manifest_run_settings);
|
||||
|
||||
assert_eq!(features.session_sandboxes, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_sandbox_provider_uses_manifest_defaults() {
|
||||
let source = r#"
|
||||
|
|
|
|||
|
|
@ -57,9 +57,6 @@ async fn run_responses_include_ask_fabro_affordance() {
|
|||
let settings = settings_from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
",
|
||||
);
|
||||
let state = fabro_server::test_support::TestAppStateBuilder::new()
|
||||
|
|
@ -149,9 +146,6 @@ goal = "Ship it"
|
|||
|
||||
[cli.output]
|
||||
verbosity = "verbose"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#
|
||||
}]);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,7 @@ client_id = "Iv1.abcdef"
|
|||
let top_level = body
|
||||
.as_object()
|
||||
.expect("server settings response should be an object");
|
||||
assert_eq!(top_level.len(), 2);
|
||||
assert!(top_level.contains_key("server"));
|
||||
assert!(top_level.contains_key("features"));
|
||||
|
||||
assert_eq!(body["server"]["listen"]["type"], "tcp");
|
||||
assert_eq!(body["server"]["listen"]["address"], "127.0.0.1:32276");
|
||||
|
|
@ -68,7 +66,7 @@ client_id = "Iv1.abcdef"
|
|||
body["server"]["integrations"]["github"]["client_id"],
|
||||
"Iv1.abcdef"
|
||||
);
|
||||
assert_eq!(body["features"]["session_sandboxes"], false);
|
||||
assert!(body.get("features").is_none());
|
||||
assert!(body.get("cli").is_none());
|
||||
assert!(body.get("run").is_none());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,9 +138,9 @@ async fn get_system_info_returns_runtime_fields() {
|
|||
assert_eq!(body["runs"]["total"], 0);
|
||||
assert_eq!(body["runs"]["active"], 0);
|
||||
assert!(body["uptime_secs"].as_i64().is_some());
|
||||
assert_eq!(
|
||||
body["features"],
|
||||
serde_json::json!({ "session_sandboxes": false })
|
||||
assert!(
|
||||
body.get("features").is_none(),
|
||||
"system info should not include a features field"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ use std::path::Path;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::settings::{
|
||||
CliNamespace, FeaturesNamespace, InterpString, ObjectStoreSettings, ProjectNamespace,
|
||||
RunNamespace, ServerNamespace, WorkflowNamespace,
|
||||
CliNamespace, InterpString, ObjectStoreSettings, ProjectNamespace, RunNamespace,
|
||||
ServerNamespace, WorkflowNamespace,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ServerSettings {
|
||||
pub server: ServerNamespace,
|
||||
pub features: FeaturesNamespace,
|
||||
pub server: ServerNamespace,
|
||||
}
|
||||
|
||||
impl ServerSettings {
|
||||
|
|
@ -43,8 +42,7 @@ fn override_local_object_store_root(
|
|||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct UserSettings {
|
||||
pub cli: CliNamespace,
|
||||
pub features: FeaturesNamespace,
|
||||
pub cli: CliNamespace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ pub struct AskFabro {
|
|||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum AskFabroUnavailableReason {
|
||||
FeatureDisabled,
|
||||
NoSandbox,
|
||||
SandboxNotReady,
|
||||
LlmUnconfigured,
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
//! Features domain.
|
||||
//!
|
||||
//! `[features]` is a reserved cross-cutting namespace for Fabro capability
|
||||
//! flags only. It has a high admission bar and must not become a junk drawer.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A structurally resolved `[features]` view for consumers.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FeaturesNamespace {
|
||||
pub session_sandboxes: bool,
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
//! Namespaced settings schema.
|
||||
//!
|
||||
//! Top-level schema is strictly namespaced with `_version`, `[project]`,
|
||||
//! `[workflow]`, `[run]`, `[cli]`, `[server]`, and `[features]`. Value-language
|
||||
//! helpers live alongside the tree: durations, byte sizes, model references,
|
||||
//! and env interpolation.
|
||||
//! `[workflow]`, `[run]`, `[cli]`, and `[server]`. Value-language helpers live
|
||||
//! alongside the tree: durations, byte sizes, model references, and env
|
||||
//! interpolation.
|
||||
//!
|
||||
//! Stage 6.5b promoted these modules up out of the transitional
|
||||
//! `settings/v2/` subdirectory, so the `::v2::` path prefix no longer
|
||||
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
pub mod cli;
|
||||
pub mod duration;
|
||||
pub mod features;
|
||||
pub mod interp;
|
||||
pub mod model_ref;
|
||||
pub mod project;
|
||||
|
|
@ -26,7 +25,6 @@ pub use cli::{
|
|||
CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetSettings, CliUpdatesSettings,
|
||||
};
|
||||
pub use duration::{Duration, ParseDurationError};
|
||||
pub use features::FeaturesNamespace;
|
||||
pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved};
|
||||
pub use model_ref::{
|
||||
AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,6 @@ models/execute-query-response.ts
|
|||
models/failure-category.ts
|
||||
models/failure-detail.ts
|
||||
models/failure-reason.ts
|
||||
models/features-namespace.ts
|
||||
models/file-checkpoint.ts
|
||||
models/file-diff.ts
|
||||
models/fork-request.ts
|
||||
|
|
@ -402,7 +401,6 @@ models/system-cpu-resource-scope.ts
|
|||
models/system-cpu-resources.ts
|
||||
models/system-disk-resource-scope.ts
|
||||
models/system-disk-resources.ts
|
||||
models/system-features.ts
|
||||
models/system-info-response.ts
|
||||
models/system-memory-resource-scope.ts
|
||||
models/system-memory-resources.ts
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export interface AskFabro {
|
|||
}
|
||||
|
||||
export const AskFabroUnavailableReasonEnum = {
|
||||
FEATURE_DISABLED: 'feature_disabled',
|
||||
NO_SANDBOX: 'no_sandbox',
|
||||
SANDBOX_NOT_READY: 'sandbox_not_ready',
|
||||
LLM_UNCONFIGURED: 'llm_unconfigured'
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
/* 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 FeaturesNamespace {
|
||||
'session_sandboxes': boolean;
|
||||
}
|
||||
|
|
@ -82,7 +82,6 @@ export * from './execute-query-response-rows-inner-inner';
|
|||
export * from './failure-category';
|
||||
export * from './failure-detail';
|
||||
export * from './failure-reason';
|
||||
export * from './features-namespace';
|
||||
export * from './file-checkpoint';
|
||||
export * from './file-diff';
|
||||
export * from './fork-request';
|
||||
|
|
@ -378,7 +377,6 @@ export * from './system-cpu-resource-scope';
|
|||
export * from './system-cpu-resources';
|
||||
export * from './system-disk-resource-scope';
|
||||
export * from './system-disk-resources';
|
||||
export * from './system-features';
|
||||
export * from './system-info-response';
|
||||
export * from './system-memory-resource-scope';
|
||||
export * from './system-memory-resources';
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { FeaturesNamespace } from './features-namespace';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { ServerNamespace } from './server-namespace';
|
||||
|
|
@ -25,5 +22,4 @@ import type { ServerNamespace } from './server-namespace';
|
|||
*/
|
||||
export interface ServerSettings {
|
||||
'server': ServerNamespace;
|
||||
'features': FeaturesNamespace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Server-level capability flags.
|
||||
*/
|
||||
export interface SystemFeatures {
|
||||
/**
|
||||
* Whether session sandboxes are enabled.
|
||||
*/
|
||||
'session_sandboxes'?: boolean;
|
||||
}
|
||||
|
|
@ -13,9 +13,6 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemFeatures } from './system-features';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemRunCounts } from './system-run-counts';
|
||||
|
|
@ -69,5 +66,4 @@ export interface SystemInfoResponse {
|
|||
* Effective sandbox provider for launched runs.
|
||||
*/
|
||||
'sandbox_provider'?: string;
|
||||
'features'?: SystemFeatures;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue