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/
base.css. Form controls + .btn / .ghost / .appbar primitives come
from shared_static/ui-base.css. This file holds the patterns specific
to the coordinator view: the right-rail sidebar and the pinned approval
dock.
to the coordinator view: the right-rail sidebar, the inline tool-batch
construct (paired tool calls + approval flow + results), and the
drag-and-drop overlay.
========================================================================== */
/* ==========================================================================
@@ -40,235 +41,6 @@
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
dragging files from the OS over the chat pane. Composer wires this on
@@ -301,9 +73,411 @@
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. */
.approval-dock button.act .kbd {
/* ==========================================================================
Tool batch construct — pairs tool calls with their results and
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;
padding: 0 3px;
font-family: var(--font-mono);
@@ -312,10 +486,44 @@
border: 1px solid var(--hair);
border-radius: 2px;
}
/* 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 {
.coord-tool-actions button.act.primary .kbd {
color: color-mix(in srgb, var(--ok) 70%, var(--ink-3));
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/coordinator/coordinator.css">
<style>
/* Coordinator-specific layout glue. Messages, header, approval dock,
and sidebar chrome live in shared_static/{chat,ui-base}.css and
console/static/style.css; what remains here is the page-level flex
wiring (chat pane + right sidebar), tree-view row metadata (indent,
state dots, child highlight), and the <700px responsive accordion.
Rules that target .msg / .appbar / .sidebar / .approval-dock are
intentionally absent — those primitives ship from the shared sheets
and we don't restyle them here. */
/* Coordinator-specific layout glue. Messages, header, and sidebar
chrome live in shared_static/{chat,ui-base}.css and
console/static/style.css; the inline tool-batch construct lives
in coordinator.css. What remains here is the page-level flex
wiring (chat pane + right sidebar), tree-view row metadata
(indent, state dots, child highlight), and the <700px responsive
accordion. Rules that target .msg / .appbar / .sidebar are
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; }
/* Main layout — chat pane (2fr) + sidebar (1fr) with shared
@@ -219,10 +220,10 @@
color: var(--ink-3);
}
/* Recommendation chip inside the disclosure footer — same 12/38/70%
colour-mix scheme as the dock chips at `#coord-approval-bar
.dctx code.rec-*`, scoped to the row's disclosure so the inline
chip is actually styled (the dock-scoped rules don't reach this
surface). */
colour-mix scheme as the inline tool-batch verdict chips
(.coord-tool-row-verdict code.rec-*), scoped here to the row's
disclosure since this children-tree surface uses its own
.approval-disclosure container. */
.ch-row .approval-disclosure code.rec-approve,
.ch-row .approval-disclosure code.rec-review,
.ch-row .approval-disclosure code.rec-deny {
@@ -311,13 +312,13 @@
justify-content: flex-end;
margin-top: 2px;
}
/* Inline .act buttons — duplicates the colour/border treatment from
shared_static/design/patterns/approval-dock.css :162-225 because
the dock rules are scoped to `.approval-dock button.act` and the
children-tree row isn't inside a dock. Compact sizing applied
via .sm. Keeping the duplication local-scoped means a future
hoist of the dock rules to a global `.act` primitive could
drop these without affecting the dock surface. */
/* Inline .act buttons for the children-tree approval block —
compact (.sm) variant of the colour/border treatment used by the
coord chat's tool-batch action row (coordinator.css
.coord-tool-actions button.act). Duplicated locally because the
children-tree row sits in the right-rail sidebar with its own
parent class; if we ever lift `.act` to a shared primitive these
local overrides can drop. */
.ch-row .approval-actions .act {
padding: 3px 10px;
font: inherit;
@@ -436,95 +437,11 @@
.ch-row.highlight { transition: none; }
}
/* Override the .approval-dock pattern's viewport-pinned positioning.
The pattern defaults to position: fixed bottom:22px (designed for
the fleet dashboard overlay case); in the coordinator chat we need
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 — drives the .coord-tool-row-verdict
code.judging spinner. Defined here because this page doesn't
link feed.css (no .feed-item usage). */
@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
below). */
#coord-sidebar-toggle {
@@ -597,37 +514,6 @@
don't re-announce partial content on every token. -->
<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.
Mirrors the interactive pane's `.ws-status-bar`: model alias,
token / context-window usage with effort suffix, tool calls