Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .flow/epics/fn-4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"branch_name": "fn-4",
"created_at": "2026-01-24T19:04:00.779982Z",
"depends_on_epics": [],
"id": "fn-4",
"next_task": 1,
"plan_review_status": "unknown",
"plan_reviewed_at": null,
"spec_path": ".flow/specs/fn-4.md",
"status": "open",
"title": "Bond UI - Forensic Timeline Frontend",
"updated_at": "2026-01-24T19:04:39.520502Z"
}
158 changes: 158 additions & 0 deletions .flow/specs/fn-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Bond UI - Forensic Timeline Frontend

## Overview

A polished single-page web app that connects to Bond's streaming endpoints (SSE or WebSocket), renders the agent lifecycle as a live timeline (text/thinking/tool-call/tool-result), and supports "replay mode" (scrub + pause) for forensic debugging.

## Scope

**In Scope:**
- React + Vite + Tailwind + shadcn/ui + Framer Motion frontend
- SSE streaming transport (WebSocket optional follow-up)
- Event-driven store: BondEvents → Blocks reducer
- Timeline rendering with text/thinking/tool_call blocks
- Replay mode with pause/scrub functionality
- Inspector panel for block details
- Demo mode with pre-recorded events
- Dark mode only (premium devtool aesthetic)
- **Run header + status line** (trace ID, status, event count, connection indicator)
- **Active cursor affordance** (soft glow + shimmer on streaming block)

**Out of Scope:**
- Authentication, accounts, persistence
- Multi-run browsing
- Complex theming system
- Backend modifications (beyond simple stream endpoint if needed)
- Mobile/responsive design

## Approach

### Architecture
- **State**: Event-driven reducer pattern - store full event stream, derive visible blocks
- **Transport**: EventSource API with reconnection and cleanup
- **Rendering**: Framer Motion for block animations, shadcn/ui primitives
- **Replay**: Client-side re-reduction from event history (optimize with caching if needed)

### Two-Mode Event Story (Critical)

The UI handles two distinct event formats:

**Live Mode (WS/SSE frames)** — tiny payloads, maximum responsiveness:
```json
{"t":"block_start","kind":"text","idx":0}
{"t":"text","c":"Hello"}
{"t":"thinking","c":"Let me think..."}
{"t":"tool_delta","n":"read_file","a":"{\"path\":"}
{"t":"tool_exec","id":"call_123","name":"read_file","args":{"path":"/foo"}}
{"t":"tool_result","id":"call_123","name":"read_file","result":"contents..."}
{"t":"block_end","kind":"text","idx":0}
{"t":"complete","data":null}
```

SSE uses named event types: `event: text\ndata: {"c": "Hello"}`

**Replay/Demo Mode (TraceEvent NDJSON)** — richer metadata + timing:
- Uses `TraceEvent` schema with `timestamp` + `wall_time`
- Demo file: `ui/public/demo-events.ndjson`
- Timing driven by `timestamp` field (monotonic clock)

This separation prevents forcing trace semantics onto live streaming.

### Key Technical Decisions
1. **Event normalization**: Map wire format (`t/n/a/c/idx`) to internal BondEvent type
2. **Block correlation**: Use `(kind, index)` for block ID; tool calls correlated by `id` field
3. **Tool delta handling**: Attach to currently active tool_call block (no id in delta)
4. **Auto-scroll**: Intersection Observer, only auto-scroll when at bottom
5. **High-frequency batching**: requestAnimationFrame batching (add virtualization only if needed)

### Backend Integration
The UI connects to Bond's existing streaming infrastructure:
- `create_sse_handlers()` from `src/bond/utils.py:125-167`
- `create_websocket_handlers()` from `src/bond/utils.py:20-122`
- TraceEvent schema from `src/bond/trace/_models.py:38-60`

**Note:** Bond provides handlers, not routes. The UI takes a URL via env var + input field.

## Quick Commands

```bash
# Start development server
cd ui && pnpm dev

# Type check
cd ui && pnpm tsc --noEmit

# Build for production
cd ui && pnpm build

# Run with demo mode (no backend needed)
cd ui && pnpm dev # Then click "Run Demo"
```

## Acceptance Criteria

- [ ] App loads with clean shell (header + sidebar + timeline)
- [ ] **Run header shows**: trace ID/name, status (live/paused/replay), event count
- [ ] **Connection indicator** shows latency/disconnect state
- [ ] Can connect to SSE endpoint and see blocks appear live
- [ ] Text blocks render with prose styling
- [ ] Thinking blocks are visually distinct (subtle, collapsible)
- [ ] Tool blocks show streaming args, status pill, result panel
- [ ] **Active cursor**: currently streaming block has soft glow + subtle shimmer
- [ ] Can pause stream and scrub timeline backwards
- [ ] Can click any block to see details in inspector panel
- [ ] Demo mode plays canned events without backend (using TraceEvent timing)
- [ ] Keyboard shortcuts work (Space=pause, L=live, J/K=step)
- [ ] UI feels like Linear/Vercel quality, not "React starter"

## Resolved Questions (from review)

### Wire Format — Canonical
The wire format is defined in `create_websocket_handlers()`:
- `{"t":"block_start","kind":str,"idx":int}`
- `{"t":"text","c":str}` / `{"t":"thinking","c":str}`
- `{"t":"tool_delta","n":str,"a":str}`
- `{"t":"tool_exec","id":str,"name":str,"args":dict}`
- `{"t":"tool_result","id":str,"name":str,"result":str}`
- `{"t":"complete","data":Any}`

SSE uses `send(event_type, data)` — named event types work perfectly with EventSource.

### Block Kind Values
Treat `kind` as opaque enum string from PydanticAI (`TextPartDelta`, `ThinkingPartDelta`, `ToolCallPartDelta`). Style-map known values, handle unknowns safely.

### Parallel Tool Correlation
Tool execution and result are correlated by `id` field. `tool_delta` doesn't include id — attach to currently active tool_call block.

### Timestamps for Replay
- Live streams don't include timestamps (not needed)
- Demo/replay uses `TraceEvent.timestamp` (monotonic) for timing
- Alternative: fixed cadence client-side for simpler demos

### Maximum Event Rate
Implement requestAnimationFrame batching early. Add virtualization only if perf issues arise.

## Edge Cases to Handle

- Empty stream (timeout/loading state)
- Very long tool results (truncate/lazy-load)
- Stream disconnect mid-block (show "interrupted" state)
- Browser tab backgrounded (catch-up on focus)
- Invalid JSON in payloads (graceful degradation)
- Unknown `kind` values (render as generic block)

## References

### Backend Code
- Event types: `src/bond/trace/_models.py:14-23`
- TraceEvent model: `src/bond/trace/_models.py:38-60`
- SSE handlers: `src/bond/utils.py:125-167`
- WebSocket handlers: `src/bond/utils.py:20-122`
- StreamHandlers: `src/bond/agent.py:28-74`
- TraceReplayer: `src/bond/trace/replay.py:15-145`

### Documentation
- [Vite + React Setup](https://vite.dev/guide/)
- [shadcn/ui Installation](https://ui.shadcn.com/docs/installation/vite)
- [Framer Motion Lists](https://motion.dev/docs/react-animate-presence)
- [MDN EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
23 changes: 23 additions & 0 deletions .flow/tasks/fn-4.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"assignee": "bordumbb@gmail.com",
"claim_note": "",
"claimed_at": "2026-01-24T19:20:05.624754Z",
"created_at": "2026-01-24T19:04:50.787904Z",
"depends_on": [],
"epic": "fn-4",
"evidence": {
"commits": [
"852672a360959781775004d0561294b54a092e73"
],
"prs": [],
"tests": [
"pnpm tsc --noEmit"
]
},
"id": "fn-4.1",
"priority": null,
"spec_path": ".flow/tasks/fn-4.1.md",
"status": "done",
"title": "Project scaffold + styling baseline",
"updated_at": "2026-01-24T19:29:15.712322Z"
}
63 changes: 63 additions & 0 deletions .flow/tasks/fn-4.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# fn-4.1 Project scaffold + styling baseline

## Description

Set up the frontend project with Vite, React, TypeScript, Tailwind CSS, shadcn/ui, and Framer Motion. Create the app shell layout with header, sidebar, and main timeline area.

## Implementation

1. Create `ui/` directory in project root
2. Initialize Vite + React + TypeScript project
3. Install and configure Tailwind CSS v4
4. Initialize shadcn/ui with components: Card, Button, ScrollArea, Badge
5. Install Framer Motion (motion package)
6. Create minimal dark theme: `zinc-950` background, `zinc-800` borders
7. Implement App shell layout:
- Header with logo, "Run Demo" and "Connect" buttons
- **Run header/status line**: trace ID, status indicator, event count, connection state
- Sidebar card showing session info
- Main timeline area with ScrollArea

## Files to Create

- `ui/package.json` - dependencies
- `ui/vite.config.ts` - Vite config with path aliases
- `ui/tsconfig.json` - TypeScript config
- `ui/src/index.css` - Tailwind imports + CSS variables
- `ui/src/App.tsx` - Shell layout
- `ui/src/main.tsx` - Entry point
- `ui/index.html` - HTML template
- `ui/components.json` - shadcn/ui config

## References

- App shell layout from plan.md lines 44-90
- [Vite React setup](https://vite.dev/guide/)
- [shadcn/ui Vite installation](https://ui.shadcn.com/docs/installation/vite)
## Acceptance
- [ ] `pnpm dev` starts dev server without errors
- [ ] `pnpm tsc --noEmit` passes type check
- [ ] App loads with header showing "Bond" logo and buttons
- [ ] Sidebar shows "Session" card with placeholder text
- [ ] **Run header shows**: trace ID placeholder, status (idle), event count (0)
- [ ] **Connection indicator**: shows "disconnected" state
- [ ] Timeline area shows "Waiting for events..." placeholder
- [ ] Dark theme applied: zinc-950 background, zinc-800 borders
- [ ] Typography feels premium (not default browser styles)
## Done summary
- Created ui/ directory with Vite + React + TypeScript scaffold
- Configured Tailwind CSS v4 with @tailwindcss/vite plugin
- Added shadcn/ui components (Button, Card, ScrollArea, Badge) with dark theme
- Installed Framer Motion for animations

- Sets foundation for all subsequent UI tasks
- Dark theme and component library provide premium devtool aesthetic

- `pnpm tsc --noEmit` passes without errors
- Dev server starts successfully

- lib/utils.ts created for shadcn component utilities
## Evidence
- Commits: 852672a360959781775004d0561294b54a092e73
- Tests: pnpm tsc --noEmit
- PRs:
25 changes: 25 additions & 0 deletions .flow/tasks/fn-4.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"assignee": "bordumbb@gmail.com",
"claim_note": "",
"claimed_at": "2026-01-24T19:30:03.981082Z",
"created_at": "2026-01-24T19:04:52.566678Z",
"depends_on": [
"fn-4.1"
],
"epic": "fn-4",
"evidence": {
"commits": [
"572993dbf3ce2cad47d093be9f5c904a41d3691c"
],
"prs": [],
"tests": [
"pnpm tsc --noEmit"
]
},
"id": "fn-4.2",
"priority": null,
"spec_path": ".flow/tasks/fn-4.2.md",
"status": "done",
"title": "Event schema + block model",
"updated_at": "2026-01-24T19:32:11.849689Z"
}
97 changes: 97 additions & 0 deletions .flow/tasks/fn-4.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# fn-4.2 Event schema + block model

## Description

Define TypeScript types for Bond events and the Block model. Implement the reducer that transforms streaming events into renderable blocks.

## Implementation

### Two-Mode Event Story

The UI handles two distinct formats:

**Live Mode (canonical wire format from `create_websocket_handlers()`):**
```json
{"t":"block_start","kind":"text","idx":0}
{"t":"text","c":"Hello"}
{"t":"thinking","c":"Let me think..."}
{"t":"tool_delta","n":"read_file","a":"{\"path\":"}
{"t":"tool_exec","id":"call_123","name":"read_file","args":{"path":"/foo"}}
{"t":"tool_result","id":"call_123","name":"read_file","result":"..."}
{"t":"block_end","kind":"text","idx":0}
{"t":"complete","data":null}
```

SSE uses named event types: `event: text\ndata: {"c": "Hello"}`

**Replay/Demo Mode (TraceEvent NDJSON):**
- Uses full `TraceEvent` schema with `timestamp` + `wall_time`
- Richer metadata for timing-accurate replay

### Steps

1. Define `WireEvent` type matching canonical wire format (`t/n/a/c/idx`)

2. Define internal `BondEvent` union type (normalized):
- `block_start`, `block_end` (kind, index)
- `text_delta`, `thinking_delta` (content)
- `tool_call_delta` (name, args deltas)
- `tool_execute`, `tool_result` (id, name, args/result)
- `complete`

3. Define `Block` type:
- Text block: id, kind, content, isClosed, **isActive**
- Thinking block: id, kind, content, isClosed, **isActive**
- Tool block: id, kind, draft state, final state, status, result, **isActive**

4. Create normalization layer:
- `normalizeWireEvent(wire: WireEvent): BondEvent`
- `normalizeSSEEvent(type: string, data: object): BondEvent`
- `normalizeTraceEvent(trace: TraceEvent): BondEvent`

5. Implement reducer:
- Handle block_start: create new block, mark as active
- Handle deltas: append to active block
- Handle tool_execute: transition to "executing", correlate by `id`
- Handle tool_result: attach result by `id`, mark "done"
- Handle block_end: close block, clear active state

**Key insight:** `tool_delta` has no `id` — attach to currently active tool_call block.

## Files to Create

- `ui/src/bond/types.ts` - WireEvent, BondEvent, Block, TraceEvent types
- `ui/src/bond/reducer.ts` - Event reducer with active block tracking
- `ui/src/bond/normalize.ts` - Normalization for WS/SSE/TraceEvent formats

## References

- Type definitions from plan.md lines 109-143
- Reducer skeleton from plan.md lines 144-230
- Backend event types: `src/bond/trace/_models.py:14-23`
- SSE format: `src/bond/utils.py:158-167`
## Acceptance
- [ ] `WireEvent` type matches canonical wire format (`t/n/a/c/idx`)
- [ ] `BondEvent` type covers all 8 normalized event types
- [ ] `Block` type supports text, thinking, tool_call with `isActive` flag
- [ ] `normalizeWireEvent()` converts wire format to BondEvent
- [ ] `normalizeSSEEvent()` handles named SSE event types
- [ ] `normalizeTraceEvent()` handles TraceEvent for replay/demo
- [ ] Reducer tracks active block and marks it correctly
- [ ] Reducer handles tool_delta attaching to active tool_call block
- [ ] Reducer correlates tool_execute/tool_result by `id` field
- [ ] Unknown `kind` values handled gracefully (generic block)
## Done summary
- Created types.ts with WireEvent, BondEvent, Block, TraceEvent types
- Created normalize.ts with normalizeWireEvent, normalizeSSEEvent, normalizeTraceEvent
- Created reducer.ts with bondReducer and reduceEvents helper

- Establishes foundation for streaming transport and timeline rendering
- Two-mode event story cleanly separates live vs replay formats

- `pnpm tsc --noEmit` passes without errors
- All type definitions match canonical wire format from backend
## Evidence
- Commits: 572993dbf3ce2cad47d093be9f5c904a41d3691c
- Tests: pnpm tsc --noEmit
- PRs:
Loading