# Mobile Unified Live Timeline Design

Status: Implementation design
Date: 2026-05-15
Owner: CTM / Phone Access

## Goal

The phone session view should feel like one conversation, not a transcript panel
plus a terminal panel. Users should see durable history immediately and then see
new output appear at the bottom of the same timeline while an agent is still
working.

Internal data can come from two sources:

1. durable conversation history from `/api/session/messages`;
2. live terminal/headless output from the running CTM session.

The UI must hide that distinction unless there is an error. The user-facing
model is a single session timeline.

## User Experience

### Timeline States

On opening a session from phone:

1. CTM loads the durable conversation history.
2. CTM opens the authenticated WebSocket.
3. CTM subscribes to semantic stream events.
4. CTM subscribes to a throttled terminal-tail feed.
5. The phone renders all of this as one scrollable timeline.

The bottom row can be provisional:

```text
Claude · Live
Working through the mobile stream path...
```

When a canonical assistant/tool event arrives from the conversation stream, CTM
deduplicates or suppresses the provisional row. The final visible result should
be one coherent conversation, not repeated terminal text.

The phone detail view must also re-sync from `/api/session/messages` after
stream, terminal-tail, or status updates. The live tail is a short-lived preview;
the normalized session-message API is the canonical transcript shared with the
desktop Conversation view. This keeps phone and desktop behavior aligned for
Codex control records, stale large-session caches, and final-summary updates.

### Scrolling

- If the user is already near the bottom, the timeline follows live updates.
- If the user scrolls upward, live updates do not steal the viewport.
- A small `New updates` affordance can be added later; the MVP simply preserves
  scroll position while not at bottom.

### Reconnect

Mobile browsers can pause timers and sockets while backgrounded. On `pageshow`,
`focus`, `online`, or visible `visibilitychange`, the phone reconnects and
resubscribes to both the semantic stream and terminal-tail feed. The server sends
a fresh tail snapshot after every terminal-tail subscription, so the phone does
not rely on having received every delta while suspended.

## Transport Choice

Use the existing WebSocket connection.

Reasons:

- CTM already uses an authenticated WebSocket for phone control.
- The phone needs bidirectional behavior: watch output and send replies.
- Server-sent events would add a second connection and auth surface for one-way
  data only.
- WebSocket has no native backpressure, so terminal-tail messages must be
  throttled, size-capped, and snapshot-based.

References:

- MDN WebSocket API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
- MDN WebSocket client lifecycle and `pagehide`/`pageshow` guidance:
  https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications
- MDN Server-sent events / EventSource:
  https://developer.mozilla.org/en-US/docs/Web/API/EventSource
- MDN Page Visibility API:
  https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API

## Server Contract

### Subscribe

```json
{ "type": "subscribe-terminal-tail", "sessionId": "ctm-session-id" }
```

Requires the same WebSocket `read` authorization as `subscribe-stream`.

### Update

```json
{
  "type": "terminal-tail",
  "id": "ctm-session-id",
  "seq": 12,
  "text": "latest visible terminal tail",
  "timestamp": 1778790000000,
  "reason": "output"
}
```

The payload is a snapshot of the bounded visible tail, not raw ANSI bytes. The
client should treat it as replace-current-live-row, not append-every-packet.

### Unsubscribe

```json
{ "type": "unsubscribe-terminal-tail", "sessionId": "ctm-session-id" }
```

## Server Implementation

- Maintain `sessionId -> Set<WebSocket>` terminal-tail subscribers.
- Listen to `session:output`.
- If no phone/browser is subscribed to that session, do nothing.
- When output arrives, schedule a flush at a short throttle interval.
- Before reading text, flush the session's pending headless terminal feed.
- Read text using the existing headless terminal worker `getText` path.
- Normalize control characters and cap the result by lines and characters.
- Increment a per-session sequence and broadcast a `terminal-tail` snapshot only
  when the text changes.

This avoids streaming raw PTY data to the phone and avoids duplicating xterm.js
rendering logic in the mobile UI.

## Mobile Implementation

The mobile detail view keeps an in-memory timeline state:

- `messages`: canonical history plus semantic stream events.
- `seenKeys`: event/message keys used for dedupe.
- `liveTail`: latest provisional terminal-tail snapshot.

Rendering builds one DOM:

```text
canonical message rows
optional provisional live terminal turns
composer
```

The provisional live terminal area is suppressed when its text is already
covered by recent canonical messages. When the terminal tail includes a known
prompt marker such as `›`, `❯`, `▶`, `▸`, `You:`, or `User:`, the phone renders
that prompt as a live `You` row and renders following output as live assistant
rows. CLI chrome such as Codex model/path status lines is filtered out so the
phone timeline looks like the conversation log instead of raw terminal paint.
Native terminal composer placeholders, such as Codex's trailing
`› Explain this codebase` prompt, are suppressed until they become a real
submitted prompt with following output.
Tail text without a recognized prompt marker still renders as one live
assistant row with the normal markdown/table formatter.

## Security

- Only authenticated WebSocket clients with `read` scope can subscribe.
- No raw HTML is rendered from terminal-tail text.
- Terminal-tail snapshots are capped in memory and over the wire.
- No new durable storage is added; existing scrollback persistence remains the
  source for terminal history.
- Origin and device-token checks remain on the existing WebSocket handshake.

## Test Plan

1. Unit test WebSocket auth rules for the new terminal-tail message types.
2. Browser/mobile test that mocked WebSocket messages render:
   - durable history first;
   - semantic stream events in the same timeline;
   - terminal-tail prompt/output as provisional conversation-style rows;
   - terminal-tail markdown tables using the same formatter as conversation logs;
   - tail updates replace the provisional row rather than appending duplicates.
3. Browser/mobile layout test on iPhone viewport.
4. Isolated CTM dev smoke test against a random port pair, never the primary
   `3456` instance.
