feat(coord): inline tool-batch construct replaces approval dock

The pinned bottom approval-dock didn't scale: a 10-call spawn_workstream
fan-out filled the whole pane with a wall of repeated verdict chips,
and the call → approval → result lifecycle was split across three
disconnected surfaces (.msg.tool bubble + dock + .msg.tool result).

Replaces it with one chat-stream construct per dispatch turn that
pairs each tool call with its result and embeds the approval gate:

  - .coord-tool-batch--solo      single-call serial turn
  - .coord-tool-batch--parallel  ≥2 calls; rows share a left rail
                                 + per-row tick so they read as
                                 siblings of one assistant decision

Lifecycle: rows render with optional "judge evaluating…" placeholder,
upgrade in place when intent_verdict arrives, and on tool_result the
output lands paired under the originating row.  When the batch needs
approval, one Approve/Deny/Always action row renders inside the
construct (envelope-level — server semantics resolve siblings
together).  After approval_resolved the action row morphs into a
✓ approved / ✗ denied status pill that stays as a receipt.

Critical bug closed: when a page reload races a pending approval,
pre-scan tool_call_ids in history; turns whose call_ids have no
matching tool result are rendered pending (not resolved-approved).
The SSE approve_request replay then upgrades the existing batch
in place — drops --approved/--denied, adds --pending, swaps the
status pill for actions, and assigns activeBatch.  Without this
the operator was locked out of any approval pending at reload.

Defence-in-depth follow-ups from the same review:

  - approval_resolved falls back to a DOM lookup if activeBatch
    is null (cross-tab resolution where this tab never set it).
  - _appendVerdictLineTo dedupes via a row.dataset.verdictSig so
    SSE reconnect storms + repeat intent_verdict events don't
    tear down + rebuild an unchanged verdict line.
  - judgeVerdicts Map soft-capped at 500 entries (FIFO eviction)
    via _cacheJudgeVerdict.
  - toolRows entries hold {batch, row} only — the originating
    item payload is no longer pinned for the page lifetime.
  - _scheduleScroll coalesces messagesEl.scrollTop writes through
    requestAnimationFrame so history replay doesn't reflow once
    per appended message.
  - Rationale <details> now inserts immediately after the verdict
    line (was tail-appending, breaking ordering once a result
    landed below).
  - .coord-tool-batch--error wired: _appendResultToRow lifts a
    row's error onto the enclosing batch; _renderBatchRow does
    the same for policy-blocked rows at construction.
  - _buildStatusPill extracted; both _morphBatchResolved and the
    appendToolBatch resolved-replay branch route through it.

Removed: ~248 lines of dead .approval-dock CSS, the dock <aside>
element from index.html, and the dead helpers showApproval's
prior body, hideApproval, claimApprovalFocus,
claimApprovalFocusForVerdict, applyJudgeVerdictToRow,
applyJudgePendingToRow, ensureDctxAfterRow, removeRationale,
setApprovalButtonsDisabled, the appendToolCall single-row wrapper,
and window.coordApprove.  Five stale comment blocks referencing
the dock as if live also swept.

Children-tree's renderApprovalBlock is independent and untouched
(different surface, different .approval-block / .approval-pill
vocabulary).
This commit is contained in:
Patrick Buckley
2026-04-28 19:13:39 -07:00
parent 08c6eeb1e5
commit caa07e6118
3 changed files with 1150 additions and 678 deletions
@@ -6,8 +6,9 @@
Tokens (--panel, --hair, --ok, --warn, etc.) come from shared_static/ Tokens (--panel, --hair, --ok, --warn, etc.) come from shared_static/
base.css. Form controls + .btn / .ghost / .appbar primitives come base.css. Form controls + .btn / .ghost / .appbar primitives come
from shared_static/ui-base.css. This file holds the patterns specific from shared_static/ui-base.css. This file holds the patterns specific
to the coordinator view: the right-rail sidebar and the pinned approval to the coordinator view: the right-rail sidebar, the inline tool-batch
dock. construct (paired tool calls + approval flow + results), and the
drag-and-drop overlay.
========================================================================== */ ========================================================================== */
/* ========================================================================== /* ==========================================================================
@@ -40,235 +41,6 @@
color: var(--ink-4); color: var(--ink-4);
} }
/* ==========================================================================
Approval dock — bottom-pinned strip that appears when pending approvals
exist. Signature product pattern: a neutral dock (not a modal, not
inline) that surfaces the approval contract without hijacking focus.
Layout:
.approval-dock position: fixed bottom
.dhead 11px uppercase warn kicker + count on right
.dcall risk pill + function name + arg preview
.dctx context code snippets
.drow right-aligned action cluster + nav spacer
Actions (action cluster):
button.act neutral default ("dismiss" / "view")
button.act.primary ok-tinted green per the .ts-approval-btn--approve
convention in shared_static/chat.css. The original
Claude Design spec preferred amber; turnstone
deliberately broke from it to keep colour-family
parity with the Approve button's existing green.
1.5px border, --r-md squared.
button.act.always dashed border — "Always approve for this rule"
button.act.danger err-tinted red — "Deny"
Keyboard shortcuts (wired in coordinator.js):
Enter → primary approve
D → deny
⇧A → always approve
Focus policy: when the dock opens, move focus to button.act.primary so
keyboard users can confirm without hunting. Do NOT trap focus.
========================================================================== */
.approval-dock {
position: fixed;
left: 0;
right: 0;
bottom: 22px; /* clears the statusbar if one is present */
z-index: 20;
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 20px;
background: var(--panel);
border-top: 1px solid var(--hair);
box-shadow: 0 -6px 24px -12px rgba(21, 24, 27, 0.18);
}
.approval-dock::before {
content: "";
position: absolute;
top: -1px;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(
to right,
transparent,
color-mix(in srgb, var(--warn) 50%, transparent),
transparent
);
}
.approval-dock .dhead {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--warn);
}
.approval-dock .dhead::before {
content: "⚠";
font-size: 12px;
}
.approval-dock .dhead .dcount {
margin-left: auto;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
color: var(--ink-3);
}
/* Inline code-panel framing — the .dcall row reads as "the exact call you
are approving," so we frame it like a mini inspectable code line rather
than bare text on the dock surface. */
.approval-dock .dcall {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
padding: 6px 10px;
background: var(--panel);
border: 1px solid var(--hair);
border-radius: var(--r-sm);
}
.approval-dock .dcall .risk { flex-shrink: 0; }
.approval-dock .dcall .dfn {
font-family: var(--font-mono);
font-weight: 600;
color: var(--ink);
}
.approval-dock .dcall .dargs {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
font-size: 12px;
color: var(--ink-3);
}
.approval-dock .dctx {
display: flex;
gap: 14px;
flex-wrap: wrap;
font-size: 11px;
color: var(--ink-3);
}
.approval-dock .dctx code {
padding: 0 4px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--ink-2);
background: var(--panel-2);
border: 1px solid var(--hair);
border-radius: 3px;
}
.approval-dock .drow {
display: flex;
align-items: center;
gap: 6px;
}
.approval-dock .drow .spacer { flex: 1; }
.approval-dock .drow .nav {
padding: 4px 8px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--ink-3);
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
}
.approval-dock .drow .nav:hover {
background: var(--panel-2);
color: var(--ink);
}
/* Action buttons — 1.5px border, --r-md squared (NOT pill — these are
primary-action surfaces, not inline buttons). */
.approval-dock button.act {
padding: 7px 16px;
font: inherit;
font-size: 12px;
font-weight: 500;
color: var(--ink-2);
background: var(--panel);
border: 1.5px solid var(--hair-2);
border-radius: var(--r-md);
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.approval-dock button.act:hover {
color: var(--ink);
border-color: var(--ink-4);
}
.approval-dock button.act:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Approve and Always are siblings — same ok hue, differentiated by fill
(filled vs outlined) and border-style (solid vs dashed). Matches the
.ts-approval-btn--approve convention in shared_static/chat.css. Four
stacked non-colour cues for WCAG 1.4.1: fill state, border style,
label, position. */
.approval-dock button.act.primary {
background: color-mix(in srgb, var(--ok) 28%, var(--panel));
color: var(--ok-text);
border-color: color-mix(in srgb, var(--ok) 65%, var(--hair));
font-weight: 600;
}
.approval-dock button.act.primary:hover {
background: color-mix(in srgb, var(--ok) 40%, var(--panel));
color: var(--ink);
border-color: var(--ok);
}
.approval-dock button.act.always {
background: transparent;
border-style: dashed;
border-color: color-mix(in srgb, var(--ok) 65%, var(--hair));
color: var(--ok-text);
}
.approval-dock button.act.always:hover {
background: color-mix(in srgb, var(--ok) 15%, var(--panel));
color: var(--ink);
border-color: var(--ok);
}
.approval-dock button.act.danger {
color: var(--err);
border-color: color-mix(in srgb, var(--err) 42%, var(--hair));
}
.approval-dock button.act.danger:hover {
background: var(--err-soft);
color: var(--err);
border-color: var(--err);
}
/* ========================================================================== /* ==========================================================================
Drag-and-drop overlay — applied to #coord-main while the user is Drag-and-drop overlay — applied to #coord-main while the user is
dragging files from the OS over the chat pane. Composer wires this on dragging files from the OS over the chat pane. Composer wires this on
@@ -301,9 +73,411 @@
z-index: 10; z-index: 10;
} }
/* Match .btn .kbd (in shared_static/ui-base.css) — --ink-3 clears AA at /* ==========================================================================
10px, --ink-4 is borderline on light panels. */ Tool batch construct — pairs tool calls with their results and
.approval-dock button.act .kbd { embeds the approval flow. Replaces the bottom approval dock + the
duplicate .msg.tool bubbles for tool-call rendering.
One construct per dispatch turn:
- solo (1 call, serial): .coord-tool-batch--solo
- parallel (≥2 calls): .coord-tool-batch--parallel
rows share a left rail so the
operator reads them as siblings
of one assistant decision.
Sub-elements:
.coord-tool-batch-head label + count + tier glyph
.coord-tool-row per-call row (call line + verdict + result)
.coord-tool-row-call [idx] name args ellipsized
.coord-tool-row-verdict judge verdict chip + rationale teaser
.coord-tool-row-result paired tool_result <pre> under the row
.coord-tool-row-status per-row pill (auto-approved / error)
.coord-tool-actions approve / deny / always
.coord-tool-status resolved status pill (replaces actions)
States (modifiers on the batch):
.coord-tool-batch--pending approval gate visible
.coord-tool-batch--approved resolved approve
.coord-tool-batch--denied resolved deny — rows dimmed
.coord-tool-batch--auto all auto-approved, no gate ever shown
========================================================================== */
.coord-tool-batch {
margin: 4px 0;
background: var(--panel);
border: 1px solid var(--hair);
border-left: 3px solid var(--hair-2);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
overflow: hidden;
}
/* State left-stripe — neutral default; warn when gating; ok when
resolved-approved; err when denied or any row errored. Three stacked
non-colour cues for WCAG 1.4.1: pill text in the head, rail colour,
row dimming on deny. */
.coord-tool-batch--pending {
border-left-color: var(--warn);
}
.coord-tool-batch--approved {
border-left-color: color-mix(in srgb, var(--ok) 65%, var(--hair-2));
}
.coord-tool-batch--auto {
border-left-color: var(--hair-2);
}
.coord-tool-batch--denied,
.coord-tool-batch--error {
border-left-color: var(--err);
}
.coord-tool-batch--denied .coord-tool-row {
opacity: 0.6;
}
/* Header strip — small uppercase kicker + per-batch metadata. */
.coord-tool-batch-head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px;
padding: 6px 10px;
background: var(--panel-2);
border-bottom: 1px solid var(--hair);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-3);
}
.coord-tool-batch-kicker {
color: var(--ink-3);
}
.coord-tool-batch--pending .coord-tool-batch-kicker {
color: var(--warn);
}
.coord-tool-batch--approved .coord-tool-batch-kicker {
color: color-mix(in srgb, var(--ok) 70%, var(--ink-2));
}
.coord-tool-batch--denied .coord-tool-batch-kicker {
color: var(--err);
}
.coord-tool-batch-summary {
font-family: var(--font-mono);
font-weight: 500;
letter-spacing: 0;
text-transform: none;
color: var(--ink-2);
}
.coord-tool-batch-tier {
margin-left: auto;
font-family: var(--font-mono);
font-weight: 400;
letter-spacing: 0;
text-transform: none;
color: var(--ink-4);
font-size: 10px;
}
/* Row container. In parallel batches, rows are framed by a left rail
so they read as siblings of a single assistant decision; in solo
batches the rail is suppressed to keep visual weight low. */
.coord-tool-row {
position: relative;
padding: 8px 10px;
}
.coord-tool-row + .coord-tool-row {
border-top: 1px solid var(--hair);
}
.coord-tool-batch--parallel .coord-tool-row {
padding-left: 28px;
}
.coord-tool-batch--parallel .coord-tool-row::before {
/* Vertical rail tick — connects rows visually as a parallel group.
Stops 4px short of the row's top + bottom edges so consecutive
rows look continuous; the dot at the row's center marks the call. */
content: "";
position: absolute;
left: 14px;
top: 0;
bottom: 0;
width: 1px;
background: var(--hair-2);
}
.coord-tool-batch--parallel .coord-tool-row:first-of-type::before {
top: 4px;
}
.coord-tool-batch--parallel .coord-tool-row:last-of-type::before {
bottom: 4px;
}
.coord-tool-batch--parallel .coord-tool-row::after {
content: "";
position: absolute;
left: 11px;
top: 14px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--panel);
border: 1.5px solid var(--hair-2);
}
.coord-tool-batch--parallel.coord-tool-batch--approved .coord-tool-row::after {
border-color: color-mix(in srgb, var(--ok) 65%, var(--hair-2));
}
.coord-tool-batch--parallel.coord-tool-batch--denied .coord-tool-row::after,
.coord-tool-row.error::after {
border-color: var(--err);
}
/* Call line — index/N pill, monospace tool name, ellipsized args. */
.coord-tool-row-call {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.coord-tool-row-idx {
flex-shrink: 0;
padding: 1px 6px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--ink-3);
background: var(--panel-2);
border: 1px solid var(--hair);
border-radius: 3px;
}
.coord-tool-row-name {
flex-shrink: 0;
font-family: var(--font-mono);
font-weight: 600;
color: var(--ink);
}
.coord-tool-row.error .coord-tool-row-name {
color: var(--err);
}
.coord-tool-row-args {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
color: var(--ink-3);
}
/* Verdict line — judge chip + optional rationale teaser. */
.coord-tool-row-verdict {
margin-top: 4px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--ink-3);
}
.coord-tool-row-verdict code {
padding: 1px 6px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--ink-2);
background: var(--panel-2);
border: 1px solid var(--hair);
border-radius: 3px;
}
.coord-tool-row-verdict code.rec-approve {
color: color-mix(in srgb, var(--ok) 70%, var(--ink-2));
border-color: color-mix(in srgb, var(--ok) 38%, var(--hair));
background: color-mix(in srgb, var(--ok) 12%, var(--panel-2));
}
.coord-tool-row-verdict code.rec-review {
color: color-mix(in srgb, var(--warn) 70%, var(--ink-2));
border-color: color-mix(in srgb, var(--warn) 38%, var(--hair));
background: var(--warn-tint);
}
.coord-tool-row-verdict code.rec-deny {
color: color-mix(in srgb, var(--err) 70%, var(--ink-2));
border-color: color-mix(in srgb, var(--err) 38%, var(--hair));
background: color-mix(in srgb, var(--err) 12%, var(--panel-2));
}
.coord-tool-row-verdict code.judging {
display: inline-flex;
align-items: center;
gap: 5px;
color: var(--ink-3);
}
.coord-tool-row-verdict code.judging .spin {
width: 8px;
height: 8px;
border-radius: 50%;
border: 1.5px solid var(--accent);
border-top-color: transparent;
animation: ts-spin 0.9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.coord-tool-row-verdict code.judging .spin { animation: none; }
}
/* Rationale disclosure — collapsible block under a row. Renders the
judge's reasoning prose; `details` element so a click toggles without
stealing focus. */
.coord-tool-row-rationale {
margin-top: 4px;
font-size: 11px;
color: var(--ink-3);
}
.coord-tool-row-rationale > summary {
cursor: pointer;
color: var(--accent);
font-size: 11px;
list-style: none;
user-select: none;
}
.coord-tool-row-rationale > summary::before {
content: "▸ ";
display: inline-block;
margin-right: 2px;
transition: transform 120ms ease;
}
.coord-tool-row-rationale[open] > summary::before {
transform: rotate(90deg);
}
.coord-tool-row-rationale-body {
margin: 4px 0 0 14px;
padding: 6px 10px;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.5;
color: var(--ink-3);
background: var(--panel-2);
border-left: 2px solid var(--hair);
border-radius: 0 3px 3px 0;
white-space: pre-wrap;
}
/* Per-row status pill (auto-approved, error). */
.coord-tool-row-status {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 6px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
border-radius: 3px;
border: 1px solid var(--hair);
color: var(--ink-3);
}
.coord-tool-row-status--auto {
color: color-mix(in srgb, var(--ok) 70%, var(--ink-2));
border-color: color-mix(in srgb, var(--ok) 38%, var(--hair));
background: color-mix(in srgb, var(--ok) 12%, var(--panel-2));
}
.coord-tool-row-status--error {
color: var(--err);
border-color: color-mix(in srgb, var(--err) 38%, var(--hair));
background: color-mix(in srgb, var(--err) 12%, var(--panel-2));
}
/* Tool result block — paired under its row, mono pre-block with
collapse-when-tall affordance. Maxes at 240px and reveals an
"expand" affordance like .tool-output.collapsed in the interactive UI. */
.coord-tool-row-result {
margin-top: 6px;
padding: 6px 10px;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.5;
color: var(--ink-2);
background: var(--panel-2);
border-left: 2px solid var(--hair);
border-radius: 0 3px 3px 0;
white-space: pre-wrap;
word-break: break-word;
max-height: 240px;
overflow: auto;
}
.coord-tool-row.error .coord-tool-row-result {
border-left-color: var(--err);
color: color-mix(in srgb, var(--err) 75%, var(--ink-2));
}
.coord-tool-row-result-lead {
display: inline-block;
margin-right: 4px;
color: var(--ink-4);
font-weight: 600;
}
/* Action row — Approve / Deny / Always. Renders inside a pending
tool-batch construct as the operator's gate for the dispatch. The
.act button vocabulary (primary/always/danger) is local to this
surface; the children-tree's .ch-row .approval-actions reuses the
same colour/border treatment in compact .sm sizing. */
.coord-tool-actions {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
background: var(--panel-2);
border-top: 1px solid var(--hair);
}
.coord-tool-actions .spacer { flex: 1; }
.coord-tool-actions button.act {
padding: 6px 14px;
font: inherit;
font-size: 12px;
font-weight: 500;
color: var(--ink-2);
background: var(--panel);
border: 1.5px solid var(--hair-2);
border-radius: var(--r-md);
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.coord-tool-actions button.act:hover {
color: var(--ink);
border-color: var(--ink-4);
}
.coord-tool-actions button.act:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.coord-tool-actions button.act:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.coord-tool-actions button.act.primary {
background: color-mix(in srgb, var(--ok) 28%, var(--panel));
color: var(--ok-text);
border-color: color-mix(in srgb, var(--ok) 65%, var(--hair));
font-weight: 600;
}
.coord-tool-actions button.act.primary:hover {
background: color-mix(in srgb, var(--ok) 40%, var(--panel));
color: var(--ink);
border-color: var(--ok);
}
.coord-tool-actions button.act.always {
background: transparent;
border-style: dashed;
border-color: color-mix(in srgb, var(--ok) 65%, var(--hair));
color: var(--ok-text);
}
.coord-tool-actions button.act.always:hover {
background: color-mix(in srgb, var(--ok) 15%, var(--panel));
color: var(--ink);
border-color: var(--ok);
}
.coord-tool-actions button.act.danger {
color: var(--err);
border-color: color-mix(in srgb, var(--err) 42%, var(--hair));
}
.coord-tool-actions button.act.danger:hover {
background: var(--err-soft);
color: var(--err);
border-color: var(--err);
}
.coord-tool-actions button.act .kbd {
margin-left: 6px; margin-left: 6px;
padding: 0 3px; padding: 0 3px;
font-family: var(--font-mono); font-family: var(--font-mono);
@@ -312,10 +486,44 @@
border: 1px solid var(--hair); border: 1px solid var(--hair);
border-radius: 2px; border-radius: 2px;
} }
.coord-tool-actions button.act.primary .kbd {
/* Tinted keycap on the primary Approve button — uses the parent's
--ok hue so the keycap reads as part of the green action surface. */
.approval-dock button.act.primary .kbd {
color: color-mix(in srgb, var(--ok) 70%, var(--ink-3)); color: color-mix(in srgb, var(--ok) 70%, var(--ink-3));
border-color: color-mix(in srgb, var(--ok) 40%, var(--hair)); border-color: color-mix(in srgb, var(--ok) 40%, var(--hair));
} }
/* Resolved status pill — replaces the action row after approve/deny. */
.coord-tool-status {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: var(--panel-2);
border-top: 1px solid var(--hair);
font-size: 11px;
font-weight: 500;
color: var(--ink-3);
}
.coord-tool-status--approved {
color: color-mix(in srgb, var(--ok) 70%, var(--ink-2));
}
.coord-tool-status--denied,
.coord-tool-status--error {
color: var(--err);
}
.coord-tool-status-feedback {
font-family: var(--font-mono);
font-size: 11px;
color: var(--ink-3);
}
/* Mobile (<700px) — keep action targets ≥44px for WCAG 2.5.5. */
@media (max-width: 700px) {
.coord-tool-actions {
flex-wrap: wrap;
}
.coord-tool-actions button.act {
flex: 1 1 30%;
min-height: 44px;
font-size: 13px;
}
}
File diff suppressed because it is too large Load Diff
+23 -137
View File
@@ -14,14 +14,15 @@
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/coordinator/coordinator.css"> <link rel="stylesheet" href="/static/coordinator/coordinator.css">
<style> <style>
/* Coordinator-specific layout glue. Messages, header, approval dock, /* Coordinator-specific layout glue. Messages, header, and sidebar
and sidebar chrome live in shared_static/{chat,ui-base}.css and chrome live in shared_static/{chat,ui-base}.css and
console/static/style.css; what remains here is the page-level flex console/static/style.css; the inline tool-batch construct lives
wiring (chat pane + right sidebar), tree-view row metadata (indent, in coordinator.css. What remains here is the page-level flex
state dots, child highlight), and the <700px responsive accordion. wiring (chat pane + right sidebar), tree-view row metadata
Rules that target .msg / .appbar / .sidebar / .approval-dock are (indent, state dots, child highlight), and the <700px responsive
intentionally absent — those primitives ship from the shared sheets accordion. Rules that target .msg / .appbar / .sidebar are
and we don't restyle them here. */ intentionally absent — those primitives ship from the shared
sheets and we don't restyle them here. */
body { display: flex; flex-direction: column; height: 100vh; margin: 0; } body { display: flex; flex-direction: column; height: 100vh; margin: 0; }
/* Main layout — chat pane (2fr) + sidebar (1fr) with shared /* Main layout — chat pane (2fr) + sidebar (1fr) with shared
@@ -219,10 +220,10 @@
color: var(--ink-3); color: var(--ink-3);
} }
/* Recommendation chip inside the disclosure footer — same 12/38/70% /* Recommendation chip inside the disclosure footer — same 12/38/70%
colour-mix scheme as the dock chips at `#coord-approval-bar colour-mix scheme as the inline tool-batch verdict chips
.dctx code.rec-*`, scoped to the row's disclosure so the inline (.coord-tool-row-verdict code.rec-*), scoped here to the row's
chip is actually styled (the dock-scoped rules don't reach this disclosure since this children-tree surface uses its own
surface). */ .approval-disclosure container. */
.ch-row .approval-disclosure code.rec-approve, .ch-row .approval-disclosure code.rec-approve,
.ch-row .approval-disclosure code.rec-review, .ch-row .approval-disclosure code.rec-review,
.ch-row .approval-disclosure code.rec-deny { .ch-row .approval-disclosure code.rec-deny {
@@ -311,13 +312,13 @@
justify-content: flex-end; justify-content: flex-end;
margin-top: 2px; margin-top: 2px;
} }
/* Inline .act buttons — duplicates the colour/border treatment from /* Inline .act buttons for the children-tree approval block —
shared_static/design/patterns/approval-dock.css :162-225 because compact (.sm) variant of the colour/border treatment used by the
the dock rules are scoped to `.approval-dock button.act` and the coord chat's tool-batch action row (coordinator.css
children-tree row isn't inside a dock. Compact sizing applied .coord-tool-actions button.act). Duplicated locally because the
via .sm. Keeping the duplication local-scoped means a future children-tree row sits in the right-rail sidebar with its own
hoist of the dock rules to a global `.act` primitive could parent class; if we ever lift `.act` to a shared primitive these
drop these without affecting the dock surface. */ local overrides can drop. */
.ch-row .approval-actions .act { .ch-row .approval-actions .act {
padding: 3px 10px; padding: 3px 10px;
font: inherit; font: inherit;
@@ -436,95 +437,11 @@
.ch-row.highlight { transition: none; } .ch-row.highlight { transition: none; }
} }
/* Override the .approval-dock pattern's viewport-pinned positioning. /* @keyframes ts-spin — drives the .coord-tool-row-verdict
The pattern defaults to position: fixed bottom:22px (designed for code.judging spinner. Defined here because this page doesn't
the fleet dashboard overlay case); in the coordinator chat we need link feed.css (no .feed-item usage). */
it inline above the composer so it doesn't cover the input area.
Dock sits as the second flex child inside #coord-main between
messages and composer, with a hair top border as the
separator. */
#coord-approval-bar.approval-dock {
position: static;
bottom: auto;
left: auto;
right: auto;
z-index: auto;
box-shadow: none;
flex: 0 0 auto;
}
/* Keep the warm top-stripe cue; just make it hug the top edge of the
in-flow dock instead of the top of a fixed viewport bar. */
#coord-approval-bar.approval-dock::before {
top: -1px;
}
/* Hide the dock when no approval is pending. [hidden] toggle; the
approval-dock pattern defines display: flex so we need the
!important override to win specificity. */
.approval-dock[hidden] { display: none !important; }
/* Judge verdict chips — colour-code by recommendation so the
reviewer can triage at a glance without reading the chip text.
approve=ok, review=warn, deny=err. Uses the same 12/38/70% mix
scheme as the primitive k-badge tokens. */
#coord-approval-bar .dctx code.rec-approve {
color: color-mix(in srgb, var(--ok) 70%, var(--ink-2));
border-color: color-mix(in srgb, var(--ok) 38%, var(--hair));
background: color-mix(in srgb, var(--ok) 12%, var(--panel-2));
}
#coord-approval-bar .dctx code.rec-review {
color: color-mix(in srgb, var(--warn) 70%, var(--ink-2));
border-color: color-mix(in srgb, var(--warn) 38%, var(--hair));
background: var(--warn-tint);
}
#coord-approval-bar .dctx code.rec-deny {
color: color-mix(in srgb, var(--err) 70%, var(--ink-2));
border-color: color-mix(in srgb, var(--err) 38%, var(--hair));
background: color-mix(in srgb, var(--err) 12%, var(--panel-2));
}
/* Local @keyframes ts-spin — primitives/feed.css owns the canonical
definition but this page doesn't link feed.css (no .feed-item
usage). Defined here so the .judging .spin chip below animates. */
@keyframes ts-spin { to { transform: rotate(360deg); } } @keyframes ts-spin { to { transform: rotate(360deg); } }
/* "judge evaluating…" spinner chip — shown while a .dcall is
pending a verdict. */
#coord-approval-bar .dctx code.judging {
display: inline-flex;
align-items: center;
gap: 5px;
color: var(--ink-3);
}
#coord-approval-bar .dctx code.judging .spin {
width: 8px;
height: 8px;
border-radius: 50%;
border: 1.5px solid var(--accent);
border-top-color: transparent;
animation: ts-spin 0.9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
#coord-approval-bar .dctx code.judging .spin {
animation: none;
}
}
/* Judge rationale — the judge's reasoning text, rendered below the
dctx chips as a block quote. Full text wraps; no truncation —
justification is the whole point of showing this. */
#coord-approval-bar .drationale {
margin-top: 4px;
padding: 6px 10px;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.5;
color: var(--ink-3);
background: var(--panel-2);
border-left: 2px solid var(--hair);
border-radius: 0 3px 3px 0;
white-space: pre-wrap;
}
/* Sidebar mobile toggle (desktop hides; mobile shows via media query /* Sidebar mobile toggle (desktop hides; mobile shows via media query
below). */ below). */
#coord-sidebar-toggle { #coord-sidebar-toggle {
@@ -597,37 +514,6 @@
don't re-announce partial content on every token. --> don't re-announce partial content on every token. -->
<div id="coord-messages" role="log" aria-live="polite"></div> <div id="coord-messages" role="log" aria-live="polite"></div>
<!-- Approval dock — overridden to inline positioning (see the
position: static override in the style block above). Sits
between the message log and the composer so it doesn't occlude
the user input. role="region" (not alertdialog) because we do
not trap focus; buttons are reachable in normal tab order.
aria-live="assertive" preserves announce-on-queue behaviour. -->
<aside id="coord-approval-bar"
class="approval-dock"
role="region"
aria-label="Approval required"
aria-live="assertive"
hidden>
<div id="coord-approval-label" class="dhead">
Approval required
<span id="coord-approval-count" class="dcount"></span>
</div>
<div id="coord-approval-tools"></div>
<div class="drow">
<div class="spacer"></div>
<button id="coord-deny-btn" class="act danger" type="button" onclick="coordApprove(false, false)">
Deny<span class="kbd">D</span>
</button>
<button id="coord-approve-always-btn" class="act always" type="button" onclick="coordApprove(true, true)">
Always<span class="kbd">⇧A</span>
</button>
<button id="coord-approve-btn" class="act primary" type="button" onclick="coordApprove(true, false)">
Approve<span class="kbd"></span>
</button>
</div>
</aside>
<!-- Per-coordinator status bar — pinned above the composer. <!-- Per-coordinator status bar — pinned above the composer.
Mirrors the interactive pane's `.ws-status-bar`: model alias, Mirrors the interactive pane's `.ws-status-bar`: model alias,
token / context-window usage with effort suffix, tool calls token / context-window usage with effort suffix, tool calls