paperclip/packages/paperclip-runner/docs/design/capability-issue-thread-ux-...

30 KiB
Raw Permalink Blame History

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):    │   320640px)  │
│   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 320640px 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 planeruntime_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 sections 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.