feat(lanes): Antigravity lanes, release exited panes, fresh Codex starts

- antigravity: a third lane tool running Antigravity's `agy` CLI. `-i`
  carries the first prompt, `--continue` resumes; `agy` has no model,
  effort or permission flag, and `--dangerously-skip-permissions` is never
  emitted. No pipeline, no agents install, and no AI Router (native only,
  else EUNSUPPORTED). Add lane gains an Antigravity choice and the binary
  probe maps it to `agy`.
- UI: the lane card no longer renders a frame holding only a "Codex" chip,
  and the prompt box names the lane's CLI ("Ask Codex anything…"), via
  LANE_TOOL_LABEL.
- runs: a pane that exits is released on a 10 s server beat
  (healAllRunIds, DASHBOARD_RUN_HEAL_MS). Codex and Antigravity fire no
  hook, so their lanes stayed "running" until something re-read them.
- codex: Start no longer sends resumeLast — only "Resume last session"
  continues the previous thread.
This commit is contained in:
2026-09-28 10:22:07 +07:00
parent 952e8584f0
commit 9d096b61f7
31 changed files with 257 additions and 93 deletions
+12 -10
View File
@@ -395,8 +395,8 @@ export function Workspace() {
}, [lanes, searchParams, setSearchParams]);
useEffect(() => {
Promise.all([api.run.binary("claude"), api.run.binary("codex")])
.then(([claude, codex]) => setBinaryStatus({ claude, codex }))
Promise.all([api.run.binary("claude"), api.run.binary("codex"), api.run.binary("antigravity")])
.then(([claude, codex, antigravity]) => setBinaryStatus({ claude, codex, antigravity }))
// Fetch failure (server unreachable, proxy misrouted, etc.) isn't proof
// `claude` or `codex` is missing from PATH — leave the probe unresolved rather than
// showing a misleading "binary missing" banner for an unrelated fault.
@@ -523,7 +523,8 @@ export function Workspace() {
}, [refreshList]);
const currentLane = selectedLaneId !== null ? lanes.find((l) => l.id === selectedLaneId) : null;
const isCodexLane = currentLane?.tool === "codex";
// Only claude lanes carry a pipeline (codex and antigravity emit no hooks).
const noPipeline = !!currentLane && currentLane.tool !== "claude";
// Feature list follows the selected lane, resets the viewer on lane switch.
useEffect(() => {
@@ -816,16 +817,16 @@ export function Workspace() {
<Pencil className="h-3 w-3 shrink-0 text-fg-muted opacity-0 transition-opacity group-hover:opacity-100" />
</button>
)}
{/* Codex lanes carry no pipeline: no time-at-stage, no pipeline
or feature picker, no map. */}
{!isCodexLane && (
{/* Non-claude lanes carry no pipeline: no time-at-stage, no
pipeline or feature picker, no map. */}
{!noPipeline && (
<span className="shrink-0 tabular-nums text-xs text-fg-muted">
{since(currentLane.stage_seconds)}
</span>
)}
{showInfo && (
<>
{!isCodexLane && (
{!noPipeline && (
<select
data-testid="pipeline-picker"
aria-label={tLanes("pipelinePicker.label")}
@@ -855,13 +856,14 @@ export function Workspace() {
))}
</select>
)}
<LaneRouterRow lane={currentLane} />
{/* `agy` has no endpoint to point at a router. */}
{currentLane.tool !== "antigravity" && <LaneRouterRow lane={currentLane} />}
{/* No stage chip and no "auto: <stage>" chip here: the
PipelineMap right below this header already draws both
— the declared node as the current ring, the detected
one as its amber badge — so the chips were a second,
smaller copy of the same two facts. */}
{!isCodexLane && features.length > 0 && (
{!noPipeline && features.length > 0 && (
<select
data-testid="feature-picker"
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs"
@@ -921,7 +923,7 @@ export function Workspace() {
onSelectLane={setSelectedLaneId}
/>
</div>
{!isCodexLane && (
{!noPipeline && (
<div className="mb-3">
<PipelineMap
nodes={