Repository navigation
fix(tui): keep the Main Menu highlight on Ctrl+S and draw the preview at the configured Color Level - #784
Merged
sirmalloc merged 8 commits intoNov 5, 2026
Conversation
Pressing Ctrl+S on the main menu after making changes moved the highlight back to the entry that was last opened. Saving swaps Save & Exit and Exit without saving for a single Exit, and List reset its highlight to initialSelection whenever the number of items changed. The main menu's initialSelection is the entry last opened, not the one highlighted, so the cursor left the entry the user had moved to and the next Enter opened a different screen. List gets a keepHighlightOnItemsChange prop, and the main menu sets it. With it, a new initialSelection still moves the highlight, but when just the number of items changes, the highlight stays on the item it was on; if that item is gone, it stays on the same row, inside the list. On the main menu, Install stays Install after a save, and Save & Exit or Exit without saving become the Exit that replaces them. Other lists still move the highlight to initialSelection when their item count changes. Line selection and Powerline Setup keep their initialSelection on the highlight and set it to place the highlight: appending a line highlights the new line, and turning Powerline on highlights Separator.
In a terminal that chalk detects at fewer colors than the configured color level, such as tmux with TERM=screen or ssh with TERM=xterm, the TUI preview and color menus drew different colors from the real status line. At the default 256 colors, cyan, magenta and bright black widgets were drawn black; a truecolor setting previewed 256-color approximations. chalk works out a 256-color or hex style's escape code when the style is created, at the level set at that moment. COLOR_MAP creates its styles when colors.ts loads, so its codes follow the level chalk detected for the terminal. The piped render sets chalk.level and then calls updateColorMap() to rebuild them; the TUI set chalk.level on load, on import and in Terminal Options, but never rebuilt the map. The TUI now calls updateColorMap() each time it sets chalk.level, as the piped render does. The test sandbox rebuilds the map when it puts chalk's level back, so later test files get the codes they expect.
# Conflicts: # src/tui/App.tsx # src/tui/__tests__/App.test.ts # src/tui/__tests__/helpers/render-app.ts # src/tui/components/MainMenu.tsx
The main menu now records its highlighted row as it moves, so initialSelection is that row's index rather than the entry last opened. Keeping the highlight on its entry across a save is still needed: the save swaps Save & Exit and Exit without saving for one Exit, which moves the entries below, so the index alone would land on another entry. The comments on MainMenu and List's keepHighlightOnItemsChange say so.
… list The main menu highlight tests pressed down through a hard-coded list of entries, so an entry another change adds or makes selectable (Configure Status Line opening before an install, say) broke them. They now press down until the target entry is highlighted, waiting for the highlight to move on each press.
# Conflicts: # src/tui/__tests__/App.test.ts
With sirmalloc#765 Terminal Options lists only Color Level, so the steps that went through Terminal Width land on Color Level when the menu opens.
Contributor
|
Reproduced on current Red: 5 failing / 68 total. Both halves reproduce:
Green: with the full PR head checked out, 68 passing / 0 failing. One note: the PR body says 8 App tests fail on main; on this run 4 App tests (+1 import test) failed — the remaining highlight cases (Install / Save & Exit / GitHub star) passed, likely timing-sensitive. Both bug halves reproduce either way.
|
This was referenced Nov 7, 2026
Closed
Closed
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
Two TUI fixes:
TERM=screenin tmux or GNU screen, orTERM=xtermover ssh, with noCOLORTERM), cyan, magenta and bright black widgets were drawn black. A Truecolor setting in a terminal seen as 256-color was previewed in 256-color approximations.Why
Listreset its highlight toinitialSelectionwhenever its number of selectable items changed:Before Claude Code is installed, the Main Menu has 11 selectable entries with unsaved changes (Save & Exit, Exit without saving) and 10 after a save (Exit). App passes
initialSelection={menuSelections.main}, and that value is set only when an entry is opened or left, not when the highlight moves. So Ctrl+S re-ran the effect, and the highlight jumped back to the entry last opened.On
main: change Color Level in Terminal Options, Escape, Down to Install to Claude Code, Ctrl+S. Right after✓ Configuration savedappears, the highlight moves to▶ 💻 Terminal Options, and Enter opens Terminal Options.chalk works out the escape code of an
ansi256()orhex()style when the style is created, at the level chalk has at that moment.COLOR_MAPincolors.tscreates its styles when the module loads, so their codes follow the level chalk detected for the terminal. The piped render setschalk.levelfrom the settings and then callsupdateColorMap()to rebuild them. The TUI setchalk.levelon load, on import and in Terminal Options, but never rebuilt the map. The preview and Edit Colors read their codes from that map.Settings
{"version":4,"colorLevel":2,"lines":[[{"id":"1","type":"model"}]]}, run in a pty withTERM=screenand neitherCOLORTERMnorTERM_PROGRAMset (cat -v):main^[[30mModel: Claude(black)^[[38;5;30mModel: Claude^[[38;5;30mModel: Claude^[[30mModel: Claude^[[38;5;30mModel: Claude^[[38;5;30mModel: ClaudeWith
"colorLevel":3andTERM=xterm-256color, the preview onmaindrew^[[38;5;37munder both runtimes. The piped render and this branch's preview print^[[38;2;6;152;154m.How
Listgets akeepHighlightOnItemsChangeprop, and the Main Menu sets it.Listkeeps track of theinitialSelectionand items it last fitted the highlight to. A newinitialSelectionstill moves the highlight there. With the prop set, when only the number of items changes, the highlight stays on the item it was on, matched by value. If that item is gone, it keeps the same position among the selectable items, or moves to the last one if the list is now shorter.selectableItemsis now memoized, so the effect can depend on the list itself. On the Main Menu this also covers the startup install check: if it finishes after the highlight has moved, Configure Status Line becoming selectable no longer sends the highlight back to the first entry.Other lists still move the highlight to
initialSelectionwhen their item count changes, as onmain. Line selection and Powerline Setup rely on that. They feed the highlight back intoinitialSelection, so with Back highlighted, (a) adding a line highlights the new line, and (t) turning Powerline on highlights Separator. Keeping the highlight on the same item would leave it on Back in both.The TUI calls
updateColorMap()each time it setschalk.level: when settings load, after an import (applyTuiImport), and in both Color Level paths of Terminal Options (with and without the custom-colors warning). The piped render already does this. The App test sandbox rebuilds the map when it puts chalk's level back, so later test files get the codes they expect. ItsrenderApp()helper gets agetRawFrame()that keeps the escape codes.New tests in
App.test.ts:38;5;30. Picking Truecolor in Terminal Options redraws it as38;2;6;152;154, with and without the custom-colors warning.applyTuiImportwith an imported 256 Color level rebuilds the map.New tests for the lists that keep moving the highlight:
LineSelector.test.ts: (a) with Back highlighted highlights the new Line 3, and Enter opens it.PowerlineSetup.test.ts: (t) turning Powerline on without a confirm highlights Separator, and Enter opens the separator editor.Left as is:
settings.jsonthat failed to load, Ctrl+S opens a confirm screen first. Coming back from it mounts the Main Menu again, so the highlight returns to the entry last opened. That comes from leaving the menu, not from its item count changing.COLOR_MAPstill takes its codes from chalk's global level, so any new code that setschalk.levelhas to callupdateColorMap()too. Building each color tier from its own fixed-level chalk instance would remove that need, but it changescolors.tsfor the piped render as well, so it is left for a separate change.Demo
Powerline
Ctrl+S on the Main Menu. Color Level has an unsaved change, and Terminal Options was the entry last opened. Down to Install to Claude Code, Ctrl+S, then Enter. Before, the save moves the highlight back to Terminal Options and Enter opens it. After, Install stays highlighted and Enter opens the install screen.
Before:
After:
The preview in a terminal that reports 16 colors (
TERM=screen,COLORTERMandTERM_PROGRAMunset), with Color Level at 256 Color. The segments have cyan, bright black, magenta and yellow backgrounds. The TUI opens, is closed with Ctrl+C, and the example payload is then piped, so the real status line prints under the preview. Before, the first three segments are drawn in the terminal's black (dark gray in this theme) and yellow is the terminal's own yellow. After, the preview matches the piped line.Before:
After:
Plain
Ctrl+S on the Main Menu, as above.
Before:
After:
The preview in a 16-color terminal, as above, with cyan, bright black, magenta and yellow text. Before, Model, Context Length and Git Branch are drawn in the terminal's black. After, the preview matches the piped line.
Before:
After:
Testing
mainas of fix: use env override and settings for usable context ratio calculation #407 (71e8b42): merges cleanly,bun run lintis clean, andbun testgives 3948 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matchesmainin plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly. It adds no JavaScript or Node API newer than Node 14, the build's target.bun run lintis clean.bun test: 3178 pass, 0 fail across 178 files, against 3168 pass, 0 fail onmain. 10 new tests: 8 inApp.test.ts, 1 inLineSelector.test.ts, 1 inPowerlineSetup.test.ts.App.test.tstests fail onmain, under Bun and under Node. TheLineSelectorandPowerlineSetuptests pass onmain, where those lists already behave this way. Both fail when every list keeps its highlight on the same item, under Bun and under Node.App.test.ts,TerminalOptionsMenu.test.ts,LineSelector.test.tsandPowerlineSetup.test.tsunder Node (v26.10.0, Vitest): 65 of 65 pass, against 55 of 55 onmain.dist/under Bun 1.4.2 and Node v26.10.0: the baseline plain and Powerline renders agree across runtimes and matchmain. The TUI opens and exits cleanly under both.TERM=screen: the table in "Why".