pixtuoid v0.5.0
▸ booting office… ok
▸ loading themes… ok
▸ spawning agents… ok
▸ ready
press any key to skip

← back to pixtuoid

Architecture

How a running coding-agent session becomes a moving sprite in the office.

This file is the single source for pixtuoid’s architecture overview. It renders on the website at /architecture and on GitHub (the diagram below is native Mermaid). CLAUDE.md (the agent guide) links here; per-crate “sharp edges” live in the nested CLAUDE.md files.

The shape of it

pixtuoid is a Cargo workspace of three crates wired as a strict producer → reducer → renderer pipeline:

Dependency direction is one-way: pixtuoid → pixtuoid-core. The Renderer trait is the inversion point that keeps the core terminal-free (so the same pixel pass can drive a PNG/GIF export, not just the terminal).

Data flow

pixtuoid data flowA hook or transcript event flows from Claude Code / Codex through the pixtuoid-hook shim and pixtuoid-core (socket listener, decoder, Transport-tagged reducer, scene state) into the pixtuoid TUI renderer's terminal-agnostic pixel pass and half-block flush.

pixtuoid (binary · TUI)

pixtuoid-core (headless)

pixtuoid-hook (shim)

(Hook, AgentEvent)

(Jsonl, AgentEvent)

scope tree:

cascade ↓ · liveness ↑

hook event

writes transcript JSONL

Arc per mutation

Claude Code / Codex

enrich + forward

200ms timeout · exit 0

HookSocketListener

(Unix socket)

decode_hook_payload

JsonlWatcher · walk_jsonl

Reducer::apply

(Transport-tagged)

SceneState

watch<Arc<SceneState>>

TuiRenderer

render_to_rgb_buffer

(terminal-agnostic)

flush · ½-block cells

Walking the pipeline (real symbols):

  1. Ingest. Claude Code fires a hook → the pixtuoid-hook shim (enrich_payload stamps _pixtuoid_source, a 200 ms write timeout, exit 0) → HookSocketListener on a Unix socket → decode_hook_payload turns the JSON into an AgentEvent. In parallel, JsonlWatcherwalk_jsonl tails each agent’s transcript file (with a first-sight gate so historical/ended sessions don’t resurrect) and decodes lines via a per-source decoder (decode_cc_line / decode_codex_line).
  2. One channel. Every source multiplexes onto a single mpsc::Sender<(Transport, AgentEvent)> (buffer 256). The Transport (Hook | Jsonl) tag is load-bearing: the reducer uses it for hook-wins dedup so a hook and its transcript echo don’t double-count.
  3. Reduce. reducer_task drains the channel into Reducer::apply, which updates a SceneState, runs garbage-collection/stale sweeps on a 1 Hz tick, and delegates single-slot transitions to the FSM. After every change it publishes a fresh Arc<SceneState> on a watch channel.
  4. Render. TuiRenderer borrows the latest scene (O(1), no lock) and paints it through render_to_rgb_buffer — a terminal-agnostic pixel pass — then flush_buffer_to_term compresses pairs of pixel rows into half-block () terminal cells.

Seams & invariants

These are load-bearing — see CLAUDE.md and the nested guides before changing them.

Where to go next

Source of truth: docs/ARCHITECTURE.md — this page renders it verbatim (the diagram is the same Mermaid block GitHub shows).