Skip to content

Tracking: terminal output rendering — work streams for agent team pickup #152

@khaliqgant

Description

@khaliqgant

Architecture (established from relay source + CLI capabilities)

Relay supports two spawn transports. The default for Claude/Codex is PTY, but transport: 'headless' (piped stdin/stdout) works for any CLI — relay just forwards the chunks:

Runtime CLIs How structured output works
pty Any CLI (default for Claude, Codex, Grok) ANSI escape sequences → xterm renders raw
headless Any CLI (default for OpenCode) piped stdin/stdout → relay forwards chunks via worker_stream

Claude — no relay changes needed

Pear passes --output-format stream-json --include-partial-messages as spawn args. Relay spawns Claude as a piped subprocess. Claude outputs NDJSON to stdout. Relay forwards chunks via worker_streambroker:pty-chunk IPC. Pear renderer parses NDJSON and formats for xterm (V1) or custom UI (V2).

NDJSON shape to parse:

{ "event": { "type": "content_block_delta", "delta": { "type": "text_delta", "text": "..." } } }

Codex — relay has native app-server support

codex_session.rs in the relay broker uses .stdin(Stdio::piped()).stdout(Stdio::piped()) — Codex's app-server protocol is already understood by relay. Spawn with transport: 'headless' and relay handles the rest.

OpenCode — headless via HTTP SSE (different mechanism)

OpenCode runs opencode serve --port N and exposes /event SSE. Events: message.delta, file.changed, permission.requested.

Grok — PTY only (for now)

No structured output format available from xAI yet. Tracked in #141.


What "no PTY" means for user-facing features

Feature PTY (current) Headless (Claude/Codex/OpenCode)
Slash commands (/clear, /compact) ✓ typed into PTY stdin ✓ written to piped stdin — Claude/Codex honor them
Image/screenshot paste ✓ xterm clipboard → PTY ✗ no clipboard path — needs explicit upload UI
Trust/permission dialogs ✓ relay auto-handles Claude stream-json: no interactive prompts; --dangerously-skip-permissions or MCP trust needed
Relay agent-relay view attach ✓ PTY snapshot ✗ headless has no view

Image paste is the main regression risk when moving off PTY. For V1, this is acceptable if documented. V2 can add an explicit image attach button.


Work streams

Stream 1 — Merge immediately (safe, no dependencies) ✅

Merge order: #142 and #143 first, then #138 (resolve trivial 3-way conflict in SpawnAgentDialog, TerminalPane, SpawnAgentCli).


Stream 2 — Claude headless + NDJSON rendering (V1)

Goal: spawn Claude with --output-format stream-json, parse NDJSON chunks in the renderer, display clean formatted text in xterm. No raw JSON visible.

Steps:

  1. broker.ts: route Claude spawns through spawnCli({ transport: 'headless', args: ['--output-format', 'stream-json', '--include-partial-messages'] }).
  2. Track runtime per agent session in BrokerManager.
  3. Renderer: for headless Claude agents, parse broker:pty-chunk NDJSON before passing to xterm — extract content_block_delta.text_delta.text, emit clean text only.
  4. Handle trust/permissions: either --dangerously-skip-permissions flag or confirm MCP trust approach in relay.

Note: PR #139 has the right shape for this work but has two blockers — relay 8.3.0 lockfile not updated, and the NDJSON parsing (step 3) is absent so raw JSON hits xterm. Fix both before merging.


Stream 3 — Codex headless + app-server rendering (V1)

Relay's broker already understands Codex's app-server protocol (codex_session.rs). Steps:

  1. broker.ts: route Codex spawns through spawnCli({ transport: 'headless' }).
  2. Parse Codex app-server event chunks in renderer: AgentMessageDelta, FileChangePatchUpdated, ApplyPatchApprovalParams.
  3. Emit formatted text to xterm.

Can be done in same PR as Stream 2 or separately.


Stream 4 — OpenCode headless routing + SSE rendering (V1)

PR #140 has the right idea but depends on PR #139. Once #139 is fixed and merged:

  1. Route OpenCode through spawnCli({ transport: 'headless' }).
  2. Parse message.delta SSE chunks → formatted xterm text.

Stream 5 — V2 native Pear UI (all headless CLIs)

No xterm for headless agents. Replace terminal pane with React components:

  • message.delta → message bubbles
  • file.changed → diff cards
  • permission.requested → native approval dialogs
  • Claude tool use events → collapsible tool call cards

Separate PR track, not blocking V1.


Stream 6 — Grok headless (blocked on xAI, tracked in #141)

Nothing to build until xAI ships structured output. When it arrives, follows Stream 2 pattern.


PRs at a glance

PR Title Status Action
#138 Grok harness Open, ready Merge after #142/#143
#139 relay 8.3 + headless Claude/Codex routing ⛔ DO NOT MERGE yet Fix two blockers: (1) run npm install and commit lockfile, (2) add NDJSON parsing in renderer before merging
#140 OpenCode headless routing ⛔ DO NOT MERGE yet Depends on #139 being fixed first
#141 Grok headless Open issue Blocked on xAI
#142 Terminal visual tweaks Open, ready Merge first
#143 OpenCode in spawn UI Open, ready Merge first
#146 Cloud workspace key Draft Separate track (#125)
#132 Slack integration fixes Open Separate track

Agent team pickup checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions