mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-13 23:42:25 -06:00
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:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user