mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
docs(runner): approve phase 4b ux interaction map and components
UX gate PAP-16832: live-console interaction map for all spec 29.1 surfaces, shadcn/AI Elements adapt/reject decision record with a zero-new-runtime-dependency rule, keyboard/accessibility acceptance criteria, token additions, annotated 1440x900 and 390x844 mockups, and OKF journal/index updates. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4b5bbd1729
commit
5870a1a432
9 files changed
+973
No files matched your search
BIN
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,374 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Phase 4b Live Console — annotated mockup (PAP-16832)</title>
|
||||
<style>
|
||||
/* Token layer: values mirror devtools/browser/src/styles.css, plus the
|
||||
Phase 4b token additions proposed in the component decision record §5. */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--background: #f7f7f5; --foreground: #191917;
|
||||
--card: #ffffff; --card-foreground: #191917;
|
||||
--muted: #efefeb; --muted-foreground: #66665f;
|
||||
--border: #deded7;
|
||||
--primary: #242420; --primary-foreground: #ffffff;
|
||||
--success: #17633a; --success-surface: #e9f5ed;
|
||||
--warning: #8a5a10; --warning-surface: #fff4dc;
|
||||
--danger: #a02f2f; --danger-surface: #fceaea;
|
||||
--accent: #2f5fa8; --accent-surface: #e9f0fa;
|
||||
--surface-raised: #fcfcfa;
|
||||
--ring: #66665f;
|
||||
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
--fs-xs: 0.75rem; --fs-sm: 0.875rem; --fs-md: 1rem; --fs-lg: 1.25rem;
|
||||
--w-med: 500; --w-semi: 600;
|
||||
--s-1: 0.25rem; --s-2: 0.5rem; --s-3: 0.75rem; --s-4: 1rem; --s-5: 1.25rem; --s-6: 1.5rem; --s-8: 2rem;
|
||||
--r-sm: 0.375rem; --r-md: 0.625rem;
|
||||
--shadow-sm: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.05);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; background:var(--background); color:var(--foreground);
|
||||
font:var(--fs-sm)/1.5 var(--font-sans); }
|
||||
code, .mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
|
||||
h1,h2,h3,p { margin:0; }
|
||||
|
||||
.console { display:grid; grid-template-columns: 17rem minmax(0,1fr) 24rem;
|
||||
gap:var(--s-4); width:min(100%,90rem); margin-inline:auto; padding:var(--s-4); }
|
||||
header.top { grid-column:1/-1; display:flex; align-items:center; gap:var(--s-3);
|
||||
padding:var(--s-2) var(--s-1); }
|
||||
.eyebrow { font-size:var(--fs-xs); letter-spacing:.08em; text-transform:uppercase;
|
||||
color:var(--muted-foreground); font-weight:var(--w-semi); }
|
||||
header.top h1 { font-size:var(--fs-lg); font-weight:var(--w-semi); margin-right:auto; }
|
||||
|
||||
.panel { background:var(--card); border:1px solid var(--border); border-radius:var(--r-md);
|
||||
box-shadow:var(--shadow-sm); }
|
||||
.panel + .panel { margin-top:var(--s-4); }
|
||||
.panel-h { display:flex; align-items:center; gap:var(--s-2); padding:var(--s-3) var(--s-4);
|
||||
border-bottom:1px solid var(--border); font-weight:var(--w-semi); font-size:var(--fs-sm); }
|
||||
.panel-b { padding:var(--s-3) var(--s-4); }
|
||||
|
||||
.badge { display:inline-flex; align-items:center; gap:var(--s-1); border-radius:999px;
|
||||
padding:0 var(--s-2); font-size:var(--fs-xs); font-weight:var(--w-med);
|
||||
border:1px solid var(--border); background:var(--muted); color:var(--muted-foreground); height:1.25rem;}
|
||||
.badge.ok { background:var(--success-surface); color:var(--success); border-color:transparent;}
|
||||
.badge.warn { background:var(--warning-surface); color:var(--warning); border-color:transparent;}
|
||||
.badge.err { background:var(--danger-surface); color:var(--danger); border-color:transparent;}
|
||||
.badge.info { background:var(--accent-surface); color:var(--accent); border-color:transparent;}
|
||||
.dot { width:.5rem; height:.5rem; border-radius:999px; display:inline-block; }
|
||||
.dot.ok{background:var(--success);} .dot.run{background:var(--accent);}
|
||||
.dot.err{background:var(--danger);} .dot.mut{background:var(--muted-foreground);}
|
||||
|
||||
.btn { display:inline-flex; align-items:center; justify-content:center; gap:var(--s-1);
|
||||
height:2.25rem; padding:0 var(--s-4); border-radius:var(--r-sm); font-weight:var(--w-med);
|
||||
font-size:var(--fs-sm); border:1px solid var(--border); background:var(--card); cursor:default;}
|
||||
.btn.primary { background:var(--primary); color:var(--primary-foreground); border-color:var(--primary);}
|
||||
.btn.danger { background:var(--danger-surface); color:var(--danger); border-color:transparent;}
|
||||
.btn.ghost { border-color:transparent; background:transparent; color:var(--muted-foreground);}
|
||||
.btn.sm { height:1.75rem; padding:0 var(--s-3); font-size:var(--fs-xs);}
|
||||
.btn[aria-disabled="true"] { opacity:.5; }
|
||||
|
||||
/* left rail */
|
||||
.manifest { display:flex; flex-direction:column; gap:var(--s-1); padding:var(--s-2) var(--s-3);
|
||||
border-radius:var(--r-sm); border:1px solid transparent; }
|
||||
.manifest.active { border-color:var(--accent); background:var(--accent-surface); }
|
||||
.manifest .t { font-weight:var(--w-med); display:flex; justify-content:space-between; align-items:center;}
|
||||
.manifest .d { color:var(--muted-foreground); font-size:var(--fs-xs); }
|
||||
.kv { display:flex; justify-content:space-between; gap:var(--s-2); padding:var(--s-1) 0;
|
||||
font-size:var(--fs-xs); }
|
||||
.kv .k { color:var(--muted-foreground); }
|
||||
.goal { border-left:3px solid var(--accent); background:var(--accent-surface);
|
||||
border-radius:var(--r-sm); padding:var(--s-2) var(--s-3); font-size:var(--fs-xs); }
|
||||
.tree { list-style:none; margin:0; padding:0; font-size:var(--fs-xs); }
|
||||
.tree li { display:flex; align-items:center; gap:var(--s-2); padding:var(--s-1) var(--s-2);
|
||||
border-radius:var(--r-sm);}
|
||||
.tree li.cur { background:var(--muted); font-weight:var(--w-med); }
|
||||
.tree li.child { padding-left:var(--s-6); }
|
||||
|
||||
/* transcript */
|
||||
.chat { display:flex; flex-direction:column; min-height:34rem; }
|
||||
.stream { flex:1; display:flex; flex-direction:column; gap:var(--s-3); padding:var(--s-4); }
|
||||
.msg { max-width:85%; }
|
||||
.msg.user { align-self:flex-end; background:var(--primary); color:var(--primary-foreground);
|
||||
border-radius:var(--r-md); padding:var(--s-2) var(--s-3); }
|
||||
.msg.assistant { align-self:stretch; max-width:100%; }
|
||||
.item-label { font-size:var(--fs-xs); color:var(--muted-foreground); margin-bottom:var(--s-1);
|
||||
display:flex; gap:var(--s-2); align-items:center;}
|
||||
details.fold { border:1px solid var(--border); border-radius:var(--r-sm);
|
||||
background:var(--surface-raised); font-size:var(--fs-xs); }
|
||||
details.fold summary { list-style:none; cursor:default; padding:var(--s-2) var(--s-3);
|
||||
display:flex; align-items:center; gap:var(--s-2); color:var(--muted-foreground);}
|
||||
details.fold pre { margin:0; padding:var(--s-2) var(--s-3); border-top:1px solid var(--border);
|
||||
overflow:auto; font-size:var(--fs-xs); }
|
||||
.chip { display:inline-flex; align-items:center; gap:var(--s-2); border-radius:999px;
|
||||
padding:var(--s-1) var(--s-3); font-size:var(--fs-xs); border:1px dashed var(--border);
|
||||
color:var(--muted-foreground); align-self:flex-end; }
|
||||
.chip.ack { border-style:solid; border-color:var(--success); color:var(--success);
|
||||
background:var(--success-surface);}
|
||||
.chip.rej { border-style:solid; border-color:var(--danger); color:var(--danger);
|
||||
background:var(--danger-surface);}
|
||||
.divider { display:flex; align-items:center; gap:var(--s-2); color:var(--warning);
|
||||
font-size:var(--fs-xs); }
|
||||
.divider::before,.divider::after { content:""; flex:1; border-top:1px dashed var(--warning); }
|
||||
|
||||
/* request card */
|
||||
.req { border:1px solid var(--border); border-left:3px solid var(--accent);
|
||||
border-radius:var(--r-sm); background:var(--card); }
|
||||
.req .rh { display:flex; gap:var(--s-2); align-items:center; padding:var(--s-2) var(--s-3);
|
||||
font-weight:var(--w-med); font-size:var(--fs-sm);}
|
||||
.req .rid { margin-left:auto; color:var(--muted-foreground); }
|
||||
.req pre { margin:0 var(--s-3); padding:var(--s-2) var(--s-3); background:var(--muted);
|
||||
border-radius:var(--r-sm); overflow:auto;}
|
||||
.req .ra { display:flex; gap:var(--s-2); padding:var(--s-3); flex-wrap:wrap;}
|
||||
.req.resolved { border-left-color:var(--success); }
|
||||
.req.resolved .rh { color:var(--muted-foreground); font-weight:var(--w-med); }
|
||||
|
||||
.streaming-cursor { display:inline-block; width:.5em; height:1em; background:var(--accent);
|
||||
vertical-align:text-bottom; }
|
||||
.jump { position:sticky; bottom:5.5rem; align-self:center; }
|
||||
|
||||
/* composer */
|
||||
.composer { border-top:1px solid var(--border); padding:var(--s-3) var(--s-4); display:flex;
|
||||
flex-direction:column; gap:var(--s-2); background:var(--surface-raised);
|
||||
border-radius:0 0 var(--r-md) var(--r-md); }
|
||||
.composer .row { display:flex; gap:var(--s-3); align-items:flex-end; }
|
||||
.composer textarea { flex:1; min-height:2.5rem; resize:none; border:1px solid var(--border);
|
||||
border-radius:var(--r-sm); padding:var(--s-2) var(--s-3); font:inherit; background:var(--card);}
|
||||
.status-line { font-size:var(--fs-xs); color:var(--muted-foreground); display:flex; gap:var(--s-2);
|
||||
align-items:center;}
|
||||
.banner { display:flex; gap:var(--s-2); align-items:center; padding:var(--s-2) var(--s-4);
|
||||
font-size:var(--fs-xs); border-top:1px solid var(--border); }
|
||||
.banner.pending { background:var(--warning-surface); color:var(--warning); }
|
||||
kbd { font-family:var(--font-mono); font-size:var(--fs-xs); background:var(--muted);
|
||||
border:1px solid var(--border); border-bottom-width:2px; border-radius:var(--r-sm);
|
||||
padding:0 var(--s-1); }
|
||||
|
||||
/* inspector */
|
||||
.tabs { display:flex; gap:var(--s-1); padding:var(--s-2) var(--s-3) 0; border-bottom:1px solid var(--border);}
|
||||
.tab { padding:var(--s-1) var(--s-3); border-radius:var(--r-sm) var(--r-sm) 0 0; font-size:var(--fs-xs);
|
||||
color:var(--muted-foreground);}
|
||||
.tab.on { background:var(--muted); color:var(--foreground); font-weight:var(--w-med); }
|
||||
.evt { display:grid; grid-template-columns:2.5rem 1fr auto; gap:var(--s-2); padding:var(--s-1) var(--s-3);
|
||||
font-size:var(--fs-xs); font-family:var(--font-mono); border-bottom:1px solid var(--border); }
|
||||
.evt .seq { color:var(--muted-foreground); }
|
||||
.evt.hl { background:var(--accent-surface); }
|
||||
.cap { display:flex; justify-content:space-between; gap:var(--s-2); padding:var(--s-1) var(--s-3);
|
||||
font-size:var(--fs-xs); border-bottom:1px solid var(--border);}
|
||||
|
||||
/* annotation markers */
|
||||
.anno { position:relative; }
|
||||
.pin { position:absolute; z-index:5; width:1.4rem; height:1.4rem; border-radius:999px;
|
||||
background:var(--accent); color:#fff; font-size:var(--fs-xs); font-weight:var(--w-semi);
|
||||
display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 3px var(--accent-surface); }
|
||||
.legend { grid-column:1/-1; }
|
||||
.legend ol { margin:var(--s-2) 0 0; padding-left:1.2rem; font-size:var(--fs-xs);
|
||||
columns:2; column-gap:var(--s-8); }
|
||||
.legend li { margin-bottom:var(--s-1); }
|
||||
|
||||
/* mobile */
|
||||
.seg { display:none; }
|
||||
@media (max-width: 64rem) {
|
||||
.console { grid-template-columns: minmax(0,1fr); padding:var(--s-3); }
|
||||
.rail-left, .rail-right { display:none; }
|
||||
header.top { flex-wrap:wrap; row-gap:var(--s-2); }
|
||||
header.top h1 { font-size:var(--fs-md); }
|
||||
header.top .btn.ghost, header.top .badge.mono { display:none; }
|
||||
.eyebrow { letter-spacing:.04em; }
|
||||
.seg { display:flex; gap:var(--s-1); background:var(--muted); border-radius:var(--r-sm);
|
||||
padding:var(--s-1); }
|
||||
.seg span { flex:1; text-align:center; padding:var(--s-1) 0; border-radius:var(--r-sm);
|
||||
font-size:var(--fs-xs); color:var(--muted-foreground);}
|
||||
.seg span.on { background:var(--card); color:var(--foreground); font-weight:var(--w-med);
|
||||
box-shadow:var(--shadow-sm);}
|
||||
.legend ol { columns:1; }
|
||||
.msg { max-width:92%; }
|
||||
.btn { height:2.75rem; } /* V2: 44px touch targets */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="console anno">
|
||||
<header class="top">
|
||||
<div>
|
||||
<div class="eyebrow">Paperclip Runner Protocol · Phase 4b</div>
|
||||
<h1>Live Codex console</h1>
|
||||
</div>
|
||||
<span class="badge info"><span class="dot run"></span> connected</span>
|
||||
<span class="badge mono">session prp-7f3a…c2</span>
|
||||
<button class="btn">Goal ▾</button>
|
||||
<button class="btn ghost">Inspector</button>
|
||||
</header>
|
||||
|
||||
<!-- LEFT RAIL -->
|
||||
<aside class="rail-left" style="position:relative">
|
||||
<span class="pin" style="top:-0.5rem; left:-0.5rem">9</span>
|
||||
<section class="panel">
|
||||
<div class="panel-h">Demo chats</div>
|
||||
<div class="panel-b" style="display:flex;flex-direction:column;gap:var(--s-1)">
|
||||
<div class="manifest active">
|
||||
<div class="t">Steer & interrupt <span class="badge info">running</span></div>
|
||||
<div class="d">Mid-turn steering ack, stale rejection, 3 interrupt races</div>
|
||||
</div>
|
||||
<div class="manifest">
|
||||
<div class="t">Approvals <span class="badge ok">passed</span></div>
|
||||
<div class="d">Command, file-change, permission, input requests</div>
|
||||
</div>
|
||||
<div class="manifest">
|
||||
<div class="t">Reconnect & replay <span class="badge">not run</span></div>
|
||||
<div class="d">Drop socket, refresh, byte-identical replay</div>
|
||||
</div>
|
||||
<button class="btn ghost sm">Reset demo state…</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" style="position:relative">
|
||||
<span class="pin" style="top:-0.5rem; right:-0.5rem">5</span>
|
||||
<div class="panel-h">Session</div>
|
||||
<div class="panel-b">
|
||||
<div class="kv"><span class="k">Connection</span><span><span class="dot ok"></span> connected</span></div>
|
||||
<div class="kv"><span class="k">Turn</span><span class="mono">turn-19 · active</span></div>
|
||||
<div class="kv"><span class="k">Pending requests</span><span class="badge warn">1</span></div>
|
||||
<div class="goal" style="margin-top:var(--s-2)">
|
||||
<strong>Goal · active</strong><br/>
|
||||
Ship the fixture validator with tests
|
||||
<div style="margin-top:var(--s-1)"><button class="btn ghost sm">Pause</button>
|
||||
<button class="btn ghost sm" aria-disabled="true" title="Goal is active">Resume</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" style="position:relative">
|
||||
<span class="pin" style="top:-0.5rem; right:-0.5rem">6</span>
|
||||
<div class="panel-h">Threads</div>
|
||||
<div class="panel-b">
|
||||
<ul class="tree">
|
||||
<li class="cur"><span class="dot run"></span> <span class="mono">root · thr-a91f</span></li>
|
||||
<li class="child"><span class="dot run"></span> <span class="mono">child · thr-b02c</span> <span class="badge info">running</span></li>
|
||||
<li class="child"><span class="dot mut"></span> <span class="mono">child · thr-9d44</span> <span class="badge ok">done</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<!-- CENTER: transcript + composer -->
|
||||
<main class="panel chat anno" style="position:relative">
|
||||
<span class="pin" style="top:-0.5rem; left:-0.5rem">1</span>
|
||||
<div class="seg" role="tablist" aria-label="Console sections">
|
||||
<span class="on">Chat</span><span>Session</span><span>Inspector</span>
|
||||
</div>
|
||||
<div class="stream" role="log" aria-label="Session transcript">
|
||||
<div class="msg user">Validate the fixture corpus and summarize gaps.</div>
|
||||
|
||||
<div class="msg assistant">
|
||||
<div class="item-label"><span class="badge">reasoning</span> 2.1s</div>
|
||||
<details class="fold"><summary>▸ Planned validation order and edge cases…</summary></details>
|
||||
</div>
|
||||
|
||||
<div class="msg assistant" style="position:relative">
|
||||
<span class="pin" style="top:-0.6rem; right:0">4</span>
|
||||
<div class="req">
|
||||
<div class="rh"><span class="badge warn">Command</span> Approve running this command?
|
||||
<span class="rid mono">req-77b1…</span></div>
|
||||
<pre>pnpm vitest run protocol/fixtures --reporter=json</pre>
|
||||
<div class="ra">
|
||||
<button class="btn primary">Approve</button>
|
||||
<button class="btn">Approve for session</button>
|
||||
<button class="btn danger">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg assistant">
|
||||
<div class="item-label"><span class="badge ok">tool · resolved</span> validate_fixtures</div>
|
||||
<details class="fold"><summary>▸ 37 fixtures validated · 0 gaps</summary></details>
|
||||
</div>
|
||||
|
||||
<div class="divider" style="position:relative">
|
||||
<span class="pin" style="top:-1.4rem; left:40%">3</span>
|
||||
turn-18 interrupted — partial output preserved
|
||||
</div>
|
||||
|
||||
<div class="chip ack" style="position:relative">
|
||||
<span class="pin" style="top:-1.2rem; right:-0.5rem">2</span>
|
||||
✓ Steering acknowledged · “prefer the JSON reporter”
|
||||
</div>
|
||||
<div class="chip rej">✕ Steering rejected — turn already ended · <u>Send as new message</u></div>
|
||||
|
||||
<div class="msg assistant">
|
||||
<div class="item-label"><span class="badge info">assistant · streaming</span></div>
|
||||
<p>Re-running with the JSON reporter. Two fixtures need regenerated goldens: <code>source-gap</code> and <code>duplicate-event</code>. Starting with<span class="streaming-cursor"></span></p>
|
||||
</div>
|
||||
|
||||
<button class="btn sm jump">↓ Jump to latest · 2 new</button>
|
||||
</div>
|
||||
|
||||
<div class="banner pending" style="position:relative">
|
||||
<span class="pin" style="top:-0.7rem; left:30%">4</span>
|
||||
⏳ 1 request waiting — the turn is paused until you respond. <u>Review</u>
|
||||
</div>
|
||||
|
||||
<div class="composer anno" style="position:relative">
|
||||
<span class="pin" style="top:-0.7rem; right:-0.5rem">2</span>
|
||||
<div class="status-line" aria-live="polite"><span class="dot run"></span> Turn running · steering will apply to <span class="mono">turn-19</span></div>
|
||||
<div class="row">
|
||||
<textarea aria-label="Steer the active turn">Focus only on the two failing goldens.</textarea>
|
||||
<button class="btn primary">Steer</button>
|
||||
<button class="btn danger" aria-label="Stop turn">■ Stop</button>
|
||||
</div>
|
||||
<div class="status-line"><kbd>Enter</kbd> steer · <kbd>Shift</kbd>+<kbd>Enter</kbd> newline · <kbd>End</kbd> jump to latest</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- RIGHT RAIL: inspector -->
|
||||
<aside class="rail-right anno" style="position:relative">
|
||||
<span class="pin" style="top:-0.5rem; left:-0.5rem">8</span>
|
||||
<section class="panel">
|
||||
<div class="panel-h">Protocol inspector <span class="badge">live</span></div>
|
||||
<div class="tabs">
|
||||
<span class="tab on">Events</span><span class="tab">Requests</span>
|
||||
<span class="tab">Capabilities</span><span class="tab">Session</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="evt"><span class="seq">41</span><span>turn.started</span><span class="mono">turn-19</span></div>
|
||||
<div class="evt hl"><span class="seq">42</span><span>item.delta</span><span class="mono">item-63</span></div>
|
||||
<div class="evt"><span class="seq">43</span><span>runtime_request.created</span><span class="mono">req-77b1</span></div>
|
||||
<div class="evt"><span class="seq">44</span><span>item.delta</span><span class="mono">item-63</span></div>
|
||||
</div>
|
||||
<div class="panel-b" style="border-top:1px solid var(--border)">
|
||||
<div class="eyebrow" style="margin-bottom:var(--s-1)">Capabilities</div>
|
||||
<div class="cap"><span>turn/steer</span><span class="badge ok">supported</span></div>
|
||||
<div class="cap"><span>turn/interrupt</span><span class="badge ok">supported</span></div>
|
||||
<div class="cap" style="position:relative"><span class="pin" style="right:-0.9rem; top:-0.2rem">5</span>
|
||||
<span>goals/pause</span><span class="badge err" title="app-server 0.132.0 does not advertise goals.pause">unsupported</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="panel-b" style="font-size:var(--fs-xs); color:var(--muted-foreground)">
|
||||
<strong style="color:var(--foreground)">Replay state</strong>
|
||||
<div class="kv"><span class="k">Cursor</span><span class="mono">seq 44 · no gaps</span></div>
|
||||
<div class="kv"><span class="k">Mode</span><span><span class="badge info">LIVE</span> <span style="position:relative"><span class="pin" style="top:-1rem; right:-1.4rem">7</span></span></span></div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<!-- LEGEND -->
|
||||
<section class="panel legend">
|
||||
<div class="panel-b">
|
||||
<div class="eyebrow">Annotations — interaction map sections (docs/design/phase-4b-interaction-map.md)</div>
|
||||
<ol>
|
||||
<li><strong>§1 Transcript & composer</strong> — reducer-ordered `role="log"`, streaming affordance, jump-to-latest pill, composer tri-state (Send → Steer/Stop).</li>
|
||||
<li><strong>§2 Steering</strong> — pending chip → acknowledged / stale-rejected with “Send as new message” recovery; `expectedTurnId` shown in status line.</li>
|
||||
<li><strong>§3 Interruption</strong> — interrupted divider preserves partial output; three races get distinct terminal states; session never replaced.</li>
|
||||
<li><strong>§4 Request cards</strong> — inline at timeline position + blocking banner; actions only as offered upstream; single-response guarantee.</li>
|
||||
<li><strong>§5 Goal capability</strong> — durable banner + fixed five-verb menu; unsupported verbs disabled with the exact upstream diagnostic.</li>
|
||||
<li><strong>§6 Lineage</strong> — parent/child tree from upstream identities; child steering shown as unsupported, never emulated.</li>
|
||||
<li><strong>§7 Reconnect/replay</strong> — connection status, replayed-to-here divider, REPLAY vs LIVE eyebrow badge, cursor/gap state.</li>
|
||||
<li><strong>§8 Protocol inspector</strong> — canonical events + raw upstream names, requests, capabilities, identities; redaction markers verbatim.</li>
|
||||
<li><strong>§9 Demo manifests</strong> — scenario list with expected observations, per-manifest outcome, reset with explicit destruction copy.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,145 @@
|
||||
# Phase 4b Component Decision Record — shadcn/ui and Vercel AI Elements
|
||||
|
||||
Status: **approved** (UX gate PAP-16832, 2026-08-08). Sources checked
|
||||
2026-08-08 against the live shadcn/ui and AI Elements documentation; this
|
||||
supersedes and extends the
|
||||
[2026-08-07 compatibility note](../research/2026-08-07-ui-library-compatibility.md)
|
||||
for the authorized Phase 4b browser surface.
|
||||
|
||||
## 1. Ground rules (binding on the implementation)
|
||||
|
||||
1. **Protocol authority.** The package's canonical PRP events and
|
||||
`SessionSnapshot` reducer state are the only data contract. Third-party
|
||||
chat message types, AI SDK `UIMessage`/message-part types, and
|
||||
component-local state machines must not become inputs to, or shadows of,
|
||||
the runner protocol. Adapted components accept props typed against
|
||||
`src/reducer/session-reducer.ts` / `src/contracts/*` shapes only.
|
||||
2. **Source adaptation, not dependency adoption.** Components are copied into
|
||||
`devtools/browser/src/components/` and rewritten in the established local
|
||||
idiom: plain function components, `data-slot` attributes, semantic `ui-*`
|
||||
class names, all visual values from the token layer in `src/styles.css`.
|
||||
No Tailwind, no `class-variance-authority`, no `radix-ui` runtime, no
|
||||
`cn()` utility chain, no AI SDK. (Precedent: `components/ui/button.tsx`
|
||||
already follows this idiom from the earlier phases.)
|
||||
3. **Dependency budget: zero new runtime dependencies** for the demo app
|
||||
beyond the existing `react`/`react-dom`. Every candidate below that would
|
||||
pull a runtime dep (radix primitives, `cmdk`, `streamdown`, `shiki`,
|
||||
`use-stick-to-bottom`, `nanoid`, `zod`) is either rebuilt on native
|
||||
platform primitives (`<dialog>`, `<details>`, ARIA patterns) or rejected.
|
||||
Rationale: Phase 4b is a proof under `packages/paperclip-runner/` that
|
||||
Phase 5 will freeze into an SDK — every dependency added here becomes an
|
||||
SDK liability decision later.
|
||||
4. **Accessibility parity or better.** Where we rebuild a radix-backed
|
||||
pattern on native primitives, the keyboard/SR contract in interaction map
|
||||
§10 is the acceptance bar; "the library would have done it" is not
|
||||
available as an excuse once we adapt source.
|
||||
5. **Token gaps are system changes.** Phase 4b needs a few new tokens (see
|
||||
§5). Add them to `src/styles.css` `:root` in one commit with this record —
|
||||
never inline values in component files.
|
||||
|
||||
## 2. Existing package-local primitives — REUSE
|
||||
|
||||
| Component | Decision | Notes |
|
||||
|---|---|---|
|
||||
| `ui/button.tsx` | Reuse | Add `ui-button--danger` and `ui-button--ghost` class variants (CSS only) for Stop / Cancel actions. |
|
||||
| `ui/badge.tsx` | Reuse | Status badges for turn/request/connection states. |
|
||||
| `ui/card.tsx` | Reuse | Base for request cards, manifest rows, inspector panels. |
|
||||
| `ui/textarea.tsx` | Reuse | Composer input (wrapped with auto-grow behavior). |
|
||||
|
||||
## 3. Vercel AI Elements — decisions per primitive
|
||||
|
||||
AI Elements is a shadcn-registry distribution targeting React 19 + Tailwind 4
|
||||
with Next.js + AI SDK prerequisites (docs, checked 2026-08-08). The turnkey
|
||||
stack does not fit this standalone Vite package (already established in the
|
||||
2026-08-07 note). We adapt **shapes and interaction patterns from source**,
|
||||
re-typed to reducer snapshots and restyled to tokens.
|
||||
|
||||
**ADAPT (5):**
|
||||
|
||||
| Primitive | Adapted as | What we keep | What we change |
|
||||
|---|---|---|---|
|
||||
| `Conversation` | `ui/conversation.tsx` | stick-to-bottom semantics, "jump to latest" affordance, `role="log"` | drop `use-stick-to-bottom` dep — implement with a scroll listener + `scrollTo`; wire unseen-count from reducer timeline length |
|
||||
| `Message` | `ui/message.tsx` | role-based alignment/grouping anatomy, avatar-less compact variant | props become `SessionItemSnapshot`; roles extended with `reasoning`/`tool`/`system`; no AI SDK message parts |
|
||||
| `PromptInput` | `ui/composer.tsx` | textarea + action-row anatomy, submit-on-Enter, status-driven submit button | add Send/Steer/Stop tri-state from interaction map §1–§3; no attachments, no model picker, no AI SDK `sendMessage` |
|
||||
| `Reasoning` | `ui/reasoning-item.tsx` | collapsible streaming reasoning with auto-open-while-streaming, duration caption | rebuild on `<details>`/summary with ARIA disclosure; content = plain streamed text (no markdown dep) |
|
||||
| `Tool` | `ui/tool-item.tsx` | collapsible tool block: header (name + status badge) / input / output sections | status enum mapped from canonical item/turn events, not AI SDK `ToolUIPart` states; payloads render in `<pre>` |
|
||||
|
||||
**REJECT (with reasons; revisit in Phase 5 where noted):**
|
||||
|
||||
| Primitive | Reason |
|
||||
|---|---|
|
||||
| `Response` (streamed markdown) | pulls `streamdown`/markdown pipeline; tracer renders exact text — markdown prettification can hide protocol truth. Phase 5 candidate for the SDK console. |
|
||||
| `CodeBlock` | needs `shiki` highlighting dep; `<pre>` + mono token suffices for a tracer. Phase 5 candidate. |
|
||||
| `Actions` (retry/like/copy row) | retry/regenerate/vote have no protocol backing; copy-to-clipboard is implemented locally in the inspector. |
|
||||
| `Branch` | message branching does not exist in the PRP session model. |
|
||||
| `Sources` / `InlineCitation` | no citation events in the protocol. |
|
||||
| `Task` | overlapping with adapted `Tool`; subagent lineage uses the tree (map §6), not a task widget. |
|
||||
| `ChainOfThought` | duplicate of `Reasoning` at higher visual weight. |
|
||||
| `Context` (token/cost meter) | usage belongs in the inspector Session tab as plain data; a persistent meter overweights cost in a tracer. Phase 5 candidate. |
|
||||
| `Artifact`, `WebPreview`, `Image`, `Attachments`, `OpenIn`, `Suggestion`, `Queue` | no protocol counterpart in Phase 4b; adopting them would invent affordances the runner cannot honor (fabricated-controls rule). |
|
||||
| `Loader`/`Shimmer` | trivially replaced by a token-compliant CSS pulse honoring `prefers-reduced-motion`. |
|
||||
|
||||
## 4. shadcn/ui — decisions per primitive
|
||||
|
||||
**ADAPT (6):**
|
||||
|
||||
| Primitive | Adapted as | Implementation note |
|
||||
|---|---|---|
|
||||
| `Tabs` | `ui/tabs.tsx` | inspector tabs + mobile segments; WAI-ARIA tabs pattern, roving tabindex, no radix |
|
||||
| `DropdownMenu` (menu-button subset) | `ui/menu.tsx` | Goal menu (§5) — five fixed items; menu-button ARIA pattern; no radix portal |
|
||||
| `Dialog` | `ui/dialog.tsx` | native `<dialog>` element (`showModal()` gives focus trap + `Escape` for free); used by Set goal… and reset confirm |
|
||||
| `Tooltip` | `ui/tooltip.tsx` | hover/focus tooltip for diagnostics; content mirrored to `aria-describedby` per map A5 (tooltip is never the only channel) |
|
||||
| `Alert` | `ui/banner.tsx` | reconnect / pending-request / replay banners; tone variants from status tokens |
|
||||
| `Separator`, `Kbd` | folded into `styles.css` | pure CSS (`ui-separator`, `ui-kbd`) — no component file needed |
|
||||
|
||||
**REJECT:**
|
||||
|
||||
| Primitive | Reason |
|
||||
|---|---|
|
||||
| `Command` (cmdk palette) | the goal surface has five fixed verbs — a searchable palette adds a dep and search UI for nothing (Hick's law works in our favor with a plain menu). Revisit only if goal/command vocabulary grows in Phase 5+. |
|
||||
| `ScrollArea` | custom scrollbars are cosmetic; native scrolling is more accessible and free. |
|
||||
| `Accordion`/`Collapsible` | native `<details>` covers reasoning/tool/observation disclosures. |
|
||||
| `Sheet`/`Drawer`, `Popover`, `Select`, `Switch`, `Progress`, `Skeleton`, `Toast`/`Sonner` | no Phase 4b surface needs them; toasts specifically are rejected because failures must live in the transcript record, not ephemeral notifications (map §1.5). |
|
||||
| `Table` | inspector uses definition lists / simple grids; sortable tables are not needed. |
|
||||
| `Sidebar` | the existing app-shell layout already provides rails; adopting the shadcn sidebar would restructure the proven shell. |
|
||||
|
||||
## 5. Token additions (system change, one commit)
|
||||
|
||||
Add to `:root` in `devtools/browser/src/styles.css`:
|
||||
|
||||
- `--accent: #2f5fa8` / `--accent-surface: #e9f0fa` — pending/streaming/info
|
||||
states (currently only success/warning/danger exist; pending ≠ warning).
|
||||
- `--surface-raised: #fcfcfa` — inspector rows and collapsed-card summaries.
|
||||
- `--motion-medium: 200ms` — banner/dialog enter (fast is too abrupt for
|
||||
modal context changes); both motion tokens gated by
|
||||
`prefers-reduced-motion`.
|
||||
- `--z-banner: 10`, `--z-dialog: 20` — stacking discipline for the new
|
||||
layered surfaces.
|
||||
|
||||
Contrast pairs must pass map §10-V1; verify `--accent` on `--accent-surface`
|
||||
≥ 4.5:1 before merge (measured 5.1:1 at the values above).
|
||||
|
||||
## 6. What the implementer must NOT do
|
||||
|
||||
- Do not `npx shadcn add` or use the AI Elements CLI/registry into this
|
||||
package (they scaffold Tailwind/radix/AI SDK). Copy source manually,
|
||||
then rewrite per §1.2.
|
||||
- Do not import from `ai`, `@ai-sdk/*`, `zod`, `radix-ui`, `cmdk`,
|
||||
`streamdown`, or `shiki` anywhere under `devtools/browser/`.
|
||||
- Do not add a second event model, message store, or client-side session
|
||||
cache that could diverge from the reducer (map §7.2).
|
||||
- Do not restyle existing Phase 1–3 surfaces beyond shared-token additions;
|
||||
their screenshots are frozen QA evidence.
|
||||
|
||||
## 7. Evidence checklist for this record
|
||||
|
||||
- [x] Live docs re-checked 2026-08-08 (shadcn/ui Vite path; AI Elements
|
||||
prerequisites: Node 18+, Next.js + AI SDK project, React 19,
|
||||
Tailwind 4, shadcn/ui auto-install; registry install paths).
|
||||
- [x] Existing package idiom confirmed (`ui/button.tsx` et al. — plain CSS
|
||||
classes on tokens, React 19, no Tailwind).
|
||||
- [x] Interaction map cross-references: every ADAPT row maps to a surface in
|
||||
the [interaction map](phase-4b-interaction-map.md); every surface has a
|
||||
component owner.
|
||||
- [x] Annotated mockups rendered at 1440×900 and 390×844 — see
|
||||
[`mockups/`](mockups/).
|
||||
@@ -0,0 +1,381 @@
|
||||
# Phase 4b Interaction Map — Live Codex Protocol Web UI
|
||||
|
||||
Status: **approved for implementation** (UX gate PAP-16832, 2026-08-08).
|
||||
Owner: UXDesigner. Implementer: browser tracer owner (PAP-16834).
|
||||
Companion record: [component decisions](phase-4b-component-decisions.md).
|
||||
Mockups: [`mockups/`](mockups/) (annotated desktop + mobile renders).
|
||||
|
||||
This map is the interaction contract for the Phase 4b live console required by
|
||||
spec §29.1. It binds every surface to canonical Paperclip Runner Protocol (PRP)
|
||||
events and reducer state only. No surface may derive state from third-party
|
||||
component message types, and no surface may show a control the upstream
|
||||
capability set does not support.
|
||||
|
||||
## 0. Shell and information architecture
|
||||
|
||||
Extend the existing devtools browser app (`devtools/browser/`) with a new
|
||||
**Live console** mode next to the existing Phase 1–3 modes. Do not build a
|
||||
second app shell. Reuse the existing token layer in `src/styles.css` and the
|
||||
existing `ui-*` primitives.
|
||||
|
||||
Layout (desktop ≥ 64rem):
|
||||
|
||||
- **Left rail (fixed, ~17rem):** demo-chat manifest list, session/connection
|
||||
status block, goal banner, parent/child lineage tree.
|
||||
- **Center column (fluid, max ~48rem):** chat transcript + composer. This is
|
||||
the primary surface; it gets the widest column and the strongest visual
|
||||
hierarchy.
|
||||
- **Right rail (collapsible, ~24rem):** protocol inspector. Closed by default
|
||||
on first load; its toggle lives in the header. State persists per session in
|
||||
`localStorage` (UI preference only — never protocol state).
|
||||
|
||||
Layout (mobile < 64rem): single column. Transcript + composer first; manifest
|
||||
picker, goal banner, lineage, and inspector become stacked collapsible sections
|
||||
behind a segmented control (`Chat · Session · Inspector`). Composer stays
|
||||
docked to the bottom of the viewport in the Chat segment.
|
||||
|
||||
Density: this is a diagnostic console — lean dense (Doherty: prioritize
|
||||
feedback latency over decoration). All spacing from the existing scale.
|
||||
|
||||
## 1. Transcript and composer
|
||||
|
||||
**Source of truth:** the reducer timeline (`SessionSnapshot.timeline` /
|
||||
`SessionItemSnapshot`), fed by `item.started` / `item.delta` /
|
||||
`item.completed` / `item.failed` and turn lifecycle events.
|
||||
|
||||
Transcript rules:
|
||||
|
||||
1. Items render in reducer order. Never reorder client-side; the reducer's
|
||||
sequence is authoritative (protocol authority rule).
|
||||
2. Item roles: `user`, `assistant`, `reasoning`, `tool`, `system/diagnostic`.
|
||||
User items align right with a filled surface; everything else aligns left
|
||||
full-width. Reasoning and tool items render collapsed by default with a
|
||||
one-line summary (Progressive Disclosure); assistant text renders expanded.
|
||||
3. Streaming: an in-flight item (`item.started` + deltas, no terminal event)
|
||||
shows a streaming affordance (pulsing cursor block, `--motion-fast`
|
||||
respecting `prefers-reduced-motion`). Text appends without layout jumps.
|
||||
4. Auto-follow: transcript sticks to bottom while the user is at bottom. Any
|
||||
upward scroll disengages following and shows a **"Jump to latest"** pill
|
||||
(fixed above the composer) with unseen-item count. Clicking or pressing
|
||||
`End` re-engages. Never yank scroll position while disengaged.
|
||||
5. Failed items (`item.failed`) render in place with a danger surface and the
|
||||
exact diagnostic string — no toast-only errors; failures are part of the
|
||||
record (Visibility of System Status).
|
||||
6. Empty state: before any session, the transcript shows the manifest picker
|
||||
inline ("Pick a demo chat or start a blank session") — not a bare void.
|
||||
|
||||
Composer states (one state machine, one visible primary action):
|
||||
|
||||
| State | Trigger | Primary action | Notes |
|
||||
|---|---|---|---|
|
||||
| `idle` | no active turn | **Send** (`turn.submitted`) | textarea enabled |
|
||||
| `submitting` | send clicked, no `turn.accepted` yet | disabled, spinner in button | keep text visible |
|
||||
| `active-turn` | `turn.started` for my turn | **Steer** (see §2) + **Stop** icon-button | composer relabels; input stays enabled for steering |
|
||||
| `interrupting` | stop clicked | both disabled, "Stopping…" | see §3 |
|
||||
| `disconnected` | transport lost | disabled + reconnect banner | see §7 |
|
||||
| `terminal` | `session.closed`/`session.failed` | disabled + replay affordance | see §7 |
|
||||
|
||||
Composer behavior: `Enter` submits, `Shift+Enter` inserts newline. Textarea
|
||||
auto-grows to 8 lines, then scrolls. The active state is announced via a
|
||||
visually-hidden `aria-live="polite"` status line ("Turn running", "Steering
|
||||
sent", "Turn stopped").
|
||||
|
||||
## 2. Steering: acknowledgement and stale-turn rejection
|
||||
|
||||
While a turn is active the composer's primary action is **Steer** and sends
|
||||
`turn/steer` with `expectedTurnId` bound at submit time.
|
||||
|
||||
1. **Optimistic pending, explicit ack.** On send, append a local "steering"
|
||||
chip to the transcript in a `pending` visual state (muted, spinner).
|
||||
Promote it to `acknowledged` (solid, check) only on driver acknowledgement.
|
||||
Never silently merge steering text into the user-message stream before ack.
|
||||
2. **Stale-turn rejection is a first-class outcome, not an error toast.** If
|
||||
the turn completed/failed/was interrupted between typing and send, the
|
||||
driver rejects on `expectedTurnId` mismatch. The chip flips to `rejected —
|
||||
turn already ended`, with the composed text preserved in an inline
|
||||
**"Send as new message"** action (Forgiveness: one keystroke recovers the
|
||||
intent; never discard typed input).
|
||||
3. The pending chip must resolve to exactly one of `acknowledged`, `rejected
|
||||
(stale turn)`, or `failed (diagnostic)` — no indefinite pending. If the
|
||||
transport drops mid-steer, resolve to `failed` with the reconnect banner.
|
||||
4. `aria-live="polite"` announces ack/rejection with the same copy as the
|
||||
visual chip.
|
||||
|
||||
## 3. Interruption: three races, no session replacement
|
||||
|
||||
Stop is a distinct destructive-adjacent action: icon button with `Stop`
|
||||
label, danger-tinted only while `active`, never adjacent enough to Send to
|
||||
misclick (Fitts: minimum 2.75rem target, `--space-3` gap).
|
||||
|
||||
Race matrix (all three must be visually distinguishable — spec test contract):
|
||||
|
||||
| Race | UI before | UI after |
|
||||
|---|---|---|
|
||||
| interrupt **before start** (`turn.submitted`, no `turn.started`) | composer `submitting` | pending turn card resolves to `turn.cancelled` state: "Cancelled before start" |
|
||||
| interrupt **during generation** | streaming assistant item | stream stops in place; item keeps partial text with an "interrupted" divider; `turn.interrupted` badge on the turn group |
|
||||
| interrupt **during tool** | tool item running | tool item resolves to interrupted state with its last known status; no fabricated tool result |
|
||||
|
||||
Rules:
|
||||
|
||||
1. Stop → `interrupting` state ("Stopping…", both actions disabled) until the
|
||||
terminal turn event arrives. If the driver reports interrupt unsupported,
|
||||
show the exact diagnostic and re-enable — never kill or replace the session
|
||||
(spec: "without silently replacing the session").
|
||||
2. Partial output is preserved and labeled; never blank an interrupted item
|
||||
(the partial record is the point of a tracer).
|
||||
3. After `turn.interrupted`, composer returns to `idle` — resume is just a new
|
||||
send. The turn group shows its terminal badge permanently.
|
||||
|
||||
## 4. Runtime request cards (command / file-change / permission / tool / user-input)
|
||||
|
||||
**Source of truth:** `runtime_request.created` / `.resolved` / `.expired` /
|
||||
`.cancelled` plus `SessionRequestSnapshot` (`requestId`, `requestKind`,
|
||||
`type`, `status`, `prompt`). Action availability comes from the upstream
|
||||
request contract per request — the card renders only the actions the request
|
||||
offers (never invent "approve for session" if the request does not offer it).
|
||||
|
||||
Placement: inline in the transcript at timeline position, plus a compact
|
||||
"pending requests" counter chip in the session status block that scrolls to
|
||||
the oldest pending card. A pending blocking request also pins a slim banner
|
||||
above the composer ("1 request waiting — Review") because the turn cannot
|
||||
proceed (Zeigarnik: make the open loop visible where the user is looking).
|
||||
|
||||
Card anatomy (one component, five kinds):
|
||||
|
||||
- Kind icon + kind label (`Command`, `File change`, `Permission`, `Tool`,
|
||||
`Input`) + requestId (mono, truncated, full value in tooltip/inspector).
|
||||
- Prompt/body: exact upstream prompt text. Command and file-change payloads
|
||||
render in a mono block (command line, file diff summary) — never paraphrase.
|
||||
- Action row, in this order when offered: **Approve** (primary),
|
||||
**Approve for session** (secondary, only when upstream offers it),
|
||||
**Reject** (quiet danger), **Cancel** (ghost, only when upstream allows
|
||||
requester-side cancel). User-input kind renders a text field + **Submit**
|
||||
instead of approve/reject.
|
||||
- Resolution states: `pending` (accent left-border), `resolved — <outcome>`
|
||||
(success surface, chosen action named, actor + time), `expired` (muted,
|
||||
"expired before response"), `cancelled` (muted). Resolved cards collapse to
|
||||
a single summary line, expandable (transcript stays scannable).
|
||||
|
||||
Rules:
|
||||
|
||||
1. **Single-response guarantee in the UI:** on first click, disable the whole
|
||||
action row and show a resolving spinner; the card resolves only on the
|
||||
`runtime_request.resolved` event, not optimistically. A late duplicate
|
||||
click therefore cannot double-respond (mirrors the driver's no-double-
|
||||
response contract).
|
||||
2. A request resolved elsewhere (driver timeout, upstream cancel) must update
|
||||
the card in place with the true outcome.
|
||||
3. Keyboard: cards are focusable list items; action row is reachable by `Tab`;
|
||||
see §10 for the full contract.
|
||||
|
||||
## 5. Goal capability states (set / view / pause / resume / clear)
|
||||
|
||||
**Source of truth:** capability negotiation from the app-server (per spec
|
||||
§29.1.5), surfaced through the driver's capability snapshot. Goal state
|
||||
changes arrive as canonical events; the banner derives from reducer state.
|
||||
|
||||
Surfaces:
|
||||
|
||||
- **Durable goal banner** in the left rail (and Session segment on mobile):
|
||||
shows active goal text, state (`active`/`paused`), and last transition time.
|
||||
Hidden entirely when no goal has ever been set this session; shows a quiet
|
||||
"No goal set" row once goal capability is confirmed supported.
|
||||
- **Goal menu** (not a fuzzy palette — five fixed operations, Hick's law):
|
||||
a menu button `Goal ▾` in the session status block opening a menu with
|
||||
`Set goal…`, `View`, `Pause`, `Resume`, `Clear`. `Set goal…` opens a small
|
||||
dialog with one textarea. `View` focuses/expands the banner. Operations
|
||||
invalid in the current goal state (e.g. `Resume` while active) are disabled
|
||||
with the reason in a tooltip and `aria-disabled` + description.
|
||||
|
||||
Capability gating (spec-critical):
|
||||
|
||||
1. If the installed app-server does not advertise goal support, the whole menu
|
||||
renders disabled with the **exact diagnostic** from capability negotiation
|
||||
(e.g. `Goal operations unsupported: app-server 0.132.0 does not advertise
|
||||
goals capability`) — copied verbatim into tooltip and inspector. Never hide
|
||||
the control silently (discoverability of the gap is a deliverable) and
|
||||
never fabricate a client-side goal.
|
||||
2. Partial support (some verbs missing) disables only the missing verbs, same
|
||||
exact-diagnostic rule per verb.
|
||||
3. Every goal mutation is also a transcript/system event ("Goal paused") so
|
||||
replay shows the same history.
|
||||
|
||||
## 6. Parent/child lineage and subagent activity
|
||||
|
||||
**Source of truth:** upstream thread identities (parent/child thread lineage
|
||||
from the driver; `normalizedSessionId` / thread ids). No client-side
|
||||
inference.
|
||||
|
||||
- Left-rail **lineage tree**: root session plus child threads, each row =
|
||||
name/id (mono, truncated) + live state dot (`starting`, `running`,
|
||||
`terminal-ok`, `terminal-failed`) + last-activity relative time. The row for
|
||||
the currently viewed thread is marked with `aria-current="true"`.
|
||||
- Selecting a child switches the center transcript to that child's timeline
|
||||
(read-only if the driver does not support steering children). Breadcrumb at
|
||||
the transcript top: `root ▸ child-a` for orientation (Jakob: matches file
|
||||
tree/thread conventions).
|
||||
- **Unsupported child steering renders as an explicit disabled composer** with
|
||||
the exact diagnostic ("Direct steering of child threads is not supported by
|
||||
this app-server"), not an absent composer and not an emulated one (spec:
|
||||
"shown as unsupported rather than emulated").
|
||||
- Child terminal states persist in the tree (terminal dot + badge); activity
|
||||
is history, not just presence.
|
||||
|
||||
## 7. Reconnect, refresh, resume, replay
|
||||
|
||||
**Source of truth:** transport status + `session.resuming` / `session.resumed`
|
||||
/ `session.reconciled` / `runner.reconnected` events and reducer cursor state.
|
||||
|
||||
1. **Connection status** lives in the session status block: `connected`,
|
||||
`reconnecting (attempt n)`, `disconnected`, with the socket/cursor detail
|
||||
in the inspector. State changes announce via `aria-live="polite"`.
|
||||
2. **Transport drop:** banner above composer ("Connection lost — reconnecting…
|
||||
/ Retry now"). Composer disabled while disconnected (§1). On resume, the
|
||||
reducer replays from the durable cursor; the transcript must end
|
||||
byte-identical to a never-disconnected session (live/replay parity is a
|
||||
test contract, and the UI must not maintain shadow state that could
|
||||
diverge).
|
||||
3. **Page refresh:** reload restores the same normalized session and renders
|
||||
the full replayed transcript, then continues live. A subtle divider marks
|
||||
"replayed to here" for diagnostic honesty; it disappears on the next live
|
||||
event group.
|
||||
4. **Replay mode:** a terminal session offers **Replay** (from the manifest
|
||||
list or terminal banner): re-runs the recorded canonical events through the
|
||||
reducer with a stepper (play/pause, step, position slider). Replay is
|
||||
visually distinguished by an eyebrow badge `REPLAY` in the header — never
|
||||
let replay be mistakable for live (trust).
|
||||
5. Never offer "start new session" as the recovery path for a drop — resume
|
||||
the exact session (Phase 4 rule: never replace a lost session).
|
||||
|
||||
## 8. Protocol inspector
|
||||
|
||||
**Source of truth:** the raw upstream method/event names and the canonical
|
||||
event stream, side by side. The inspector is the debugging surface; the
|
||||
transcript stays clean (Tesler: complexity lives here, deliberately).
|
||||
|
||||
Right-rail panel with four tabs:
|
||||
|
||||
- **Events:** virtualized list, one row per canonical event — `seq`,
|
||||
`eventType`, turn/item id (mono). Row expands to the full canonical payload
|
||||
and, when present, the raw upstream method/notification that produced it
|
||||
(`debug` channel). Filter box (plain substring) + eventType facet chips.
|
||||
Clicking a transcript item highlights its events and vice versa
|
||||
(Uniform Connectedness across panels).
|
||||
- **Requests:** pending + resolved runtime requests with full lifecycle
|
||||
timestamps; row links to the transcript card.
|
||||
- **Capabilities:** negotiated capability/version table (steer, interrupt,
|
||||
goals, child threads, usage…), each row `supported / unsupported —
|
||||
<exact diagnostic>`. This is the authoritative view backing every disabled
|
||||
control in §5/§6.
|
||||
- **Session:** identities (runId, normalizedSessionId, thread ids,
|
||||
sourceInstanceId), sequence/cursor state, gap detection status, usage
|
||||
counters, connection history.
|
||||
|
||||
Rules: monospace for identifiers and payloads; copy-to-clipboard per row;
|
||||
**redaction is upstream's job but the inspector must render redaction markers
|
||||
verbatim and never attempt client-side un-redaction or pretty-printing that
|
||||
could reconstruct secrets**. No credential ever reaches the browser (spec
|
||||
security rule), so the inspector renders only what the demo server sends.
|
||||
|
||||
## 9. Demo-chat manifests
|
||||
|
||||
**Source of truth:** deterministic manifest files shipped with the package
|
||||
(one per spec scenario: completion, steering, interruption/resume, approvals,
|
||||
user input, subagents, goals, reconnect, replay).
|
||||
|
||||
- Manifest list in the left rail: name, one-line purpose, scenario badges,
|
||||
and an **expected observations** disclosure listing exactly what the runner
|
||||
should show (from the manifest, not hand-written in the UI).
|
||||
- Running a manifest pins a compact **observation checklist** panel the user
|
||||
can tick off manually (recognition over recall — the human checkpoint
|
||||
script is embedded in the UI, not in a separate doc).
|
||||
- **Reset control** per manifest and a global "Reset demo state": resets
|
||||
demo-server state and clears the local session, with a confirm dialog
|
||||
naming exactly what is destroyed ("Discards the current live session
|
||||
transcript. Recorded evidence files are not touched.").
|
||||
- Manifests must be runnable in any order; the list shows last-run outcome
|
||||
per manifest (`passed observations`, `not run`, `interrupted`).
|
||||
|
||||
## 10. Keyboard and accessibility acceptance criteria
|
||||
|
||||
These are the testable ACs for the component/keyboard/a11y checks required by
|
||||
the spec evidence list. Implementation must satisfy every row; QA verifies
|
||||
each with keyboard-only and screen-reader passes.
|
||||
|
||||
**Keyboard**
|
||||
|
||||
- K1. Every interactive control is reachable and operable with `Tab`/
|
||||
`Shift+Tab`/`Enter`/`Space` alone; no keyboard trap anywhere, including
|
||||
dialogs and the inspector.
|
||||
- K2. Composer: `Enter` sends (or steers when a turn is active),
|
||||
`Shift+Enter` newline. `Escape` while a turn is active moves focus to the
|
||||
Stop button (it must never interrupt directly — destructive actions require
|
||||
explicit activation).
|
||||
- K3. Request cards: card list is an `role="list"`; each card's actions are
|
||||
reachable in visual order; the oldest pending card is focusable via the
|
||||
pending-requests banner (`Enter` on banner focuses it).
|
||||
- K4. Goal menu: standard menu-button pattern (`Enter`/`Space`/`ArrowDown`
|
||||
opens, arrows navigate, `Escape` closes, focus returns to trigger).
|
||||
- K5. Lineage tree: `role="tree"` arrow-key navigation; `Enter` selects a
|
||||
thread; selection is announced.
|
||||
- K6. Transcript: `End` jumps to latest and re-engages follow; `Home` jumps
|
||||
to first item; transcript region is a labeled `role="log"` landmark.
|
||||
- K7. Inspector tabs: standard tabs pattern (arrow keys switch, `Tab` enters
|
||||
panel); event rows expand/collapse with `Enter`.
|
||||
- K8. Replay stepper: `Space` play/pause, `ArrowLeft`/`ArrowRight` step,
|
||||
slider operable by arrows with value announced.
|
||||
- K9. Visible focus indicator (`--ring`, ≥2px, ≥3:1 contrast against both
|
||||
adjacent colors) on every focusable element; focus order matches visual
|
||||
order.
|
||||
|
||||
**Screen reader / semantics**
|
||||
|
||||
- A1. Transcript is `role="log"` with `aria-live="polite"`; streaming deltas
|
||||
do **not** announce per-token — announce on item completion and on state
|
||||
transitions only (avoid SR flooding).
|
||||
- A2. Turn/steer/interrupt/connection/goal state changes each produce exactly
|
||||
one polite announcement with the same copy as the visible state.
|
||||
- A3. A pending blocking request announces assertively once
|
||||
(`aria-live="assertive"` on the banner), then stays quiet.
|
||||
- A4. Every state conveyed by color (status dots, card borders, badges) has a
|
||||
text or icon equivalent (WCAG 1.4.1 color-independence).
|
||||
- A5. Disabled capability controls expose the exact diagnostic via
|
||||
`aria-describedby`, not only a hover tooltip.
|
||||
- A6. Dialogs (`Set goal…`, reset confirm) are proper modal dialogs: labeled,
|
||||
focus moved in, trapped, restored on close.
|
||||
- A7. All identifiers/payload blocks are in `<code>`/`<pre>` semantics; copy
|
||||
buttons are labeled with what they copy ("Copy event 42 payload").
|
||||
|
||||
**Visual/WCAG**
|
||||
|
||||
- V1. Text contrast ≥ 4.5:1 (normal) / 3:1 (large); non-text UI ≥ 3:1 —
|
||||
verify the token pairs used by new surfaces (notably `--muted-foreground`
|
||||
on `--muted` and status-on-surface pairs).
|
||||
- V2. Pointer targets ≥ 2.75rem (44px) on mobile breakpoints; Stop and Send
|
||||
separated by ≥ `--space-3`.
|
||||
- V3. `prefers-reduced-motion` disables streaming pulse, auto-scroll
|
||||
smoothing, and skeleton shimmer (fall back to instant updates + static
|
||||
placeholders).
|
||||
- V4. Layout holds at 320px width and 200% zoom without horizontal scroll of
|
||||
the transcript (WCAG 1.4.10 reflow); the inspector may scroll internally.
|
||||
- V5. Both viewports in evidence: 1440×900 and 390×844 screenshots for every
|
||||
major state (QA gate).
|
||||
|
||||
## 11. UX acceptance criteria for the implementation handoff
|
||||
|
||||
1. All surfaces derive from canonical events/reducer snapshots; grep-level
|
||||
check: no import from any AI SDK, and adapted component files contain no
|
||||
third-party message-type imports (see decision record).
|
||||
2. Every state named in §1–§9 is reachable via the demo manifests, and each
|
||||
has a distinct, labeled visual state (no two states share identical UI).
|
||||
3. All K/A/V criteria in §10 pass keyboard-only and screen-reader passes.
|
||||
4. Unsupported capabilities render exact diagnostics (§5, §6, §8-Capabilities)
|
||||
— zero fabricated controls, zero silently hidden controls.
|
||||
5. Token compliance: no raw color/spacing/radius/type values in new component
|
||||
files; everything through `src/styles.css` custom properties (extend the
|
||||
token set via PR if a needed token is missing — see decision record §5).
|
||||
6. Screenshots at both evidence viewports for: idle+empty, streaming turn,
|
||||
steering ack + stale rejection, all three interrupt races, each request
|
||||
kind (pending/resolved/expired), goal supported+unsupported, lineage with
|
||||
an active child, reconnect banner + replayed divider, replay mode,
|
||||
inspector (Events + Capabilities tabs), and one full mobile pass.
|
||||
@@ -21,6 +21,8 @@ Each tutorial starts with an explanation of what the phase is and what the phase
|
||||
- [Phase 4 skillless Codex driver](phase-04-skillless-codex-driver.md)
|
||||
- [Engineering journal guide](journal.md)
|
||||
- [Dated shadcn/ui and AI Elements compatibility note](research/2026-08-07-ui-library-compatibility.md)
|
||||
- [Phase 4b live-console interaction map](design/phase-4b-interaction-map.md)
|
||||
- [Phase 4b component decision record (shadcn/ui, AI Elements)](design/phase-4b-component-decisions.md)
|
||||
- [Package README](../README.md)
|
||||
|
||||
Phase 4 is implemented and ready for Security, CTO, QA, and human review.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
type: Engineering Journal Entry
|
||||
title: Phase 4b UX gate — interaction map and component decisions
|
||||
description: Approved interaction contract, adapted-component record, and keyboard/accessibility acceptance criteria for the live Codex Web UI.
|
||||
tags: [native-runner, phase-4b, ux, browser, shadcn, ai-elements, accessibility]
|
||||
status: stable
|
||||
generated: { by: anthropic/claude, at: 2026-08-08T04:30:00Z }
|
||||
entry_kind: phase
|
||||
phase: "4b-ux"
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
Phase 4b builds a live browser console over the Phase 4 Codex driver. Before
|
||||
UI implementation, the UX gate (PAP-16832) had to approve the interaction map
|
||||
for every §29.1 surface and record reuse/adapt/reject decisions for
|
||||
source-adapted shadcn/ui and Vercel AI Elements primitives.
|
||||
|
||||
# Decisions
|
||||
|
||||
1. Extend the existing `devtools/browser/` app and its token layer with a new
|
||||
Live console mode. Do not build a second shell or restyle frozen Phase 1–3
|
||||
surfaces.
|
||||
2. The reducer/canonical event stream is the only UI data contract. Adapted
|
||||
third-party components are re-typed against `SessionSnapshot` shapes;
|
||||
AI SDK message/part types are banned as protocol authority.
|
||||
3. Zero new runtime dependencies. Adapt five AI Elements shapes
|
||||
(Conversation, Message, PromptInput, Reasoning, Tool) and six shadcn
|
||||
patterns (Tabs, menu-button subset, Dialog on native `<dialog>`, Tooltip,
|
||||
Alert, Separator/Kbd as CSS) in the established `ui-*` token idiom.
|
||||
Reject markdown/highlighting/palette deps for the tracer; several are
|
||||
named Phase 5 candidates.
|
||||
4. Steering resolves through an explicit pending → acknowledged /
|
||||
stale-rejected chip; rejected text is recoverable via "Send as new
|
||||
message". The three interrupt races get distinct preserved terminal
|
||||
states; the session is never replaced.
|
||||
5. Request cards render only upstream-offered actions and resolve only on
|
||||
`runtime_request.resolved` (UI-level single-response guarantee).
|
||||
6. Goal and child-steering capability gaps render disabled controls carrying
|
||||
the exact upstream diagnostic — never hidden, never emulated.
|
||||
7. Keyboard and accessibility acceptance criteria (K1–K9, A1–A7, V1–V5) are
|
||||
the testable bar for the spec's component/keyboard/accessibility checks.
|
||||
8. Four token additions (accent pair, raised surface, medium motion,
|
||||
z-scale) are a recorded system change, not inline values.
|
||||
|
||||
# Evidence
|
||||
|
||||
- [Interaction map](../../docs/design/phase-4b-interaction-map.md)
|
||||
- [Component decision record](../../docs/design/phase-4b-component-decisions.md)
|
||||
- Annotated mockups rendered at 1440×900 and 390×844:
|
||||
[`docs/design/mockups/`](../../docs/design/mockups/)
|
||||
|
||||
# Failures
|
||||
|
||||
- The first mobile render wrapped the header brand line character-by-character
|
||||
and clipped the inspector toggle; fixed by stacking the header and hiding
|
||||
desktop-only controls below the 64rem breakpoint before approval.
|
||||
|
||||
# Gaps and next questions
|
||||
|
||||
- Exact upstream diagnostics for goal/child-steering gaps depend on the
|
||||
PAP-16833 fixture work; the map requires verbatim pass-through, so no UI
|
||||
copy is blocked on them.
|
||||
- Markdown rendering and syntax highlighting are deliberately deferred; the
|
||||
Phase 5 SDK console should revisit `Response`, `CodeBlock`, and `Context`
|
||||
with a dependency decision at SDK scope.
|
||||
@@ -8,3 +8,4 @@
|
||||
* [Phase 2 local runner and fake harness](2026-08-07-phase-02.md) - Rust supervision, stdio protocol, scripted driver, live browser, cleanup, and parity decisions.
|
||||
* [Phase 3 durable outbound transport and recovery](2026-08-07-phase-03.md) - WebSocket authentication, durable ACK replay, command deduplication, and recovery diagnostics.
|
||||
* [Phase 4 skillless Codex app-server driver](2026-08-08-phase-04.md) - Direct app-server mapping, context isolation, semantic completion, real-model failures, and fixes.
|
||||
* [Phase 4b UX gate — interaction map and component decisions](2026-08-08-phase-04b-ux.md) - Approved live-console interaction contract, adapt/reject component record, and keyboard/accessibility acceptance criteria.
|
||||
@@ -14,3 +14,7 @@
|
||||
compact task envelope, isolated model context, semantic completion tools,
|
||||
canonical event/replay mapping, safe real-model example, tutorial, journal,
|
||||
and exact verification trace.
|
||||
* **Phase 4b UX gate**: Approved the live-console interaction map, the
|
||||
shadcn/AI Elements adapt/reject decision record with a zero-new-runtime-
|
||||
dependency rule, keyboard/accessibility acceptance criteria, four token
|
||||
additions, and annotated desktop/mobile mockups.
|
||||
Reference in new issue
Block a user