mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 23:36:51 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The runner package already provides the production protocol and execution spine. > - Contributors still need stable SDK surfaces, deterministic test tools, and local inspection tools. > - Those surfaces share generated contracts and must change as one package boundary. > - This pull request adds the package-local SDK, labs, examples, and drift checks. > - The benefit is a reviewable developer platform that does not change application execution selection. ## Linked Issues or Issue Description **Subsystem affected** `packages/paperclip-runner` — runner SDK, conformance tools, and developer tooling. **Problem or motivation** The production runner spine is present, but package consumers cannot build deterministic integrations, inspect sessions, or verify provider-neutral behavior through supported surfaces. **Proposed solution** Add browser, React, standalone, live-session, scenario, conformance, and evaluation surfaces. Add generated contract inventories and package-local verification scripts. Keep production application routing unchanged. **Alternatives considered** We considered splitting each generated catalog, SDK surface, and demo into separate pull requests. Those changes share exports, fixtures, and drift gates. Splitting them would create intermediate package states that do not build. **Roadmap alignment** No overlapping item appears in `ROADMAP.md`. This work extends the runner package that is already on `master`. ## What Changed - Add browser, React, standalone, live-session, and issue-thread SDK surfaces. - Add deterministic mock control-plane, scenario, conformance, replay, and evaluation tools. - Add bounded Codex, OpenCode, and ACPX development transports and fixtures. - Keep deferred managed-provider execution fail-closed. Persisted compatibility data remains readable. - Add generated capability inventories with their source files and drift checks. - Add examples, package documentation, browser checks, and clean-consumer checks. - Preserve the reviewed protocol bounds, replay compatibility aliases, process environment isolation, and semantic redaction limits. - Update the ACPX package patch that the existing workspace patch registry already tracks. - Do not change `pnpm-lock.yaml`, repository workflows, server runtime selection, or the application UI. ## Verification GitHub Actions is the verification authority for this pull request. The repository CI, package TypeScript and Rust checks, package tests, generated-output drift checks, browser checks, security scans, and Greptile review must pass on the exact head. Local test suites were not run because this series uses parallel GitHub Actions for verification. ## Risks This is a large greenfield package change. The main risks are public export drift, generated-output drift, and optional React consumer compatibility. Package boundary checks, clean-consumer checks, and browser tests cover those risks. Production adapter selection and server execution are outside this pull request. ## Stack 1. **This PR:** runner SDK and developer tooling. 2. [Codex production server integration](https://github.com/paperclipai/paperclip/pull/12616). 3. [Provider-neutral task-thread UI](https://github.com/paperclipai/paperclip/pull/12617). ## Model Used OpenAI Codex, GPT-5, high-reasoning mode, with tool use and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have described the issue in-PR following the feature request template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
430 lines
30 KiB
Markdown
430 lines
30 KiB
Markdown
# Capability — Issue-Thread UX Contract
|
||
|
||
Status: **binding** for track 7G (Paperclip-style Web UI) and reviewable by tracks 7E/7K.
|
||
Source of authority: Capability plan (TASK-16897 `plan` document), Revision 3 §"Page information
|
||
architecture", §"Native issue-thread interactions", §"Live execution contract", and Revision 4
|
||
§"Final evidence eligibility". Capability vocabulary: Capability generated contract
|
||
(`generated/capability/capabilities.yaml`, `mcp-tool-map.yaml`, `eval-traceability.yaml`,
|
||
`contract-schema.json` — TASK-16939).
|
||
|
||
If a corner of this contract is underspecified, 7G comments on TASK-16940 and waits for a
|
||
revision. 7G does not diverge silently. Deviations are either fixed in the implementation or
|
||
written back into this document as an explicit revision.
|
||
|
||
---
|
||
|
||
## 0. Vocabulary (normative)
|
||
|
||
All UI copy and all implementation identifiers use the 7A terms. The UI never invents
|
||
capability language.
|
||
|
||
- **Dispositions** (closed enum): `control_plane_owned`, `always_agent_tool`,
|
||
`optional_agent_tool`.
|
||
- **Always semantic operations** (thread-visible verbs): `get_task_context`,
|
||
`report_progress`, `answer_status_question`, `finish_task`, `block_task`, `request_review`,
|
||
`write_document` (+ revision reads), `request_human_input` (kinds `questions`,
|
||
`confirmation`, `checkbox`, `suggest_tasks`, `item_verdicts`), `register_deliverable`.
|
||
- **Optional operation families**: discovery (`scoped_discovery`, `search_tasks`,
|
||
`list_agents`, …), delegation/dependencies (`create_task`, `delegate_task`,
|
||
`set_dependencies`, `create_blocked_task`), approvals (`request_approval`,
|
||
`decide_approval`, …), cases, workspace runtime, routines, company skills, secrets, admin,
|
||
escape hatch. Grants render exactly as 7A writes them (e.g. `rf:read_or_write`).
|
||
- **Mock-state kinds**: `operation_result`, `runtime_decision_record`, `active_task_context`.
|
||
- **Eval keys**: case `id` (e.g. `ix-confirmation-plan-01`), `group` (16 groups),
|
||
`fixtureProfile` (e.g. `hb-baseline`), `browserEvidenceRecipe` (e.g. `hb/hb-context-01`),
|
||
`expectedSemanticOperations`, `forbiddenOperations`.
|
||
- **Modes**: `live` (real Codex via real runnerd), `fake` (deterministic fake agent),
|
||
`replay` (recorded canonical events). A surface never shows an unlabeled mode.
|
||
|
||
Human-readable labels (UI copy) for the dispositions are fixed: `Agent tool — always`,
|
||
`Agent tool — granted`, `Control plane`. The machine term appears in the debug panel and in
|
||
tooltips, never invented synonyms ("system tool", "built-in", etc. are forbidden).
|
||
|
||
## 1. Identity and mode banner (always visible)
|
||
|
||
The page must answer, at all times and on every viewport, four questions: who is the agent,
|
||
what runs it, what control plane it talks to, and whether this session is live.
|
||
|
||
1. **Three identity chips**, rendered in the issue header and never hidden by scroll
|
||
(header is sticky):
|
||
- `Real Codex` — cyan chip, only when the active session mode is `live`. In `fake` mode the
|
||
chip reads `Fake agent`; in `replay` it reads `Replay`.
|
||
- `Real runnerd` — neutral chip with a live process dot (pulse while a runnerd session is
|
||
attached; static gray when detached). Shown in `live` mode only; `fake`/`replay` show
|
||
`In-process runner`.
|
||
- `Mock Paperclip` — amber chip, shown in **all** modes. Tooltip: "All issue records are
|
||
mock. No real Paperclip API is reachable."
|
||
2. **Mode is data, not styling**: the root element carries `data-session-mode="live|fake|replay"`
|
||
and the chips render from the server-reported session record. A session artifact with
|
||
`mode=fake` must be visually distinguishable from `live` in every screenshot (Revision 4
|
||
evidence eligibility).
|
||
3. **Mock identifier scheme**: mock issues use a reserved prefix that cannot be confused with
|
||
a real company (fixture default: `MCK-<n>`, e.g. `MCK-31`). The UI never renders a
|
||
`/PAP/...`-style link to a real control plane; mock entity links navigate inside the
|
||
explorer only.
|
||
4. **No credential surface**: no header, panel, tooltip, error, or diff may contain a token,
|
||
key, or `Authorization` value. Redacted fields render as `••• redacted` with the redaction
|
||
rule name from the authorization record.
|
||
|
||
## 2. Page information architecture
|
||
|
||
### 2.1 Desktop (reference viewport 1440×900)
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┬───────────────┐
|
||
│ Issue header (sticky): │ │
|
||
│ MCK-31 · title · StatusBadge · PriorityIcon · assignee │ Debug panel │
|
||
│ [Fake agent|Real Codex] [Real runnerd] [Mock Paperclip] │ (collapsed │
|
||
│ run state · Scenario ▾ · Replay · Reset · Stop │ by default; │
|
||
├──────────────────────────────────────────────────────────────┤ resizable │
|
||
│ Thread column (centered, max-width 760px, single column): │ 320–640px) │
|
||
│ turn groups: user message → agent activity → responses │ │
|
||
│ comment / interaction / document / deliverable / │ │
|
||
│ disposition cards in chronological order │ │
|
||
│ system notices (wakes, reconciliation) as one-line rows │ │
|
||
├──────────────────────────────────────────────────────────────┤ │
|
||
│ Composer (pinned bottom of thread column) │ │
|
||
└──────────────────────────────────────────────────────────────┴───────────────┘
|
||
```
|
||
|
||
- The **thread is the primary readable surface**. Default panel state is **collapsed**; the
|
||
user's open/width choice persists per browser (`localStorage`), and deep links may force it
|
||
(§10). The page never leads with raw protocol events.
|
||
- Thread column measure: max-width 760px, centered in the remaining space; `text-sm` body
|
||
scale per the Paperclip type ramp.
|
||
- Debug panel: right side, resizable 320–640px with a keyboard-operable splitter
|
||
(`role="separator"`, `aria-valuenow`), collapse toggle in the header (`Evidence` button with
|
||
open/closed state).
|
||
- Below 1100px viewport width the panel switches from side-by-side to an overlay sheet from
|
||
the right (same content, same tab order), so the thread column never drops below ~600px.
|
||
|
||
### 2.2 Mobile (reference viewport 390×844)
|
||
|
||
- One column. Sticky condensed header: row 1 = `MCK-31` + StatusBadge + overflow menu `⋯`
|
||
(Scenario, Replay, Reset inside the menu; **Stop stays outside the menu** whenever a turn is
|
||
active). Row 2 = the three identity chips, wrapping to a second line if needed — chips never
|
||
cause horizontal page scroll.
|
||
- A **segmented control** with exactly two segments — `Thread` and `Evidence` — sits under the
|
||
header (pattern validated in the Scenario chat mobile review; a drawer was rejected there and
|
||
stays rejected). Thread is default. Badge on `Evidence` shows the current turn's
|
||
authorization-denial count when nonzero.
|
||
- Composer is fixed to the bottom of the `Thread` segment, above the keyboard inset
|
||
(`env(safe-area-inset-bottom)`).
|
||
- **No horizontal page scroll at 390px** (`document.scrollingElement.scrollWidth <=
|
||
clientWidth` is an automated acceptance check). `pre`/code/diff blocks wrap or scroll inside
|
||
their own container only.
|
||
- Touch targets ≥ 44×44 CSS px for every actionable element, including chip tooltips
|
||
(tap-to-toggle on touch), accordion headers, and interaction-card controls.
|
||
|
||
## 3. Thread item taxonomy
|
||
|
||
Every thread item renders from a **mock-core record or canonical event** — the browser holds
|
||
no state authority, computes no claim/policy/parity result, and never mutates mock state
|
||
outside an interaction response (§5). Items in chronological order, grouped by turn:
|
||
|
||
| # | Item | Source | Anatomy |
|
||
|---|------|--------|---------|
|
||
| T1 | **User message** | thread record | Right-aligned bubble style is **not** used; Paperclip comment card with author "You (board user)", timestamp, markdown body. |
|
||
| T2 | **Agent response** | model output items | Comment card, author = agent identity chip (`Fake agent` / `Real Codex`), streaming state per §6. Model prose only — never confused with durable records (see T3). |
|
||
| T3 | **Durable progress comment** | `report_progress` / `answer_status_question` `operation_result` | Distinct comment card with a `Recorded to mock thread` marker (filled corner tag + tooltip naming the semantic operation). This is the visual boundary between ephemeral model text (T2) and durable mock records. |
|
||
| T4 | **Tool activity strip** | semantic call + typed result | One line per call inside the turn group: status glyph (`✓ ok`, `✕ denied`, `⏳ running`), operation id (`write_document`), one-line human summary, `›` expander. Expanded: request args (redacted per rules), typed result, and a `View in Evidence` link that opens the debug panel pre-filtered to that call. Strips are collapsed by default; a turn shows at most 3 strips + `N more…` expander (progressive disclosure). |
|
||
| T5 | **Interaction card** | `request_human_input` record | §5. Rendered at its chronological position. |
|
||
| T6 | **Document card** | `write_document` result | Document key + title, revision chain (`r3 → r4`), author, `View diff` (opens Evidence → State), and stale marker when a later revision exists. |
|
||
| T7 | **Deliverable card** | `register_deliverable` result | File/ref name, kind (attachment bytes / external ref / workspace file), size, registered-by, download affordance for attachment-backed deliverables. |
|
||
| T8 | **Dependency / delegation card** | optional-op results (`create_task`, `set_dependencies`, …) | Created mock child issues with identifiers and blocker edges (`MCK-32 blocks MCK-31`). |
|
||
| T9 | **Disposition card** | `finish_task` / `block_task` / `request_review` result | Terminal banner card: new status via StatusBadge, explanation body, named blocker owner where applicable. After a terminal disposition the composer enters `disabled` (§6). |
|
||
| T10 | **Denial notice** | typed policy denial (`operation_result` with deny) | Inline red-bordered strip variant of T4: `✕ create_task — denied: missing grant su:read_or_write`. Shows deny reason **from the authorization record verbatim**; never leaks protected state or credentials. |
|
||
| T11 | **System notice** | `runtime_decision_record` (wake, checkout, reconciliation, budget stop, session events) | One-line, muted, icon + text (e.g. `⚙ Wake: issue_blockers_resolved → turn 3 started`). Progressive disclosure into Evidence → Control plane. Never a card; system notices must read quieter than work content. |
|
||
|
||
Turn grouping: each turn renders a hairline group header `Turn N · <mode> · <n> tool calls ·
|
||
<hh:mm:ss>` binding T2/T4/T5-T10 items produced within it. Thread auto-follows the newest item
|
||
only while the user is at the bottom; a `Jump to latest` pill appears when scrolled up
|
||
(≥ 300px) or when new items arrive off-screen.
|
||
|
||
## 4. Composer contract
|
||
|
||
States (mutually exclusive; `data-composer-state` attribute is the test/screenshot hook):
|
||
|
||
| State | Trigger | Visual | Controls |
|
||
|-------|---------|--------|----------|
|
||
| `ready` | session attached, no active turn, issue non-terminal | normal input, `Send` primary | Send (Cmd/Ctrl+Enter), attach disabled in Capability |
|
||
| `sending` | message posted, turn not yet streaming | input cleared, inline spinner on Send | Send disabled |
|
||
| `streaming` | active turn | input stays **editable** (steer), primary button becomes `Stop` (destructive-outline), helper text `Codex is working — send to steer, or stop the turn.` | Send = steer (queued as next user input), Stop |
|
||
| `waiting` | pending interaction card requires the user | input disabled, helper `Answer the pending request above to continue.` with an anchor link that scrolls to and focuses the pending card | none |
|
||
| `reconnecting` | transport lost | input disabled, helper `Reconnecting… your session is preserved.` | Retry now |
|
||
| `disabled` | terminal disposition, replay mode, or budget stop | input disabled with reason line (`Issue is done`, `Replay is read-only`, `Budget limit reached`) | New scenario / Reset |
|
||
|
||
Draft text survives refresh (localStorage per session id). Stop never discards the transcript;
|
||
a stopped turn's partial output stays in the thread with a `Stopped by user` marker on the
|
||
turn header.
|
||
|
||
## 5. Interaction cards (native lifecycle)
|
||
|
||
All five `request_human_input` kinds render as native cards using the typed payload's prompt,
|
||
options, labels, and validation — never as markdown asking the user to type an answer.
|
||
|
||
Kinds and their answer controls:
|
||
|
||
- `questions` (ask_user_questions): typed form — per-question control (radio/select/short
|
||
text), one submit.
|
||
- `confirmation` (request_confirmation): target summary + Accept / Reject buttons; reject
|
||
reason textarea when the payload requires it. **Revision-bound**: the card shows the target
|
||
(`plan · r4`) and links the exact revision.
|
||
- `checkbox` (request_checkbox_confirmation): checkbox list with min/max enforcement,
|
||
default-selected ids, Accept label / Reject label from payload.
|
||
- `suggest_tasks`: proposed-task list (title + description); board accepts a subset; accepted
|
||
tasks appear as T8 cards afterwards.
|
||
- `item_verdicts` (request_item_verdicts): per-item Approve / Reject / Defer segmented
|
||
buttons; reason required per `requireReasonOn`; supports partial submit (submitted items
|
||
lock, remaining stay editable, card stays `pending`).
|
||
|
||
**Response authority path (normative):** the card's submit posts the typed response to the
|
||
package server; the **mock control plane stores the response before the runner receives it**,
|
||
and only then does the same Codex session resume with the typed result. The card's UI state
|
||
moves `pending → submitting → resolved` only on server acknowledgment (no optimistic
|
||
resolution). This is the *only* browser-initiated mock mutation; there is no other write path
|
||
from the page.
|
||
|
||
**State matrix (all states are required and visually distinct):**
|
||
|
||
| State | Store shape | Card treatment |
|
||
|-------|------------|----------------|
|
||
| `pending` | status `pending` | Accent left border (violet), controls enabled, `Waiting for you` chip, focus lands on first control when the card is the reason the composer is `waiting`. |
|
||
| `submitting` | in flight | Controls disabled, inline spinner. |
|
||
| `accepted` | status `accepted` | Green check chip `Accepted`, chosen values summarized inline, controls collapse to read-only summary. |
|
||
| `answered` | status `answered` | Same as accepted with `Answered` chip (questions / verdicts complete). |
|
||
| `rejected` | status `rejected` | Red chip `Changes requested`, reason quoted in the card. |
|
||
| `stale_target` | status `expired`, `result.outcome=stale_target` | Gray chip `Stale — plan moved to r5`, link to the superseding revision, controls removed, body dimmed (see revision 2). |
|
||
| `superseded_by_comment` | status `expired`, `result.outcome=superseded_by_comment` | Gray chip `Superseded by a later comment`, link to that comment. |
|
||
| `expired` | status `expired`, no outcome | Gray chip `Expired <relative time>`. |
|
||
| `withdrawn` | status `cancelled`, `result.outcome=withdrawn` | Gray chip `Withdrawn`, optional reason. |
|
||
| `issue_closed` | status `cancelled`/`expired`, `result.outcome=issue_closed` | Gray chip `Issue closed`. |
|
||
|
||
Resolved/expired cards are durable history: they stay in the thread at their chronological
|
||
position, are keyboard-reachable, and expose `View request evidence` linking the debug panel
|
||
to the related request, policy decision, mock mutation, wake, and resume events (the plan's
|
||
required debug linkage). Pending cards survive refresh and reconnect (§6) — rehydrated from
|
||
mock state, not from browser memory.
|
||
|
||
## 6. Session lifecycle behaviors
|
||
|
||
- **Refresh (F5)**: full restore from server state — same session, same turn ids, transcript,
|
||
pending interaction cards, composer state, and mode chips. The session **never restarts
|
||
because the browser reconnected**. Scroll restores to latest; a `Restored session` system
|
||
notice (T11) is *not* emitted (silent restore) — the evidence panel's session record is the
|
||
proof.
|
||
- **Reconnect**: on transport drop the composer enters `reconnecting`, a slim amber banner
|
||
pins under the header (`Connection lost — retrying (attempt n)`), and streaming indicators
|
||
freeze with a `paused` glyph. On reconnect, missed canonical events replay by ordinal (no
|
||
duplicates, no gaps) and the banner resolves to a 3s `Reconnected` confirmation.
|
||
`prefers-reduced-motion` replaces the banner slide with opacity.
|
||
- **Stop**: header Stop and composer Stop are the same action — bounded cancel of the active
|
||
turn. Post-state: partial output retained + `Stopped by user` turn marker; session and
|
||
pending interactions unaffected; composer returns to `ready`.
|
||
- **Reset**: destructive — always behind a confirm dialog (`Reset scenario? This clears the
|
||
mock state and starts a clean session. The transcript will be lost.`; confirm button
|
||
`Reset scenario`, destructive style; cancel is default focus). Reset re-seeds the fixture,
|
||
rotates/clears session authority (Revision 4), and lands on a clean thread with a fresh
|
||
`Turn 0` seeded context. Reset affects only the current browser session's scenario instance.
|
||
- **Replay**: mode `replay` re-renders a recorded run from canonical events. Composer
|
||
`disabled` (`Replay is read-only`), identity chip row shows `Replay` + `Mock Paperclip`,
|
||
and a top progress strip allows step/next-turn/play-all with a deterministic `?at=<ordinal>`
|
||
deep-link parameter. Replay of a `fake` recording must still be labeled as fake-derived
|
||
(chip `Replay · fake source`) so replay evidence can never satisfy a live criterion.
|
||
- **Stop/Reset/Replay/Scenario controls** live in the header on desktop; on mobile
|
||
Replay/Reset/Scenario collapse into `⋯`, Stop stays exposed while a turn is active (§2.2).
|
||
|
||
## 7. Side debug panel (Evidence)
|
||
|
||
Named **Evidence** in UI copy. Content scope: a **turn selector** at the top (`Turn N ▾`,
|
||
default = latest; `All turns` option for the state and parity sections). Below it, eight
|
||
accordion sections in this order (accordion, not tabs — >7 categories, and multiple sections
|
||
must be open simultaneously for review):
|
||
|
||
1. **Tools exposed** — the turn's visible tool list grouped `Agent tool — always`, then
|
||
`Agent tool — granted` (each with its grant, e.g. `rf:read_or_write`), then a separated
|
||
muted list `Control plane (not exposed to the agent)` naming `control_plane_owned`
|
||
operations relevant to the fixture. Negative evidence is first-class: the control-plane
|
||
list exists precisely to show what the model *cannot* call.
|
||
2. **Calls & results** — chronological semantic calls: raw Codex request → dispatched command
|
||
→ typed result/denial, with operation id, version, and redaction annotations.
|
||
3. **Authorization** — one record per decision: operation, claims considered, allow/deny +
|
||
reason, redactions applied, resulting state change ref. Deny records use the same red
|
||
accent as T10.
|
||
4. **Control plane** — `runtime_decision_record` stream: checkout/lock, wake scheduling,
|
||
budget, idempotency/retry, reconciliation, session lifecycle.
|
||
5. **Runner & events** — canonical PRP events and runnerd/Codex process diagnostics (session
|
||
id, thread id, process state, cleanup evidence).
|
||
6. **State diff** — before/after per entity class (tasks, comments, documents, interactions,
|
||
approvals, artifacts, blockers, workspace, budget, run). Per-turn by default; `All turns`
|
||
shows fixture-seed → current. Rendered from immutable snapshots; the browser never
|
||
computes a diff from its own bookkeeping.
|
||
7. **Traceability** — the fixture's 7A anchors: capability rows (id + `sourceAnchor`), eval
|
||
case id/group/`browserEvidenceRecipe`, `expectedSemanticOperations`,
|
||
`forbiddenOperations`, `requiredCapabilityGrants`.
|
||
8. **Parity** — assertion list with verdict chips (`pass` green / `fail` red /
|
||
`intentional gap` gray + note), summarized as `n/m` in the section header.
|
||
|
||
Cross-linking contract: every T4 strip, T10 denial, and interaction card deep-links into the
|
||
matching Evidence record (`View in Evidence`), and every Evidence record links back to its
|
||
thread anchor. Deep-link target = section + record id, e.g. `?panel=authorization&rec=<id>`.
|
||
|
||
Mobile: the same eight sections render inside the `Evidence` segment, full-width accordions,
|
||
turn selector pinned under the segmented control.
|
||
|
||
## 8. Visual language
|
||
|
||
Follow the Paperclip design language without importing the product `ui/` package:
|
||
|
||
- Dark theme default, OKLCH neutral grays; semantic tokens only (background/card/muted/
|
||
accent/destructive/border/ring equivalents defined package-locally). No raw hex in
|
||
components.
|
||
- Type ramp: page title `text-xl font-bold`; card titles `text-sm font-medium`; body
|
||
`text-sm`; metadata `text-xs text-muted-foreground`; identifiers and operation ids
|
||
`font-mono text-xs`.
|
||
- Status/priority renders with StatusBadge/StatusIcon-equivalent components using the
|
||
product's status hue table (todo blue, in_progress indigo, in_review violet, done green,
|
||
blocked red, backlog/cancelled gray).
|
||
- Radii ≤ `rounded-xl`; shadows ≤ `shadow-sm`; density = product issue page, not a marketing
|
||
layout.
|
||
- Interaction cards use the product interaction-card anatomy (title row + prompt + controls +
|
||
state chip) so the mock thread reads as a Paperclip issue thread (Jakob's Law is the point
|
||
of this phase's demo).
|
||
|
||
## 9. Accessibility acceptance (blocking)
|
||
|
||
7G is not acceptable until all of these pass; 7K re-verifies them clean-room:
|
||
|
||
1. **Keyboard tour** (documented, testable): Tab order = header controls → thread (each card
|
||
is a focusable group; Enter expands) → composer → Evidence toggle → panel. The splitter is
|
||
arrow-key resizable. Pending interaction controls are reachable without pointer; Escape
|
||
closes the mobile `⋯` menu and the desktop overlay sheet.
|
||
2. **Focus management**: opening Evidence moves focus to its heading; resolving a card moves
|
||
focus to the card's state chip; `waiting` composer's anchor link moves focus to the pending
|
||
card's first control. Focus ring = 3px ring token, never suppressed.
|
||
3. **Live regions**: streaming agent text in `aria-live="polite"` chunk announcements (throttled
|
||
≥ 2s); turn completion, denial notices, and interaction resolution announce via a single
|
||
polite status region; reconnect banner is `role="status"`, Stop confirmation `role="alert"`.
|
||
4. **Structure**: one `h1` (issue title), landmarks `header/main/complementary` (Evidence),
|
||
`form` for composer; interaction cards are `section`s labeled by their prompt; tool strips
|
||
are disclosure buttons with `aria-expanded`.
|
||
5. **Color independence**: every state chip pairs color with a glyph + text (`✓ Accepted`,
|
||
`✕ Denied`, `⏳`); parity verdicts likewise. Contrast ≥ 4.5:1 for text, ≥ 3:1 for UI
|
||
glyphs, verified in dark theme.
|
||
6. **Reduced motion**: `prefers-reduced-motion` disables pulse dots, banner slides, streaming
|
||
shimmer, and smooth scrolling (instant jumps).
|
||
7. **Automated gate**: axe (or equivalent) run against every screenshot route in §10 with
|
||
zero serious/critical violations, executed in CI alongside screenshot capture.
|
||
|
||
## 10. Deterministic screenshot contract
|
||
|
||
### 10.1 Route scheme
|
||
|
||
- Base route: `#/issue/<fixtureProfile>` (e.g. `#/issue/hb-baseline`). Scenario/fixture ids
|
||
come from 7A `fixtureProfile`; per-case evidence uses the 7A `browserEvidenceRecipe` path
|
||
(`<group>/<case-id>`) as the canonical evidence id.
|
||
- Screenshot state param: `?shot=<slug>` seeds the named deterministic state below in `fake`
|
||
mode with: fixed clock (all timestamps render from fixture time), animations/caret/pulse
|
||
disabled, network idle, fonts loaded.
|
||
- Panel/deep-link params: `?panel=<section>[&rec=<id>]`, `?at=<ordinal>` (replay),
|
||
`?seg=thread|evidence` (mobile segment).
|
||
- Settle signal: the root element sets `data-thread-state="settled"` when hydration, fixture
|
||
load, and auto-scroll are complete. Capture tooling waits for it — never for timeouts.
|
||
|
||
### 10.2 Required matrix (12 slugs × 2 viewports = 24 PNGs)
|
||
|
||
Viewports: desktop `1440×900`, mobile `390×844`. Output path:
|
||
`.paperclip-local/evidence/capability/ui/<slug>--<desktop|mobile>.png` (package-local). Every capture
|
||
also asserts `scrollWidth <= clientWidth` on the scrolling element at 390×844.
|
||
|
||
| Slug | Seeded state | Must be visible |
|
||
|------|--------------|-----------------|
|
||
| `thread-baseline` | settled 3-turn fake run on `hb-baseline` | header w/ 3 identity chips + mode; T1/T2/T3/T4 items; turn headers; composer `ready` |
|
||
| `turn-streaming` | mid-turn stream | streaming indicator, composer `streaming` w/ Stop, editable steer input |
|
||
| `interaction-question-pending` | `ix-questions-01` pending card | typed question form, `Waiting for you` chip, composer `waiting` w/ anchor helper |
|
||
| `interaction-confirmation-pending` | `ix-confirmation-plan-01` pending | revision-bound target (`plan · r4`) on card, Accept/Reject, reject-reason affordance |
|
||
| `interaction-resolved-mixed` | history incl. accepted + rejected + `stale_target` + `superseded_by_comment` | four visually distinct resolved/expired treatments per §5 |
|
||
| `denial-optional-tool` | denied `create_task` (missing `su:read_or_write`) | T10 denial strip w/ verbatim deny reason; Evidence badge increment |
|
||
| `document-revision` | `dp-plan-doc-01` after `write_document` | T6 card w/ revision chain `r3 → r4` + View diff |
|
||
| `deliverable-registered` | `ar-upload-before-done-01` | T7 deliverable card w/ kind + registered-by |
|
||
| `disposition-terminal` | `st-done-comment-01` finished | T9 terminal card, StatusBadge `done`, composer `disabled` w/ reason |
|
||
| `debug-panel-open` | baseline + `?panel=authorization` | desktop: panel open at 384px w/ 8 sections, authorization records; mobile: `Evidence` segment active |
|
||
| `reconnect-banner` | forced transport drop | amber reconnect banner, composer `reconnecting`, frozen stream glyph |
|
||
| `replay-mode` | replay of recorded fake run `?at=12` | `Replay · fake source` chip, read-only composer, progress strip |
|
||
|
||
Determinism rule: two captures of the same slug/viewport from a clean checkout must be
|
||
pixel-identical (the Scenario chat byte-identical bar). Anything time-, random-, or
|
||
locale-dependent renders from fixture data.
|
||
|
||
### 10.3 Evidence naming
|
||
|
||
Per-eval-case evidence (7F/7K scope) reuses `browserEvidenceRecipe` verbatim:
|
||
`.paperclip-local/evidence/capability/cases/<group>/<case-id>--<viewport>.png`. The §10.2 matrix is
|
||
the UI acceptance set; case evidence is additive and follows the same settle/determinism
|
||
rules.
|
||
|
||
## 11. Authority and safety rules (UI-side restatement)
|
||
|
||
- The browser renders mock-core records, snapshots, and canonical events. It computes no
|
||
claim, policy, diff, or parity result client-side. UI-side state math is a defect.
|
||
- The only browser-initiated mock mutation is an interaction response (§5). Composer messages
|
||
go to the runner session, not to mock state.
|
||
- No provider, runner, or control-plane credential ever reaches the browser; redactions render
|
||
by rule name. Real Paperclip URLs/API paths never appear.
|
||
- Policy and state authority live in the package server + mock `ControlPlanePort`; refresh
|
||
and reconnect re-derive everything from them.
|
||
|
||
## 12. 7G handoff checklist
|
||
|
||
Implementation acceptance (UXDesigner review) requires:
|
||
|
||
1. All §3 item types and all §5 interaction states implemented and reachable via fixtures.
|
||
2. All §4 composer states with `data-composer-state` hooks.
|
||
3. §6 behaviors demonstrated: refresh restore, reconnect replay, stop, reset confirm, replay
|
||
read-only.
|
||
4. §7 Evidence panel with all eight sections, turn selector, and bidirectional deep links.
|
||
5. §10 matrix: 24 deterministic PNGs recorded at the named routes, byte-stable across two
|
||
clean runs, plus the 390px no-horizontal-scroll assertion per capture.
|
||
6. §9 accessibility gate green (axe + documented keyboard tour).
|
||
7. Screenshot review posted to the 7G issue for UXDesigner acceptance before 7G closes.
|
||
|
||
Questions or gaps → comment on TASK-16940.
|
||
|
||
---
|
||
|
||
## Revisions
|
||
|
||
### Revision 2 — expired-family dimming (2026-08-10, written back by 7G)
|
||
|
||
Revision 1 specified a literal 60% opacity on the body of `stale_target` and the
|
||
other expired-family cards (§5). Measured against the card surface in the dark
|
||
theme, that renders the card's text at ~3.16:1, which fails §9.5's 4.5:1 bar and
|
||
the blocking axe gate in §9.7 — the two rules cannot both hold.
|
||
|
||
§9 wins because it is the blocking gate. "Dimmed" is now specified as a
|
||
**recessed treatment**: the card drops to the sunken surface token and its
|
||
secondary text drops to the muted-foreground token, both of which clear 4.5:1.
|
||
The gray state chip, removed controls, and neutral left border are unchanged, so
|
||
the card still reads as history at a glance.
|
||
|
||
Implemented in `devtools/issue-thread/src/issue-thread.css` and covered by the
|
||
axe gate on the `interaction-resolved-mixed` slug at both viewports.
|
||
|
||
### Revision 3 — `interaction-resolved-mixed` capture framing (2026-08-10, written by the contract owner during the 7G gate)
|
||
|
||
Revision 1's §10.2 row required four resolved/expired card treatments visible
|
||
in the `interaction-resolved-mixed` frame. Two of this contract's own rules
|
||
make that impossible in one capture: §10.1's settle signal ends with
|
||
auto-scroll to the latest thread item, and the §5/§8 card anatomy makes four
|
||
resolved cards ~1100px tall — taller than either viewport. The seeded history
|
||
(answered + accepted + rejected + `stale_target` + `superseded_by_comment`)
|
||
cannot fit one auto-scrolled frame.
|
||
|
||
The row now reads: the seeded state must contain **all five** treatments
|
||
(answered, accepted, rejected, `stale_target`, `superseded_by_comment`), the
|
||
frame shows the latest-scrolled portion with the expired-family chips
|
||
(`Stale …`, `Superseded …`) fully visible, and the visual distinctness of all
|
||
five treatments is asserted by the browser suite plus a scrolled review pass
|
||
at the gate. Splitting the slug into two frames was rejected because the
|
||
24-PNG matrix count is referenced by the 7F/7K evidence and the §9.7 axe gate.
|