-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
Replace hand-rolled ANSI cursor overwrite logic with a Live Block architecture using log-update. This solves parallel tool display, task status updates, and todo list rendering in a single proven pattern.
Context
Problems with current approach
\x1b[1A(move up 1 line) only overwrites the immediately previous line- Parallel tools create orphaned start lines that never get their completion marker
- No way to show running tasks or todo list as live-updating content
- Every new live-updating feature (subagent panels, progress bars) hits the same wall
Research evidence
- Docker/Podman uses this exact "live block" pattern for parallel layer downloads
- AIChat and Aider use append-only (no live updates) — simpler but cant show parallel status
- Claude Code uses React/Ink with a custom-rewritten renderer — too heavy for oclite
- log-update (17.3M weekly downloads) is the battle-tested primitive for this pattern
- OpenTUI (built for opencode.ai) is promising but alpha — not ready
Architecture decision
Evaluated 3 approaches:
- Live Block (log-update) — ~120 LOC integration, Docker-proven ✅ CHOSEN
- Content Panes (absolute positioning) — 250+ LOC, breaks with scrollback ❌
- Buffered Frames (game engine diff) — 200+ LOC, overkill ❌
Design
Three-Zone Terminal Layout
┌──────────────────────────────────────────────┐
│ ZONE 1: Frozen output (append-only) │
│ Text, completed tools, prose — never changes │
│ Scrolls up naturally in terminal │
│ │
├──────────────────────────────────────────────┤
│ ZONE 2: Live Block (rewrites in place) │
│ Active tools: ◇ bash git status │
│ Active tasks: ⠋ @git-agent (12s) │
│ Todos: [✓] Research [ ] Fix #57 │
│ ────────────────────────────────────────── │
├──────────────────────────────────────────────┤
│ ZONE 3: Input (LineEditor) │
│ project-manager ❯ █ │
└──────────────────────────────────────────────┘
- Zone 1: Plain
process.stdout.write()— append only, frozen forever - Zone 2: Managed by
log-update— rewrites N lines on every state change - Zone 3: Existing
LineEditor— handles user input
When Zone 2 has nothing active, it shrinks to zero. When everything completes, it freezes into Zone 1 via logUpdate.done().
Live Block State
The live block renders from a state object:
interface LiveState {
tools: Array<{ id: string, name: string, summary: string, status: "running" | "done" | "error" }>
tasks: Array<{ id: string, agent: string, description: string, elapsed: number, status: "running" | "done" }>
todos: Array<{ content: string, status: "pending" | "in_progress" | "completed" }>
}On ANY state change (tool start, tool end, task update, todo update), re-render the entire block via logUpdate(). The library handles cursor math internally.
Key Pattern: log-update
import logUpdate from "log-update"
// Render active state
logUpdate([
"◇ bash git status",
"◇ rg *.ts",
"⠋ @git-agent: Get project state (12s)",
"────────────",
"[✓] Research [ ] Fix #57",
].join("\n"))
// On tool completion — update entire block
logUpdate([
"✓ bash git status", // changed
"◇ rg *.ts",
"⠋ @git-agent: Get project state (15s)",
"────────────",
"[✓] Research [ ] Fix #57",
].join("\n"))
// When everything done — freeze and move on
logUpdate.done()Transition Logic
- Text chunk arrives → if live block is active, call
logUpdate.done()to freeze it, then write text to Zone 1 - Tool/task/todo event arrives → update state, re-render live block via
logUpdate() - All tools+tasks complete →
logUpdate.done(), live block disappears - New tool starts after text → create fresh live block
Dependencies
| Package | Purpose | Size | Downloads/week |
|---|---|---|---|
| log-update | Live block rendering | Small | 17.3M |
Optionally also:
nanospinner(20 KB) — if we want to replace our custom spinnerpicocolors(7 KB) — if we want to replace our custom ANSI colorsansi-escapes(20 KB) — for edge case escape sequences
Minimum viable: just log-update. Everything else we already have.
Bun Compatibility
log-update uses ansi-escapes and cli-cursor internally — both are pure JS, no native modules. Should work with Bun. Verify during implementation.
Implementation Plan
Phase 1: Core Live Block
- Add
log-updatedependency - Create
packages/opencode/src/cli/lite/liveblock.ts(~120 lines) - Manage tool state (Map of active tools by ID)
- Render tool lines with status icons (◇ running, ✓ done, ✗ error)
- Freeze block when all tools complete
- Wire into
handleMessage()andhandleCustomCommand()in index.ts
Phase 2: Task + Todo Display
- Add task state tracking (running subagent tasks)
- Add todo list rendering from TodoWrite events
- Render tasks with spinner + elapsed time
- Render todos with checkbox icons
Phase 3: Polish
- Truncate lines to terminal width
- Handle terminal resize (SIGWINCH)
- Dedup identical tool calls with counter (×N)
- Transition animations (fade completed items)
Acceptance Criteria
- Parallel tools update in-place correctly
- Sequential tools still work
- Tasks show live status with elapsed time
- Todos display with checkbox state
- Text output flows cleanly above live block
-
logUpdate.done()freezes block when section completes - No flickering or visual artifacts
-
bun run typecheckpasses -
bun testpasses - Works in iTerm, Terminal.app, tmux
References
- log-update: https://github.com/sindresorhus/log-update
- Docker progress writer pattern
- Research issue [Research] oclite: Terminal chat TUI architecture — findings from AIChat, Aider, blessed #76 (AIChat, Aider, blessed findings)
- Adversarial findings on parallel tool overwrite ([UX] oclite: Tool completion should overwrite start line — fix for parallel tools #81)
Supersedes
- [Architecture] oclite: Adopt append-only output with inline status patterns #74 (append-only output patterns) — Live Block is the evolution of this
- [UX] oclite: Tool completion should overwrite start line — fix for parallel tools #81 parallel tool limitations — solved by Live Block
- [UX] oclite: Live subagent status panel #68 (live subagent status) — enabled by Live Block
- [UI] Live Task Status Panel #19 (live task status panel) — enabled by Live Block