Skip to content

refactor(leantui): decompose the model into cohesive subsystems#3455

Merged
rumpl merged 2 commits into
docker:mainfrom
rumpl:refactor/leantui-cohesion
Jul 3, 2026
Merged

refactor(leantui): decompose the model into cohesive subsystems#3455
rumpl merged 2 commits into
docker:mainfrom
rumpl:refactor/leantui-cohesion

Conversation

@rumpl

@rumpl rumpl commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

Progressively decomposes pkg/leantui's large model struct and update.go into cohesive, independently-testable subsystems. No functional change — purely structural. Done as a series of behavior-preserving commits.

Commits

1. Extract usage/tool trackers and split update.go

  • usageTracker (usage.go): per-session token/cost aggregation — the session stack, root/latest bookkeeping, and "active session" resolution. Replaces four model fields (usageBySession, rootSessionID, latestUsageSessionID, sessionStack) with one.
  • toolTracker (toolstate.go): in-flight tool-call state (tools map + toolOrder); finish() returns an immutable snapshot for the model to commit.
  • Runtime event handling moves to events.go, leaving update.go focused on input and command dispatch.
  • Deduplicated run-lifecycle boilerplate (beginRun) and thinking-level guard/error handling.

2. Extract the transcript type

  • Groups everything that scrolls — finalized blocks, the in-progress streamed block, and in-flight tool calls — into one transcript type that owns its rendering (the former conversationLines, now transcript.lines).
  • resetConversation's manual pending/tool reset becomes transcript.clearActive(), which keeps committed scrollback intact.

Net effect

The model struct drops from ~30 fields to 18, and update.go from 761 to ~417 lines. The three most self-contained concerns (usage, conversation content, tool state) are now typed components with their behavior exercisable in isolation.

Validation

  • go build ./... — passes
  • go test ./pkg/leantui/... — passes (tests touched only for renamed field paths; no assertions changed)
  • golangci-lint run ./pkg/leantui/... (v2.12.2, repo config) — 0 issues

Deliberately left alone

The differential renderer, the editor, and the keys parser are already cohesive single-responsibility units. A runState extraction (busy/runCancel/queue) was considered but held back — it is borderline because the actual app.Run calls must stay on the model; the run-lifecycle fields are grouped together to make that a clean follow-up if desired.

The model struct and update.go had accumulated several distinct concerns.
This splits them into cohesive units without changing behavior:

- Extract per-session usage aggregation into a usageTracker (usage.go),
  replacing four model fields (usageBySession, rootSessionID,
  latestUsageSessionID, sessionStack) with one.
- Extract in-flight tool-call state into a toolTracker (toolstate.go),
  replacing the tools map and toolOrder slice; finish() returns an
  immutable snapshot for the model to commit.
- Move runtime event handling out of update.go into events.go, leaving
  update.go focused on input and command dispatch (761 -> 418 lines).
- Deduplicate run-lifecycle boilerplate via beginRun().
- Deduplicate thinking-level guard/error handling via
  thinkingLevelChangeable() and reportThinkingLevelError().

No functional change; existing tests updated for the new field names.
@rumpl rumpl requested a review from a team as a code owner July 3, 2026 18:12
@rumpl rumpl enabled auto-merge July 3, 2026 18:14
Group the scrollable conversation content — finalized blocks, the
in-progress streamed block, and in-flight tool calls — into a single
transcript type that owns its rendering (the former conversationLines).

- model loses blocks, pending, and toolz (3 fields -> 1 transcript),
  dropping from 21 to 18 fields.
- block, pendingBlock, blockKind, spinnerFrames and the spinner/pending
  rendering move into transcript.go alongside the type.
- conversationLines becomes transcript.lines(width, spinnerFrame, busy,
  sessionState); the busy flag is now an explicit render input.
- resetConversation's manual pending/tool reset becomes
  transcript.clearActive(), which keeps committed scrollback intact.

No functional change; tests updated for the new field paths.
@aheritier aheritier added area/tui For features/issues/fixes related to the TUI kind/refactor PR refactors code without behavior change labels Jul 3, 2026
@rumpl rumpl changed the title refactor(leantui): extract usage/tool trackers and split update.go refactor(leantui): decompose the model into cohesive subsystems Jul 3, 2026
@rumpl rumpl merged commit 16fdef4 into docker:main Jul 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/tui For features/issues/fixes related to the TUI kind/refactor PR refactors code without behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants