Repository navigation
feat(tui): opt-in dev reload that restarts the TUI in place - #700
Merged
Merged
Conversation
Picking up a code change meant quitting the TUI and redoing any unsaved setup and navigation by hand. With CCSTATUSLINE_DEV_RELOAD=1 the interactive TUI runs under a small supervisor process. ctrl+r writes a snapshot (unsaved and original settings, current screen, selected line, menu positions, line editor cursor) and exits with code 75; the supervisor starts a fresh process on the latest code, which resumes from the snapshot. Screens that carry their own flow state (confirm dialogs, install/update, import/export) resume on the main menu. If the new code fails to start, the supervisor keeps the snapshot and offers Enter to retry once it's fixed, or q to quit. Without the variable nothing changes: no supervisor, no key, and the module is only loaded through a dynamic import in the interactive branch, so the status line render path is unaffected.
The dev reload cursor test checked the first frame and the cursor callback after fixed 25 ms delays, which a busy machine can outrun. It now retries the assertions until they pass (up to 3 s) with a waitFor helper. Once they pass, waitFor gives React two setImmediate turns: React draws a frame, then attaches the new input listener in a scheduled effect, so a key sent the moment the frame shows could reach no listener.
The App always passes the line selector its settings. The hotkey test now does too, so it matches real use and keeps compiling if they become required.
…resume - The retry prompt takes the terminal and the supervisor takes the function that runs each child, so both run in tests: Enter retries, q gives up, other keys wait, and the supervisor relaunches the same command with a snapshot path and removes the snapshot at the end. - A test renders the App as a reloaded child: it opens on the snapshot's line editor with the unsaved settings, and ctrl+r writes a new snapshot of where it is. Startup checks on Claude Code, package managers and fonts are stubbed.
The supervisor kept its snapshot at os.tmpdir()/ccstatusline-dev-reload- <pid>.json and removed it only when it returned normally. The TUI it starts restores whatever is at that path, so: - a snapshot left by a supervisor that was killed came back in a later run that got the same PID, as current unsaved settings, and ctrl+s then wrote it over settings.json; - on a shared /tmp another local user could plant the file, e.g. with a Custom Command widget, and have it restored the same way. Each run now creates its own directory with mkdtemp (new, mode 0700) and keeps the snapshot inside, so the first child can only find what this run wrote. The whole directory is removed when the supervisor ends.
The supervisor ran the TUI with spawnSync and handled no signals. SIGTERM, SIGINT or SIGHUP sent to the supervisor alone (e.g. kill <pid>) ended it but not the TUI, which kept running with PPID 1, out of the foreground; under Bun it later printed an EIO stack trace over the shell prompt. The snapshot was left behind too, also when closing the terminal ended both. The TUI now runs as an asynchronous child, so the supervisor can handle signals while it runs. On any of the three it passes the signal on, waits for the TUI to exit (so the shell gets the terminal back after the TUI has restored it), removes the snapshot directory and exits with 128 + the signal number. A TUI that ends this way is not a crash, so no retry prompt is shown. At the prompt there is no TUI to wait for, so the supervisor cleans up and exits straight away. The supervisor's process (prompt terminal, signals, exit) is a parameter defaulting to process, like the child launcher, so tests can drive it.
main now has the same helper in src/tui/__tests__/helpers/wait-for-ink.ts, so the file's own copy of waitFor and letReactCatchUp goes.
# Conflicts: # src/tui/App.tsx # src/tui/components/ItemsEditor.tsx
The supervisor hands every TUI it starts the same snapshot path, including the one it relaunches when Enter is pressed at "Reload failed". The snapshot was only ever written by ctrl+r and never refreshed or removed afterwards, so it outlived any save made after that ctrl+r. So after ctrl+r, a save, and then a crash, the retry restored the settings from before the save, paired with the original from before the save. The TUI showed widgets that had been deleted and saved, offered Save & Exit for changes that weren't there, and the next save wrote the older settings back over the newer file. Each change to originalSettings after the first load follows a write to the settings file (ctrl+s, Save & Exit, or an install, update or uninstall recording itself), so the TUI now deletes the snapshot when that happens. A retry after such a write loads the file. A crash before any write, the case the retry is for, still resumes from the snapshot in place.
This was referenced Oct 26, 2026
eric-engberg
added a commit
to eric-engberg/ccstatusline
that referenced
this pull request
Oct 27, 2026
#4 merged upstream as sirmalloc#700, with sirmalloc's guard and live menu selections, so this branch now builds on main rather than #4's branch. #4's ItemsEditor effect that reports the selected row came in twice, once from main and once from this branch, where the picker code moved it; one copy stays.
This was referenced Oct 27, 2026
axisrow
added a commit
to axisrow/ccstatusline
that referenced
this pull request
Oct 27, 2026
…it names, sirmalloc#717 claude-settings test isolation, sirmalloc#705 jj detached HEAD, sirmalloc#703 hooks save warning, sirmalloc#700 dev reload, sirmalloc#698 renderer merge ends at hidden widget, sirmalloc#697 block reset timer under-limit hide, sirmalloc#745 dev-deps bumps Conflict policy: upstream infra wins, fork features ported on top (fork-only src/daemon/, src/widgets/editors/*, parity fixtures and fork widgets kept). Notable joins: sirmalloc#703 returns SkippedWidgetHooks through the fork's saveSettingsTo wrapper; sirmalloc#697 keeps the fork's zero hide state alongside upstream's under-limit state; sirmalloc#698 replaces the fork's merge-glue palette semantics with upstream's merge-ends-at-unmerged-empty-widget rule; dev-reload (ctrl+r) composes with the fork's first-run preset picker. Co-Authored-By: Claude Code <[email protected]>
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)
This was referenced Oct 31, 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.
Builds on #669 (merged), which freed ctrl+r: before it, the line editor's plain
ralso fired on ctrl+r.Why
To pick up a code change while working on the TUI, you had to quit and then redo any unsaved setup and navigation by hand. That's slow when you keep separate windows for plain and powerline setups.
Usage
The header shows
dev reload: ctrl+r. Press ctrl+r to restart on the latest code exactly where you were. The--configfile is optional; it just keeps each window's saved setup separate from your real config.How it works
bun run startbecomes a small supervisor. It relaunches the same command (runtime, script and original arguments) as a child process, and the child runs the TUI.EX_TEMPFAIL). The supervisor then starts a fresh process, which resumes from the snapshot. The snapshot holds:Reload failed (exit N). Fix the code, then press Enter to retry, or q to quit.and keeps the snapshot.kill, or closing the terminal), it passes the signal on to the TUI, waits for it, removes the directory and exits with 128 + the signal, so no TUI is left running on its own.Without the variable nothing changes: no supervisor, no key, and no header note. The module is only loaded through a dynamic import in the interactive branch. The status-line render path loads the same code as before, about 0.7 KB more for the environment check in the entry.
ItemsEditorgains optionalinitialSelectedIndexandonSelectedIndexChangeprops, so the cursor survives a reload. Entering a line still starts at the top, as before.Update: Drop the snapshot once the settings file is written
discardDevReloadSnapshotinsrc/utils/dev-reload.tswheneveroriginalSettingschanges after the first load. After that load, it only changes once the settings file has been written: ctrl+s, Save & Exit, or an install, update or uninstall recording itself. One effect covers all of those writes without touching each save path, so the change stays inside the dev reload code. A retry after a write loads the file. A crash before any write, which is the case the retry exists for, still resumes from the snapshot on the same screen.Demo
Update
Dev reload (CCSTATUSLINE_DEV_RELOAD=1) on the demo's default four-widget line, with sample data only. The flow: delete Git Changes, ctrl+r, delete Git Branch, ctrl+s, then the TUI is killed to stand in for a crash and Enter at "Reload failed" retries. Before, the retry reopens on Edit Line 1 with Git Branch back, and the main menu offers Save & Exit for changes that were already saved. After, it opens the saved line (Model, Context Length) on the main menu with nothing unsaved.
Powerline, before and after:
Plain, before and after:
Testing
bun test: 3075 pass.bun run lint: clean.dist/under Node) exits with 143 and leaves no TUI process or snapshot directory; the same at the retry prompt; closing the tmux session (SIGHUP) leaves nothing behindmain: the built CLI's plain and Powerline renders matchmainunder Bun 1.4.2 and Node 26.10.0, and ctrl+r driven in tmux under both replaces the TUI process and resumes on the same line editor widget.Update:
src/tui/__tests__/App-dev-reload.test.ts: 'keeps the snapshot for a crash after the reload, but drops it once the settings are saved'. It starts from a snapshot, checks the snapshot survives the load, presses ctrl+s and checks it is gone. Without the fix it fails at that last step. With the fix deliberately changed to drop the snapshot on the first load, both this test's first assertion and the existing resume test fail. The file'sbeforeEachnow also pointsCLAUDE_CONFIG_DIRat the temp dir, because a save syncs widget hooks into Claude Code's settings.bun test(scratch HOME and CLAUDE_CONFIG_DIR): 3189 pass, 0 fail, 9144expect()calls across 180 files.bun run lint: exit 0 (tsc --noEmitand ESLint with--max-warnings=0).node node_modules/vitest/vitest.mjs run src/tui/__tests__/App-dev-reload.test.ts src/utils/__tests__/dev-reload.test.ts: 2 files, 20 tests passed.