REPL Command Handlers Refactor — Plan¶
Summary¶
Move slash-command dispatch and command-specific business logic out of repl.go and
handlers.go into a dedicated command_handlers.go file. After the refactor:
repl.go→ model construction,RunREPL, rendering, viewport/textarea gluehandlers.go→ Bubble Tea message routing (key events, LLM stream events, spinner ticks)command_handlers.go→ slash-command dispatch + each command's logic
Current state¶
| Function | Current file | Responsibility |
|---|---|---|
handleEnterKey |
repl.go |
Dispatches slash commands AND starts LLM chat stream |
handleThinkingCommand |
repl.go |
/thinking business logic |
handleLogout |
repl.go |
/logout business logic |
handleClear |
repl.go |
/clear and /new business logic |
startCompaction |
repl.go |
/compact business logic |
startModelSelection |
repl.go |
/model UI trigger |
getHelpText |
repl.go |
/help text generation |
handleKeyMsg |
handlers.go |
Bubble Tea key routing (delegates to handleEnterKey) |
handleLLM* |
handlers.go |
LLM stream event handling |
handlePermissionKeyMsg |
handlers.go |
Permission dialog key handling |
handleSessionPickerKeyMsg |
handlers.go |
Session picker key handling |
The boundary is fuzzy — handleEnterKey in repl.go contains both the dispatch
table and the general "send to LLM" path, while handlers.go contains both Bubble Tea
routing and some command-adjacent logic.
Proposed file layout after refactor¶
repl.go (model + lifecycle)¶
replContext,replModelstruct definitionsinitialModel(),RunREPL()Init(),View()- Viewport/textarea helpers:
applyWindowSize,adjustTextareaHeight,updateViewportContent,scrollToBottomIfFollowing,renderInputArea,inputMetaView startStreamContext,clearStreamCancelupdateLLMClientreplayLoadedSession- Constants, loading texts, loading spinners
handlers.go (Bubble Tea message/key routing)¶
Update,updateNormalModehandleKeyMsg— key dispatch only (enter → call into command_handlers)handleLLMStreamMsg,handleLLMChunk,handleLLMReasoningChunk,handleLLMDone,handleLLMError,handleLLMIncomplete,handleLLMRetry,handleLLMUsagehandleToolStart,handleToolEndhandleCompactionDone,handleCompactionErrorhandleSpinnerTickconsumeModelSelectionResulthandlePermissionKeyMsghandleSessionPickerKeyMsghandleSuggestionKeyMsg,handleFileModeSelectionhandleUpdateCheckMsginterruptStreamwaitForAsyncEvent
command_handlers.go (NEW — slash-command dispatch + logic)¶
dispatchCommand(input string) (replModel, tea.Cmd)— the slash-command routing extracted fromhandleEnterKeyhandleHelpCommand() (replModel, tea.Cmd)handleModelCommand() (replModel, tea.Cmd)handleLogoutCommand() (replModel, tea.Cmd)(renamed fromhandleLogout)handleClearCommand() (replModel, tea.Cmd)(renamed fromhandleClear)handleThinkingCommand(input string) (replModel, tea.Cmd)(moved as-is)handleCompactCommand(input string) (replModel, tea.Cmd)(wrapsstartCompaction)handleSessionsCommand() (replModel, tea.Cmd)handleExitCommand() (replModel, tea.Cmd)getHelpText() string(moved)startModelSelection() replModel(moved)startCompaction(extraPrompt string) (replModel, tea.Cmd)(moved)
Detailed changes¶
1. Create internal/cli/repl/command_handlers.go¶
- [ ] Add package declaration and required imports
- [ ] Move
getHelpText()fromrepl.go - [ ] Move
startModelSelection()fromrepl.go - [ ] Move
handleThinkingCommand()fromrepl.go - [ ] Move
handleLogout()→ rename tohandleLogoutCommand()for consistency - [ ] Move
handleClear()→ rename tohandleClearCommand() - [ ] Move
startCompaction()fromrepl.go - [ ] Extract a new
dispatchCommand(input string) (replModel, tea.Cmd, bool): - Contains the slash-command
if/switchchain currently inhandleEnterKey - Returns
(model, cmd, handled)— ifhandled == false, the caller sends to LLM - [ ] Extract
handleExitCommand,handleHelpCommand,handleModelCommand,handleSessionsCommand,handleCompactCommandas thin wrappers if warranted, or leave them inlined indispatchCommandwhere the logic is ≤5 lines
2. Simplify handleEnterKey in repl.go¶
- [ ] After extracting command dispatch,
handleEnterKeybecomes:func (m *replModel) handleEnterKey() (replModel, tea.Cmd) { input := m.textarea.Value() if input == "" || m.streamHandler.IsActive() || m.isCompacting { return *m, nil } m.output.AddUserInput(input, repltheme.PromptStyle) m.history.Push(input) if updated, cmd, handled := m.dispatchCommand(input); handled { return updated, cmd } // ... existing "send to LLM" logic stays here } - [ ] Move
handleEnterKeyto stay inrepl.go(it's lifecycle/textarea plumbing) OR move it tohandlers.go(it's called fromhandleKeyMsg). Decision: keep inhandlers.gosince it's triggered by key events and closely related to message routing.
3. Move handleEnterKey from repl.go to handlers.go¶
- [ ] This puts all key-triggered actions in one file
- [ ] The command dispatch call goes to
command_handlers.go
4. Create/update tests¶
- [ ] Create
internal/cli/repl/command_handlers_test.go: - Move tests for
/help,/model,/clear,/thinking,/logout,/compactfromrepl_test.goto the new test file - Add a test for
dispatchCommandrouting (unknown commands fall through) - [ ] Keep LLM stream and key-routing tests in
handlers_test.go - [ ] Keep model construction and rendering tests in
repl_test.go
5. Verify¶
- [ ]
go test ./internal/cli/repl/...— all pass - [ ]
go test ./...— full suite passes - [ ]
go mod tidy - [ ] No behavior changes — purely structural refactor
Implementation order¶
- Create
command_handlers.gowith moves (no renames yet) — verify tests pass - Extract
dispatchCommandfromhandleEnterKey - Move
handleEnterKeytohandlers.go - Rename handlers for consistency (
handleLogout→handleLogoutCommand, etc.) - Reorganize tests into the appropriate
_test.gofiles - Final
go test ./...
Risk mitigation¶
- No behavior changes — this is a pure code-motion refactor
- Incremental moves — each step leaves tests green
- No new dependencies — same package, same imports reshuffled
Acceptance criteria (from issue)¶
- [x] Slash-command routing has a single obvious home →
command_handlers.go - [x]
handlers.gono longer owns command-specific business logic - [x] Existing command behavior is preserved
- [x] Existing tests pass, with focused tests added where command dispatch moves