# Human-centered agent activity

audience: AI coding agents first.

status: design complete
plan slug: `agent-activity-story`

## Outcome

Replace robot-facing agent event tables with readable chronological work story. Keep exact evidence available on demand. Recover preserved `DiffViewer`; NEVER rebuild equivalent parser/viewer from scratch.

## Product contract

When owner clicks any agent:

1. Open dedicated agent detail view.
2. Lead with task goal, agent state, elapsed duration, current work, and essential controls.
3. Render activity as human-readable chronological story.
4. Group only routine/repetitive reads, searches, and commands. Keep edits, decisions, tests, warnings, failures, blockers, and outcomes as individual rows.
5. Let each grouped row expand to underlying actions.
6. Let right-click on any event open diagnostics in right rail.
7. Provide visible `Diagnostics` button for keyboard, touch, and discoverability. Right-click MUST NOT be sole path.
8. Keep raw commands, outputs, payloads, IDs, model, account, branch, and exact timestamps out of default story. Show them in diagnostics when authoritative data exists.
9. Render each file edit as `Edited (path)` row with `See more`.
10. `See more` expands GitHub-style side-by-side diff directly below edit row. Keep unified mode available inside expanded viewer.

## Chosen approach

Use story + event inspector + inline diff.

| Dimension | Assessment |
|---|---|
| Robustness | Default remains readable when diagnostics or diff text are absent. Every detail surface reports missing evidence honestly. |
| Long-term | Story projection, diagnostics projection, and diff rendering keep separate contracts. Raw event schema may evolve without rewriting visual hierarchy. |
| Scalability | Selective grouping bounds visual noise; collapsed diffs bound DOM size; existing event cap remains enforced. |
| Performance | Parse diff only after `See more`; lazy-mount diagnostics content; no eager syntax engine. |
| Reversibility | Two-way door. Existing raw event endpoint and factory trace data remain unchanged. |
| Infra cost | No new service or storage system. Reuse collector and phase diff records. |

**Weakness:** Human summaries depend on event vocabulary quality. Unknown event types need safe fallback labels rather than raw JSON in primary story.

### Rejected approach: conversation-first

Agent assistant messages alone hide tool work, edits, tests, and failures. Does not satisfy complete observability.

### Rejected approach: dashboard-first

Persistent metrics and metadata recreate current robot-facing hierarchy. Owner asked for work narrative first.

## Existing implementation to recover

Preserved source provenance:

| File | SHA-256 | Bytes |
|---|---|---:|
| `DiffViewer.tsx` | `5ed4046d29a55e95220500a3ab44a626638c7e8a1c9c5130f805461d1e2c178a` | 12,161 |
| `DiffViewer.css` | `ab6f99f4fcdc86718bd3e616041a8160b83596f10490ad5a60878fd9656af001` | 5,124 |
| `DiffViewer.test.tsx` | `98037ba68c389eb1dcc66b7264442454aa691df995ad64e2c1590911f3e56bd7` | 1,382 |

Source root: `/home/user/Projects/overdeck/temp-user/project-onboarding/overdeck-project-control-implementation/files/packages/deck-ui/src/components/DiffViewer/`.

Before implementation, copy these three exact hashed artifacts into a durable recovery commit or recorded worktree location. Stop if hashes differ. Never depend on temporary source remaining available.

Retain when tests prove behavior:

- `parseUnifiedDiff(input: string): DiffFile[]`
- split/unified presentation
- changed-file selection
- context-run collapse/expand
- added/deleted/modified/renamed states
- empty state
- keyboard-reachable controls

Repair before export:

- Match current repository formatting and TypeScript conventions.
- Replace private CSS token system and fallback raw values with current deck-ui/theme tokens; both themes required.
- Compose current shared controls where available. Do not hand-roll a platform primitive.
- Register all meaningful states in `/design-system`.
- Add current barrel export surgically; NEVER copy preserved stale barrel.
- Expand parser tests for multi-file patches, add/delete/rename, empty input, no-newline marker, unmatched deletion/addition runs, and malformed metadata.
- Verify large diff containment and horizontal scrolling without body overflow.

The package's broad 525-path patch MUST NOT be applied. It contains stale paths and fails current apply-check. Recover only reconciled artifacts.

## Architecture

### 1. Activity projection

Create `apps/web/src/components/plans/agent-activity-projection.ts`. It alone maps transport records and evidence into presentation items. `packages/deck-ui` receives presentation-only types and callbacks; it MUST NOT import `HarnessEvent`, collector response types, query hooks, or resolve event IDs.

Contract shape:

```ts
type AgentActivityItem =
  | AgentProgressItem
  | AgentGroupItem
  | AgentEditItem
  | AgentTestItem
  | AgentDecisionItem
  | AgentWarningItem
  | AgentOutcomeItem
```

Common fields:

```ts
interface AgentActivityBase {
  id: string
  at: number
  title: string
  summary?: string
  diagnosticEventIds: string[]
}
```

Edit seam:

```ts
interface AgentEditItem extends AgentActivityBase {
  kind: 'edit'
  path: string
  insertions: number | null
  deletions: number | null
  diffText: string | null
  truncated: boolean
}
```

Rules:

- Implement one explicit vocabulary registry in `agent-activity-projection.ts`. Each entry pins source event kind, required payload fields, story title formatter, grouping category, severity, and whether row is individually preserved. Tests enumerate registry entries and reject unclassified known kinds.
- Initial categories MUST cover attempt lifecycle, prompt/reply, tool call/result, file edit, test/gate, decision, warning/error, pause/resume/stop, and completion. Unknown kinds use category `other`, severity `info`, title `Recorded activity`, available human-authored `summary`/`text`, and never raw JSON.
- Group adjacent routine events only when same authoritative `runId`, `taskId`, `attemptId`, and grouping category. Reads/searches and non-mutating commands are routine; mutation, edit, test/gate, decision, warning/error, control, and completion are never grouped.
- Group identity is `${attemptId}:${category}:${firstEventId}` and never changes as group grows.
- Sort by parsed timestamp, then authoritative stream/list order. Deduplicate exact IDs; same ID with conflicting content keeps first record, opens an explicit diagnostics warning, and never merges evidence.
- Late event inserts at chronological position. It may join a group only when adjacency and all immutable grouping fields still match; existing group identity remains first event ID.
- Unknown event kind renders safe `Recorded activity` title plus available human summary. Never emit raw JSON as title.
- Preserve exact underlying event IDs for diagnostics.
- Event cap MUST produce visible `Earlier activity is not loaded` notice. Use existing supported history refetch/pagination path; if no continuation exists, state that older records are unavailable and never imply completeness.

### 2. Agent activity story

Evolve `packages/deck-ui/src/AgentFeed.tsx` to accept presentation-only `AgentActivityItem[]`; evolve `TurnCard.tsx` into typed story-row rendering or replace it with `AgentActivityRow.tsx` when tests prove current `TurnCard` cannot express grouped/edit rows. Do not create a second feed component. `AgentFeed` exposes `onOpenDiagnostics(itemId)` and `onToggleGroup(itemId)` callbacks; transport and evidence lookup stay in `AgentApp`.

Behavior:

- Story rows use plain language and restrained timeline markers.
- Current row announces updates through polite live region without moving keyboard focus.
- Group row control says `See N actions`; expanded state lists child actions chronologically.
- Event row supports browser `contextmenu`, `Shift+F10`, Context Menu key, and visible diagnostics action through one callback.
- `Enter`/`Space` activate visible controls. Expanders expose `aria-expanded` and `aria-controls`; controlled region has stable ID.
- Expanding groups/diffs never steals focus. Opening diagnostics moves focus into inspector; closing restores triggering control.
- Stream appends announce only state changes, warnings, failures, and completion—not every event. Respect reduced-motion preference.
- Ended attempt states remain readable history; no disabled-looking whole page.

### 3. Diagnostics inspector

Reuse `packages/deck-ui/src/DetailDrawer.tsx` with `modal={false}` on wide screens and modal default on narrow screens. Wide agent layout reserves drawer width so fixed inspector does not cover story content. Narrow screens use backdrop and focus trap. Do not create another drawer primitive.

Diagnostics content order:

1. human title and relative time; exact ISO time through existing tooltip convention
2. event kind and status
3. file/task/attempt context when authoritative
4. command/input/output sections when present
5. IDs and raw payload in final collapsed section

Rules:

- Open from right-click, `Shift+F10`, Context Menu key, and visible row `Diagnostics` control.
- Header `Diagnostics` opens agent-level diagnostics only: identity, task, attempt, connection/coverage state, model/account/branch when authoritative. It MUST NOT silently select newest event.
- Selecting another row updates same inspector; no stacked drawers.
- Opening moves focus into inspector. Escape closes. Closing restores exact trigger when still mounted; otherwise restore feed heading.
- Wide non-modal inspector leaves story interactive. Narrow modal inspector traps focus through existing drawer behavior.
- Missing field omitted or shown as em dash only where comparison requires a cell.
- Render commands, inputs, outputs, IDs, and payloads as inert escaped text. Never interpret HTML, ANSI control sequences, links, or terminal escapes.
- Apply existing collector redaction before browser delivery. Browser presentation MUST NOT become the redaction boundary.
- Bound each diagnostic text section to 24 KiB and label truncation. Full authoritative logs use existing log surface/link when available; never inject unbounded output into drawer.

### 4. Inline diff

`DiffViewer` contract remains reusable domain component:

```ts
interface DiffParseResult {
  files: DiffFile[]
  warnings: DiffParseWarning[]
}

DiffViewer({
  diff?: string
  files?: DiffFile[]
  selectedPath?: string
  defaultView?: 'split' | 'unified'
  allowViewToggle?: boolean
  collapseContextAfter?: number
  ariaLabel?: string
  parseWarnings?: DiffParseWarning[]
  onFileChange?: (file: DiffFile) => void
}): JSX.Element
```

Agent edit behavior:

- `See more` lazy-mounts viewer directly beneath row.
- Default split view.
- Expanded label changes to `Show less`.
- One edit can expand independently of others.
- Missing diff: show `Diff was not recorded for this edit.` Never synthesize patch text.
- Truncated diff: show explicit coverage warning before viewer.
- Multi-file phase diff: `selectedPath` selects exact row path when parser contains it. Path mismatch shows full phase patch with explicit mismatch warning; never claims row-level exactness.
- Binary, mode-only, submodule, rename-only, quoted-path, missing-newline, malformed-hunk, and truncated-input cases produce structured warnings. Render supported metadata honestly; never invent line changes.
- Status indicators use full words plus symbols/text, never status letters or color alone. Diff tables include semantic old/new line and code column headers.
- Viewer scrolls internally; page body never gains horizontal scroll.

### 5. Authoritative data flow

Current factory trace exposes `FactoryPhaseDiffView`:

```ts
interface FactoryPhaseDiffView {
  phaseId: string
  attempt: number | null
  files: FactoryDiffFileView[]
  insertions: number | null
  deletions: number | null
  diffText: string | null
  truncated: boolean
  createdAt: string | null
}
```

Join contract:

1. `runId/adwId` identifies same run across agent state and factory detail.
2. `taskId` identifies agent-owned task.
3. `attemptId` identifies agent execution attempt.
4. `phaseId` identifies phase producing `FactoryPhaseDiffView`.
5. `path` identifies file evidence within phase diff.

Current contracts do not expose a proven complete chain: harness events have `attemptId` but no guaranteed `phaseId`; phase diffs have `phaseId` plus numeric phase attempt but no event/agent attempt ID. Therefore implementation MUST add explicit collector linkage before attaching a diff to an agent story row. Preferred typed addition is `attemptId: string | null` on factory phase/phase-diff views sourced from authoritative trace storage, plus `phaseId: string | null` on relevant harness events when source records carry it. If trace storage lacks either relation, add the relation at write time and mark legacy rows unavailable; never backfill by time proximity.

Presentation rules:

- A phase diff may create one `Edited (path)` row per file only after run + agent attempt + phase linkage is proven.
- Label evidence `Phase changes` when only phase-level attribution exists. Never present final phase diff as exact output of one tool/edit event without event-to-diff linkage.
- Exact event edit rows require matching authoritative event ID or explicit edit operation ID carried into diff evidence.
- Missing/conflicting identifiers produce no attachment and explicit diagnostics coverage warning.
- Keep collector linkage typed. Browser MUST NOT join separate responses by timestamps, numeric attempt coincidence, or assumed ID equality.

## Visual hierarchy

Permanent header:

- agent identity
- current state
- elapsed duration via `LiveDuration`
- task goal
- essential controls: pause/resume/stop where supported
- visible `Diagnostics` button

Default view MUST NOT permanently show model, account, IDs, branch, raw timestamps, event type table, run-offset table, empty technical cards, or raw payload blocks.

Responsive behavior:

- Wide: story plus diagnostics rail.
- Narrow: diagnostics uses existing full-width drawer behavior; split diff may switch to unified only when layout cannot preserve readable columns, while user can still select mode.
- All panes own their scroll.

## Error handling

- Collector unavailable: existing query boundary with retry.
- SSE disconnect: preserve received story, label reconnection state, retry through existing stream behavior.
- Malformed event payload: safe generic story row; raw evidence remains diagnostics-safe serialized text.
- Malformed diff: viewer renders supported metadata and structured warnings; never crashes entire feed or implies unsupported content was reviewed.
- Binary/mode-only/submodule/rename-only diff: render metadata-only change with explicit type; no empty fake code table.
- Missing attempt/phase correlation: preserve current fail-closed controls, attach no diff, and clearly state why control/diagnostics precision is unavailable.
- Oversized/truncated diff: explicit warning; never imply complete review evidence.

## Testing

### Deterministic tests

- Event-to-story mapping for every known category.
- Selective grouping and non-groupable critical rows.
- Stable grouping under append, late insertion, duplicate IDs, and conflicting duplicate content.
- Event-cap incompleteness notice and continuation/unavailable state.
- Collector linkage contract: matching identifiers attach correct phase/file diff; missing/conflicting run, task, attempt, phase, or path never attach evidence.
- Phase-level evidence is labeled phase-level; no event-level exactness claim without explicit linkage.
- Right-click, `Shift+F10`, Context Menu key, and visible-button diagnostics parity.
- Keyboard opening, close, focus restore, and selected-event update.
- Edit `See more` lazy expansion and independent collapse.
- Missing/truncated/malformed/multi-file diff states plus binary, mode-only, submodule, rename-only, quoted-path, and missing-newline cases.
- Diagnostics redaction, escaping, ANSI/HTML inertness, 24 KiB bounds, and truncation labels.
- Full status labels, non-color indicators, semantic diff headers, reduced motion, restrained live announcements, and no focus theft.
- Preserved parser behavior plus edge cases listed above.
- Both themes and gallery registration.
- Narrow/wide overflow containment.

### Integration

- Agent page receives authoritative events and phase diff, renders readable story, opens diagnostics, expands exact file diff.
- Historical ended attempt remains usable.
- Unsupported attempt correlation remains fail closed.

### Browser proof

Use installed app and real recorded agent data. Prove:

1. clicking agent opens story-first interface
2. routine actions expand
3. edit row opens exact side-by-side diff
4. right-click and Diagnostics button open same rail
5. raw IDs/payload absent until requested
6. light and dark themes
7. narrow viewport has no body horizontal scroll

## Architecture decisions

- Keep one activity projection module: deleting it would scatter event vocabulary/grouping across UI; boundary earns place.
- Keep recovered `DiffViewer`: parser and rendering hide non-trivial patch complexity and serve agent, factory, and future review surfaces.
- Reuse `DetailDrawer`: second inspector primitive would be decorative duplication.
- Do not create event repository/service layer: current query hooks already own transport; second adapter has no demonstrated need.
- Do not change trace storage solely for presentation. Add collector linkage only if authoritative correlation is absent.

## Acceptance criteria

- Agent detail reads as human work story without opening diagnostics.
- Routine events selectively grouped and expandable.
- Every event has right-click diagnostics plus visible accessible button path.
- Every authoritative edit row has `See more`; exact patch expands inline in split view.
- Preserved `DiffViewer` recovered surgically, repaired to current design-system law, tested, exported, and gallery-registered.
- No wholesale preserved patch application and no duplicate diff viewer.
- No fabricated summaries, diffs, timestamps, statuses, or correlations.
- Required UI tests, typechecks, build, slopgate, and real browser proof pass without ignored warnings.
