docs(runner): Phase 7I UX spec — mobile chat with visible mock activity

Interaction map, annotated responsive mockup, and desktop/mobile
acceptance captures for the Phase 7I Codex-style chat where every
turn's mock control-plane activity stays visible (PAP-16914).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 15:21:06 +00:00
1 parent ab7b85878a
commit 570f3d7a16
7 files changed
+776

No files matched your search

@@ -0,0 +1,424 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Phase 7I scenario chat — annotated mockup (ix-checkbox-01)</title>
<style>
/* Static mockup only. Values mirror the package --pcr-* token layer;
the implementation must consume the real tokens, never these copies.
Query params: ?view=activity (mobile Activity segment), ?state=streaming. */
:root {
--bg: #f7f7f5; --fg: #191917; --card: #ffffff; --muted: #efefeb;
--muted-fg: #66665f; --border: #deded7; --primary: #242420; --primary-fg: #fff;
--success: #17633a; --success-surface: #e9f5ed;
--warning: #8a5a10; --warning-surface: #fff4dc;
--danger: #a02f2f; --danger-surface: #fceaea;
--accent: #2f5fa8; --accent-fg: #fff; --accent-surface: #e9f0fa;
--raised: #fcfcfa; --ring: #66665f;
--sans: Inter, ui-sans-serif, system-ui, sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--r-sm: 0.375rem; --r-md: 0.625rem;
--shadow: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.05);
--touch: 2.75rem;
}
* { box-sizing: border-box; min-width: 0; }
html, body { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 400 0.875rem/1.5 var(--sans);
display: flex; flex-direction: column; }
header.app, .legend, .segbar { flex: none; }
.mono { font-family: var(--mono); }
.xs { font-size: 0.75rem; }
.muted-fg { color: var(--muted-fg); }
/* annotation callouts */
.co { display: inline-flex; align-items: center; justify-content: center; width: 1.05rem; height: 1.05rem;
border-radius: 999px; background: var(--accent); color: var(--accent-fg); font-size: 0.68rem;
font-weight: 600; flex: none; vertical-align: text-bottom; }
.legend { display: flex; flex-wrap: wrap; gap: 0.3rem 0.75rem; padding: 0.3rem 1rem;
background: var(--accent-surface); border-bottom: 1px solid var(--border); font-size: 0.7rem; }
.legend span.item { display: inline-flex; gap: 0.25rem; align-items: center; color: var(--fg); }
header.app { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 1rem;
background: var(--raised); border-bottom: 1px solid var(--border); }
header.app h1 { font-size: 0.875rem; font-weight: 600; margin: 0; }
header.app .corpus { margin-left: auto; color: var(--muted-fg); font-size: 0.75rem; }
.badge { display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 999px; padding: 0.05rem 0.5rem;
font-size: 0.7rem; font-weight: 500; border: 1px solid var(--border); background: var(--muted);
color: var(--fg); white-space: nowrap; }
.badge.cp { background: var(--muted); color: var(--muted-fg); }
.badge.always { background: var(--accent-surface); color: var(--accent); border-color: transparent; }
.badge.optional { background: var(--warning-surface); color: var(--warning); border-color: transparent; }
.badge.deny { background: var(--danger-surface); color: var(--danger); border-color: transparent; }
.badge.allow { background: var(--success-surface); color: var(--success); border-color: transparent; }
.badge.live { background: var(--accent-surface); color: var(--accent); border-color: transparent; }
.badge.grp { background: var(--card); }
.btn { border: 1px solid var(--border); background: var(--card); border-radius: var(--r-sm);
padding: 0.3rem 0.9rem; font: inherit; font-size: 0.8rem; font-weight: 500; min-height: 2rem; }
.btn.primary { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.btn:disabled { opacity: 0.5; }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
.seg button { border: 0; background: var(--card); padding: 0.25rem 0.75rem; font: inherit; font-size: 0.78rem; color: var(--muted-fg); }
.seg button.on { background: var(--primary); color: var(--primary-fg); }
.shell { display: grid; grid-template-columns: 17rem minmax(0,1fr) 26rem; flex: 1; min-height: 0; }
/* ---------- left rail: reused 7F picker (slim render) ---------- */
nav.picker { border-right: 1px solid var(--border); background: var(--raised); overflow-y: auto;
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
.search { display: block; width: 100%; height: 2rem; border: 1px solid var(--border); border-radius: var(--r-sm);
padding: 0 0.5rem; font: inherit; background: var(--card); color: var(--muted-fg); }
.group-h { font-size: 0.75rem; font-weight: 600; color: var(--muted-fg); margin: 0.4rem 0 0.1rem;
display: flex; justify-content: space-between; }
ul.cases { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.case { display: grid; gap: 0 0.5rem; padding: 0.375rem 0.5rem; border-radius: var(--r-sm); border-left: 3px solid transparent; }
.case .id { font-family: var(--mono); font-size: 0.72rem; color: var(--muted-fg); }
.case .t { font-size: 0.78rem; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.case.sel { background: var(--accent-surface); border-left-color: var(--accent); }
.facet-note { font-size: 0.72rem; color: var(--muted-fg); border: 1px dashed var(--border);
border-radius: var(--r-sm); padding: 0.35rem 0.5rem; }
/* ---------- center: chat ---------- */
main.chat { display: flex; flex-direction: column; overflow: hidden; }
.chat-scroll { overflow-y: auto; padding: 1rem 1.25rem 0.5rem; display: flex; flex-direction: column;
gap: 0.6rem; max-width: 48rem; width: 100%; margin: 0 auto; flex: 1; }
.sess { display: flex; flex-direction: column; gap: 0.4rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }
.sess .row1 { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.sess h2 { margin: 0; font-size: 1rem; font-weight: 600; }
.sess .controls { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.cpa { background: var(--muted); border-radius: var(--r-md); padding: 0.5rem 0.75rem; display: flex;
gap: 0.5rem; align-items: flex-start; }
.cpa .glyph { color: var(--muted-fg); font-size: 0.8rem; line-height: 1.4; }
.cpa p { margin: 0; font-size: 0.8rem; }
.cpa .why { color: var(--muted-fg); font-size: 0.72rem; }
.u-turn { align-self: flex-end; max-width: 85%; background: var(--accent-surface); border-radius: var(--r-md);
padding: 0.45rem 0.75rem; font-size: 0.85rem; }
.m-turn { background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md);
padding: 0.5rem 0.75rem; box-shadow: var(--shadow); max-width: 95%; }
.m-turn p { margin: 0.15rem 0; font-size: 0.85rem; }
.caret::after { content: "▌"; color: var(--muted-fg); animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
.toolcall { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--raised);
margin-left: 1.25rem; }
.toolcall .sumline { display: flex; gap: 0.5rem; align-items: center; padding: 0.45rem 0.75rem;
font-size: 0.8rem; flex-wrap: wrap; min-height: var(--touch); }
.toolcall .op { font-family: var(--mono); font-weight: 600; font-size: 0.78rem; }
.toolcall .outcome { color: var(--muted-fg); font-size: 0.75rem; }
.toolcall .body { border-top: 1px solid var(--border); padding: 0.5rem 0.75rem; display: grid; gap: 0.4rem; }
.kv { display: grid; grid-template-columns: 6.5rem 1fr; gap: 0.15rem 0.5rem; font-size: 0.75rem; margin: 0; }
.kv dt { color: var(--muted-fg); }
.kv dd { margin: 0; font-family: var(--mono); font-size: 0.72rem; overflow-wrap: anywhere; }
pre.json { margin: 0; background: var(--muted); border-radius: var(--r-sm); padding: 0.4rem 0.5rem;
font-family: var(--mono); font-size: 0.7rem; line-height: 1.45; overflow-x: auto; }
.toolcall.denied { border-color: var(--danger); }
.toolcall.denied .sumline { background: var(--danger-surface); border-radius: var(--r-md) var(--r-md) 0 0; }
.deny-detail { padding: 0.5rem 0.75rem; border-top: 1px solid var(--border); font-size: 0.78rem; }
.deny-detail strong { color: var(--danger); }
.redaction { display: inline-flex; gap: 0.25rem; align-items: center; background: var(--muted);
border-radius: 999px; padding: 0 0.4rem; font-size: 0.7rem; color: var(--muted-fg); }
.turn-strip { display: flex; gap: 0.5rem; align-items: center; width: 100%; text-align: left;
border: 1px solid var(--border); background: var(--card); border-radius: var(--r-md);
padding: 0.45rem 0.75rem; font: inherit; font-size: 0.75rem; color: var(--muted-fg);
min-height: var(--touch); cursor: pointer; flex-wrap: wrap; }
.turn-strip .lbl { font-weight: 600; color: var(--fg); }
.turn-strip .ok { color: var(--success); }
.turn-strip .bad { color: var(--danger); }
.composer { border-top: 1px solid var(--border); background: var(--raised); padding: 0.6rem 1.25rem 0.75rem; }
.composer-inner { max-width: 48rem; margin: 0 auto; display: flex; flex-direction: column; gap: 0.3rem; }
.composer-row { display: flex; gap: 0.5rem; align-items: flex-end; }
.composer textarea { flex: 1; resize: none; border: 1px solid var(--border); border-radius: var(--r-md);
font: inherit; font-size: 0.85rem; padding: 0.5rem 0.6rem; min-height: var(--touch);
background: var(--card); }
.composer .hint { font-size: 0.7rem; color: var(--muted-fg); }
/* ---------- right rail: activity stream ---------- */
aside.activity { border-left: 1px solid var(--border); background: var(--raised); overflow-y: auto;
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
.act-head { display: flex; align-items: baseline; gap: 0.5rem; }
.act-head h3 { margin: 0; font-size: 0.8rem; font-weight: 600; }
.filters { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.fchip { border: 1px solid var(--border); background: var(--card); border-radius: 999px;
padding: 0.15rem 0.6rem; font: inherit; font-size: 0.72rem; color: var(--muted-fg); min-height: 1.75rem; }
.fchip.on { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.turn-group { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--card); }
.tg-head { display: flex; gap: 0.5rem; align-items: center; padding: 0.5rem 0.65rem; font-size: 0.78rem;
font-weight: 600; flex-wrap: wrap; min-height: var(--touch); }
.tg-head .sum { font-weight: 400; color: var(--muted-fg); font-size: 0.72rem; }
.tg-body { border-top: 1px solid var(--border); padding: 0.5rem 0.65rem; display: flex; flex-direction: column; gap: 0.55rem; }
.sect h4 { margin: 0 0 0.2rem; font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.04em; color: var(--muted-fg); }
.row { display: flex; gap: 0.4rem; align-items: baseline; font-size: 0.75rem; padding: 0.15rem 0; flex-wrap: wrap; }
.row .mono { font-size: 0.72rem; }
.row.deny { background: var(--danger-surface); border-radius: var(--r-sm); padding: 0.25rem 0.4rem; }
.diff-domain { display: flex; justify-content: space-between; font-size: 0.75rem; padding: 0.15rem 0; gap: 0.5rem; }
.diff-field { font-family: var(--mono); font-size: 0.7rem; overflow-wrap: anywhere; color: var(--fg);
background: var(--muted); border-radius: var(--r-sm); padding: 0.2rem 0.4rem; margin: 0.15rem 0; }
.unchanged { color: var(--muted-fg); font-size: 0.72rem; }
.verdict { display: flex; gap: 0.4rem; align-items: center; font-size: 0.78rem; font-weight: 600;
color: var(--success); }
.verdict.session { border-top: 1px dashed var(--border); padding-top: 0.4rem; }
/* ---------- mobile ---------- */
.segbar { display: none; }
@media (max-width: 63.98rem) {
.shell { display: flex; flex-direction: column; }
nav.picker, aside.activity { display: none; }
body.view-activity main.chat { display: none; }
body.view-activity aside.activity { display: flex; border-left: 0; flex: 1; }
.segbar { display: flex; position: sticky; top: 0; z-index: 5; gap: 0.25rem; padding: 0.4rem 0.75rem;
background: var(--raised); border-bottom: 1px solid var(--border); }
.segbar button { flex: 1; min-height: var(--touch); border: 1px solid var(--border); background: var(--card);
border-radius: var(--r-sm); font: inherit; font-size: 0.8rem; display: inline-flex;
align-items: center; justify-content: center; gap: 0.35rem; }
.segbar button.on { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.chat-scroll { padding: 0.75rem 0.75rem 0.5rem; }
.composer { padding: 0.5rem 0.75rem 0.65rem; }
.toolcall { margin-left: 0.5rem; }
header.app .corpus { display: none; }
}
/* ---------- streaming variant ---------- */
.streaming-only { display: none; }
body.state-streaming .streaming-only { display: block; }
body.state-streaming .settled-only { display: none; }
</style>
</head>
<body>
<header class="app">
<h1>Phase 7 scenario chat — mock control plane</h1>
<span class="corpus">106 scenarios · 16 groups · mock adapter only, no credentials in the browser</span>
</header>
<div class="legend" aria-label="Annotation legend">
<span class="item"><span class="co">1</span> session header + status + reset/replay</span>
<span class="item"><span class="co">2</span> control-plane strip (turn 0 seed)</span>
<span class="item"><span class="co">3</span> user turn</span>
<span class="item"><span class="co">4</span> model output</span>
<span class="item"><span class="co">5</span> semantic tool call (expanded)</span>
<span class="item"><span class="co">6</span> denied operation</span>
<span class="item"><span class="co">7</span> turn activity strip</span>
<span class="item"><span class="co">8</span> composer</span>
<span class="item"><span class="co">9</span> per-turn activity group</span>
<span class="item"><span class="co">10</span> mobile segments + deny chip</span>
</div>
<div class="segbar" role="radiogroup" aria-label="Chat sections">
<button type="button" id="seg-chat" role="radio" aria-checked="true"><span class="co">10</span> Chat</button>
<button type="button" id="seg-activity" role="radio" aria-checked="false">Activity <span class="badge deny">1 deny</span></button>
</div>
<div class="shell">
<nav class="picker" aria-label="Scenario picker">
<input class="search" value="" placeholder="Search 106 scenarios…" aria-label="Search scenarios" />
<div class="facet-note">7F picker reused as-is: group · case · disposition · role · claim · parity facets</div>
<div class="group-h"><span>Interactions <span class="mono">ix</span></span><span>9</span></div>
<ul class="cases">
<li class="case"><span class="id">ix-questions-01</span><span class="t">Structured questions use ask-user form</span></li>
<li class="case sel"><span class="id">ix-checkbox-01</span><span class="t">Subset selection from a known list is a checkbox confirmation</span></li>
<li class="case"><span class="id">ix-confirmation-01</span><span class="t">Yes/no plan approval binds the revision</span></li>
</ul>
<div class="group-h"><span>Approvals <span class="mono">ap</span></span><span>6</span></div>
<ul class="cases">
<li class="case"><span class="id">ap-mcp-gate-01</span><span class="t">Gated tool call opens one approval card</span></li>
</ul>
</nav>
<main class="chat" aria-label="Scenario chat">
<div class="chat-scroll">
<div class="sess">
<div class="row1">
<span class="co">1</span>
<span class="mono xs muted-fg">ix-checkbox-01</span>
<h2>Subset selection from a known list is a checkbox confirmation</h2>
</div>
<div class="row1">
<span class="badge grp">Interactions <span class="mono">ix</span></span>
<span class="badge always">Always tool</span>
<span class="badge live">Live · Codex (bounded)</span>
<span class="badge allow">✓ Parity · 2 turns pass</span>
</div>
<div class="controls">
<span class="seg" role="radiogroup" aria-label="Session mode">
<button type="button">Replay (scripted)</button>
<button type="button" class="on">Codex (bounded)</button>
</span>
<button type="button" class="btn settled-only">Reset session…</button>
<button type="button" class="btn settled-only">Replay</button>
<button type="button" class="btn streaming-only" disabled title="Disabled while a turn is running">Reset session…</button>
<span class="streaming-only xs muted-fg">Controls disabled while turn 3 is running</span>
</div>
</div>
<div class="cpa">
<span class="glyph">⌘</span>
<div>
<p><span class="co">2</span> <strong>Control plane</strong> seeded <span class="mono">ix-checkbox-01</span>, routed the scoped wake, and checked out task <span class="mono">MCK-102</span> — no agent tool exists for this.</p>
<p class="why">Session start · 4 events · exposure: 8 always · 2 optional · 5 control-plane (no tool)</p>
</div>
</div>
<div class="u-turn"><span class="co">3</span> Read the task and confirm which files I should delete before you act.</div>
<div class="m-turn">
<p><span class="co">4</span> I read task <span class="mono">MCK-102</span>. It asks for a cleanup of generated fixture files. Before touching anything I need your selection, so I'm raising a checkbox confirmation with the candidate files.</p>
</div>
<div class="toolcall">
<div class="sumline">
<span class="co">5</span>
<span class="op">request_human_input</span>
<span class="badge always">Always tool</span>
<span class="outcome">kind=checkbox · committed</span>
</div>
<div class="body">
<dl class="kv">
<dt>operation</dt><dd>request_human_input@1</dd>
<dt>claims</dt><dd>(always catalog — none required)</dd>
<dt>idempotency</dt><dd>checkbox:MCK-102:cleanup:rev-3 · first commit</dd>
<dt>redactions</dt><dd><span class="redaction">••• redacted secret-hygiene</span></dd>
</dl>
<pre class="json">{ "kind": "checkbox", "prompt": "Check the files to delete.",
"options": [ { "id": "draft-report-march" }, { "id": "tmp-export-2025" } ],
"minSelected": 0 }</pre>
</div>
</div>
<div class="toolcall denied">
<div class="sumline">
<span class="co">6</span>
<span class="op">decide_approval</span>
<span class="badge optional">Optional tool</span>
<span class="badge deny">✕ Denied</span>
</div>
<div class="deny-detail">
<strong>Denied:</strong> missing claim <span class="mono">approval:decide</span>.
The authorization engine rejected the call before any state was read — no protected
state is included in this record.
</div>
</div>
<div class="cpa">
<span class="glyph">⌘</span>
<div>
<p><strong>Control plane</strong> registered the pending interaction and scheduled the continuation wake — interaction continuations are routed by the control plane, not requested again.</p>
<p class="why">state change: interactions +1 · wake scheduled (fixture time t+3)</p>
</div>
</div>
<button type="button" class="turn-strip">
<span class="co">7</span>
<span class="lbl">Turn 2 activity</span>
<span>2 tools</span> · <span class="bad">✕ 1 denied</span> · <span>2 control plane</span> ·
<span>diff 2 domains</span> · <span class="ok">✓ parity pass</span>
</button>
<div class="m-turn streaming-only">
<p class="caret">Checking the interaction result before finishing the task</p>
</div>
</div>
<div class="composer">
<div class="composer-inner">
<div class="composer-row settled-only">
<span class="co">8</span>
<textarea rows="2" aria-label="Message">Once I answer the checkbox, finish the task.</textarea>
<button type="button" class="btn primary">Send</button>
</div>
<p class="hint settled-only">Enter sends. Shift and Enter add a line.</p>
<div class="composer-row streaming-only">
<span class="co">8</span>
<textarea rows="2" aria-label="Message" placeholder="Steer the running turn…"></textarea>
<button type="button" class="btn primary">Steer</button>
<button type="button" class="btn">Stop</button>
</div>
<p class="hint streaming-only">Enter steers the running turn. Escape moves focus to Stop.</p>
</div>
</div>
</main>
<aside class="activity" aria-label="Mock activity stream">
<div class="act-head"><h3>Activity</h3><span class="xs muted-fg">every record emitted by the mock core</span></div>
<div class="filters">
<button type="button" class="fchip on">All</button>
<button type="button" class="fchip">Tools</button>
<button type="button" class="fchip">Authorization</button>
<button type="button" class="fchip">Control plane</button>
<button type="button" class="fchip">Diffs</button>
<button type="button" class="fchip">Parity</button>
</div>
<div class="turn-group">
<div class="tg-head">Session start <span class="sum">4 control-plane events · exposure 8 / 2 / 5</span></div>
</div>
<div class="turn-group">
<div class="tg-head">Turn 1 <span class="sum">1 tool · 1 allow · diff 0 · ✓ pass</span></div>
</div>
<div class="turn-group">
<div class="tg-head"><span class="co">9</span> Turn 2 <span class="sum">2 tools · 1 denied · 2 control plane · ✓ pass</span></div>
<div class="tg-body">
<div class="sect">
<h4>Exposed tools</h4>
<div class="row"><span class="badge always">Always · 8</span><span class="badge optional">Optional · 2</span><span class="badge cp">Control plane — no tool · 5</span></div>
</div>
<div class="sect">
<h4>Calls and results</h4>
<div class="row"><span class="mono">request_human_input</span><span class="badge allow">✓ committed</span></div>
<div class="row deny"><span class="mono">decide_approval</span><span class="badge deny">✕ denied</span></div>
</div>
<div class="sect">
<h4>Authorization</h4>
<div class="row"><span class="badge allow">✓ Allow</span><span class="mono">request_human_input</span><span class="xs muted-fg">always catalog</span></div>
<div class="row deny"><span class="badge deny">✕ Deny</span><span class="mono">decide_approval</span><span class="xs">missing <span class="mono">approval:decide</span></span></div>
</div>
<div class="sect">
<h4>Control plane</h4>
<div class="row"><span class="badge cp">Control plane</span><span class="xs">interaction registered · continuation wake scheduled (t+3)</span></div>
</div>
<div class="sect">
<h4>State diff — turn 2</h4>
<div class="diff-domain"><span>interactions</span><span class="xs muted-fg">1 added</span></div>
<div class="diff-field">interactions[itx-91].status: (none) → pending</div>
<div class="diff-domain"><span>tasks</span><span class="xs muted-fg">1 changed</span></div>
<div class="diff-field">tasks[MCK-102].status: in_progress → in_review</div>
<div class="diff-domain"><span>comments · documents · approvals · artifacts · blockers · workspace · budget · run</span><span class="unchanged">unchanged</span></div>
</div>
<div class="sect">
<h4>Parity — turn 2</h4>
<div class="verdict">✓ Pass · 3 assertions · 3 pass</div>
</div>
</div>
</div>
<div class="turn-group streaming-only">
<div class="tg-head">Turn 3 <span class="sum">running — records append as the mock core emits them</span></div>
</div>
<div class="verdict session settled-only">✓ Session parity · Pass · 6 assertions · 6 pass · 0 forbidden operations invoked</div>
</aside>
</div>
<script>
// Deterministic mockup states for capture: ?view=activity, ?state=streaming
const params = new URLSearchParams(location.search);
if (params.get("view") === "activity") {
document.body.classList.add("view-activity");
document.getElementById("seg-chat").setAttribute("aria-checked", "false");
const seg = document.getElementById("seg-activity");
seg.setAttribute("aria-checked", "true");
seg.classList.add("on");
} else {
document.getElementById("seg-chat").classList.add("on");
}
if (params.get("state") === "streaming") document.body.classList.add("state-streaming");
</script>
</body>
</html>
@@ -0,0 +1,352 @@
# Phase 7I Interaction Map — Mobile Chat with Visible Mock Activity
Status: **approved UX contract for 7I implementation** (PAP-16914, 2026-08-09).
Owner: UXDesigner. Implementer: 7I implementation (PAP-16916), consuming the
existing 7C mock adapter, 7D semantic catalog, and 7F explorer components.
Companion records: [Phase 7 interaction map](phase-7-scenario-explorer-ux.md)
(the 7F contract — everything not overridden here is inherited verbatim),
[Phase 4b interaction map](phase-4b-interaction-map.md) (chat console
baseline), [Phase 5 component decisions](phase-5-component-decisions.md).
Mockup: [`mockups/phase-7i-mobile-chat.html`](mockups/phase-7i-mobile-chat.html)
(annotated desktop + mobile renders in `knowledge/evidence/phase-07/`,
`phase-7i-*` slugs).
Phase 7I adds a **Codex-style interactive chat** over the same package-local
Phase 7 runner and mock `ControlPlanePort`. The board opens a scenario, chats
with the agent, and — this is the point of the phase — **sees the mock
Paperclip activity for every turn** instead of having it hidden behind chat.
The 7F explorer remains unchanged as the corpus-review surface; the chat is a
sibling surface in the same app shell, one React tree, sharing the picker,
transcript grammar, inspector components, and token layer.
The 7F authority rule is binding here too: the chat UI renders records the
runtime produced. It computes no exposure, evaluates no claim, applies no
redaction, holds no credential, and never re-times or reorders the artifact
stream.
## 0. Naming, vocabulary, and the turn model
- Surface name in the UI: **Scenario chat** (header: “Phase 7 scenario chat —
mock control plane”).
- All 7F §0 vocabulary carries over exactly: **task** in copy; machine values
(case IDs, `MCK-102`, grants, JSON) verbatim in `--pcr-font-mono`;
disposition labels **Control plane / Always tool / Optional tool**; parity
statuses **Pass / Fail / Intentional gap / Not run** always icon + text.
- A **turn** is one user message and everything the runtime records until the
agent settles (model output, semantic calls/results, authorization
decisions, control-plane actions, state changes, reconciliation events,
and the turn parity verdict). Turns are numbered from 1 in artifact order.
- **Session start** is turn 0: the control-plane-owned seeding record (wake
payload, checkout, initial tool exposure) that exists before any user
input. It renders with full control-plane styling — the demo must show
that work happened with no agent tool before the first message.
## 1. Shell and information architecture
Same Vite entry, same app shell as 7F (`examples/phase7-explorer/`), new
routes under `#/chat/…` (§9). No second app, no second token sheet.
Desktop (≥ 64rem), three regions, one React tree:
- **Left rail (`--pcr-rail-width`, 17rem):** the existing 7F scenario picker,
reused as-is (search, facets, grouped case list). Selecting a case in chat
context seeds a chat session for that scenario instead of a fake run.
- **Center column (fluid, max `--pcr-center-width`, 48rem):** the **chat** —
session header, conversation, and composer. Primary surface, strongest
hierarchy. The composer is pinned to the bottom of the column.
- **Right rail (26rem, proposed token `--pcr-activity-width`):** the
**Activity stream** — the ordered per-turn evidence (§4). Open by default
on desktop; it carries the acceptance evidence, so it is never a
collapsed-by-default drawer on desktop.
Mobile (< 64rem): single column behind the established segmented control
pattern (a `radiogroup`, per 7F revision 4), with exactly two segments:
**Chat · Activity**. The segmented control is sticky at the top; the
composer is sticky at the bottom of the Chat segment (thumb zone). The
Activity segment contains the complete §4 stream as stacked cards — mobile
gets **all** the evidence, never a summary-only view. A count chip on the
Activity segment surfaces attention states without opening it
(“Activity · 1 deny”).
Density: diagnostic-console dense. All values from `--pcr-*` tokens only
(`DESIGN.md` principles 2–4); the two 7F token-layer overrides (badge
min-width / text-transform, 7F revision 5) apply to the chat surface too.
## 2. Session header, scenario selection, reset and replay
The session header replaces the 7F run header on chat routes:
- **Row 1:** case ID (mono) + title, group badge, disposition badge.
- **Row 2 — session status chip**, exactly one of:
- **Live · Codex (bounded)** (accent surface) — a real model is connected
through the server-side relay;
- **Replay · deterministic** (muted surface) — the scripted session is
playing or played;
- **Idle** (muted) — seeded, no turns yet.
- **Row 3 — controls:** mode segmented control (`Replay (scripted)` default /
`Codex (bounded)` when the relay is reachable — same availability and
disabled-reason rules as 7F §3.1), **Reset session**, and **Replay**
(re-runs the recorded session deterministically). Reset is a destructive
affordance: it clears the session back to the seeded turn-0 state and asks
for confirmation via the SDK `Dialog` (“Reset this session? The transcript
and mock state return to the seeded scenario.” — Forgiveness). Replay
needs no confirmation; it reproduces the identical timeline (7F
determinism contract).
- **Parity-so-far chip:** “Parity · 2 turns pass” (success) or
“Parity · turn 2 fail” (danger), deep-linking the newest parity entry in
the Activity stream.
Scenario switching mid-session prompts the same reset confirmation (the mock
core is seeded per scenario; two scenarios never share mutable state).
## 3. Chat transcript
The conversation composes the frozen SDK chat kit: `conversation`,
`message`, `tool-item`, `reasoning-item`, `Composer`. Three actors render
with three distinct visual grammars (Similarity + Common Region — a stranger
must attribute any row to its actor in two seconds):
1. **User turns** — right-aligned message bubbles on `--pcr-accent-surface`.
2. **Model output** — left-aligned message cards on `--pcr-card` with the
standard streaming affordances (token reveal, jump-to-latest, auto-follow
inherited verbatim from Phase 4b §1; reduced-motion renders instantly).
Reasoning summaries, when the driver emits them, use `reasoning-item`
behind its existing disclosure.
3. **Agent tool work** — semantic calls render inline in the conversation as
collapsed `tool-item` cards on `--pcr-surface-raised`: operation ID
(mono) + disposition badge + one-phrase outcome. Expansion shows the 7D
descriptor fields exactly as in 7F §3.2 (arguments, result, claims,
idempotency, redaction chips). Denied calls render in place on
`--pcr-danger-surface` with the typed denial (operation, missing claim,
reason) — never toast-only, never any protected state.
4. **Control-plane work** — full-width strips on `--pcr-muted` with the
“Control plane” badge and the 7F gutter glyph, copy naming the actor
plainly (“Control plane registered the interaction and scheduled the
continuation wake — no agent tool exists for this.”). Turn 0 seeding uses
this grammar.
**Turn activity strip (new, binding):** after each completed turn the
conversation appends a full-width summary strip for that turn:
`Turn 2 activity · 3 tools · 1 denied · 2 control plane · diff 2 domains ·
parity ✓`, rendered as a single button. Counts are icon + text, never color
alone. Activation opens that turn’s group in the Activity stream (desktop:
scroll + flash the group, same connect cue as 7F authorization rows;
mobile: switch to the Activity segment anchored at the group, with a
“Back to chat” affordance that restores scroll position). The strip is the
guaranteed per-turn bridge — evidence is reachable from the turn itself,
not only by scanning the rail (Recognition over Recall).
The composer is the SDK `Composer` with its existing state machine —
`idle / submitting / active-turn (Steer) / interrupting / disconnected /
terminal` — and keyboard contract (Enter sends, Shift+Enter newline, Escape
moves focus to Stop during a turn). In Replay mode the composer renders
`disconnected`-style with the reason “Scripted replay — switch to Codex
(bounded) to chat live” and the Replay control adjacent; free-text input is
a live-driver capability, not a replay capability.
## 4. Activity stream (per-turn evidence)
**Source of truth:** the ordered run artifact timeline plus per-turn
records (§10 data contract). The stream is grouped by turn; groups render
newest-last and auto-follow the conversation. Each **turn group** is a
collapsible section headed by `Turn N` + the same counts as the turn strip;
the newest turn group is expanded by default, earlier groups collapse to
their header (progressive disclosure; Miller — never all turns fully open).
Inside a group, ordered sections, each rendered only when non-empty, always
in this order:
1. **Exposed tools** — the turn’s exposure record: Always (N) / Optional (N,
each with its unlocking grant) / Control plane — no tool (N), reusing the
7F `ExposureList`. Collapsed to counts by default; the acceptance-critical
full list is one disclosure away.
2. **Calls and results** — one row per semantic call: operation (mono),
disposition badge, outcome; expansion identical to the inline tool card.
Rows and their inline conversation twins highlight each other on
selection (uniform connectedness).
3. **Authorization** — the turn’s slice of the 7F `AuthorizationTable`:
decision (Allow/Deny, icon + text), claims considered, reason,
redactions; denied rows on `--pcr-danger-surface`.
4. **Control plane** — the turn’s control-plane actions with their decision
records (inputs considered, resulting state change, audit reference).
5. **State diff** — the 7F `StateDiff` scoped to the turn: per-domain change
summaries, unchanged domains as one muted row, `field: before → after`
in mono. Wake/reconciliation events (continuation scheduling, blocker
resolution, terminal reconciliation) render here as rows in their
domains, badged “Control plane”.
6. **Parity** — the turn verdict row (icon + text + assertion counts),
expanding to the 7F `ParityPanel` assertion rows for that turn. The
session-level verdict lives in the session footer group.
The stream ends with a **Session** group: cumulative diff against the seed,
session parity verdict, and the restraint note when the correct behavior was
absence (“No further operations — N forbidden operations, none invoked”,
7F §3.2 rule carried over).
A filter chip row at the top of the stream (`All · Tools · Authorization ·
Control plane · Diffs · Parity`) narrows sections across all turns;
filtering never deletes groups, it collapses non-matching sections (the
turn skeleton stays scannable). Filter state is view state, mirrored to the
route (§9), never protocol state.
## 5. Actor distinction (binding summary)
| Actor | Surface | Marker |
| --- | --- | --- |
| User | `--pcr-accent-surface` bubble, right-aligned | — |
| Model output | `--pcr-card` message card, left-aligned | streaming caret |
| Agent tool work | `--pcr-surface-raised` collapsed card, conversation-indented | mono operation + disposition badge |
| Control plane | `--pcr-muted` full-width strip | “Control plane” badge + gutter glyph |
| Denied (any) | `--pcr-danger-surface` in place | Deny icon + text |
No view — chat, activity, expanded detail — may render a fact without its
actor attribution. If a record ever lacks attribution, that is a 7C artifact
defect to fix upstream, not a UI guess.
## 6. Credentials, redaction, denial
7F §5 applies verbatim (including revision 11: redaction chips render
`••• redacted <rule>` inline, no tooltip-only disclosure). Additionally for
chat: the composer submits user text to the package-local runner only; in
Codex mode the provider credential stays server-side behind the same relay
boundary proven in Phase 5, and no user-typed text is ever written to
`localStorage` (a page reload may drop an unsent draft; that is the accepted
trade against persisting conversation content).
## 7. States that must be designed, not defaulted
| State | Surface | Rendering |
| --- | --- | --- |
| No scenario selected | Chat + activity | Intro card (corpus stats + “Pick a scenario to start chatting” + three example deep links); activity rail shows the 7F corpus summary. No dead panels. |
| Seeded, no turns | Chat | Turn-0 control-plane strips render immediately (checkout, exposure); composer focused and ready in Codex mode; primed hint in Replay mode (“Replay the scripted session, or switch to Codex (bounded)”). |
| Turn streaming | All | Model card streams; composer in `active-turn` (Steer + Stop); the pending turn’s activity group shows a live-updating header (“Turn 3 · running”); controls that would corrupt the run (Reset, Replay, mode switch, scenario switch) disable with reasons. |
| Turn failed (harness/driver error) | Chat | Danger strip in place with the exact diagnostic and a “Retry turn” affordance; the partial activity group stays visible and is labeled partial. Never silently reset. |
| Denied operation | Chat + activity | §3/§4 danger rendering; the Activity segment chip counts denials. |
| Relay unavailable | Session header + composer | Codex option disabled with reason; Replay mode fully usable; composer disabled-reason names the relay, not a generic error. |
| Session terminal | Composer | `terminal` state with its existing hint (“This session is closed. Replay it or reset the session state.”). |
| Reset confirm | Dialog | §2; focus returns to the composer after either outcome. |
| Long JSON / long code / long unbroken strings | Everywhere | `overflow-wrap: anywhere` on prose and key-value values; `pre` blocks may scroll **internally** (x) with a visible affordance, but the page never scrolls horizontally at any viewport ≥ 320px — the 7F `min-w-0` discipline is a hard gate at 390×844. |
| Unknown case ID in route | Chat | The 7F named error card (“No scenario named …”) — not a silent home fallback (closes 7F revision 10’s accepted gap for chat routes). |
## 8. Touch, keyboard, focus, accessibility
- Touch targets ≥ `--pcr-touch-target` (2.75rem) for segments, turn strips,
group headers, filter chips, composer buttons.
- Landmarks: `nav` (picker), `main` (chat), `complementary` (activity);
one `h1`; `F6` / `Control+Period` region cycling and the skip link carry
over from 7F (revision 13) with the activity rail as the third region.
- Composer keyboard contract per §3; the turn strip and group headers are
real `button`s; the mobile segment switch preserves and restores scroll
position per segment.
- Focus order on a completed turn: model card → inline tool cards → turn
strip → composer. After “Back to chat”, focus returns to the originating
turn strip.
- Live regions: turn settle announces politely (“Turn 2 settled — 1 denied
operation, parity pass”); a denial announces assertively once, when it
lands (upgrading 7F revision 10’s interim).
- Streaming, flash cues, and auto-follow honor `prefers-reduced-motion`
(instant/static variants, Phase 5 rule). Contrast from existing token
pairs; every status icon + text.
## 9. Routes and determinism
Hash routing, extending the 7F scheme:
```
#/chat chat home (picker + intro)
#/chat/<case-id> live session, seeded (turn 0)
#/chat/<case-id>?replay=fake deterministic scripted session, auto-played
#/chat/<case-id>?replay=fake&view=<chat|activity>&turn=<n>
#/chat/<case-id>?replay=fake&filter=<tools|authorization|control|diff|parity>
```
- `replay=fake` auto-plays the scripted session to completion; when the
conversation and all activity groups are settled the shell sets
`data-chat-state="settled"` (Playwright wait target; `pending` until then).
Fixture time only, no wall-clock values, pixel-identical across loads at
the same viewport — the 7F determinism contract applies unchanged.
- `view=activity` selects the Activity segment on mobile and scrolls the
rail on desktop; `turn=<n>` anchors that turn’s group and expands it.
- Live chat routes (`#/chat/<case-id>` without `replay`) are the primary
board experience and are exempt from pixel determinism; QA acceptance of
live behavior uses the scripted routes plus one real Codex turn.
- Stable `data-testid`s (minimum new set): `chat-shell`, `session-header`,
`session-status`, `chat-conversation`, `chat-turn-<n>`,
`turn-activity-strip-<n>`, `activity-stream`, `activity-turn-<n>`,
`activity-section-<turn>-<name>`, `activity-filter-<name>`, `composer`,
`reset-session`, `replay-session`, `segment-chat`, `segment-activity`.
7F testids keep their meanings.
## 10. Package-local implementation guidance (7I)
- **Reuse first.** The chat column composes the SDK chat kit
(`conversation`, `message`, `tool-item`, `reasoning-item`, `Composer`,
`Dialog`, `Badge`, `Banner`) — `RunnerConsoleApp` itself is *not* the
shell (same 7F ruling: compose parts, don’t adopt the console’s
session-timeline IA). The activity stream composes the existing explorer
components: `ExposureList`, `AuthorizationTable`, `StateDiff`,
`ParityPanel`, `ChannelEntry`, `RedactionChip`.
- **New explorer-local components** (one job each): `SessionHeader`,
`TurnStrip` (the per-turn summary button), `ActivityStream` (turn
grouping + filter chips), `ActivityTurnGroup`. Tokens only; the two
candidate new tokens (`--pcr-activity-width: 26rem`,
`--pcr-touch-target` if not yet minted) are proposed as token additions
in the 7I PR, never inlined.
- **Data contract extension (owned by the runtime, not the UI):** timeline
entries gain a `turn` number (0 = session seed); the artifact gains
`turns[]` with per-turn `exposure`, `authorizationRecords`, `diff`,
`reconciliationEvents`, and `parity`, each shaped exactly like their
session-level 7F counterparts. A `user_message` timeline kind joins the
existing five kinds. The session-level records remain and feed the
Session group. The UI must not derive turn boundaries, per-turn diffs, or
per-turn verdicts itself — if the runtime can’t supply a per-turn record,
extend the runtime.
- **Driver seam:** Replay mode consumes recorded fixtures; Codex mode uses
the bounded driver through the Phase 5 relay. The chat UI is
driver-agnostic — it renders whatever the artifact stream says happened.
- **Testing hooks 7I must ship:** component tests for `TurnStrip`,
`ActivityStream` grouping/filtering, and per-turn diff/parity rendering;
keyboard tests (composer contract, strip→group→back-to-chat focus
round-trip, region cycling with the new rail); the §9 route determinism
test; a 390×844 no-horizontal-scroll assertion on a long-JSON fixture;
reduced-motion test; screenshot capture script for §11.
## 11. Screenshot acceptance matrix
Evidence lands in `knowledge/evidence/phase-07/` as
`phase-7i-<slug>-<viewport>.png` at **1440×900** and **390×844**, captured
from §9 routes after `data-chat-state="settled"`. Mobile captures must show
the correct active segment and no horizontal scrollbar.
| # | Slug | Route | Expected visible evidence |
| --- | --- | --- | --- |
| 1 | `chat-home` | `#/chat` | Picker + chat intro; no dead panels. |
| 2 | `chat-session` | `#/chat/ix-checkbox-01?replay=fake` | Turn-0 control-plane strips; user/model/tool/control-plane grammars all distinguishable; turn activity strips with counts; composer in its mode-correct state; desktop shows the activity rail with the newest turn expanded. |
| 3 | `chat-denied` | `#/chat/ap-mcp-gate-01?replay=fake&view=activity&turn=1` | Denied call on danger surface in both conversation and authorization section; missing claim + reason; Activity segment chip counting the deny (mobile). |
| 4 | `chat-activity-diff` | `#/chat/ix-checkbox-01?replay=fake&view=activity&turn=2` | Turn group with exposure counts, calls, authorization, control-plane rows, per-turn state diff (changed vs unchanged domains), reconciliation/wake row, turn parity verdict. |
| 5 | `chat-streaming` | live route, staged | Streaming model card, composer `active-turn` (Steer/Stop), pending activity group header, disabled session controls with reasons. Exempt from pixel determinism; capture once per viewport. |
The scripted fixtures for `ix-checkbox-01` and `ap-mcp-gate-01` chat
sessions are new 7I fixtures (the 7F fake-agent plans are single-shot, not
conversational); they must script at least two user turns each so turn
grouping is actually exercised.
Acceptance procedure: UXDesigner reviews all ten images against this matrix
before 7I implementation is accepted; deviations are fixed or recorded as
revisions in this document — silent divergence fails the gate (7F §9 rule).
## 12. Out of scope / rejected
- All 7F §10 rejections stand (no Tailwind/Radix, no dark mode, no markdown
renderer, no virtualization, no parity re-judging, no live-Paperclip
affordances).
- **No bottom-sheet drawer on mobile.** The segmented Chat · Activity
pattern is proven, keeps one React tree, and route-mirrors; a drawer adds
a new component and an un-linkable state for no evidence gain. Rejected
after comparison, not by omission.
- **No hiding of activity behind a “developer mode”.** Visible mock
activity is the phase’s purpose; there is no toggle that suppresses the
activity stream or turn strips.
- **No per-message edit/regenerate affordances.** The mock core’s
determinism story is session-scoped (reset/replay), not message-scoped.
- Multi-session tabs, session persistence across reloads, and export are
out of scope for the demo.
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB