Skip to content

fix(tui): stop editor keys pausing on a global ccstatusline install - #751

Merged
sirmalloc merged 2 commits into
sirmalloc:mainfrom
eric-engberg:fix/tui-editor-key-pause
Oct 29, 2026
Merged

sirmalloc merged 2 commits into
sirmalloc:mainfrom
eric-engberg:fix/tui-editor-key-pause

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

When Claude Code runs the global ccstatusline command (a pinned install, which is the install menu's default, or a self-managed one), most keys in the line editor paused the TUI for a quarter to half a second, and keys pressed during the pause could be lost. The TUI now looks up the global command once, and again only when the installation changes, instead of on every redraw.

Why (evidence)

App looked up the global command in its render body. inspectActiveGlobalCommand() runs which -a ccstatusline, npm prefix -g and bun pm bin -g with execFileSync. For a pinned install, getPinnedGlobalRelaunchCommand() also runs which -a and the bin-dir lookup again, even when no version mismatch is shown. npm prefix -g alone takes about 170 ms. The label editor's live preview (#710), every widget edit (r, m, d, move-mode arrows, accepting a label) and every screen change redraw App, so each of those keys waited for 3 to 5 subprocesses before the preview, and the next key, were handled.

Measured in a real pty on macOS (Node 26, Bun 1.4.2). The setup is a scratch HOME with a fake bun global install, and real npm and bun answer the lookups:

npx (control) self-managed pinned
label letter → preview 2-37 ms 224-587 ms 267-551 ms
subprocesses per letter 0 3 5
raw-value toggle (r) 2-10 ms 222-368 ms 253-262 ms
12 letters typed 50 ms apart, until all shown 624-645 ms 863-1252 ms 1023-1037 ms

Keys that arrive during a pause reach Ink in one read. Ink 6.2 parses a read of several backspaces as no key, and several arrows as one. With a pinned install, 6 backspaces 60 ms apart deleted 2-3 characters, and 6 ↓ in move mode moved the widget 2 places.

How

  • The lookups move into a useMemo keyed on what they depend on: the saved installation metadata, Claude Code's status line command, the installed state, and whether loading has finished.
  • They run once loading finishes, and again after an install, update or uninstall from the TUI, since each changes one of those. For example, after the TUI updates a pinned install, it looks again, so the version-mismatch screen still clears.
  • Classifying the installation now takes the installation metadata instead of the whole settings object, so the memo doesn't re-run on every settings edit. App picks classifyInstalledStatusLine() when Claude Code runs ccstatusline and classifyInstallation() otherwise.
  • Ctrl+S and Check for Updates still look the command up when pressed, so the save guard keeps checking what's on disk.
  • An install or update made in another terminal while the TUI is open is picked up on Ctrl+S, after an install/update/uninstall in the TUI, or on relaunch, rather than on the next key.

What the screens show doesn't change. With pinned, self-managed and version-mismatch setups, Main Menu, Manage Installation and the mismatch screen render byte-for-byte as on main.

Demo

Relabelling Model and Context Length with a pinned global install. Before: backspaces pressed during a pause are dropped, so the old labels aren't fully cleared ("MoChatting with", "Ctcontext used:"). After: every key lands as typed.

Powerline

Before:

Relabelling widgets with a pinned install before the fix, Powerline

After:

Relabelling widgets with a pinned install after the fix, Powerline

Plain

Before:

Relabelling widgets with a pinned install before the fix, plain

After:

Relabelling widgets with a pinned install after the fix, plain

Testing

  • New App tests in a real Ink render:
    • With a pinned or a self-managed install, the lookup runs once across opening a line, typing three label letters, accepting, toggling raw value and going back to the menu. Before the fix it ran 14 times (fails on main under Bun and Node).
    • After the TUI updates a pinned install whose on-disk version changes, the lookup runs again and the mismatch screen clears. A memo that never refreshes fails this test.
  • bun test: 3171 pass, 0 fail. bun run lint: clean.
  • App.test.ts under Node (vitest): 30/30 (27/27 on main).
  • Built dist/ under Bun 1.4.2 and Node 26.10.0: piped renders agree and match main; the TUI opens and exits.
  • pty after the fix: pinned and self-managed match the npx control: 2-42 ms per label letter with 0 subprocesses per key, raw toggle 2-4 ms, 12 letters shown in 644-653 ms, and all 6 queued backspaces and arrows land. Subprocesses at startup drop from 10-14 to 7 (self-managed) and 9 (pinned).

When Claude Code runs the global `ccstatusline` command (a pinned
install, which is the install menu's default, or a self-managed one),
App looked that command up in its render body. The lookup runs
`which -a`, `npm prefix -g` and `bun pm bin -g` synchronously, and a
pinned install runs `which` and the bin-dir lookup again for the
relaunch path. The label editor's live preview and every widget edit
redraw App, so each of those keys blocked the TUI for a quarter to half
a second on macOS. Keys pressed meanwhile reached Ink as one read,
which it parses as a single key or none: of six quick backspaces, two
or three took effect.

Run the lookups in a memo keyed on what they depend on: the saved
installation metadata, Claude's status line command and the install
state. They now run once loading finishes, and again after an install,
update or uninstall from the TUI, since each changes one of those. The
screens show the same thing. Ctrl+S and Check for Updates still look
the command up when pressed.
getCurrentInstallation took an isClaudeInstalled flag that switched
between two behaviours: with ccstatusline installed in Claude Code, the
saved metadata stands in until the status line command is read;
otherwise the command alone is classified. Each caller already holds
that state, so each now picks: classifyInstalledStatusLine for the
installed case, classifyInstallation otherwise.

Check for Updates passes the saved metadata as settings?.installation
instead of special-casing missing settings, since no saved metadata
already classifies the command alone.
@sirmalloc
sirmalloc merged commit cd04d86 into sirmalloc:main Oct 29, 2026
3 checks passed
axisrow added a commit to axisrow/ccstatusline that referenced this pull request Oct 29, 2026
sirmalloc#755 live picker preview, sirmalloc#754 fast mode widget, sirmalloc#753 thinking-effort test stub, sirmalloc#752 transcript block fallback fixes, sirmalloc#751 editor keys no longer pause on global install lookup, sirmalloc#750 git cache TTL starts after the run, sirmalloc#749 npx cache skip, sirmalloc#747 Block Usage hides at 100% and swaps with Block Reset Timer)

Conflict policy: upstream infra wins, fork features ported on top. Joins:
sirmalloc#750 stamps the git cache after the run while keeping the fork's execGit
hardening/env snapshot and daemon prefetch bookkeeping; sirmalloc#752's
isApiErrorMessage guard lands in the fork's shared collectTimestampFromLine
(sync + async twins), upstream's duplicate getActivityTimestamp dropped;
sirmalloc#747 adds SESSION_AT_LIMIT alongside the fork's zero hide state
(BlockResetTimer keeps zero + under-limit via isAtSessionLimit, SessionUsage
is no-data/zero/at-limit); sirmalloc#755 replaces the fork's picker window + "Showing
X-Y of N" note with upstream's renderWindowedList/getPickerMaxVisible and
live preview (markers ↑/↓ N more cover the counts); sirmalloc#751's installationState
useMemo replaces the fork's inline lookups, the fork's mainMenuIndex helper
is kept.

Co-Authored-By: Claude Code <noreply@anthropic.com>
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Oct 29, 2026
Upstream: Extra Usage Limit widget (sirmalloc#756), live widget preview while picking (sirmalloc#755), Fast Mode widget (sirmalloc#754), transcript block fallback fixes (sirmalloc#752), editor keys on a global install (sirmalloc#751), git cache TTL (sirmalloc#750), npx cached copy skip (sirmalloc#749), Block Usage hide at 100% (sirmalloc#747), Block Reset Timer hide until 100% (sirmalloc#697), fish-style branch abbreviation (sirmalloc#614), jj detached HEAD (sirmalloc#705), hooks save warning (sirmalloc#703), dev reload (sirmalloc#700), merge ends at a hidden widget (sirmalloc#698), Block Forecast and Block Limit Timer widgets (sirmalloc#699), contributor guide (sirmalloc#706), POSIX path case fix (sirmalloc#707), whitespace-distinct conflict filenames (sirmalloc#709), Quick Start reorder (sirmalloc#715), bunx launch guidance (sirmalloc#714), jj diff summary counts (sirmalloc#711), powerline manual separators (sirmalloc#713), keychain credential by account (sirmalloc#532), Keychain probe timeout (sirmalloc#760), usage cache scoped per CLAUDE_CONFIG_DIR (sirmalloc#266), test isolation (sirmalloc#717, sirmalloc#753), dev-dependency bumps (sirmalloc#745)

Conflicts resolved per the standard-resolution table; fork deletions re-applied.

Hand edits outside conflicts:
- src/utils/global-command-resolution.ts: fork-rename (ccstatusline@ -> ccstatusline-usage@ in upstream's new bunx pattern)
- src/utils/__tests__/global-package-manager.test.ts: fork-rename (package dir ccstatusline-usage in upstream's new tests)
- src/utils/usage-fetch.ts: compat fix - cacheUsageData gained a fetchedAtMs arg (upstream sirmalloc#266); adapted the fork-only fetchFreshUsageData call site
- src/utils/__tests__/usage-fetch.test.ts: compat fixes - cache/lock now hashed per CLAUDE_CONFIG_DIR (usageFilePath helper) in four fork tests; upstream fableUsage renamed to fork's weeklyFableUsage; seedUsageCache takes TokenHome
- src/utils/__tests__/usage-prefetch.test.ts: compat fix - mockFetchUsageData renamed to upstream's mockFetchUsage in cleanly-merged tests
- README.md: took upstream's Quick Start move (sirmalloc#715) and dropped the fork's old copy; fork-rename of its ccstatusline@ specifiers; links to fork-deleted docs (docs/USAGE.md, docs/WINDOWS.md) replaced with the fork's inline text
- CONTRIBUTING.md: links to the fork-deleted docs/DEVELOPMENT.md pointed at the README Development section
- docs/test-retirements.md: ledger entries for the 12 upstream test renames (sirmalloc#697, sirmalloc#698, sirmalloc#700, sirmalloc#705, sirmalloc#713, sirmalloc#760)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants