Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
5c21955
fix CPU usage by replacing spinlocks with blocking mutexes
ozgurulukir Jul 21, 2026
f7da4f2
set OMP_WAIT_POLICY=PASSIVE for ONNX Runtime
ozgurulukir Jul 21, 2026
f01d9c8
ignore fff C library build artifact
ozgurulukir Jul 21, 2026
262d3c2
docs(agents): capture TUI module-split gotchas and CPU spinlock fix
ozgurulukir Jul 21, 2026
6a53448
refactor(tui): extract captureEvent into tui/event_router.zig (R1)
ozgurulukir Jul 21, 2026
c529ad0
refactor(tui): extract handleCommandKey into tui/command_router.zig (…
ozgurulukir Jul 21, 2026
c5256ff
refactor(tui): move TreePicker arm into command_router.zig (R2.2)
ozgurulukir Jul 21, 2026
b3d5377
refactor(tui): move ProviderPicker arm into command_router.zig (R2.3)
ozgurulukir Jul 21, 2026
fcbdda0
refactor(tui): move ModelPicker arm into command_router.zig (R2.4)
ozgurulukir Jul 21, 2026
ecd4386
refactor(tui): move SessionPicker arm into command_router.zig (R2.5)
ozgurulukir Jul 21, 2026
4323a69
refactor(tui): move Lanes arm into command_router.zig (R2.6)
ozgurulukir Jul 21, 2026
7d02353
refactor(tui): move CommandMenu arm into command_router.zig (R2.7)
ozgurulukir Jul 21, 2026
01801dd
refactor(tui): split mention popup out of Transcript (R2.8a)
ozgurulukir Jul 21, 2026
4df0fa7
refactor(tui): split block navigation out of Transcript (R2.8b)
ozgurulukir Jul 21, 2026
a792eaa
refactor(tui): split lane switch out of Transcript (R2.8c)
ozgurulukir Jul 21, 2026
bae70a6
refactor(tui): extract at_search state into app_state.AtSearchState (…
ozgurulukir Jul 21, 2026
eb69c75
refactor(tui): extract input widgets into app_state.InputState (R3.2)
ozgurulukir Jul 21, 2026
275e7cd
refactor(tui): extract picker state into app_state.PickerStates (R3.3)
ozgurulukir Jul 21, 2026
b0cd00e
refactor(tui): extract navigation cursors into app_state.NavState (R3.4)
ozgurulukir Jul 21, 2026
947a10b
refactor(tui): extract background modal state (R3.5)
ozgurulukir Jul 21, 2026
4d2240b
refactor(tui): extract visual feedback state into MetricsState (R3.6)
ozgurulukir Jul 21, 2026
b4dd077
refactor(tui): extract background delivery into tui/background_delive…
ozgurulukir Jul 21, 2026
194416c
docs: align AGENTS.md/README.md with post-split module layout
ozgurulukir Jul 21, 2026
f07196a
refactor(tui): extract BackgroundJobsWidget + PermissionWidget to wid…
ozgurulukir Jul 21, 2026
16a96d6
refactor(tui): extract LoadingWidget to widgets/loading.zig (R5.1c)
ozgurulukir Jul 21, 2026
d52ef00
refactor(tui): extract diff widget family to widgets/diff.zig (R5.1b)
ozgurulukir Jul 21, 2026
c37bd05
refactor(tui): extract TranscriptWidget to widgets/transcript.zig (R5…
ozgurulukir Jul 21, 2026
874d70e
test(background): rewrite flaky reader-thread tests as state-machine …
ozgurulukir Jul 21, 2026
87cfd8c
docs(agents): trim tool-specific notes, add TUI module-split patterns
ozgurulukir Jul 21, 2026
a09d145
refactor(tui): extract drawLaneColumn to tui/lane_column.zig (R5.2a)
ozgurulukir Jul 21, 2026
785ddc2
refactor(tui): extract drawDiffViewer to tui/diff_viewer_overlay.zig …
ozgurulukir Jul 21, 2026
ef10a0f
refactor(tui): extract rootLayout math to tui/layout.zig (R5.2c)
ozgurulukir Jul 21, 2026
cb213c0
docs(readme): list new tui modules from R5.2 (layout, lane_column, di…
ozgurulukir Jul 21, 2026
5d21030
refactor(tui): promote 4 helpers for R6.1 InputWidget extraction (R6.…
ozgurulukir Jul 21, 2026
e29e60c
refactor(tui): extract InputWidget family to widgets/input.zig (R6.1)
ozgurulukir Jul 21, 2026
2e98484
refactor(tui): extract drawRoot to tui/root_layout.zig (R6.2)
ozgurulukir Jul 21, 2026
ac58ce6
refactor(tui): promote 5 helpers for R6.3a deinit extraction (R6.3 prep)
ozgurulukir Jul 21, 2026
d04eee2
refactor(tui): extract App.deinit to tui/lifecycle.zig (R6.3a)
ozgurulukir Jul 21, 2026
0705bdc
refactor(tui): extract handleTick + drainAgentEvents to lifecycle.zig…
ozgurulukir Jul 21, 2026
0dba1a4
refactor(tui): extract createParallelLane to lifecycle.zig (R6.3c)
ozgurulukir Jul 21, 2026
098739e
refactor(tui): extract handleDiffBrowseKey + closeDiff to lifecycle.z…
ozgurulukir Jul 21, 2026
2f890a9
docs(readme): add root_layout, lifecycle, input to Architecture section
ozgurulukir Jul 21, 2026
0e8ab3e
refactor(tui): extract OverlayWidget family to widgets/overlay.zig (R…
ozgurulukir Jul 21, 2026
d9326ae
docs(_pm): reflect R7.1 (OverlayWidget extraction) status
ozgurulukir Jul 21, 2026
0b14cc3
refactor(tui): extract handleDiffSearchKey + handleDiffCommentKey to …
ozgurulukir Jul 21, 2026
f5d3315
refactor(tui): extract RootWidget handlers to lifecycle.zig (R7.3)
ozgurulukir Jul 21, 2026
36918c4
docs(_pm): reflect R7.1-R7.3 status
ozgurulukir Jul 21, 2026
5d899aa
refactor(tui): extract high-ROI blocks (R7.4)
ozgurulukir Jul 21, 2026
b23e0b6
refactor(tui): extract Provider/Model setup to tui/provider_model.zig…
ozgurulukir Jul 21, 2026
3764ae8
docs(_pm): reflect R7.4-R7.5 status
ozgurulukir Jul 21, 2026
67db102
docs(_pm): final maintenance — reflect R7.4-R7.5, mark sub-project co…
ozgurulukir Jul 22, 2026
4d46504
docs(readme): sync Architecture with R7.5 module state
ozgurulukir Jul 22, 2026
20ad85d
docs: remove stale _pm reference from module split note
ozgurulukir Jul 22, 2026
6c38502
refactor(tui): extract lane lifecycle to tui/lane_lifecycle.zig
ozgurulukir Jul 22, 2026
8867b57
refactor(tui): extract diff lifecycle to tui/diff_lifecycle.zig
ozgurulukir Jul 22, 2026
c9b7a5c
refactor(tui): extract session switching to tui/session_switcher.zig
ozgurulukir Jul 22, 2026
a47a759
docs: sync module list with Phase 1-3 (lane/diff/session extraction)
ozgurulukir Jul 22, 2026
15af109
refactor(tui): extract at-search to tui/at_search.zig
ozgurulukir Jul 22, 2026
f72c70a
refactor(tui): extract transcript navigation to tui/transcript_nav.zig
ozgurulukir Jul 22, 2026
4e120c7
refactor(tui): extract permission overlay to tui/permission.zig
ozgurulukir Jul 22, 2026
e6afc7a
refactor(tui): extract event callbacks to tui/event_callbacks.zig
ozgurulukir Jul 22, 2026
516386d
docs: sync module list with Phase 4-7, archive completed _pm projects
ozgurulukir Jul 22, 2026
08a7485
refactor(tui): extract queue management to tui/queue.zig
ozgurulukir Jul 22, 2026
a472658
docs: add queue module to README, create tui-domain-extract-2 _pm pro…
ozgurulukir Jul 22, 2026
544daaa
feat(tui): professional TUI enhancements, @-mention fff search fix & …
ozgurulukir Jul 22, 2026
df8b170
feat(models): enable models.dev specifications for all AI providers
ozgurulukir Jul 22, 2026
8ee3f45
feat(tui): enrich /connect provider picker layout with descriptions &…
ozgurulukir Jul 22, 2026
6fcd1f6
fix(tui): suppress initial /connect logo message when a provider is c…
ozgurulukir Jul 22, 2026
ed346ee
docs: tighten AGENTS.md prose, add missing help_picker and queue refe…
ozgurulukir Jul 22, 2026
7bce92f
fix: replace unsafe environ indexing with createMap, restore backgrou…
ozgurulukir Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ third_party/
__pycache__

## Models
ModernBERT-bash-classifier/
ModernBERT-bash-classifier/
vendor/fff/libfff_c.so
vendor/local-models/
.code-tandem/
.opencode/
56 changes: 47 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,52 @@ This project uses Zig version 0.16

Always consult the tigerstyle skill when writing code.

## Building the TUI
## Setup

- After cloning, vendor `fff` (build into `vendor/fff/libfff_c.so`) and the ModernBERT ONNX model (`vendor/local-models/ModernBERT-bash-classifier`). Both are gitignored.
- Use `OMP_WAIT_POLICY=passive` at runtime to avoid MKL/CPU spin in the embedding worker.
- `zig build install -Doptimize=ReleaseFast --prefix $HOME/.local` produces an installable binary under `~/.local/bin/`.

We use libvaxis vxfw for building the TUI. The source code for this library is inside zig-pkg.
## Building the TUI

Prefer to use the primitives provided by the framework as much as possible.
TUI built with libvaxis vxfw (source in zig-pkg). Prefer framework primitives.

**vxfw gotcha:** widget methods like `TextField.widget()` are *mutating* — they take `*Self`, not `*const Self`. Accessors returning `*TextField` must be declared on `*App`, not `*const App`, or the call site fails to type-check.

**Zig 0.16 field rule:** `pub` cannot precede a field declaration — only functions/variables. Cross-module field access goes through `pub fn` accessors (`getX()` form, never `X()`, because of the field-vs-method name collision).

**TUI module split.** `src/tui.zig` holds the `App` lifecycle and the top-level
`RootWidget`; the rest of `src/tui/` is split by concern. See `README.md`
Architecture for the current module list (kept in sync as `tui.zig` shrinks).

**Domain extraction pattern.** Isolated domain clusters (lane lifecycle, diff
lifecycle, session switching, at-search, transcript navigation, permission,
event callbacks, queue) live under `src/tui/` as free-function modules.
Each module imports `const tui = @import("../tui.zig")` and defines `pub fn`
taking `*App` as the first parameter. The original App method stays as a
1-line delegate (Strangler Fig) so inline tests in `tui.zig` resolve via the
struct. When a private App method is needed from the new module, promote it to
`pub` — not `pub const` (that is for module-level re-exports of nested types).

**Widget extraction pattern.** Isolated widgets live under `src/tui/widgets/`.
A new widget file declares the outer border widget as
`pub const NameWidget = struct { app: *App, pub fn widget(...) vxfw.Widget { ... } }`
and a private `Inner` struct built inside `draw()` from a `vxfw.DrawContext`.
The file imports `const tui = @import("../../tui.zig");`,
`const tui_style = @import("../style.zig");`, `const panel = @import("panel.zig");`
and re-aliases `const App = tui.App;`. Nested types from other modules
(e.g. `BackgroundManager.JobView`, `ApprovalSnapshot`) are re-exported through
`pub const` in `tui.zig` so widget files can reach them as `tui.<module>.<Type>`.

**Per-mode command routing.** `src/tui/command_router.zig` holds one struct per
`App.Mode` variant. Each struct owns a `handle` method that used to be a private
method on `App`; the dispatcher is a free function delegating to the right
struct. This is the place to add new per-mode logic — don't reintroduce
private methods on `App` for key handling.

## Zig Development

Use `zigdoc` to discover current APIs for the Zig standard library and any third-party dependencies before coding.

Examples:
Use `zigdoc` to discover APIs before coding.

```bash
zigdoc std.fs
Expand Down Expand Up @@ -98,13 +133,16 @@ const output = try writer.toOwnedSlice();
## Safety

- Add assertions at API boundaries and state transitions; avoid trivial assertions.
- Keep functions small and push pure computation into helpers.
- Keep functions small; push pure computation into helpers.
- Comments should explain why, not what.

## Verifying

Run the following:
Run:

- `zig fmt`

- `zig build test`

## Known Issues

- **High CPU usage from spinlocks.** `std.atomic.Mutex` busy-waits and pegs the CPU on multi-core. Use `std.Io.Mutex` and `std.Io.Condition` instead (paired via `static_thread_pool` or similar). Symptom: 80% CPU at idle, drops to ~2% after the fix. Files affected: `lib/logger.zig`, `src/agent.zig`, `src/background.zig`, `src/session.zig`.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,51 @@ zig build run
```

Add the binary (`zig-out/bin/nova`) to your PATH so you can invoke it from anywhere.

# Architecture

The TUI is a vxfw application. `src/tui.zig` holds the `App` lifecycle
and the top-level `RootWidget`; the rest of `src/tui/` is split by
concern:

- `event_router.zig` — top-level event entry (`captureEvent`).
- `command_router.zig` — per-mode key dispatch (one struct per `App.Mode`).
- `app_state.zig` — `App` state grouped into sub-structs.
- `background_delivery.zig` — background-job poll/format/deliver, modal toggling, job cancel.
- `turn_lifecycle.zig` — turn start, interrupt, event application, cancel/reset.
- `checkpoint.zig` — git-shadow checkpoint snapshotting, readiness checks, and `/save`.
- `mode_lifecycle.zig` — command matching, slash menu checks, mode switching.
- `input_lifecycle.zig` — input buffer peeking, clearing, vertical cursor navigation.
- `transcript_lifecycle.zig` — runtime installation and transcript rebuilding.
- `lane_lifecycle.zig` — lane naming, cycling, closing, merging, `/lanes` overlay.
- `diff_lifecycle.zig` — async diff refresh pipeline (DiffCounts, schedule/cancel/drain).
- `session_switcher.zig` — resume picker, session creation, timeline navigation.
- `at_search.zig` — `@` file / `$` skill mention popup.
- `transcript_nav.zig` — transcript scrolling, auto-scroll, long-message paging.
- `permission.zig` — tool-call approval/rejection overlay.
- `event_callbacks.zig` — vxfw input-change callbacks (`inputChanged`, `paletteInputChanged`).
- `queue.zig` — enqueue, flush, and navigate queued user messages.
- `layout.zig` — `rootLayout` math for `drawRoot` (transcript / loading / input row split).
- `lane_column.zig` — per-lane bordered transcript column (split view).
- `diff_viewer_overlay.zig` — full-screen `/diff` overlay.
- `root_layout.zig` — top-level `drawRoot` layout (tile grid, loading, input, overlay stack).
- `lifecycle.zig` — `deinit`, `handleTick`, `createParallelLane`, diff key handlers, `syncFocus`, `submit`, `ensureTick`.
- `diff_utils.zig` — pure diff-count stat/numstat parsers, git label loader.
- `lanes.zig` — `MergeSource` type + lane merge helpers (`workingLaneOf`, `laneErrorText`).
- `provider_model.zig` — provider connection, model catalogue loading, model selection (~50 functions).
- `thread.zig` — `Thread` (lane) state, multi-lane state machine.
- `turn.zig` / `turn_view.zig` — turn lifecycle + render.
- `diff_viewer.zig` — `/diff` inline-diff helpers (used by `widgets/diff.zig`).
- `model_catalogue.zig` / `model_loader.zig` / `model_cache.zig` — model
catalogue, async loader, cached model handles.
- `provider_controller.zig` — provider API controller.
- `agent_worker.zig` — agent worker plumbing.
- `blackhole.zig` — startup animation.
- `metrics.zig` — runtime metrics.
- `naming.zig` / `style.zig` / `status.zig` / `tool_policy.zig` —
shared helpers and policies.

`src/tui/widgets/` holds the per-widget draw code (message, command
panel, at_search, background_jobs, permission, diff, loading, transcript,
input, overlay, lanes picker, model picker, provider picker, resume
picker, help picker, tree selector, panel layout, tree art).
53 changes: 53 additions & 0 deletions _pm/Archives/tui-domain-extract/backlog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# tui-domain-extract — Backlog

## Refactor targets

Extract remaining domain clusters from `tui.zig` (5100 lines → target ~3000).
Discovered by BFG analysis: `tui.zig` god object at 5.100 lines with 227
functions is the #1 coupling hotspot (BFG coupling score 85, code quality 70).

### Phase 1: Lane lifecycle → `tui/lane_lifecycle.zig` (~400 lines)

28 functions: lane naming, cycling, closing, merging, fullscreen toggle,
lanes picker, parked-lane management.

Functions to move:
- captureLaneContext, scheduleLaneNaming, drainLaneNaming, renameLaneBranch
- cancelLaneNaming, namingActive, reportLaneError, activeIndex
- anyTurnActive, cycleLane, switchToNextLane, toggleLaneFullscreen
- closeActiveLane, abandonLane, laneMergeDir, mergeLane
- createMergePicker, confirmMergeDest, openLanesPicker
- collectParkedLanes, laneOpenAtPath, reloadParkedLanes
- mergeSelectedParked, deleteSelectedParked, laneEntryCount
- clearLanesState, buildLaneEntries, handleLanesKey

### Phase 2: Diff lifecycle → `tui/diff_lifecycle.zig` (~110 lines)

Types + helpers for the async diff refresh pipeline.

Items to move:
- DiffCounts, DiffRefreshJob, DiffRefreshOutcome structs
- runDiffRefresh, diffCountCommand
- refreshDiffCounts, installDiffCounts, scheduleDiffRefresh
- cancelDiffRefresh, drainDiffRefresh, populateDiffFromCache
- diffCountsVisible

### Phase 3: Session switching → `tui/session_switcher.zig` (~140 lines)

Resume picker state management + session creation/switching.

Functions to move:
- openResumePicker, reloadResumeSessions, selectedResumeSummary
- visibleResumeCount, toggleSelectedResumeProject, resumeFoldIndex
- resumeClearFolds, resumeClear, syncResumeListCursor
- reloadTreeNodes, navigateToEntry, restoreCheckpointForBranch
- switchToSession, switchToNewSession, createRuntime
- reportSessionSwitchError

### Spotted but deferred

- submitMode / cancelMode (~125 lines) — stays per tui-split R6.0 audit
- At-search (~110 lines) — tightly coupled to input, low ROI
- Transcript navigation (~100 lines) — spread across metrics + lifecycle
- Queue management (~80 lines) — tightly coupled to beginSubmit
- test blocks (~200 lines) — belong with tested code
38 changes: 38 additions & 0 deletions _pm/Archives/tui-domain-extract/done.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# tui-domain-extract — Done

Completed items, kept for history.

- **Phase 1** `tui/lane_lifecycle.zig` — extract lane lifecycle *(2026-07-22, commit `6c38502`)*
- 19 pub functions + 4 internal helpers moved. Strangler Fig pattern.
- `tui.zig` 5100 → **4728** (-372, -7.3%). `lane_lifecycle.zig` +491 lines.
- `zig build` and `zig build test` pass.

- **Phase 2** `tui/diff_lifecycle.zig` — extract diff lifecycle *(2026-07-22, commit `8867b57`)*
- 3 structs (DiffCounts, DiffRefreshJob, DiffRefreshOutcome), 1 command const,
8 App methods, 1 private free function moved. Re-exported as `tui.DiffCounts`
/ `tui.DiffRefreshOutcome` for backward compat.
- `tui.zig` 4728 → **4571** (-157, -3.3%). `diff_lifecycle.zig` +193 lines.
- `zig build` and `zig build test` pass.

- **Phase 3** `tui/session_switcher.zig` — extract session switching *(2026-07-22, commit `c9b7a5c`)*
- 14 App methods + 2 private helpers + 2 free functions moved. Covers resume
picker (openResumePicker, reloadResumeSessions, etc.), session creation
(switchToSession, switchToNewSession, createRuntime), timeline navigation
(navigateToEntry, restoreCheckpointForBranch, reloadTreeNodes).
- Promotions: `templateRuntime`, `installRuntime`, `clearConversation`,
`rebuildTranscriptFromAgent` made `pub` (were `fn` private, now called from
`session_switcher.zig`). `resumeSummaryLessThan` made `pub`.
- `tui.zig` 4571 → **4409** (-162, -3.5%). `session_switcher.zig` +226 lines.
- `zig build` and `zig build test` pass.

## Total impact (all 3 phases)

| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| `tui.zig` lines | 5100 | **4409** | **-691 (-13.5%)** |
| New modules | — | 3 (491 + 193 + 226 = **910 lines**) | |
| Functions moved | — | 28 lane + 11 diff + 18 session = **57** | |
| Promotions to `pub` | — | activeIndex, templateRuntime, installRuntime, clearConversation, rebuildTranscriptFromAgent, resumeSummaryLessThan | |
| `zig build` | ✓ | ✓ | |
| `zig build test` | ✓ | ✓ | |
| Commits | — | 3 (`6c38502`, `8867b57`, `c9b7a5c`) | |
11 changes: 11 additions & 0 deletions _pm/Archives/tui-domain-extract/todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# tui-domain-extract — Todo

Items committed to, not yet started.

## Status

All phases completed:

- [x] Phase 1: Lane lifecycle → `tui/lane_lifecycle.zig` *(`6c38502`)*
- [x] Phase 2: Diff lifecycle → `tui/diff_lifecycle.zig` *(`8867b57`)*
- [x] Phase 3: Session switching → `tui/session_switcher.zig` *(`c9b7a5c`)*
15 changes: 15 additions & 0 deletions _pm/Archives/tui-domain-extract/wip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# tui-domain-extract — In progress

One item at a time (Rule 9).

## Status

All three phases committed and pushed. `tui.zig` 5100 → **4409** (-691, -13.5%).
3 new modules totalling 910 lines. `zig build && zig build test` pass.

## Constraints

- Strangler Fig pattern: original methods stay as 1-line delegates.
- `pub fn` accessors where cross-module reads need Zig 0.16 field bypass.
- `zig build && zig build test` must pass after each commit.
- atomic commits, pushed to `origin/main`.
86 changes: 86 additions & 0 deletions _pm/Archives/tui-split/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# tui-split

Refactor `src/tui.zig` into focused modules. Started 2026-07-21 from 8586 lines; down to ~5943 (-2643, -30.8%) after R1–R7 (35 atomic commits). Target ≤ 2500 lines.

## Why

BFG analysis (2026-07-21) flagged `src/tui.zig` as the worst coupling hotspot in the codebase:

- 8586 lines (Rule 10 limit: 200 ideal, 300 split threshold; this was 28× over)
- `captureEvent` 87 lines, 0.828 transform ratio (cognitive load)
- `handleCommandKey` exhaustive call graph: 882 nodes, 12491 edges, cycles=true
- 4 hubs >400 edges each (`captureEvent` 457, `handleCommandKey` 457, `cancelLaneNaming` 442, `freeDelivery` 441)
- Single change to TUI flow touches many of them — high blast radius

The file already imports 40+ submodules. It's not a monolith that knows nothing — it's a monolith that knows too much about each one.

## Approach

Strangler Fig (Rule 15):
1. Create new module file
2. Move related code
3. Update `tui.zig` to import + delegate
4. Build passes
5. Old code removed in same commit

No "compatibility shim" left behind (Rule 9 outcome-oriented).

## Modules shipped (R1–R7.5)

| File | Phase | Pulled from `tui.zig` |
| --- | --- | --- |
| `tui/event_router.zig` | R1 | `captureEvent` — key/mouse event dispatch |
| `tui/command_router.zig` | R2 | `handleCommandKey` mode switch (struct-per-mode) |
| `tui/app_state.zig` | R3 | `App` state grouped into 6 sub-structs |
| `tui/background_delivery.zig` | R4 | background-job poll/format/deliver |
| `tui/lane_column.zig` | R5.2a | `drawLaneColumn` |
| `tui/diff_viewer_overlay.zig` | R5.2b | `drawDiffViewer` |
| `tui/layout.zig` | R5.2c | `rootLayout` math |
| `tui/root_layout.zig` | R6.2 | `drawRoot` layout |
| `tui/lifecycle.zig` | R6.3–R7.3 | deinit, handleTick, createParallelLane, diff handlers, syncFocus, submit, ensureTick |
| `tui/diff_utils.zig` | R7.4 | pure diff-count parsers |
| `tui/lanes.zig` | R7.4 | MergeSource + lane helpers |
| `tui/provider_model.zig` | R7.5 | provider + model catalogue setup (~50 fns) |
| `tui/widgets/background_jobs.zig` | R5.1a | `BackgroundJobsWidget` |
| `tui/widgets/permission.zig` | R5.1a | `PermissionWidget` |
| `tui/widgets/diff.zig` | R5.1b | `DiffBodyWidget` + `DiffCommentEditor` + `DiffSearchWidget` |
| `tui/widgets/loading.zig` | R5.1c | `LoadingWidget` |
| `tui/widgets/transcript.zig` | R5.1d | `TranscriptWidget` + `MessageListBuilder` |
| `tui/widgets/input.zig` | R6.1 | `InputWidget` + 13 wrapping helpers |
| `tui/widgets/overlay.zig` | R7.1 | `OverlayWidget` + `OverlayInner` |

## Remaining

`tui.zig` ~5100 lines. No further extraction planned — each remaining
block requires 5–15 pub promotions for diminishing ROI. Key stays:
- `submitMode` (95 lines, ~25 private helpers — stays per R6.0 audit)
- App business logic (provider, session, lane, diff lifecycle)
- Event callbacks (`inputChanged`, `paletteInputChanged`)
- Inline test blocks (~200 lines)

Target: ≤ 2500 lines. 50.8% there.

## What stays in `tui.zig`

- `App` struct (with state fields moved to sub-structs)
- `RootWidget` (top-level vxfw widget)
- `init` / `run` lifecycle (deinit may move to R6 lifecycle module)
- Cross-module glue (calls into the routers)

Target: ≤ 2500 lines.

## Non-goals

- No behaviour change
- No new features
- No test additions (out of scope for this refactor; tracked separately if needed)
- No rename of public App methods (just relocate)

## Risks

- `captureEvent` is the hot path on every keypress. Inlining decisions matter. (Mitigated by R1.)
- `App` fields are read by ~30 files. Splitting state into sub-structs touches every call site. (Mitigated by R3.)
- `handleCommandKey` switch arms have shared local state — pulling them apart requires care. (Mitigated by R2.)
- R6 lifecycle extraction is blocked on private App method promotion; doing it without rethinking the boundary just churns `pub` keywords. (Documented; pick up in a dedicated session.)

Mitigation: do it in small commits, run `zig build` after each, do a manual smoke test (launch, type, submit, switch modes).
Loading