From ad77352300195ccd652e8ede5ff6cab6fb7c520d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 22:26:06 +0000 Subject: [PATCH 1/3] docs: add comprehensive architecture document (ARCHITECTURE_v1.md) Structured overview of the system architecture including component diagram (Mermaid), data flow diagrams, module boundary table for all 61 source files, and 12 architecture decision records in ADR format. https://claude.ai/code/session_013745dWh9pqJUxB6j82uhw9 --- docs/ARCHITECTURE_v1.md | 454 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 docs/ARCHITECTURE_v1.md diff --git a/docs/ARCHITECTURE_v1.md b/docs/ARCHITECTURE_v1.md new file mode 100644 index 0000000..380220b --- /dev/null +++ b/docs/ARCHITECTURE_v1.md @@ -0,0 +1,454 @@ +# Architecture v1 + +## System Overview + +Architect is a **single-process, layered desktop application** built in Zig that functions as a grid-based terminal multiplexer optimized for multi-agent AI coding workflows. It follows a five-layer architecture: a thin entrypoint delegates to an application runtime that owns the frame loop, platform abstraction (SDL3), session management (PTY + ghostty-vt terminal emulation), scene rendering, and a component-based UI overlay system. All layers run on a single thread (except the notification socket listener), communicating through a synchronous frame loop that polls events, updates state, renders the scene, then renders UI overlays on top. The application uses an action-queue pattern for UI-to-app mutations, epoch-based cache invalidation for efficient rendering, and a vtable-based component registry for extensible UI overlays. + +## Component Diagram + +```mermaid +graph TD + subgraph Entrypoint + MAIN["main.zig
Thin entrypoint; delegates to runtime"] + end + + subgraph Application Layer + RT["app/runtime.zig
Owns frame loop, lifetime, config, session spawning"] + AS["app/app_state.zig
ViewMode, AnimationState, SessionStatus enums"] + LAY["app/layout.zig
Grid sizing, scaling, terminal resize calculations"] + UH["app/ui_host.zig
Builds read-only UiHost snapshot each frame"] + GN["app/grid_nav.zig
Grid navigation, focus switching, notifications"] + GL["app/grid_layout.zig
Grid dimensions and session positioning"] + IK["app/input_keys.zig
Keyboard input encoding to VT sequences"] + IT["app/input_text.zig
IME/text input and preedit handling"] + TA["app/terminal_actions.zig
Clipboard, paste, clear operations"] + WT["app/worktree.zig
Git worktree command building and cd helpers"] + end + + subgraph Platform Layer + SDL["platform/sdl.zig
SDL3 window init, renderer creation, HiDPI"] + IM["input/mapper.zig
SDL keycodes to terminal bytes, shortcut detection"] + CZIG["c.zig
C FFI re-exports for SDL3, SDL3_ttf"] + end + + subgraph Session Layer + SS["session/state.zig
PTY, ghostty-vt terminal, process watcher, render epoch"] + SN["session/notify.zig
Background socket thread, JSON parsing, notification queue"] + SH["shell.zig
Shell process spawning with env setup"] + PTY["pty.zig
PTY abstraction (open, read, write, resize)"] + VTS["vt_stream.zig
VT stream wrapper for ghostty-vt parser"] + CWD["cwd.zig
macOS working directory detection"] + end + + subgraph Rendering Layer + REN["render/renderer.zig
Scene rendering: terminals, borders, animations"] + FONT["font.zig
Glyph rendering, HarfBuzz shaping, LRU cache"] + FC["font_cache.zig
Shared font cache keyed by pixel size"] + FP["font_paths.zig
System font path resolution"] + BD["gfx/box_drawing.zig
Procedural box-drawing characters (U+2500-U+257F)"] + PR["gfx/primitives.zig
Rounded/thick border drawing helpers"] + EA["anim/easing.zig
Cubic ease-in-out interpolation"] + end + + subgraph UI Overlay Layer + UR["ui/root.zig
Component registry, z-index dispatch, action drain"] + UC["ui/component.zig
UiComponent vtable interface definition"] + UT["ui/types.zig
UiHost, UiAction, UiAssets, UiActionQueue"] + SVS["ui/session_view_state.zig
Per-session selection, scroll, hover state"] + FFG["ui/first_frame_guard.zig
Idle throttle bypass for state transitions"] + + subgraph Components + HELP["help_overlay.zig
Keyboard shortcut reference overlay"] + WORK["worktree_overlay.zig
Git worktree picker"] + RECENT["recent_folders_overlay.zig
Recent folders picker"] + DIFF["diff_overlay.zig
Git diff viewer"] + TOAST["toast.zig
Notification display"] + QUIT["quit_confirm.zig
Quit confirmation dialog"] + RESTART["restart_buttons.zig
Dead session restart UI"] + ESC["escape_hold.zig
ESC hold-to-collapse indicator"] + SI["session_interaction.zig
Terminal mouse/scroll/link interaction"] + METRICS["metrics_overlay.zig
Performance stats overlay"] + GS["global_shortcuts.zig
Global keyboard shortcuts"] + PILL["pill_group.zig
Pill overlay coordinator"] + CWDB["cwd_bar.zig
Per-cell working directory bar"] + end + end + + subgraph Shared Utilities + GEOM["geom.zig
Rect struct and point containment"] + COL["colors.zig
Theme and ANSI palette management"] + CFG["config.zig
TOML config loading and persistence"] + URL["url_matcher.zig
URL detection in terminal output"] + MET["metrics.zig
Performance metrics collection framework"] + OPEN["os/open.zig
Cross-platform URL opening"] + end + + MAIN --> RT + RT --> SDL + RT --> SS + RT --> REN + RT --> UR + RT --> CFG + RT --> AS + RT --> LAY + RT --> UH + RT --> GN + RT --> GL + RT --> IK + RT --> IT + RT --> TA + RT --> WT + SS --> SH + SS --> PTY + SS --> VTS + SS --> CWD + SN -.->|"thread-safe queue"| RT + REN --> FONT + REN --> BD + REN --> PR + REN --> EA + FONT --> FC + FONT --> FP + UR --> UC + UR --> UT + UR --> SVS + UR --> FFG + Components --> UT + IM --> CZIG + SDL --> CZIG + REN --> CZIG +``` + +## Data Flow + +### Frame Loop (per frame, ~16ms active / ~50ms idle) + +``` + ┌──────────────────────────────────────┐ + │ SDL Event Queue │ + └──────────┬───────────────────────────┘ + │ poll + v + ┌──────────────────────────────────────┐ + │ Scale to render coordinates │ + └──────────┬───────────────────────────┘ + │ + v + ┌──────────────────────────────────────┐ + │ Build UiHost snapshot │ + │ (window size, grid, theme, etc.) │ + └──────────┬───────────────────────────┘ + │ + v + ┌──────────────────────────────────────┐ + │ ui.handleEvent() │ + │ (topmost z-index first) │ + │ consumed? ─── yes ──> skip app logic│ + └──────────┬───────────────────────────┘ + │ no + v + ┌──────────────────────────────────────┐ + │ App event switch │ + │ (shortcuts, terminal input, resize) │ + └──────────┬───────────────────────────┘ + │ + v + ┌──────────────────────────────────────┐ + │ xev loop iteration │ + │ (async process exit detection) │ + └──────────┬───────────────────────────┘ + │ + v + ┌──────────────────────────────────────┐ + │ Drain session output → ghostty-vt │ + │ Drain notification queue │ + └──────────┬───────────────────────────┘ + │ + v + ┌──────────────────────────────────────┐ + │ ui.update() + drain UiAction queue │ + │ (UI→app mutations applied here) │ + └──────────┬───────────────────────────┘ + │ + v + ┌──────────────────────────────────────┐ + │ Advance animation state │ + └──────────┬───────────────────────────┘ + │ + v + ┌──────────────────────────────────────┐ + │ renderer.render() → scene │ + │ ui.render() → overlays │ + │ SDL_RenderPresent() │ + └──────────────────────────────────────┘ +``` + +### Terminal Output Path + +``` +Shell process + │ writes to PTY + v +session.output_buf (kernel buffer → userspace read) + │ processBytes() + v +vt_stream.zig → ghostty-vt parser + │ state machine updates + v +Terminal cell buffer (content, attributes, colors) + │ session.render_epoch += 1 + v +Renderer cache dirty check (presented_epoch < render_epoch?) + │ yes → re-render + v +font.zig → HarfBuzz shaping → glyph textures + │ + v +SDL_RenderTexture() → frame presented +``` + +### Terminal Input Path + +``` +Physical keyboard + │ + v +SDL_EVENT_KEY_DOWN / SDL_EVENT_TEXT_INPUT + │ scaled to render coordinates + v +UiRoot.handleEvent() (components by z-index) + │ not consumed + v +App event switch → shortcut detection + │ not a shortcut + v +input/mapper.zig → encodeKey() → VT escape sequence bytes + │ + v +session.pending_write buffer + │ next frame + v +PTY write() → shell process stdin +``` + +### External Notification Path + +``` +External tool (Claude Code, Codex, Gemini) + │ JSON over Unix socket + v +session/notify.zig (background thread) + │ parse {"session": N, "state": "awaiting_approval"} + v +NotificationQueue (thread-safe) + │ main loop drains each frame + v +SessionStatus updated (idle → awaiting_approval) + │ + v +Renderer draws attention border (pulsing yellow / solid green) +``` + +### Entry Points + +| Entry Point | Source | Description | +|------------|--------|-------------| +| SDL event queue | Keyboard, mouse, window events | Primary user interaction | +| PTY read | Shell process stdout/stderr | Terminal content updates | +| Unix domain socket | External AI tools | Status notifications (JSON) | +| Config files | `~/.config/architect/` | Startup configuration and persistence | + +### Storage + +| Store | Location | Contents | +|-------|----------|----------| +| Terminal cell buffer | In-memory (ghostty-vt) | Current screen + scrollback (up to 10KB default) | +| Glyph cache | GPU textures + in-memory LRU | Up to 4096 shaped glyph textures | +| Render cache | GPU textures per session | Cached terminal renders, epoch-invalidated | +| config.toml | `~/.config/architect/config.toml` | User preferences (font, theme, UI flags) | +| persistence.toml | `~/.config/architect/persistence.toml` | Runtime state (window pos, font size, terminal cwds) | + +### Exit Points + +| Exit Point | Destination | Description | +|------------|-------------|-------------| +| PTY write | Shell process stdin | Encoded keyboard input | +| SDL renderer | Display | Rendered frames via GPU | +| Config write | Filesystem | Persisted window state and terminal cwds on quit | +| URL open | OS browser | Cmd+Click hyperlinks via `os/open.zig` | + +## Module Boundary Table + +| Module | Responsibility | Public API (key functions/types) | Dependencies | +|--------|---------------|----------------------------------|--------------| +| `main.zig` | Thin entrypoint | `main()` | `app/runtime` | +| `app/runtime.zig` | Application lifetime, frame loop, session spawning, config persistence | `run()`, frame loop internals | `platform/sdl`, `session/state`, `render/renderer`, `ui/root`, `config`, `app/app_state`, `app/layout`, `app/ui_host`, `app/grid_nav`, `app/grid_layout`, `app/input_keys`, `app/input_text`, `app/terminal_actions`, `app/worktree` | +| `app/app_state.zig` | Core state enums and animation interpolation | `ViewMode`, `AnimationState`, `SessionStatus`, `AnimationState.interpolateRect()`, `AnimationState.easeInOutCubic()` | `geom`, `anim/easing` | +| `app/layout.zig` | Grid sizing, DPI scaling, terminal resize calculations | `applyTerminalResize()`, sizing helpers | `app/app_state`, `geom` | +| `app/ui_host.zig` | Builds read-only UI snapshot each frame | `buildUiHost()` | `ui/types`, `app/app_state`, `colors` | +| `app/grid_nav.zig` | Grid navigation, focus switching, notification handling | Navigation functions, notification drain | `app/app_state`, `session/state` | +| `app/grid_layout.zig` | Grid dimension calculation and session positioning | Grid dimension helpers | `app/app_state` | +| `app/input_keys.zig` | Keyboard input encoding to terminal bytes | Key encoding functions | `input/mapper`, `session/state` | +| `app/input_text.zig` | IME/text input and preedit handling | Text input handlers | `session/state` | +| `app/terminal_actions.zig` | Clipboard, paste, clear operations | `paste()`, `clear()`, clipboard functions | `session/state`, `c` | +| `app/worktree.zig` | Git worktree command building and cd helpers | Worktree command builders | `session/state` | +| `platform/sdl.zig` | SDL3 initialization, window management, HiDPI | `init()`, `createWindow()`, `createRenderer()` | `c` | +| `input/mapper.zig` | SDL keycodes to VT escape sequences, shortcut detection | `encodeKey()`, modifier helpers | `c` | +| `c.zig` | C FFI re-exports (SDL3, SDL3_ttf constants) | `SDLK_*`, `SDL_*`, `TTF_*` re-exports | SDL3 system libs (via `@cImport`) | +| `session/state.zig` | Terminal session lifecycle: PTY, ghostty-vt, process watcher | `SessionState`, `init()`, `deinit()`, `ensureSpawnedWithDir()`, `render_epoch`, `pending_write` | `shell`, `pty`, `vt_stream`, `cwd`, `font`, xev | +| `session/notify.zig` | Background notification socket thread and queue | `NotificationQueue`, `startThread()`, `push()`, `drain()` | std (socket, thread) | +| `shell.zig` | Shell process spawning with PTY and env vars | `spawn()` | `pty`, std | +| `pty.zig` | PTY abstraction (open, read, write, resize) | `Pty`, `open()`, `read()`, `write()`, `resize()` | std (posix) | +| `vt_stream.zig` | VT stream wrapper for ghostty-vt parser | `VtStream`, `processBytes()` | ghostty-vt | +| `cwd.zig` | Working directory detection (macOS-only) | `getCwd()` | std (posix, macOS APIs) | +| `render/renderer.zig` | Scene rendering: terminals, borders, animations, CWD bar | `render()`, `RenderCache`, per-session texture management | `font`, `font_cache`, `gfx/box_drawing`, `gfx/primitives`, `anim/easing`, `app/app_state`, `c` | +| `font.zig` | Font rendering, HarfBuzz shaping, glyph LRU cache | `Font`, `openFont()`, `renderGlyph()`, glyph cache (4096 max) | `font_paths`, `c` (SDL3_ttf) | +| `font_cache.zig` | Shared font cache keyed by pixel size | `FontCache`, `getOrCreate()` | `font` | +| `font_paths.zig` | System font path resolution | `findFont()` | std (filesystem) | +| `gfx/box_drawing.zig` | Procedural box-drawing character rendering (U+2500-U+257F) | `renderBoxDrawing()` | `c` | +| `gfx/primitives.zig` | Rounded/thick border drawing helpers | `drawRoundedRect()`, `drawThickBorder()` | `c` | +| `anim/easing.zig` | Easing interpolation functions | `easeInOutCubic(t)` | (none) | +| `geom.zig` | Geometry primitives | `Rect`, `containsPoint()` | (none) | +| `colors.zig` | Theme and ANSI 16/256 palette management | `Theme`, `fromConfig()`, `getPaletteColor()`, `get256ColorWithTheme()` | `config`, `c` | +| `config.zig` | TOML config loading, persistence, migration | `Config`, `load()`, `save()`, `FontConfig`, `WindowConfig`, `GridConfig`, `PaletteConfig`, `UiConfig` | zig-toml, std (filesystem) | +| `metrics.zig` | Performance metrics collection framework | `Metrics`, counters for frames, glyph cache stats | (none) | +| `url_matcher.zig` | URL detection in terminal output | `matchUrl()` | (none) | +| `os/open.zig` | Cross-platform URL/file opening | `open()` | std (child process) | +| `ui/root.zig` | UI component registry, z-index dispatch, action drain | `UiRoot`, `register()`, `handleEvent()`, `update()`, `render()`, `needsFrame()` | `ui/component`, `ui/types` | +| `ui/component.zig` | UI component vtable interface | `UiComponent`, `VTable` (handleEvent, update, render, hitTest, wantsFrame, deinit) | `ui/types`, `c` | +| `ui/types.zig` | Shared UI type definitions | `UiHost`, `UiAction`, `UiActionQueue`, `UiAssets`, `SessionUiInfo` | `app/app_state`, `colors`, `font`, `geom` | +| `ui/session_view_state.zig` | Per-session UI interaction state | `SessionViewState` (selection, scroll offset, hover) | (none) | +| `ui/first_frame_guard.zig` | Idle throttle bypass for visible state transitions | `FirstFrameGuard`, `markTransition()`, `markDrawn()`, `wantsFrame()` | (none) | +| `ui/scale.zig` | DPI scaling helper | `scale(value, ui_scale)` | (none) | +| `ui/components/help_overlay.zig` | Keyboard shortcut reference overlay | Toggle via Cmd+/ | `ui/component`, `ui/types`, expanding_overlay | +| `ui/components/worktree_overlay.zig` | Git worktree picker | Toggle via Cmd+T, emits `SwitchWorktree`/`CreateWorktree`/`RemoveWorktree` | `ui/component`, `ui/types`, expanding_overlay, confirm_dialog | +| `ui/components/recent_folders_overlay.zig` | Recent folders picker | Toggle via Cmd+O, emits `ChangeDirectory` | `ui/component`, `ui/types`, expanding_overlay | +| `ui/components/diff_overlay.zig` | Git diff viewer | Toggle via Cmd+D, emits `ToggleDiffOverlay` | `ui/component`, `ui/types` | +| `ui/components/session_interaction.zig` | Terminal mouse/scroll/link interaction | Mouse selection, scrollback, Cmd+Click links | `ui/component`, `ui/types`, `ui/session_view_state`, `url_matcher` | +| `ui/components/toast.zig` | Notification toast display | `show()`, auto-dismiss timer | `ui/component`, `ui/types` | +| `ui/components/quit_confirm.zig` | Quit confirmation dialog | Emits `ConfirmQuit` | `ui/component`, `ui/types`, button | +| `ui/components/restart_buttons.zig` | Dead session restart UI | Emits `RestartSession` | `ui/component`, `ui/types`, button | +| `ui/components/escape_hold.zig` | ESC hold-to-collapse indicator | Emits `RequestCollapseFocused` on hold | `ui/component`, `ui/types`, `ui/gestures/hold` | +| `ui/components/metrics_overlay.zig` | Performance stats overlay | Toggle via Cmd+Shift+M (when enabled) | `ui/component`, `ui/types`, `metrics` | +| `ui/components/global_shortcuts.zig` | Global keyboard shortcuts | Cmd+, to open config | `ui/component`, `ui/types` | +| `ui/components/pill_group.zig` | Pill overlay coordinator | Collapses other pills when one expands | `ui/component`, `ui/types` | +| `ui/components/cwd_bar.zig` | Per-cell working directory bar | Renders cwd path with marquee scrolling | `ui/component`, `ui/types`, marquee_label | +| `ui/components/expanding_overlay.zig` | Expanding/collapsing animation helper | `ExpandingOverlay`, state machine (Closed/Expanding/Open/Collapsing) | `anim/easing` | +| `ui/components/button.zig` | Styled button rendering (default/primary/danger) | `renderButton()` | `c`, `ui/types` | +| `ui/components/confirm_dialog.zig` | Generic confirmation modal | Configurable title/message/labels, emits `UiAction` on confirm | `ui/component`, `ui/types`, button | +| `ui/components/marquee_label.zig` | Scrolling text label | `MarqueeLabel`, auto-scrolls long text | `font` | +| `ui/components/hotkey_indicator.zig` | Hotkey visual feedback | Fade in/out indicator | `ui/component`, `ui/types` | +| `ui/components/flowing_line.zig` | Animated flowing line renderer | Decorative animated lines | `ui/component`, `ui/types` | +| `ui/gestures/hold.zig` | Reusable hold gesture detector | `HoldGesture`, threshold-based hold detection | (none) | + +## Key Architectural Decisions + +### ADR-001: Five-Layer Single-Thread Architecture + +- **Decision:** Organize the application into five layers (entrypoint, platform, session, rendering, UI overlay) running on a single main thread with only the notification socket on a background thread. +- **Context:** A terminal multiplexer needs tight control over frame timing, event ordering, and GPU resource management. Multi-threaded rendering introduces synchronization complexity without clear benefit for a UI-bound application. The notification socket is the only I/O that must not block the frame loop. +- **Alternatives considered:** + - *Multi-threaded rendering* — rejected because SDL3 renderers are not thread-safe, and the complexity of synchronizing terminal state across threads outweighs the marginal throughput gain. + - *Async I/O everywhere* — rejected because the frame loop is inherently synchronous (poll → update → render → present), and async patterns add indirection without improving latency for a 60 FPS UI. +- **Date:** 2025 (initial architecture) + +### ADR-002: Component-Based UI Overlay System with VTable Dispatch + +- **Decision:** UI overlays are implemented as components registered with a central `UiRoot` registry, each conforming to a `VTable` interface (handleEvent, update, render, hitTest, wantsFrame, deinit). Components are dispatched by z-index, highest first. +- **Context:** The application has 15+ distinct UI elements (help overlay, worktree picker, diff viewer, toast, quit dialog, etc.) that need independent lifecycle management, event handling priority, and rendering order. A centralized registry prevents ad-hoc event handling scattered across the main loop. +- **Alternatives considered:** + - *Immediate-mode GUI* — rejected because retain-mode components with cached textures reduce per-frame CPU work, and the vtable pattern is idiomatic in Zig for polymorphic dispatch. + - *Ad-hoc event handling in main.zig* — rejected because it leads to unmaintainable event switch growth as UI features are added; the component pattern isolates concerns. +- **Date:** 2025 (initial architecture) + +### ADR-003: UiAction Queue for UI-to-App Mutations + +- **Decision:** UI components never mutate application state directly. Instead, they push `UiAction` values (a tagged union) to a queue that the main loop drains after all component updates complete. +- **Context:** Direct mutation from UI components would create ordering dependencies between components and the main loop. A queue decouples intent from execution, making it safe to add/remove/reorder components without breaking state transitions. +- **Alternatives considered:** + - *Direct callback functions* — rejected because callbacks create implicit coupling and make it hard to reason about mutation ordering. + - *Event bus / pub-sub* — rejected as over-engineered for a single-process application; a simple queue with a typed union is sufficient and type-safe. +- **Date:** 2025 (initial architecture) + +### ADR-004: Epoch-Based Render Cache Invalidation + +- **Decision:** Each `SessionState` maintains a monotonic `render_epoch` counter that increments on terminal content changes. The renderer's `RenderCache` tracks the last presented epoch per session and only re-renders when epochs diverge. +- **Context:** Re-rendering all terminal cells every frame is expensive (glyph shaping, texture creation). Most frames in a multi-terminal grid have no changes in most sessions. Epoch comparison is O(1) per session and avoids deep content diffing. +- **Alternatives considered:** + - *Dirty-flag per cell* — rejected because tracking individual cell changes is memory-intensive and the granularity is unnecessary when the renderer caches entire session textures. + - *Timer-based refresh* — rejected because it wastes GPU cycles re-rendering unchanged terminals and introduces visible latency for changed ones. +- **Date:** 2025 (initial architecture) + +### ADR-005: ghostty-vt for Terminal Emulation + +- **Decision:** Use ghostty-vt (from the Ghostty terminal project) as the VT state machine and ANSI parser rather than implementing one from scratch. +- **Context:** Terminal emulation is a complex domain with thousands of edge cases (escape sequences, Unicode handling, alternate screen buffers, scrollback, etc.). ghostty-vt is a mature, well-tested implementation written in Zig, making it a natural fit for a Zig application. +- **Alternatives considered:** + - *Custom VT parser* — rejected because building a correct VT100/xterm-compatible parser is a multi-year effort and a maintenance burden orthogonal to the product goal. + - *libvterm (C library)* — rejected because it requires C FFI overhead and memory management coordination; ghostty-vt integrates natively with Zig's type system and allocator model. +- **Date:** 2025 (initial dependency choice) + +### ADR-006: SDL3 for Rendering and Input + +- **Decision:** Use SDL3 as the platform abstraction layer for window management, GPU-accelerated 2D rendering, input events, and font rendering (via SDL3_ttf with HarfBuzz). +- **Context:** The application needs cross-platform window management, hardware-accelerated texture rendering, and HiDPI support. SDL3 provides all of these with a C API that Zig can import directly via `@cImport`. +- **Alternatives considered:** + - *Native platform APIs (AppKit/Metal)* — rejected because it locks the project to macOS; SDL3 allows future Linux/Windows porting. + - *Vulkan/OpenGL directly* — rejected because 2D terminal rendering does not need low-level GPU control, and SDL3's renderer API is sufficient and simpler. + - *Electron / web-based* — rejected for performance and resource usage; a native Zig application has sub-millisecond event latency and minimal memory overhead. +- **Date:** 2025 (initial dependency choice) + +### ADR-007: Lazy Session Spawning + +- **Decision:** Only session 0 spawns a shell process on startup. Additional sessions spawn on first user interaction (click or keyboard navigation). +- **Context:** Users may configure a grid with many slots but only actively use a few. Eagerly spawning all shells wastes system resources (PTY file descriptors, process table entries, memory for terminal buffers) and slows startup. +- **Alternatives considered:** + - *Eager spawn all* — rejected because startup time scales linearly with session count, and unused PTYs waste kernel resources. + - *Spawn on first output* — rejected because sessions need a shell to produce output; spawn-on-interaction is the natural trigger. +- **Date:** 2025 (initial design) + +### ADR-008: Procedural Box-Drawing Characters + +- **Decision:** Box-drawing characters (U+2500-U+257F) are rendered procedurally via line/rectangle primitives rather than using font glyphs. +- **Context:** Font-based box-drawing characters often have alignment issues: gaps between cells, inconsistent line widths, or mismatched metrics across font families. Procedural rendering guarantees pixel-perfect alignment regardless of the chosen font. +- **Alternatives considered:** + - *Font glyph rendering* — rejected because alignment varies by font and size; even monospace fonts often have subpixel gaps in box-drawing characters. + - *Pre-rendered sprite atlas* — rejected because it doesn't scale with DPI or font size changes. +- **Date:** 2025 (rendering implementation) + +### ADR-009: Thread-Safe Notification Queue for External Tool Integration + +- **Decision:** External AI tools communicate with Architect via a Unix domain socket. A dedicated background thread accepts connections, parses single-line JSON messages, and pushes to a thread-safe queue. The main loop drains this queue once per frame. +- **Context:** AI coding agents (Claude Code, Codex, Gemini) need to signal state changes (start, awaiting_approval, done) to trigger visual indicators. Socket I/O must not block the render thread, but state updates must be applied synchronously during the frame loop to avoid race conditions with rendering. +- **Alternatives considered:** + - *Polling a file or pipe* — rejected because it introduces latency and filesystem overhead; sockets provide immediate delivery. + - *D-Bus or platform IPC* — rejected because it adds platform-specific dependencies; Unix domain sockets are simple and portable across macOS and Linux. + - *Direct main-thread socket polling* — rejected because accept/read can block; a background thread with a lock-free queue provides non-blocking integration. +- **Date:** 2025 (notification system implementation) + +### ADR-010: TOML-Based Dual Configuration (User Prefs + Runtime State) + +- **Decision:** Configuration is split into two TOML files: `config.toml` for user-editable preferences (font, theme, UI flags) and `persistence.toml` for auto-managed runtime state (window position, font size, terminal cwds, recent folders). +- **Context:** Mixing user preferences with volatile runtime state in a single file leads to merge conflicts and confusion when users manually edit configuration. Separating them allows `config.toml` to be version-controlled or shared, while `persistence.toml` is machine-specific and auto-managed. +- **Alternatives considered:** + - *Single config file* — rejected because auto-saving window position into a user-edited file causes unexpected diffs. + - *JSON or YAML* — rejected because TOML is designed for configuration files, has clear section semantics, and the zig-toml library provides native Zig integration without C FFI. + - *SQLite for persistence* — rejected as over-engineered for a handful of key-value pairs; TOML is human-readable and easy to debug. +- **Date:** 2025 (configuration system implementation) + +### ADR-011: Hardcoded Keybindings + +- **Decision:** All keyboard shortcuts are hardcoded in the source code. There is no user-configurable keybinding system. +- **Context:** The application has a small, focused set of shortcuts (Cmd+N, Cmd+W, Cmd+T, Cmd+D, Cmd+/, Cmd+1-0, Cmd+Return, Cmd+Q). A configurable keybinding system adds significant complexity (parser, conflict detection, documentation generation) for marginal user benefit at this stage. +- **Alternatives considered:** + - *Config-driven keybindings* — deferred, not rejected; may be added as the shortcut set grows, but current simplicity is preferred during early development. +- **Date:** 2025 (input system implementation) + +### ADR-012: FirstFrameGuard Pattern for Idle Throttle Bypass + +- **Decision:** When a UI component transitions to a visible state (modal opens, gesture starts), it uses a `FirstFrameGuard` to signal the frame loop that an immediate render is needed, bypassing idle throttling. +- **Context:** The frame loop throttles to ~20 FPS when idle (no terminal output or user input). Without the guard, newly visible UI elements would appear with up to 250ms delay, creating a perceived lag. The guard ensures the first frame of a transition renders immediately. +- **Alternatives considered:** + - *Always render at full rate* — rejected because it wastes CPU/GPU when nothing is changing, impacting battery life on laptops. + - *SDL event injection* — rejected because synthetic events pollute the event queue and complicate event handling logic. +- **Date:** 2025 (UI system refinement) From 820d89c09092e80f7d7a3aaac05d6af00198c429 Mon Sep 17 00:00:00 2001 From: Forketyfork Date: Sun, 8 Feb 2026 08:25:18 +0100 Subject: [PATCH 2/3] docs: restructure architecture doc for usability Rename ARCHITECTURE_v1.md to ARCHITECTURE.md and restructure based on review feedback: add dependency direction rules, "Rules for New Code" section extracting key patterns from ADRs, and a "Where to Put New Code" decision table. Simplify the component diagram to show layer groups instead of individual files, and collapse the module boundary table from 50+ rows to ~18 by grouping ui/components/*, app/*, session/*, gfx/*, and shared utilities into single rows. --- CLAUDE.md | 4 +- README.md | 2 +- docs/ARCHITECTURE.md | 410 ++++++++++++++++++++++++++++++++++++ docs/ARCHITECTURE_v1.md | 454 ---------------------------------------- docs/architecture.md | 401 ----------------------------------- 5 files changed, 413 insertions(+), 858 deletions(-) create mode 100644 docs/ARCHITECTURE.md delete mode 100644 docs/ARCHITECTURE_v1.md delete mode 100644 docs/architecture.md diff --git a/CLAUDE.md b/CLAUDE.md index b3ba703..82ec575 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ Guidance for any code agent working on the Architect repo. Keep this file instru ## Documentation -- Architecture overview lives in `docs/architecture.md`. Read it to understand the app architecture. +- Architecture overview lives in `docs/ARCHITECTURE.md`. Read it to understand the app architecture. - Configuration reference lives in `docs/configuration.md`. Consult it for config.toml and persistence.toml structure. ## Quick Workflow @@ -121,7 +121,7 @@ The `<= len` pattern is only correct when `pos` represents a position *after* pr ## Documentation Hygiene (REQUIRED) - **ALWAYS** update documentation when making changes. This is not optional. - Update `README.md` for any user-facing changes: new features, configuration options, keyboard shortcuts, or behavior changes. -- Update `docs/architecture.md` when adding new components, modules, or changing the system structure. +- Update `docs/ARCHITECTURE.md` when adding new components, modules, or changing the system structure. - Update `docs/configuration.md` when adding, removing, or changing configuration options in `config.toml` or `persistence.toml`. - Keep this `CLAUDE.md` aligned when workflows or automation expectations change. - Documentation updates should be part of the same PR as the code changes. diff --git a/README.md b/README.md index 51848e3..8bbd00b 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Common settings include font family, theme colors, and grid font scale. The grid ## Documentation * [`docs/ai-integration.md`](docs/ai-integration.md): set up Claude Code, Codex, and Gemini CLI hooks for status notifications (includes `architect notify`, `architect hook ...`, and timestamped backups). -* [`docs/architecture.md`](docs/architecture.md): architecture overview and system boundaries. +* [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md): architecture overview and system boundaries. * [`docs/configuration.md`](docs/configuration.md): detailed configuration reference for `config.toml` and `persistence.toml`. * [`docs/development.md`](docs/development.md): build, test, and release process. * [`CLAUDE.md`](CLAUDE.md): agent guidelines for code assistants. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..2826d44 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,410 @@ +# Architecture + +## System Overview + +Architect is a **single-process, layered desktop application** built in Zig that functions as a grid-based terminal multiplexer optimized for multi-agent AI coding workflows. It follows a five-layer architecture: a thin entrypoint delegates to an application runtime that owns the frame loop, platform abstraction (SDL3), session management (PTY + ghostty-vt terminal emulation), scene rendering, and a component-based UI overlay system. All layers run on a single thread (except the notification socket listener), communicating through a synchronous frame loop that polls events, updates state, renders the scene, then renders UI overlays on top. The application uses an action-queue pattern for UI-to-app mutations, epoch-based cache invalidation for efficient rendering, and a vtable-based component registry for extensible UI overlays. + +## Component Diagram + +```mermaid +graph TD + subgraph Entrypoint + MAIN["main.zig"] + end + + subgraph Application Layer + RT["app/runtime.zig
Frame loop, lifetime, config, session spawning"] + APP_MODS["app_state, layout, ui_host,
grid_nav, grid_layout, input_keys,
input_text, terminal_actions, worktree"] + end + + subgraph Platform Layer + SDL["platform/sdl.zig
SDL3 window, renderer, HiDPI"] + IM["input/mapper.zig
Keycodes to VT sequences"] + CZIG["c.zig
C FFI re-exports"] + end + + subgraph Session Layer + SS["session/state.zig
PTY, ghostty-vt, process watcher"] + SN["session/notify.zig
Background socket thread"] + SESSION_MODS["shell, pty, vt_stream, cwd"] + end + + subgraph Rendering Layer + REN["render/renderer.zig
Terminals, borders, animations"] + FONT["font.zig + font_cache.zig
HarfBuzz shaping, glyph LRU"] + GFX["gfx/*
Box drawing, primitives"] + end + + subgraph UI Overlay Layer + UR["ui/root.zig
Component registry, z-index dispatch"] + UI_CORE["component.zig, types.zig,
session_view_state, first_frame_guard"] + COMPONENTS["ui/components/*
15+ overlay and widget implementations"] + end + + subgraph Shared Utilities + SHARED["geom, colors, config,
url_matcher, metrics, os/open"] + end + + MAIN --> RT + RT --> APP_MODS + RT --> SDL + RT --> SS + RT --> REN + RT --> UR + SS --> SESSION_MODS + SN -.->|"thread-safe queue"| RT + REN --> FONT + REN --> GFX + UR --> UI_CORE + COMPONENTS --> UI_CORE + IM --> CZIG + SDL --> CZIG + REN --> CZIG +``` + +## Dependency Rules + +Dependencies flow strictly downward through the layer stack. No upward or lateral dependencies between peer layers except through the Application layer. + +``` +Entrypoint + | + v +Application Layer (app/runtime.zig orchestrates everything below) + | + +-----------+-----------+-----------+ + | | | | + v v v v +Platform Session Rendering UI Overlay + | | | + v v v + Shared Utilities +``` + +**Invariants:** +- Session, Rendering, and UI Overlay layers never import from each other directly. All cross-layer communication flows through the Application layer or shared types. +- UI components communicate with the application exclusively via the `UiAction` queue (never direct state mutation). +- The only background thread is the notification socket listener (`session/notify.zig`); it communicates with the main thread via a thread-safe queue drained once per frame. +- Shared Utilities (`geom`, `colors`, `config`, `metrics`, etc.) may be imported by any layer but never import from layers above them. + +## Rules for New Code + +These patterns are mandatory for all new code. They are derived from the architectural decisions (see ADRs below) and exist to prevent the most common structural violations. + +1. **UI components use the vtable interface and communicate via UiAction queue.** Never mutate application state directly from a UI component. Push a `UiAction` to the queue; the main loop drains it after all component updates complete. (See ADR-003.) + +2. **Render invalidation uses epoch comparison.** When terminal content changes, increment `render_epoch` on the `SessionState`. The renderer checks `presented_epoch < render_epoch` and only re-renders on mismatch. Never force a full re-render. (See ADR-004.) + +3. **Blocking I/O goes on a background thread with a thread-safe queue.** The frame loop must never block. Any new external I/O source must follow the notification socket pattern: background thread + queue + main-loop drain. (See ADR-009.) + +4. **Config vs. persistence separation.** User-editable preferences go in `config.toml`. Auto-managed runtime state (window position, recent folders, terminal cwds) goes in `persistence.toml`. Never mix them. (See ADR-010.) + +5. **Use FirstFrameGuard for visibility transitions.** When a UI component moves to a visible state (modal opens, toast appears), call `markTransition()` and return `guard.wantsFrame()` from the component's `wantsFrame` method to bypass idle throttling. (See ADR-012.) + +## Where to Put New Code + +| I need to... | Put it in... | +|-------------------------------------|-------------------------------------------| +| Add a new UI element (overlay, dialog, widget) | `ui/components/`, implement `UiComponent` vtable, register in `UiRoot` | +| Add a new keyboard shortcut | `ui/components/global_shortcuts.zig` | +| Add terminal behavior or PTY logic | `session/` | +| Add a rendering primitive | `gfx/` | +| Add a new config option | `config.zig` + `config.toml` docs | +| Add a new persisted runtime value | `config.zig` (persistence section) + `persistence.toml` docs | +| Add cross-layer shared types | Shared Utilities (`geom.zig`, `colors.zig`, etc.) | +| Add a new UiAction | `ui/types.zig` (tagged union) + handler in `app/runtime.zig` | +| Add external tool integration | `session/notify.zig` (extend notification protocol) | + +## Data Flow + +### Frame Loop (per frame, ~16ms active / ~50ms idle) + +``` + +--------------------------------------+ + | SDL Event Queue | + +------------------+-------------------+ + | poll + v + +--------------------------------------+ + | Scale to render coordinates | + +------------------+-------------------+ + | + v + +--------------------------------------+ + | Build UiHost snapshot | + | (window size, grid, theme, etc.) | + +------------------+-------------------+ + | + v + +--------------------------------------+ + | ui.handleEvent() | + | (topmost z-index first) | + | consumed? --- yes --> skip app logic| + +------------------+-------------------+ + | no + v + +--------------------------------------+ + | App event switch | + | (shortcuts, terminal input, resize) | + +------------------+-------------------+ + | + v + +--------------------------------------+ + | xev loop iteration | + | (async process exit detection) | + +------------------+-------------------+ + | + v + +--------------------------------------+ + | Drain session output -> ghostty-vt | + | Drain notification queue | + +------------------+-------------------+ + | + v + +--------------------------------------+ + | ui.update() + drain UiAction queue | + | (UI->app mutations applied here) | + +------------------+-------------------+ + | + v + +--------------------------------------+ + | Advance animation state | + +------------------+-------------------+ + | + v + +--------------------------------------+ + | renderer.render() -> scene | + | ui.render() -> overlays | + | SDL_RenderPresent() | + +--------------------------------------+ +``` + +### Terminal Output Path + +``` +Shell process + | writes to PTY + v +session.output_buf (kernel buffer -> userspace read) + | processBytes() + v +vt_stream.zig -> ghostty-vt parser + | state machine updates + v +Terminal cell buffer (content, attributes, colors) + | session.render_epoch += 1 + v +Renderer cache dirty check (presented_epoch < render_epoch?) + | yes -> re-render + v +font.zig -> HarfBuzz shaping -> glyph textures + | + v +SDL_RenderTexture() -> frame presented +``` + +### Terminal Input Path + +``` +Physical keyboard + | + v +SDL_EVENT_KEY_DOWN / SDL_EVENT_TEXT_INPUT + | scaled to render coordinates + v +UiRoot.handleEvent() (components by z-index) + | not consumed + v +App event switch -> shortcut detection + | not a shortcut + v +input/mapper.zig -> encodeKey() -> VT escape sequence bytes + | + v +session.pending_write buffer + | next frame + v +PTY write() -> shell process stdin +``` + +### External Notification Path + +``` +External tool (Claude Code, Codex, Gemini) + | JSON over Unix socket + v +session/notify.zig (background thread) + | parse {"session": N, "state": "awaiting_approval"} + v +NotificationQueue (thread-safe) + | main loop drains each frame + v +SessionStatus updated (idle -> awaiting_approval) + | + v +Renderer draws attention border (pulsing yellow / solid green) +``` + +### Entry Points + +| Entry Point | Source | Description | +|------------|--------|-------------| +| SDL event queue | Keyboard, mouse, window events | Primary user interaction | +| PTY read | Shell process stdout/stderr | Terminal content updates | +| Unix domain socket | External AI tools | Status notifications (JSON) | +| Config files | `~/.config/architect/` | Startup configuration and persistence | + +### Storage + +| Store | Location | Contents | +|-------|----------|----------| +| Terminal cell buffer | In-memory (ghostty-vt) | Current screen + scrollback (up to 10KB default) | +| Glyph cache | GPU textures + in-memory LRU | Up to 4096 shaped glyph textures | +| Render cache | GPU textures per session | Cached terminal renders, epoch-invalidated | +| config.toml | `~/.config/architect/config.toml` | User preferences (font, theme, UI flags) | +| persistence.toml | `~/.config/architect/persistence.toml` | Runtime state (window pos, font size, terminal cwds) | + +### Exit Points + +| Exit Point | Destination | Description | +|------------|-------------|-------------| +| PTY write | Shell process stdin | Encoded keyboard input | +| SDL renderer | Display | Rendered frames via GPU | +| Config write | Filesystem | Persisted window state and terminal cwds on quit | +| URL open | OS browser | Cmd+Click hyperlinks via `os/open.zig` | + +## Module Boundary Table + +| Module | Responsibility | Public API (key functions/types) | Dependencies | +|--------|---------------|----------------------------------|--------------| +| `main.zig` | Thin entrypoint | `main()` | `app/runtime` | +| `app/runtime.zig` | Application lifetime, frame loop, session spawning, config persistence | `run()`, frame loop internals | `platform/sdl`, `session/state`, `render/renderer`, `ui/root`, `config`, all `app/*` modules | +| `app/*` (app_state, layout, ui_host, grid_nav, grid_layout, input_keys, input_text, terminal_actions, worktree) | Application logic decomposed by concern: state enums, grid sizing, UI snapshot building, navigation, input encoding, clipboard, worktree commands | `ViewMode`, `AnimationState`, `SessionStatus`, `buildUiHost()`, `applyTerminalResize()`, `encodeKey()`, `paste()`, `clear()` | `geom`, `anim/easing`, `ui/types`, `colors`, `input/mapper`, `session/state`, `c` | +| `platform/sdl.zig` | SDL3 initialization, window management, HiDPI | `init()`, `createWindow()`, `createRenderer()` | `c` | +| `input/mapper.zig` | SDL keycodes to VT escape sequences, shortcut detection | `encodeKey()`, modifier helpers | `c` | +| `c.zig` | C FFI re-exports (SDL3, SDL3_ttf constants) | `SDLK_*`, `SDL_*`, `TTF_*` re-exports | SDL3 system libs (via `@cImport`) | +| `session/state.zig` | Terminal session lifecycle: PTY, ghostty-vt, process watcher | `SessionState`, `init()`, `deinit()`, `ensureSpawnedWithDir()`, `render_epoch`, `pending_write` | `shell`, `pty`, `vt_stream`, `cwd`, `font`, xev | +| `session/notify.zig` | Background notification socket thread and queue | `NotificationQueue`, `startThread()`, `push()`, `drain()` | std (socket, thread) | +| `session/*` (shell, pty, vt_stream, cwd) | Shell spawning, PTY abstraction, VT parsing, working directory detection | `spawn()`, `Pty`, `VtStream.processBytes()`, `getCwd()` | std (posix), ghostty-vt | +| `render/renderer.zig` | Scene rendering: terminals, borders, animations | `render()`, `RenderCache`, per-session texture management | `font`, `font_cache`, `gfx/*`, `anim/easing`, `app/app_state`, `c` | +| `font.zig` + `font_cache.zig` | Font rendering, HarfBuzz shaping, glyph LRU cache, shared font cache | `Font`, `openFont()`, `renderGlyph()`, `FontCache`, `getOrCreate()` | `font_paths`, `c` (SDL3_ttf) | +| `gfx/*` (box_drawing, primitives) | Procedural box-drawing characters (U+2500-U+257F), rounded/thick border helpers | `renderBoxDrawing()`, `drawRoundedRect()`, `drawThickBorder()` | `c` | +| `ui/root.zig` | UI component registry, z-index dispatch, action drain | `UiRoot`, `register()`, `handleEvent()`, `update()`, `render()`, `needsFrame()` | `ui/component`, `ui/types` | +| `ui/component.zig` | UI component vtable interface | `UiComponent`, `VTable` (handleEvent, update, render, hitTest, wantsFrame, deinit) | `ui/types`, `c` | +| `ui/types.zig` | Shared UI type definitions | `UiHost`, `UiAction`, `UiActionQueue`, `UiAssets`, `SessionUiInfo` | `app/app_state`, `colors`, `font`, `geom` | +| `ui/session_view_state.zig` | Per-session UI interaction state | `SessionViewState` (selection, scroll offset, hover) | (none) | +| `ui/first_frame_guard.zig` | Idle throttle bypass for visible state transitions | `FirstFrameGuard`, `markTransition()`, `markDrawn()`, `wantsFrame()` | (none) | +| `ui/components/*` | Individual overlay and widget implementations conforming to `UiComponent` vtable. Includes: help overlay, worktree picker, recent folders picker, diff viewer, session interaction, toast, quit confirm, restart buttons, escape hold indicator, metrics overlay, global shortcuts, pill group, cwd bar, expanding overlay helper, button, confirm dialog, marquee label, hotkey indicator, flowing line, hold gesture detector. | Each component implements the `VTable` interface; overlays toggle via keyboard shortcuts and emit `UiAction` values. | `ui/component`, `ui/types`, `anim/easing`, `font`, `metrics`, `url_matcher`, `ui/session_view_state` | +| Shared Utilities (`geom`, `colors`, `config`, `metrics`, `url_matcher`, `os/open`, `anim/easing`) | Geometry primitives, theme/palette management, TOML config loading/persistence, performance metrics, URL detection, cross-platform URL opening, easing functions | `Rect`, `Theme`, `Config`, `Metrics`, `matchUrl()`, `open()`, `easeInOutCubic()` | std, zig-toml, `c` | + +## Key Architectural Decisions + +### ADR-001: Five-Layer Single-Thread Architecture + +- **Decision:** Organize the application into five layers (entrypoint, platform, session, rendering, UI overlay) running on a single main thread with only the notification socket on a background thread. +- **Context:** A terminal multiplexer needs tight control over frame timing, event ordering, and GPU resource management. Multi-threaded rendering introduces synchronization complexity without clear benefit for a UI-bound application. The notification socket is the only I/O that must not block the frame loop. +- **Alternatives considered:** + - *Multi-threaded rendering* -- rejected because SDL3 renderers are not thread-safe, and the complexity of synchronizing terminal state across threads outweighs the marginal throughput gain. + - *Async I/O everywhere* -- rejected because the frame loop is inherently synchronous (poll, update, render, present), and async patterns add indirection without improving latency for a 60 FPS UI. +- **Date:** 2025 (initial architecture) + +### ADR-002: Component-Based UI Overlay System with VTable Dispatch + +- **Decision:** UI overlays are implemented as components registered with a central `UiRoot` registry, each conforming to a `VTable` interface (handleEvent, update, render, hitTest, wantsFrame, deinit). Components are dispatched by z-index, highest first. +- **Context:** The application has 15+ distinct UI elements (help overlay, worktree picker, diff viewer, toast, quit dialog, etc.) that need independent lifecycle management, event handling priority, and rendering order. A centralized registry prevents ad-hoc event handling scattered across the main loop. +- **Alternatives considered:** + - *Immediate-mode GUI* -- rejected because retain-mode components with cached textures reduce per-frame CPU work, and the vtable pattern is idiomatic in Zig for polymorphic dispatch. + - *Ad-hoc event handling in main.zig* -- rejected because it leads to unmaintainable event switch growth as UI features are added; the component pattern isolates concerns. +- **Date:** 2025 (initial architecture) + +### ADR-003: UiAction Queue for UI-to-App Mutations + +- **Decision:** UI components never mutate application state directly. Instead, they push `UiAction` values (a tagged union) to a queue that the main loop drains after all component updates complete. +- **Context:** Direct mutation from UI components would create ordering dependencies between components and the main loop. A queue decouples intent from execution, making it safe to add/remove/reorder components without breaking state transitions. +- **Alternatives considered:** + - *Direct callback functions* -- rejected because callbacks create implicit coupling and make it hard to reason about mutation ordering. + - *Event bus / pub-sub* -- rejected as over-engineered for a single-process application; a simple queue with a typed union is sufficient and type-safe. +- **Date:** 2025 (initial architecture) + +### ADR-004: Epoch-Based Render Cache Invalidation + +- **Decision:** Each `SessionState` maintains a monotonic `render_epoch` counter that increments on terminal content changes. The renderer's `RenderCache` tracks the last presented epoch per session and only re-renders when epochs diverge. +- **Context:** Re-rendering all terminal cells every frame is expensive (glyph shaping, texture creation). Most frames in a multi-terminal grid have no changes in most sessions. Epoch comparison is O(1) per session and avoids deep content diffing. +- **Alternatives considered:** + - *Dirty-flag per cell* -- rejected because tracking individual cell changes is memory-intensive and the granularity is unnecessary when the renderer caches entire session textures. + - *Timer-based refresh* -- rejected because it wastes GPU cycles re-rendering unchanged terminals and introduces visible latency for changed ones. +- **Date:** 2025 (initial architecture) + +### ADR-005: ghostty-vt for Terminal Emulation + +- **Decision:** Use ghostty-vt (from the Ghostty terminal project) as the VT state machine and ANSI parser rather than implementing one from scratch. +- **Context:** Terminal emulation is a complex domain with thousands of edge cases (escape sequences, Unicode handling, alternate screen buffers, scrollback, etc.). ghostty-vt is a mature, well-tested implementation written in Zig, making it a natural fit for a Zig application. +- **Alternatives considered:** + - *Custom VT parser* -- rejected because building a correct VT100/xterm-compatible parser is a multi-year effort and a maintenance burden orthogonal to the product goal. + - *libvterm (C library)* -- rejected because it requires C FFI overhead and memory management coordination; ghostty-vt integrates natively with Zig's type system and allocator model. +- **Date:** 2025 (initial dependency choice) + +### ADR-006: SDL3 for Rendering and Input + +- **Decision:** Use SDL3 as the platform abstraction layer for window management, GPU-accelerated 2D rendering, input events, and font rendering (via SDL3_ttf with HarfBuzz). +- **Context:** The application needs cross-platform window management, hardware-accelerated texture rendering, and HiDPI support. SDL3 provides all of these with a C API that Zig can import directly via `@cImport`. +- **Alternatives considered:** + - *Native platform APIs (AppKit/Metal)* -- rejected because it locks the project to macOS; SDL3 allows future Linux/Windows porting. + - *Vulkan/OpenGL directly* -- rejected because 2D terminal rendering does not need low-level GPU control, and SDL3's renderer API is sufficient and simpler. + - *Electron / web-based* -- rejected for performance and resource usage; a native Zig application has sub-millisecond event latency and minimal memory overhead. +- **Date:** 2025 (initial dependency choice) + +### ADR-007: Lazy Session Spawning + +- **Decision:** Only session 0 spawns a shell process on startup. Additional sessions spawn on first user interaction (click or keyboard navigation). +- **Context:** Users may configure a grid with many slots but only actively use a few. Eagerly spawning all shells wastes system resources (PTY file descriptors, process table entries, memory for terminal buffers) and slows startup. +- **Alternatives considered:** + - *Eager spawn all* -- rejected because startup time scales linearly with session count, and unused PTYs waste kernel resources. + - *Spawn on first output* -- rejected because sessions need a shell to produce output; spawn-on-interaction is the natural trigger. +- **Date:** 2025 (initial design) + +### ADR-008: Procedural Box-Drawing Characters + +- **Decision:** Box-drawing characters (U+2500-U+257F) are rendered procedurally via line/rectangle primitives rather than using font glyphs. +- **Context:** Font-based box-drawing characters often have alignment issues: gaps between cells, inconsistent line widths, or mismatched metrics across font families. Procedural rendering guarantees pixel-perfect alignment regardless of the chosen font. +- **Alternatives considered:** + - *Font glyph rendering* -- rejected because alignment varies by font and size; even monospace fonts often have subpixel gaps in box-drawing characters. + - *Pre-rendered sprite atlas* -- rejected because it doesn't scale with DPI or font size changes. +- **Date:** 2025 (rendering implementation) + +### ADR-009: Thread-Safe Notification Queue for External Tool Integration + +- **Decision:** External AI tools communicate with Architect via a Unix domain socket. A dedicated background thread accepts connections, parses single-line JSON messages, and pushes to a thread-safe queue. The main loop drains this queue once per frame. +- **Context:** AI coding agents (Claude Code, Codex, Gemini) need to signal state changes (start, awaiting_approval, done) to trigger visual indicators. Socket I/O must not block the render thread, but state updates must be applied synchronously during the frame loop to avoid race conditions with rendering. +- **Alternatives considered:** + - *Polling a file or pipe* -- rejected because it introduces latency and filesystem overhead; sockets provide immediate delivery. + - *D-Bus or platform IPC* -- rejected because it adds platform-specific dependencies; Unix domain sockets are simple and portable across macOS and Linux. + - *Direct main-thread socket polling* -- rejected because accept/read can block; a background thread with a lock-free queue provides non-blocking integration. +- **Date:** 2025 (notification system implementation) + +### ADR-010: TOML-Based Dual Configuration (User Prefs + Runtime State) + +- **Decision:** Configuration is split into two TOML files: `config.toml` for user-editable preferences (font, theme, UI flags) and `persistence.toml` for auto-managed runtime state (window position, font size, terminal cwds, recent folders). +- **Context:** Mixing user preferences with volatile runtime state in a single file leads to merge conflicts and confusion when users manually edit configuration. Separating them allows `config.toml` to be version-controlled or shared, while `persistence.toml` is machine-specific and auto-managed. +- **Alternatives considered:** + - *Single config file* -- rejected because auto-saving window position into a user-edited file causes unexpected diffs. + - *JSON or YAML* -- rejected because TOML is designed for configuration files, has clear section semantics, and the zig-toml library provides native Zig integration without C FFI. + - *SQLite for persistence* -- rejected as over-engineered for a handful of key-value pairs; TOML is human-readable and easy to debug. +- **Date:** 2025 (configuration system implementation) + +### ADR-011: Hardcoded Keybindings + +- **Decision:** All keyboard shortcuts are hardcoded in the source code. There is no user-configurable keybinding system. +- **Context:** The application has a small, focused set of shortcuts (Cmd+N, Cmd+W, Cmd+T, Cmd+D, Cmd+/, Cmd+1-0, Cmd+Return, Cmd+Q). A configurable keybinding system adds significant complexity (parser, conflict detection, documentation generation) for marginal user benefit at this stage. +- **Alternatives considered:** + - *Config-driven keybindings* -- deferred, not rejected; may be added as the shortcut set grows, but current simplicity is preferred during early development. +- **Date:** 2025 (input system implementation) + +### ADR-012: FirstFrameGuard Pattern for Idle Throttle Bypass + +- **Decision:** When a UI component transitions to a visible state (modal opens, gesture starts), it uses a `FirstFrameGuard` to signal the frame loop that an immediate render is needed, bypassing idle throttling. +- **Context:** The frame loop throttles to ~20 FPS when idle (no terminal output or user input). Without the guard, newly visible UI elements would appear with up to 250ms delay, creating a perceived lag. The guard ensures the first frame of a transition renders immediately. +- **Alternatives considered:** + - *Always render at full rate* -- rejected because it wastes CPU/GPU when nothing is changing, impacting battery life on laptops. + - *SDL event injection* -- rejected because synthetic events pollute the event queue and complicate event handling logic. +- **Date:** 2025 (UI system refinement) diff --git a/docs/ARCHITECTURE_v1.md b/docs/ARCHITECTURE_v1.md deleted file mode 100644 index 380220b..0000000 --- a/docs/ARCHITECTURE_v1.md +++ /dev/null @@ -1,454 +0,0 @@ -# Architecture v1 - -## System Overview - -Architect is a **single-process, layered desktop application** built in Zig that functions as a grid-based terminal multiplexer optimized for multi-agent AI coding workflows. It follows a five-layer architecture: a thin entrypoint delegates to an application runtime that owns the frame loop, platform abstraction (SDL3), session management (PTY + ghostty-vt terminal emulation), scene rendering, and a component-based UI overlay system. All layers run on a single thread (except the notification socket listener), communicating through a synchronous frame loop that polls events, updates state, renders the scene, then renders UI overlays on top. The application uses an action-queue pattern for UI-to-app mutations, epoch-based cache invalidation for efficient rendering, and a vtable-based component registry for extensible UI overlays. - -## Component Diagram - -```mermaid -graph TD - subgraph Entrypoint - MAIN["main.zig
Thin entrypoint; delegates to runtime"] - end - - subgraph Application Layer - RT["app/runtime.zig
Owns frame loop, lifetime, config, session spawning"] - AS["app/app_state.zig
ViewMode, AnimationState, SessionStatus enums"] - LAY["app/layout.zig
Grid sizing, scaling, terminal resize calculations"] - UH["app/ui_host.zig
Builds read-only UiHost snapshot each frame"] - GN["app/grid_nav.zig
Grid navigation, focus switching, notifications"] - GL["app/grid_layout.zig
Grid dimensions and session positioning"] - IK["app/input_keys.zig
Keyboard input encoding to VT sequences"] - IT["app/input_text.zig
IME/text input and preedit handling"] - TA["app/terminal_actions.zig
Clipboard, paste, clear operations"] - WT["app/worktree.zig
Git worktree command building and cd helpers"] - end - - subgraph Platform Layer - SDL["platform/sdl.zig
SDL3 window init, renderer creation, HiDPI"] - IM["input/mapper.zig
SDL keycodes to terminal bytes, shortcut detection"] - CZIG["c.zig
C FFI re-exports for SDL3, SDL3_ttf"] - end - - subgraph Session Layer - SS["session/state.zig
PTY, ghostty-vt terminal, process watcher, render epoch"] - SN["session/notify.zig
Background socket thread, JSON parsing, notification queue"] - SH["shell.zig
Shell process spawning with env setup"] - PTY["pty.zig
PTY abstraction (open, read, write, resize)"] - VTS["vt_stream.zig
VT stream wrapper for ghostty-vt parser"] - CWD["cwd.zig
macOS working directory detection"] - end - - subgraph Rendering Layer - REN["render/renderer.zig
Scene rendering: terminals, borders, animations"] - FONT["font.zig
Glyph rendering, HarfBuzz shaping, LRU cache"] - FC["font_cache.zig
Shared font cache keyed by pixel size"] - FP["font_paths.zig
System font path resolution"] - BD["gfx/box_drawing.zig
Procedural box-drawing characters (U+2500-U+257F)"] - PR["gfx/primitives.zig
Rounded/thick border drawing helpers"] - EA["anim/easing.zig
Cubic ease-in-out interpolation"] - end - - subgraph UI Overlay Layer - UR["ui/root.zig
Component registry, z-index dispatch, action drain"] - UC["ui/component.zig
UiComponent vtable interface definition"] - UT["ui/types.zig
UiHost, UiAction, UiAssets, UiActionQueue"] - SVS["ui/session_view_state.zig
Per-session selection, scroll, hover state"] - FFG["ui/first_frame_guard.zig
Idle throttle bypass for state transitions"] - - subgraph Components - HELP["help_overlay.zig
Keyboard shortcut reference overlay"] - WORK["worktree_overlay.zig
Git worktree picker"] - RECENT["recent_folders_overlay.zig
Recent folders picker"] - DIFF["diff_overlay.zig
Git diff viewer"] - TOAST["toast.zig
Notification display"] - QUIT["quit_confirm.zig
Quit confirmation dialog"] - RESTART["restart_buttons.zig
Dead session restart UI"] - ESC["escape_hold.zig
ESC hold-to-collapse indicator"] - SI["session_interaction.zig
Terminal mouse/scroll/link interaction"] - METRICS["metrics_overlay.zig
Performance stats overlay"] - GS["global_shortcuts.zig
Global keyboard shortcuts"] - PILL["pill_group.zig
Pill overlay coordinator"] - CWDB["cwd_bar.zig
Per-cell working directory bar"] - end - end - - subgraph Shared Utilities - GEOM["geom.zig
Rect struct and point containment"] - COL["colors.zig
Theme and ANSI palette management"] - CFG["config.zig
TOML config loading and persistence"] - URL["url_matcher.zig
URL detection in terminal output"] - MET["metrics.zig
Performance metrics collection framework"] - OPEN["os/open.zig
Cross-platform URL opening"] - end - - MAIN --> RT - RT --> SDL - RT --> SS - RT --> REN - RT --> UR - RT --> CFG - RT --> AS - RT --> LAY - RT --> UH - RT --> GN - RT --> GL - RT --> IK - RT --> IT - RT --> TA - RT --> WT - SS --> SH - SS --> PTY - SS --> VTS - SS --> CWD - SN -.->|"thread-safe queue"| RT - REN --> FONT - REN --> BD - REN --> PR - REN --> EA - FONT --> FC - FONT --> FP - UR --> UC - UR --> UT - UR --> SVS - UR --> FFG - Components --> UT - IM --> CZIG - SDL --> CZIG - REN --> CZIG -``` - -## Data Flow - -### Frame Loop (per frame, ~16ms active / ~50ms idle) - -``` - ┌──────────────────────────────────────┐ - │ SDL Event Queue │ - └──────────┬───────────────────────────┘ - │ poll - v - ┌──────────────────────────────────────┐ - │ Scale to render coordinates │ - └──────────┬───────────────────────────┘ - │ - v - ┌──────────────────────────────────────┐ - │ Build UiHost snapshot │ - │ (window size, grid, theme, etc.) │ - └──────────┬───────────────────────────┘ - │ - v - ┌──────────────────────────────────────┐ - │ ui.handleEvent() │ - │ (topmost z-index first) │ - │ consumed? ─── yes ──> skip app logic│ - └──────────┬───────────────────────────┘ - │ no - v - ┌──────────────────────────────────────┐ - │ App event switch │ - │ (shortcuts, terminal input, resize) │ - └──────────┬───────────────────────────┘ - │ - v - ┌──────────────────────────────────────┐ - │ xev loop iteration │ - │ (async process exit detection) │ - └──────────┬───────────────────────────┘ - │ - v - ┌──────────────────────────────────────┐ - │ Drain session output → ghostty-vt │ - │ Drain notification queue │ - └──────────┬───────────────────────────┘ - │ - v - ┌──────────────────────────────────────┐ - │ ui.update() + drain UiAction queue │ - │ (UI→app mutations applied here) │ - └──────────┬───────────────────────────┘ - │ - v - ┌──────────────────────────────────────┐ - │ Advance animation state │ - └──────────┬───────────────────────────┘ - │ - v - ┌──────────────────────────────────────┐ - │ renderer.render() → scene │ - │ ui.render() → overlays │ - │ SDL_RenderPresent() │ - └──────────────────────────────────────┘ -``` - -### Terminal Output Path - -``` -Shell process - │ writes to PTY - v -session.output_buf (kernel buffer → userspace read) - │ processBytes() - v -vt_stream.zig → ghostty-vt parser - │ state machine updates - v -Terminal cell buffer (content, attributes, colors) - │ session.render_epoch += 1 - v -Renderer cache dirty check (presented_epoch < render_epoch?) - │ yes → re-render - v -font.zig → HarfBuzz shaping → glyph textures - │ - v -SDL_RenderTexture() → frame presented -``` - -### Terminal Input Path - -``` -Physical keyboard - │ - v -SDL_EVENT_KEY_DOWN / SDL_EVENT_TEXT_INPUT - │ scaled to render coordinates - v -UiRoot.handleEvent() (components by z-index) - │ not consumed - v -App event switch → shortcut detection - │ not a shortcut - v -input/mapper.zig → encodeKey() → VT escape sequence bytes - │ - v -session.pending_write buffer - │ next frame - v -PTY write() → shell process stdin -``` - -### External Notification Path - -``` -External tool (Claude Code, Codex, Gemini) - │ JSON over Unix socket - v -session/notify.zig (background thread) - │ parse {"session": N, "state": "awaiting_approval"} - v -NotificationQueue (thread-safe) - │ main loop drains each frame - v -SessionStatus updated (idle → awaiting_approval) - │ - v -Renderer draws attention border (pulsing yellow / solid green) -``` - -### Entry Points - -| Entry Point | Source | Description | -|------------|--------|-------------| -| SDL event queue | Keyboard, mouse, window events | Primary user interaction | -| PTY read | Shell process stdout/stderr | Terminal content updates | -| Unix domain socket | External AI tools | Status notifications (JSON) | -| Config files | `~/.config/architect/` | Startup configuration and persistence | - -### Storage - -| Store | Location | Contents | -|-------|----------|----------| -| Terminal cell buffer | In-memory (ghostty-vt) | Current screen + scrollback (up to 10KB default) | -| Glyph cache | GPU textures + in-memory LRU | Up to 4096 shaped glyph textures | -| Render cache | GPU textures per session | Cached terminal renders, epoch-invalidated | -| config.toml | `~/.config/architect/config.toml` | User preferences (font, theme, UI flags) | -| persistence.toml | `~/.config/architect/persistence.toml` | Runtime state (window pos, font size, terminal cwds) | - -### Exit Points - -| Exit Point | Destination | Description | -|------------|-------------|-------------| -| PTY write | Shell process stdin | Encoded keyboard input | -| SDL renderer | Display | Rendered frames via GPU | -| Config write | Filesystem | Persisted window state and terminal cwds on quit | -| URL open | OS browser | Cmd+Click hyperlinks via `os/open.zig` | - -## Module Boundary Table - -| Module | Responsibility | Public API (key functions/types) | Dependencies | -|--------|---------------|----------------------------------|--------------| -| `main.zig` | Thin entrypoint | `main()` | `app/runtime` | -| `app/runtime.zig` | Application lifetime, frame loop, session spawning, config persistence | `run()`, frame loop internals | `platform/sdl`, `session/state`, `render/renderer`, `ui/root`, `config`, `app/app_state`, `app/layout`, `app/ui_host`, `app/grid_nav`, `app/grid_layout`, `app/input_keys`, `app/input_text`, `app/terminal_actions`, `app/worktree` | -| `app/app_state.zig` | Core state enums and animation interpolation | `ViewMode`, `AnimationState`, `SessionStatus`, `AnimationState.interpolateRect()`, `AnimationState.easeInOutCubic()` | `geom`, `anim/easing` | -| `app/layout.zig` | Grid sizing, DPI scaling, terminal resize calculations | `applyTerminalResize()`, sizing helpers | `app/app_state`, `geom` | -| `app/ui_host.zig` | Builds read-only UI snapshot each frame | `buildUiHost()` | `ui/types`, `app/app_state`, `colors` | -| `app/grid_nav.zig` | Grid navigation, focus switching, notification handling | Navigation functions, notification drain | `app/app_state`, `session/state` | -| `app/grid_layout.zig` | Grid dimension calculation and session positioning | Grid dimension helpers | `app/app_state` | -| `app/input_keys.zig` | Keyboard input encoding to terminal bytes | Key encoding functions | `input/mapper`, `session/state` | -| `app/input_text.zig` | IME/text input and preedit handling | Text input handlers | `session/state` | -| `app/terminal_actions.zig` | Clipboard, paste, clear operations | `paste()`, `clear()`, clipboard functions | `session/state`, `c` | -| `app/worktree.zig` | Git worktree command building and cd helpers | Worktree command builders | `session/state` | -| `platform/sdl.zig` | SDL3 initialization, window management, HiDPI | `init()`, `createWindow()`, `createRenderer()` | `c` | -| `input/mapper.zig` | SDL keycodes to VT escape sequences, shortcut detection | `encodeKey()`, modifier helpers | `c` | -| `c.zig` | C FFI re-exports (SDL3, SDL3_ttf constants) | `SDLK_*`, `SDL_*`, `TTF_*` re-exports | SDL3 system libs (via `@cImport`) | -| `session/state.zig` | Terminal session lifecycle: PTY, ghostty-vt, process watcher | `SessionState`, `init()`, `deinit()`, `ensureSpawnedWithDir()`, `render_epoch`, `pending_write` | `shell`, `pty`, `vt_stream`, `cwd`, `font`, xev | -| `session/notify.zig` | Background notification socket thread and queue | `NotificationQueue`, `startThread()`, `push()`, `drain()` | std (socket, thread) | -| `shell.zig` | Shell process spawning with PTY and env vars | `spawn()` | `pty`, std | -| `pty.zig` | PTY abstraction (open, read, write, resize) | `Pty`, `open()`, `read()`, `write()`, `resize()` | std (posix) | -| `vt_stream.zig` | VT stream wrapper for ghostty-vt parser | `VtStream`, `processBytes()` | ghostty-vt | -| `cwd.zig` | Working directory detection (macOS-only) | `getCwd()` | std (posix, macOS APIs) | -| `render/renderer.zig` | Scene rendering: terminals, borders, animations, CWD bar | `render()`, `RenderCache`, per-session texture management | `font`, `font_cache`, `gfx/box_drawing`, `gfx/primitives`, `anim/easing`, `app/app_state`, `c` | -| `font.zig` | Font rendering, HarfBuzz shaping, glyph LRU cache | `Font`, `openFont()`, `renderGlyph()`, glyph cache (4096 max) | `font_paths`, `c` (SDL3_ttf) | -| `font_cache.zig` | Shared font cache keyed by pixel size | `FontCache`, `getOrCreate()` | `font` | -| `font_paths.zig` | System font path resolution | `findFont()` | std (filesystem) | -| `gfx/box_drawing.zig` | Procedural box-drawing character rendering (U+2500-U+257F) | `renderBoxDrawing()` | `c` | -| `gfx/primitives.zig` | Rounded/thick border drawing helpers | `drawRoundedRect()`, `drawThickBorder()` | `c` | -| `anim/easing.zig` | Easing interpolation functions | `easeInOutCubic(t)` | (none) | -| `geom.zig` | Geometry primitives | `Rect`, `containsPoint()` | (none) | -| `colors.zig` | Theme and ANSI 16/256 palette management | `Theme`, `fromConfig()`, `getPaletteColor()`, `get256ColorWithTheme()` | `config`, `c` | -| `config.zig` | TOML config loading, persistence, migration | `Config`, `load()`, `save()`, `FontConfig`, `WindowConfig`, `GridConfig`, `PaletteConfig`, `UiConfig` | zig-toml, std (filesystem) | -| `metrics.zig` | Performance metrics collection framework | `Metrics`, counters for frames, glyph cache stats | (none) | -| `url_matcher.zig` | URL detection in terminal output | `matchUrl()` | (none) | -| `os/open.zig` | Cross-platform URL/file opening | `open()` | std (child process) | -| `ui/root.zig` | UI component registry, z-index dispatch, action drain | `UiRoot`, `register()`, `handleEvent()`, `update()`, `render()`, `needsFrame()` | `ui/component`, `ui/types` | -| `ui/component.zig` | UI component vtable interface | `UiComponent`, `VTable` (handleEvent, update, render, hitTest, wantsFrame, deinit) | `ui/types`, `c` | -| `ui/types.zig` | Shared UI type definitions | `UiHost`, `UiAction`, `UiActionQueue`, `UiAssets`, `SessionUiInfo` | `app/app_state`, `colors`, `font`, `geom` | -| `ui/session_view_state.zig` | Per-session UI interaction state | `SessionViewState` (selection, scroll offset, hover) | (none) | -| `ui/first_frame_guard.zig` | Idle throttle bypass for visible state transitions | `FirstFrameGuard`, `markTransition()`, `markDrawn()`, `wantsFrame()` | (none) | -| `ui/scale.zig` | DPI scaling helper | `scale(value, ui_scale)` | (none) | -| `ui/components/help_overlay.zig` | Keyboard shortcut reference overlay | Toggle via Cmd+/ | `ui/component`, `ui/types`, expanding_overlay | -| `ui/components/worktree_overlay.zig` | Git worktree picker | Toggle via Cmd+T, emits `SwitchWorktree`/`CreateWorktree`/`RemoveWorktree` | `ui/component`, `ui/types`, expanding_overlay, confirm_dialog | -| `ui/components/recent_folders_overlay.zig` | Recent folders picker | Toggle via Cmd+O, emits `ChangeDirectory` | `ui/component`, `ui/types`, expanding_overlay | -| `ui/components/diff_overlay.zig` | Git diff viewer | Toggle via Cmd+D, emits `ToggleDiffOverlay` | `ui/component`, `ui/types` | -| `ui/components/session_interaction.zig` | Terminal mouse/scroll/link interaction | Mouse selection, scrollback, Cmd+Click links | `ui/component`, `ui/types`, `ui/session_view_state`, `url_matcher` | -| `ui/components/toast.zig` | Notification toast display | `show()`, auto-dismiss timer | `ui/component`, `ui/types` | -| `ui/components/quit_confirm.zig` | Quit confirmation dialog | Emits `ConfirmQuit` | `ui/component`, `ui/types`, button | -| `ui/components/restart_buttons.zig` | Dead session restart UI | Emits `RestartSession` | `ui/component`, `ui/types`, button | -| `ui/components/escape_hold.zig` | ESC hold-to-collapse indicator | Emits `RequestCollapseFocused` on hold | `ui/component`, `ui/types`, `ui/gestures/hold` | -| `ui/components/metrics_overlay.zig` | Performance stats overlay | Toggle via Cmd+Shift+M (when enabled) | `ui/component`, `ui/types`, `metrics` | -| `ui/components/global_shortcuts.zig` | Global keyboard shortcuts | Cmd+, to open config | `ui/component`, `ui/types` | -| `ui/components/pill_group.zig` | Pill overlay coordinator | Collapses other pills when one expands | `ui/component`, `ui/types` | -| `ui/components/cwd_bar.zig` | Per-cell working directory bar | Renders cwd path with marquee scrolling | `ui/component`, `ui/types`, marquee_label | -| `ui/components/expanding_overlay.zig` | Expanding/collapsing animation helper | `ExpandingOverlay`, state machine (Closed/Expanding/Open/Collapsing) | `anim/easing` | -| `ui/components/button.zig` | Styled button rendering (default/primary/danger) | `renderButton()` | `c`, `ui/types` | -| `ui/components/confirm_dialog.zig` | Generic confirmation modal | Configurable title/message/labels, emits `UiAction` on confirm | `ui/component`, `ui/types`, button | -| `ui/components/marquee_label.zig` | Scrolling text label | `MarqueeLabel`, auto-scrolls long text | `font` | -| `ui/components/hotkey_indicator.zig` | Hotkey visual feedback | Fade in/out indicator | `ui/component`, `ui/types` | -| `ui/components/flowing_line.zig` | Animated flowing line renderer | Decorative animated lines | `ui/component`, `ui/types` | -| `ui/gestures/hold.zig` | Reusable hold gesture detector | `HoldGesture`, threshold-based hold detection | (none) | - -## Key Architectural Decisions - -### ADR-001: Five-Layer Single-Thread Architecture - -- **Decision:** Organize the application into five layers (entrypoint, platform, session, rendering, UI overlay) running on a single main thread with only the notification socket on a background thread. -- **Context:** A terminal multiplexer needs tight control over frame timing, event ordering, and GPU resource management. Multi-threaded rendering introduces synchronization complexity without clear benefit for a UI-bound application. The notification socket is the only I/O that must not block the frame loop. -- **Alternatives considered:** - - *Multi-threaded rendering* — rejected because SDL3 renderers are not thread-safe, and the complexity of synchronizing terminal state across threads outweighs the marginal throughput gain. - - *Async I/O everywhere* — rejected because the frame loop is inherently synchronous (poll → update → render → present), and async patterns add indirection without improving latency for a 60 FPS UI. -- **Date:** 2025 (initial architecture) - -### ADR-002: Component-Based UI Overlay System with VTable Dispatch - -- **Decision:** UI overlays are implemented as components registered with a central `UiRoot` registry, each conforming to a `VTable` interface (handleEvent, update, render, hitTest, wantsFrame, deinit). Components are dispatched by z-index, highest first. -- **Context:** The application has 15+ distinct UI elements (help overlay, worktree picker, diff viewer, toast, quit dialog, etc.) that need independent lifecycle management, event handling priority, and rendering order. A centralized registry prevents ad-hoc event handling scattered across the main loop. -- **Alternatives considered:** - - *Immediate-mode GUI* — rejected because retain-mode components with cached textures reduce per-frame CPU work, and the vtable pattern is idiomatic in Zig for polymorphic dispatch. - - *Ad-hoc event handling in main.zig* — rejected because it leads to unmaintainable event switch growth as UI features are added; the component pattern isolates concerns. -- **Date:** 2025 (initial architecture) - -### ADR-003: UiAction Queue for UI-to-App Mutations - -- **Decision:** UI components never mutate application state directly. Instead, they push `UiAction` values (a tagged union) to a queue that the main loop drains after all component updates complete. -- **Context:** Direct mutation from UI components would create ordering dependencies between components and the main loop. A queue decouples intent from execution, making it safe to add/remove/reorder components without breaking state transitions. -- **Alternatives considered:** - - *Direct callback functions* — rejected because callbacks create implicit coupling and make it hard to reason about mutation ordering. - - *Event bus / pub-sub* — rejected as over-engineered for a single-process application; a simple queue with a typed union is sufficient and type-safe. -- **Date:** 2025 (initial architecture) - -### ADR-004: Epoch-Based Render Cache Invalidation - -- **Decision:** Each `SessionState` maintains a monotonic `render_epoch` counter that increments on terminal content changes. The renderer's `RenderCache` tracks the last presented epoch per session and only re-renders when epochs diverge. -- **Context:** Re-rendering all terminal cells every frame is expensive (glyph shaping, texture creation). Most frames in a multi-terminal grid have no changes in most sessions. Epoch comparison is O(1) per session and avoids deep content diffing. -- **Alternatives considered:** - - *Dirty-flag per cell* — rejected because tracking individual cell changes is memory-intensive and the granularity is unnecessary when the renderer caches entire session textures. - - *Timer-based refresh* — rejected because it wastes GPU cycles re-rendering unchanged terminals and introduces visible latency for changed ones. -- **Date:** 2025 (initial architecture) - -### ADR-005: ghostty-vt for Terminal Emulation - -- **Decision:** Use ghostty-vt (from the Ghostty terminal project) as the VT state machine and ANSI parser rather than implementing one from scratch. -- **Context:** Terminal emulation is a complex domain with thousands of edge cases (escape sequences, Unicode handling, alternate screen buffers, scrollback, etc.). ghostty-vt is a mature, well-tested implementation written in Zig, making it a natural fit for a Zig application. -- **Alternatives considered:** - - *Custom VT parser* — rejected because building a correct VT100/xterm-compatible parser is a multi-year effort and a maintenance burden orthogonal to the product goal. - - *libvterm (C library)* — rejected because it requires C FFI overhead and memory management coordination; ghostty-vt integrates natively with Zig's type system and allocator model. -- **Date:** 2025 (initial dependency choice) - -### ADR-006: SDL3 for Rendering and Input - -- **Decision:** Use SDL3 as the platform abstraction layer for window management, GPU-accelerated 2D rendering, input events, and font rendering (via SDL3_ttf with HarfBuzz). -- **Context:** The application needs cross-platform window management, hardware-accelerated texture rendering, and HiDPI support. SDL3 provides all of these with a C API that Zig can import directly via `@cImport`. -- **Alternatives considered:** - - *Native platform APIs (AppKit/Metal)* — rejected because it locks the project to macOS; SDL3 allows future Linux/Windows porting. - - *Vulkan/OpenGL directly* — rejected because 2D terminal rendering does not need low-level GPU control, and SDL3's renderer API is sufficient and simpler. - - *Electron / web-based* — rejected for performance and resource usage; a native Zig application has sub-millisecond event latency and minimal memory overhead. -- **Date:** 2025 (initial dependency choice) - -### ADR-007: Lazy Session Spawning - -- **Decision:** Only session 0 spawns a shell process on startup. Additional sessions spawn on first user interaction (click or keyboard navigation). -- **Context:** Users may configure a grid with many slots but only actively use a few. Eagerly spawning all shells wastes system resources (PTY file descriptors, process table entries, memory for terminal buffers) and slows startup. -- **Alternatives considered:** - - *Eager spawn all* — rejected because startup time scales linearly with session count, and unused PTYs waste kernel resources. - - *Spawn on first output* — rejected because sessions need a shell to produce output; spawn-on-interaction is the natural trigger. -- **Date:** 2025 (initial design) - -### ADR-008: Procedural Box-Drawing Characters - -- **Decision:** Box-drawing characters (U+2500-U+257F) are rendered procedurally via line/rectangle primitives rather than using font glyphs. -- **Context:** Font-based box-drawing characters often have alignment issues: gaps between cells, inconsistent line widths, or mismatched metrics across font families. Procedural rendering guarantees pixel-perfect alignment regardless of the chosen font. -- **Alternatives considered:** - - *Font glyph rendering* — rejected because alignment varies by font and size; even monospace fonts often have subpixel gaps in box-drawing characters. - - *Pre-rendered sprite atlas* — rejected because it doesn't scale with DPI or font size changes. -- **Date:** 2025 (rendering implementation) - -### ADR-009: Thread-Safe Notification Queue for External Tool Integration - -- **Decision:** External AI tools communicate with Architect via a Unix domain socket. A dedicated background thread accepts connections, parses single-line JSON messages, and pushes to a thread-safe queue. The main loop drains this queue once per frame. -- **Context:** AI coding agents (Claude Code, Codex, Gemini) need to signal state changes (start, awaiting_approval, done) to trigger visual indicators. Socket I/O must not block the render thread, but state updates must be applied synchronously during the frame loop to avoid race conditions with rendering. -- **Alternatives considered:** - - *Polling a file or pipe* — rejected because it introduces latency and filesystem overhead; sockets provide immediate delivery. - - *D-Bus or platform IPC* — rejected because it adds platform-specific dependencies; Unix domain sockets are simple and portable across macOS and Linux. - - *Direct main-thread socket polling* — rejected because accept/read can block; a background thread with a lock-free queue provides non-blocking integration. -- **Date:** 2025 (notification system implementation) - -### ADR-010: TOML-Based Dual Configuration (User Prefs + Runtime State) - -- **Decision:** Configuration is split into two TOML files: `config.toml` for user-editable preferences (font, theme, UI flags) and `persistence.toml` for auto-managed runtime state (window position, font size, terminal cwds, recent folders). -- **Context:** Mixing user preferences with volatile runtime state in a single file leads to merge conflicts and confusion when users manually edit configuration. Separating them allows `config.toml` to be version-controlled or shared, while `persistence.toml` is machine-specific and auto-managed. -- **Alternatives considered:** - - *Single config file* — rejected because auto-saving window position into a user-edited file causes unexpected diffs. - - *JSON or YAML* — rejected because TOML is designed for configuration files, has clear section semantics, and the zig-toml library provides native Zig integration without C FFI. - - *SQLite for persistence* — rejected as over-engineered for a handful of key-value pairs; TOML is human-readable and easy to debug. -- **Date:** 2025 (configuration system implementation) - -### ADR-011: Hardcoded Keybindings - -- **Decision:** All keyboard shortcuts are hardcoded in the source code. There is no user-configurable keybinding system. -- **Context:** The application has a small, focused set of shortcuts (Cmd+N, Cmd+W, Cmd+T, Cmd+D, Cmd+/, Cmd+1-0, Cmd+Return, Cmd+Q). A configurable keybinding system adds significant complexity (parser, conflict detection, documentation generation) for marginal user benefit at this stage. -- **Alternatives considered:** - - *Config-driven keybindings* — deferred, not rejected; may be added as the shortcut set grows, but current simplicity is preferred during early development. -- **Date:** 2025 (input system implementation) - -### ADR-012: FirstFrameGuard Pattern for Idle Throttle Bypass - -- **Decision:** When a UI component transitions to a visible state (modal opens, gesture starts), it uses a `FirstFrameGuard` to signal the frame loop that an immediate render is needed, bypassing idle throttling. -- **Context:** The frame loop throttles to ~20 FPS when idle (no terminal output or user input). Without the guard, newly visible UI elements would appear with up to 250ms delay, creating a perceived lag. The guard ensures the first frame of a transition renders immediately. -- **Alternatives considered:** - - *Always render at full rate* — rejected because it wastes CPU/GPU when nothing is changing, impacting battery life on laptops. - - *SDL event injection* — rejected because synthetic events pollute the event queue and complicate event handling logic. -- **Date:** 2025 (UI system refinement) diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index ef1db76..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,401 +0,0 @@ -# Architecture Overview - -Architect is a terminal multiplexer displaying interactive sessions in a grid with smooth expand/collapse and resize/reflow animations. It is organized around five layers: platform abstraction, input handling, session management, scene rendering, and a UI overlay system. - -``` -┌─────────────────────────────────────────────────────────────┐ -│ main.zig → app/runtime.zig │ -│ (entrypoint → lifetime, frame loop, event dispatch) │ -├─────────────────────────────────────────────────────────────┤ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ -│ │ Platform │ │ Input │ │ Notification │ │ -│ │ (SDL3 init) │ │ (mapper) │ │ (socket thread) │ │ -│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ Session Layer (src/session/) │ │ -│ │ SessionState: PTY, ghostty-vt terminal, xev watcher│ │ -│ └─────────────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ ┌─────────────────────┐ ┌───────────────────────────┐ │ -│ │ Scene Renderer │ │ UI Overlay System │ │ -│ │ (render/renderer) │ │ (src/ui/*) │ │ -│ │ terminals, borders, │ │ UiRoot → components │ │ -│ │ animations, CWD bar │ │ → UiAction queue │ │ -│ └─────────────────────┘ └───────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Runtime Flow - -**app/runtime.zig** owns application lifetime, window sizing, PTY/session startup, configuration persistence, and the frame loop, while **main.zig** is a thin entrypoint. Each frame it: - -1. Polls SDL events and scales coordinates to render space. -2. Builds a lightweight `UiHost` snapshot and lets `UiRoot` handle events first. -3. Runs remaining app logic (terminal input, resizing, keyboard shortcuts). -4. Runs `xev` loop iteration for async process exit detection. -5. Processes output from all sessions and drains async notifications. -6. Updates UI components and drains `UiAction` queue. -7. Advances animation state if transitioning. -8. Calls `renderer.render` for the scene, then `ui.render` for overlays, then presents. -9. Sleeps based on idle/active frame targets (~16ms active, ~50ms idle). - -`SessionState.render_epoch` increments on terminal updates, and the renderer cache tracks the last presented/cache epochs. When collapsing from full view to grid, the focused session’s render epoch is bumped so its cache refreshes before idle throttling resumes. - -**Terminal resizing** -- `applyTerminalResize` updates the PTY size first, then resizes the `ghostty-vt` terminal. -- The VT stream stays alive; only its handler is refreshed to repoint at the resized terminal, preserving parser state and preventing in-flight escape sequences from being misparsed. - -**renderer/render.zig** draws only the *scene*: -- Terminal cell content with HarfBuzz-shaped text runs -- Box-drawing characters (U+2500–U+257F) rendered procedurally via `gfx/box_drawing.zig` for pixel-perfect alignment regardless of font -- Grid cell backgrounds and borders (focused/unfocused) -- Expand/collapse/panning animations with eased interpolation -- Attention borders (pulsing yellow for awaiting approval, solid green for done) -- CWD bar with marquee scrolling for long paths -- Scrollback indicator strip - -**UiRoot (src/ui/)** is the registry for UI overlay components and session interaction state: -- Dispatches events topmost-first (by z-index) -- Runs per-frame `update()` on all components -- Drains `UiAction` queue for UI→app mutations -- Renders all components in z-order after the scene -- Reports `needsFrame()` when any component requires animation -- Owns per-session `SessionViewState` via `SessionInteractionComponent` (selection, hover, scrollback state) - -Grid slots are ordered independently of session IDs: slot indices drive UI focus and shortcuts, while each `SessionState` receives a monotonic `id` used for external notifications. Slots may be compacted without changing session IDs. Grid resize animations are canceled when the last terminal is relaunched so the renderer does not keep animating stale cell geometry, and runtime logs (including short frame traces) capture the close/relaunch path for debugging. - -**UiAssets** provides shared rendering resources: -- `FontCache` stores configured fonts keyed by pixel size, so terminal rendering and UI components reuse a single loaded font set instead of opening per-component instances. - -### Session cleanup -- `main.zig` tracks how many sessions were constructed and uses a single defer to deinitialize exactly those instances on any exit path. -- `SessionState.deinit` is idempotent: textures, fonts, watchers, and buffers are nulled/cleared after destruction so double-invocation during error unwinding cannot double-free GPU resources. -- Font rendering sanitizes invalid Unicode scalars (surrogates or >0x10FFFF) to U+FFFD before shaping, preventing malformed terminal output from crashing the renderer. -- Renderer treats non-text cells (`content_tag` ≠ `.codepoint`) as empty, avoiding misinterpreting color-only cells as large codepoints that would render replacement glyphs. - -## Source Structure - -``` -src/ -├── main.zig # Entry point (delegates to app/runtime.zig) -├── c.zig # C bindings (SDL3, TTF, etc.) -├── colors.zig # Theme and color palette management (ANSI 16/256) -├── config.zig # TOML config persistence -├── geom.zig # Rect + point containment -├── metrics.zig # Metrics collection framework (glyph cache stats, frame count) -├── font.zig # Font rendering, glyph caching, HarfBuzz shaping -├── font_cache.zig # Shared font cache (terminal + UI) -├── font_paths.zig # Font path resolution for system fonts -├── shell.zig # Shell process spawning -├── pty.zig # PTY abstractions -├── cwd.zig # macOS working directory detection -├── url_matcher.zig # URL detection in terminal output -├── vt_stream.zig # VT stream wrapper for ghostty-vt -│ -├── platform/ -│ └── sdl.zig # SDL3 initialization and window management -│ -├── input/ -│ └── mapper.zig # Key→bytes encoding, shortcut detection -│ -├── app/ -│ ├── app_state.zig # ViewMode, AnimationState, SessionStatus -│ ├── runtime.zig # App lifetime + frame loop -│ ├── layout.zig # Sizing, scaling, hover hit-testing, terminal resize -│ ├── ui_host.zig # UiHost snapshot + mouse context -│ ├── grid_nav.zig # Grid navigation + notifications -│ ├── input_keys.zig # Keyboard input encoding -│ ├── input_text.zig # IME/text input handling -│ ├── terminal_actions.zig # Clipboard, paste, clear -│ └── worktree.zig # Worktree command building + cd helpers -│ -├── session/ -│ ├── state.zig # SessionState: PTY, terminal, process watcher -│ └── notify.zig # Notification socket thread + queue -│ -├── render/ -│ └── renderer.zig # Scene rendering (terminals, animations) -│ -├── gfx/ -│ ├── box_drawing.zig # Procedural box-drawing character rendering (U+2500–U+257F) -│ └── primitives.zig # Rounded/thick border drawing helpers -│ -├── anim/ -│ └── easing.zig # Easing functions (cubic, etc.) -│ -├── os/ -│ └── open.zig # Cross-platform URL opening -│ -└── ui/ - ├── mod.zig # Public UI module exports - ├── root.zig # UiRoot: component registry, dispatch - ├── component.zig # UiComponent vtable interface - ├── types.zig # UiHost, UiAction, UiAssets, SessionUiInfo - ├── session_view_state.zig # Per-session UI interaction state - ├── scale.zig # DPI scaling helper - ├── first_frame_guard.zig # Idle throttling transition helper - │ - ├── components/ - │ ├── button.zig # Reusable styled button rendering helper - │ ├── confirm_dialog.zig # Generic confirmation dialog component - │ ├── escape_hold.zig # ESC hold-to-collapse indicator - │ ├── expanding_overlay.zig # Expanding overlay animation state helper - │ ├── global_shortcuts.zig # Global keyboard shortcuts (e.g., Cmd+,) - │ ├── help_overlay.zig # Keyboard shortcut overlay (? pill) - │ ├── hotkey_indicator.zig # Hotkey visual feedback indicator - │ ├── marquee_label.zig # Reusable scrolling text label - │ ├── diff_overlay.zig # Git diff overlay (Cmd+D) - │ ├── metrics_overlay.zig # Metrics overlay (Cmd+Shift+M) - │ ├── pill_group.zig # Pill overlay coordinator (collapses others) - │ ├── quit_confirm.zig # Quit confirmation dialog - │ ├── restart_buttons.zig # Dead session restart buttons - │ ├── session_interaction.zig # Terminal mouse/scroll interaction handling - │ ├── toast.zig # Toast notification display - │ └── worktree_overlay.zig # Git worktree picker (T pill) - │ - └── gestures/ - └── hold.zig # Reusable hold gesture detector -``` - -## Asset Layout - -`assets/` stores runtime assets that are embedded or packaged, including: -- `assets/macos/Architect.icns` for the macOS bundle icon -- `assets/terminfo.zig` module embedding `assets/terminfo/xterm-ghostty.terminfo` for `src/shell.zig` - -## Key Types - -### View Modes (`app_state.ViewMode`) -``` -Grid → Dynamic grid overview, all sessions visible -Expanding → Animating from grid cell to fullscreen -Full → Single session fullscreen -Collapsing → Animating from fullscreen to grid cell -PanningLeft → Horizontal pan animation (moving left) -PanningRight → Horizontal pan animation (moving right) -PanningUp → Vertical pan animation (moving up) -PanningDown → Vertical pan animation (moving down) -GridResizing → Grid is expanding or shrinking (adding/removing cells) -``` - -### Session Status (`app_state.SessionStatus`) -``` -idle → No activity -running → Process actively running -awaiting_approval→ AI assistant waiting for user approval (pulsing border) -done → AI assistant task completed (solid border) -``` - -### Animation State -- 300ms cubic ease-in-out transitions -- `start_rect` → `target_rect` interpolation -- `focused_session` and `previous_session` for panning - -### Theme (`colors.zig`) -```zig -struct { - background: SDL_Color, // Terminal background - foreground: SDL_Color, // Default text color - selection: SDL_Color, // Selection highlight - accent: SDL_Color, // UI accent (focus indicators, pills) - palette: [16]SDL_Color, // ANSI 16-color palette -} -``` -- Created from config via `Theme.fromConfig()` -- Provides `getPaletteColor(idx)` for 0-15 palette access -- `get256ColorWithTheme(idx, theme)` handles full 256-color mode (16-231: color cube, 232-255: grayscale) - -### UI Component Interface -```zig -VTable { - handleEvent: fn(*anyopaque, *UiHost, *SDL_Event, *UiActionQueue) bool - update: fn(*anyopaque, *UiHost, *UiActionQueue) void - render: fn(*anyopaque, *UiHost, *SDL_Renderer, *UiAssets) void - hitTest: fn(*anyopaque, *UiHost, x, y) bool - wantsFrame: fn(*anyopaque, *UiHost) bool - deinit: fn(*anyopaque, *SDL_Renderer) void -} -``` - -### UiAction (UI→App mutations) -```zig -union(enum) { - RestartSession: usize, // Restart dead session at index - RequestCollapseFocused: void, // Collapse current fullscreen to grid - ConfirmQuit: void, // Confirm quit despite running processes - OpenConfig: void, // Open config file (Cmd+,) - SwitchWorktree: SwitchWorktreeAction, // cd the focused shell into another worktree (no respawn) - CreateWorktree: CreateWorktreeAction, // git worktree add .architect/ -b && cd there - RemoveWorktree: RemoveWorktreeAction, // Remove a git worktree - DespawnSession: usize, // Despawn/kill a session at index - ToggleMetrics: void, // Toggle metrics overlay visibility -} -``` - -### UiHost (read-only snapshot for UI) -```zig -struct { - now_ms: i64, - window_w, window_h: c_int, - ui_scale: f32, - grid_cols, grid_rows: usize, - cell_w, cell_h: c_int, - view_mode: ViewMode, - focused_session: usize, - focused_cwd: ?[]const u8, - focused_has_foreground_process: bool, - sessions: []SessionUiInfo, // dead/spawned flags per session - theme: *const Theme, // Active color theme -} -``` -`UiHost` is rebuilt on demand using a shared, preallocated `SessionUiInfo` buffer; treat it as a transient snapshot and do not retain its slices across calls. -`focused_has_foreground_process` is populated from a short-lived cache in `main.zig` to avoid per-frame syscalls; UI code should treat it as a hint and action handlers should recheck directly when needed. - -## Data & State Boundaries - -| Layer | State Location | What it contains | -|-------|----------------|------------------| -| Scene | `src/session/state.zig` | PTY, terminal buffer, scroll position, CWD, render epoch | -| Scene | `src/app/app_state.zig` | ViewMode, animation rects, focused session index | -| Renderer | `src/render/renderer.zig` (`RenderCache`) | Per-session cache textures and last presented/cache epochs | -| UI | Component structs | Visibility flags, animation timers, cached textures | -| Shared | `UiHost` | Read-only snapshot passed each frame | - -`SessionState` bumps its render epoch on content changes; the renderer compares this against `RenderCache` epochs to decide when to redraw and when to refresh cached textures. Unspawned sessions are excluded from dirty checks and cache allocation to avoid extra GPU work. - -**Key rule**: Scene code must not own UI state; UI state lives inside components. - -## Input Routing - -1. SDL events enter `main.zig` -2. Events are scaled to render coordinates -3. `UiHost` snapshot is built -4. `ui.handleEvent()` dispatches to components (topmost-first by z-index) -5. If consumed, skip app handlers; otherwise continue to main event switch -6. `ui.hitTest()` used for cursor changes in full view - -Text input notes: -- `SDL_EVENT_TEXT_INPUT` is treated as committed text for the focused session. -- `SDL_EVENT_TEXT_EDITING` updates are treated as preedit; the prior composition is removed with delete (0x7f) before inserting the latest composition, so macOS IME/dictation updates replace in place. -- When focus switches between sessions, any in-flight preedit text is cleared from the previously focused session. - -Components that consume events: -- `HelpOverlayComponent`: ⌘? pill click or Cmd+/ to toggle overlay -- `WorktreeOverlayComponent`: ⌘T pill, Cmd+T, Cmd+1–9 to cd the focused shell into a worktree; Cmd+0 opens a creation modal that builds `.architect/` via `git worktree add -b ` and cds into it; pill is hidden when a foreground process is running; refreshes its list on every open, reads worktrees from git metadata (commondir and linked worktree dirs only), highlights rows on hover with a gradient, supports click selection, limits the list to 9 entries, and displays paths relative to the primary worktree; includes delete (×) button to remove non-root worktrees -- `EscapeHoldComponent`: ESC key down/up for hold-to-collapse -- `RestartButtonsComponent`: Restart button clicks -- `QuitConfirmComponent`: Quit confirmation dialog buttons -- `ConfirmDialogComponent`: Generic confirmation dialog (used by worktree removal, etc.) -- `PillGroupComponent`: Coordinates pill overlays (collapses one when another expands) -- `GlobalShortcutsComponent`: Handles global shortcuts like Cmd+, to open config -- `MetricsOverlayComponent`: Cmd+Shift+M to toggle metrics overlay (when enabled in config) -- `DiffOverlayComponent`: Cmd+D to toggle git diff overlay (grid and full view); runs a repo-wide `git diff` (staged + unstaged) for the focused session's working tree, shows a GitHub-style unified diff with collapsible file headers, line number gutters, and colored add/remove lines, and labels the title bar with the repo root folder; dismiss with close button or ESC - -## Rendering Order - -1. **Clear**: Background color (14, 17, 22) -2. **Scene**: `renderer.render(...)` - terminals based on view mode -3. **UI Overlay**: `ui.render(...)` - all registered components in z-order -4. **Present**: `SDL_RenderPresent` - -## Session Management - -Each `SessionState` contains: -- `shell`: Spawned shell process with PTY -- `terminal`: ghostty-vt terminal state machine -- `stream`: VT stream wrapper for output processing -- `process_watcher`: xev-based async process exit detection -- `render_epoch`: Monotonic counter for renderer invalidation -- `pending_write`: Buffered stdin for non-blocking writes - -Sessions are lazily spawned: only session 0 starts on launch; others spawn on first click/navigation. - -## Notification System - -External tools (AI assistants) signal state changes via Unix domain socket: -``` -${XDG_RUNTIME_DIR:-/tmp}/architect_notify_.sock -``` - -Protocol: Single-line JSON -```json -{"session": 0, "state": "awaiting_approval"} -{"session": 0, "state": "done"} -{"session": 0, "state": "start"} -``` - -Each shell also gets a small `architect` command in `PATH` that wraps this protocol -and reads `ARCHITECT_SESSION_ID`/`ARCHITECT_NOTIFY_SOCK` (for example, -`architect notify done`). - -A background thread (`notify.zig`) accepts connections, parses messages, and pushes to a thread-safe `NotificationQueue`. Main loop drains queue each frame. - -## First Frame Guard Pattern - -When a UI component transitions to a visible state (modal appears, gesture starts), it must render immediately even under idle throttling. Use `FirstFrameGuard`: - -```zig -// On state change: -self.first_frame.markTransition(); - -// In wantsFrame: -return self.active or self.first_frame.wantsFrame(); - -// At end of render: -self.first_frame.markDrawn(); -``` - -## Reusable UI Primitives - -### Button (`button.zig`) -Renders themed buttons with three variants: -- `default`: Selection background with accent border -- `primary`: Accent fill with blue border -- `danger`: Red fill with bright-red border - -### ExpandingOverlay (`expanding_overlay.zig`) -Animation state helper for pill-style overlays that expand/collapse: -- Tracks `State` (Closed, Expanding, Open, Collapsing) -- Calculates interpolated size and rect for animation frames -- Used by `HelpOverlayComponent` and `WorktreeOverlayComponent` - -### ConfirmDialog (`confirm_dialog.zig`) -Generic modal confirmation dialog: -- Configurable title, message, confirm/cancel labels -- Emits a `UiAction` on confirm -- Modal overlay blocks all other input -- Used for worktree removal and other destructive actions - -### PillGroup (`pill_group.zig`) -Coordinates multiple pill overlays: -- When one pill starts expanding, collapses any other open pill -- Prevents multiple overlays from being expanded simultaneously - -## DPI Scaling - -`src/ui/scale.zig` provides `scale(value, ui_scale)` to convert logical points to physical pixels. All UI sizing should use this for HiDPI support. - -## Invariants - -1. **UI routing**: All UI input/rendering goes through `UiRoot`; `main.zig` and `renderer.zig` stay scene-focused. - -2. **State separation**: No UI state or textures stored on sessions or in `app_state.zig`. - -3. **Renderer scope**: `renderer.zig` never draws help/toast/ESC/restart/quit UI; those belong to `src/ui/components/`. - -4. **Extension pattern**: New UI features register with `UiRoot` via `UiComponent`; they do not add event branches in `main.zig`. - -5. **Action-based mutation**: UI components emit `UiAction`s; they do not directly mutate app state. - -6. **Lazy spawning**: Sessions spawn on demand, not at startup (except session 0). - -7. **Cache invalidation**: Call `session.markDirty()` after any terminal content change. - -## Dependencies - -- **ghostty-vt**: Terminal emulation (VT state machine, ANSI parsing) -- **SDL3**: Window management, rendering, input -- **SDL3_ttf**: Font rendering with HarfBuzz shaping -- **xev**: Event-driven async I/O for process watching -- **System fonts**: Resolved from macOS font directories (default family is SFNSMono) From f8dbf7461c28e0e31a0b86bb3a8d4b6e9852df37 Mon Sep 17 00:00:00 2001 From: Forketyfork Date: Sun, 8 Feb 2026 08:49:52 +0100 Subject: [PATCH 3/3] docs: note c.zig exception in dependency rules The app/* modules import c.zig directly for SDL type definitions in input handling, which is technically a cross-layer dependency. Document this as an explicit exception to the downward-only dependency rule. --- docs/ARCHITECTURE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2826d44..d2df431 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -86,6 +86,7 @@ Platform Session Rendering UI Overlay - UI components communicate with the application exclusively via the `UiAction` queue (never direct state mutation). - The only background thread is the notification socket listener (`session/notify.zig`); it communicates with the main thread via a thread-safe queue drained once per frame. - Shared Utilities (`geom`, `colors`, `config`, `metrics`, etc.) may be imported by any layer but never import from layers above them. +- **Exception:** `app/*` modules may import `c.zig` directly for SDL type definitions used in input handling. This is a pragmatic shortcut for FFI constants, not a general license to depend on the Platform layer. ## Rules for New Code