refactor(leantui): decompose the model into cohesive subsystems#3455
Merged
Conversation
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.
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.
Sayt-0
approved these changes
Jul 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Progressively decomposes
pkg/leantui's largemodelstruct andupdate.gointo 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.gousageTracker(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 (toolsmap +toolOrder);finish()returns an immutable snapshot for the model to commit.events.go, leavingupdate.gofocused on input and command dispatch.beginRun) and thinking-level guard/error handling.2. Extract the
transcripttypetranscripttype that owns its rendering (the formerconversationLines, nowtranscript.lines).resetConversation's manual pending/tool reset becomestranscript.clearActive(), which keeps committed scrollback intact.Net effect
The
modelstruct drops from ~30 fields to 18, andupdate.gofrom 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 ./...— passesgo 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 issuesDeliberately left alone
The differential
renderer, theeditor, and thekeysparser are already cohesive single-responsibility units. ArunStateextraction (busy/runCancel/queue) was considered but held back — it is borderline because the actualapp.Runcalls must stay on the model; the run-lifecycle fields are grouped together to make that a clean follow-up if desired.