Skip to content

feat(tui): opt-in dev reload that restarts the TUI in place - #4

Closed
eric-engberg wants to merge 35 commits into
developfrom
feat/dev-reload
Closed

eric-engberg wants to merge 35 commits into
developfrom
feat/dev-reload

Conversation

@eric-engberg

@eric-engberg eric-engberg commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #5, which frees ctrl+r: before that fix, the line editor's plain r also fired on ctrl+r. The diff shows only the reload. For the upstream PR it rebases onto main together with, or after, the hotkey fix.

Part of stack sirmalloc#12: develop ← #5 hotkey fix ← #4 dev reload ← #1 picker preview ← #2 thinking effort ← #9 256-color grid ← #11 bar widget refactor ← #10 bar gradients.

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

CCSTATUSLINE_DEV_RELOAD=1 bun run start --config /tmp/ccstatusline-plain.json

The header shows dev reload: ctrl+r. Press ctrl+r to restart on the latest code exactly where you were. The --config file is optional; it just keeps each window's saved setup separate from your real config.

How it works

  • Supervisor. With the variable set, bun run start becomes a small supervisor. It relaunches the same command (runtime, script and original arguments) as a child process, and the child runs the TUI.
  • ctrl+r writes a snapshot to a file in a private (0700) temp directory that each supervisor run creates for itself, so a TUI never restores a snapshot left by an earlier run or planted by another user, and exits with code 75 (EX_TEMPFAIL). The supervisor then starts a fresh process, which resumes from the snapshot. The snapshot holds:
    • the unsaved settings
    • the original settings, so the unsaved-changes state and ctrl+s behave as before
    • the current screen, selected line, menu positions, and line editor cursor
  • Restoring screens. Screens that only need that data restore in place: main, the line selector and editor, colors, terminal options, global overrides, and powerline. Screens with flow state of their own (confirm dialogs, install and update, import and export) resume on the main menu. An open sub-editor isn't restored; you land on its widget in the line editor.
  • Validation. Settings in the snapshot are re-validated against the schema, because the reload may have changed it. A corrupt or invalid snapshot means a normal start.
  • Broken code. If the new code fails to start, which happens when you reload mid-edit, the supervisor prints Reload failed (exit N). Fix the code, then press Enter to retry, or q to quit. and keeps the snapshot.
  • Exit. A normal exit (ctrl+c or the Exit menu) ends the supervisor and removes the snapshot directory. If the supervisor itself is sent SIGTERM, SIGINT or SIGHUP (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.

ItemsEditor gains optional initialSelectedIndex and onSelectedIndexChange props, 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

  • Retrying after a crash no longer brings back settings from before a save. The supervisor hands every TUI it starts the same snapshot path, including the one it relaunches after "Reload failed". Until now only ctrl+r wrote that snapshot, and nothing ever removed it. So with ctrl+r, then a save, then a crash, the retry restored the pre-save settings along with the pre-save original. Deleted widgets came back, Save & Exit was offered for changes that didn't exist, and the next save wrote the older settings over the newer file. Now App deletes the snapshot through a new discardDevReloadSnapshot in src/utils/dev-reload.ts whenever originalSettings changes 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:

Before, Powerline

After, Powerline

Plain, before and after:

Before, Plain

After, Plain

Testing

  • Unit tests:
    • mode detection from the environment
    • the supervisor loop with a fake launcher: reload relaunches, a normal exit stops and cleans up, a crash prompts with retry or quit
    • snapshot round-trip, unrestorable and unknown screens going to main, and missing, corrupt or invalid snapshots giving a fresh start
  • An Ink test for the line editor's cursor props.
  • bun test: 2466 pass. bun run lint: clean.
  • Manual run in tmux, end to end:
    • an unsaved change on the line editor's first widget plus ctrl+r gives the same screen, cursor and change, and the main menu still offers "Save & Exit"
    • editing the Model widget's preview text plus ctrl+r makes the new text show up
    • a syntax error plus ctrl+r shows Bun's error and the retry prompt; fixing it and pressing Enter goes back in place
    • ctrl+c exits the supervisor with 0 and removes the snapshot
    • SIGTERM to the supervisor (Bun, and the built 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 behind
  • Tests for the private snapshot directory (a file planted at the old fixed path isn't restored; the directory is 0700, new per run, and removed) and for passing SIGTERM, SIGINT and SIGHUP on to the TUI
    • without the variable, there's no header note and ctrl+r does nothing

Update:

  • New test in 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's beforeEach now also points CLAUDE_CONFIG_DIR at 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, 9144 expect() calls across 180 files.
  • bun run lint: exit 0 (tsc --noEmit and ESLint with --max-warnings=0).
  • Node v26.10.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.
  • Manual run of the real supervisor in a PTY under Bun, with scratch HOME, CLAUDE_CONFIG_DIR and TMPDIR. Steps: ctrl+r, a save, then SIGKILL of the TUI, then Enter at the retry prompt. The TUI reopened on the saved file with no stale widget and no Save & Exit, and the file on disk was unchanged. With the same steps but no save, the TUI still resumed on Edit Line 1 with the unsaved edit.
  • Demo recorded with VHS from this branch and from the previous tip, in both Powerline and plain mode.

@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

wachen and others added 10 commits October 9, 2026 13:59
The Testing bullet said 6 test files / ~40 test cases (actual today: 127
files, ~1,470 cases) and the Available Widgets list names about 25 of the
85 registered widget types. Rather than updating the numbers and letting
them drift again, drop the hardcoded counts and point at the code:

- Testing: describe test layout instead of counting files
- Available Widgets: mark the list as highlights and point to
  src/utils/widget-manifest.ts for the full, current list


Claude-Session: https://claude.ai/code/session_019YuAUKeC6FjaSczoPWQkuP

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Matthew Breedlove <[email protected]>
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.
The registry's `version` went unchecked into the install commands the TUI
shows, and into the update it runs. For npm on Windows that runs npm.cmd
through cmd.exe, so a version such as `2.3.0 & calc` would have run a
second command. checkForUpdates now treats anything but a plain semver
version as a registry failure, and runGlobalPackageInstall refuses one.
eric-engberg and others added 4 commits October 9, 2026 14:33
Both parsers backtracked quadratically on input a repository controls:

- A `.git` file is read in every folder above the session's directory.
  `gitdir:\s*(.+)$` could split a run of whitespace between `\s*` and the
  path at every position, rescanning the line each time: a 100 KB file
  took about 5 seconds per render. The path now has to start at a
  non-space.
- A remote URL's path had its slashes trimmed with `/^\/+|\/+$/g`, which
  rescans a run of slashes from every position in it. The path is now
  split on `/` and its empty segments dropped, with the same result.
… line

The usage and status-page requests set `timeout`, which only covers an
idle socket. Behind HTTPS_PROXY the request has no socket until the proxy
answers CONNECT, so a proxy that accepts the connection and never answers
held the render forever. Destroying the request wouldn't end it either:
the proxy agent opens its own connection and keeps it until the proxy
answers, and that connection kept the process alive after printing.

- Both requests now have an overall deadline (the same 5 seconds). At the
  deadline the request is destroyed and the agent's connection to the
  proxy is closed through an AbortSignal (`createProxyAgent`).
- jj calls get git's 5 second timeout; they had none.
List new Custom Command shell commands in full before applying an import.
Require confirmation for Replace All and Merge, with Cancel selected by
default. Imports that add no new commands continue to apply immediately.

Escape control characters in preview values, paths, and commands so imported
text cannot hide commands or manipulate the terminal.
Summarize the latest security improvements, safer imports, proxy handling, cache fixes, and text rendering improvements.

Add a standalone v2.2.32 Recent Updates section, synchronize usage and development documentation, and bump the package version to 2.2.32.
@eric-engberg
eric-engberg removed this pull request from stack #12 October 9, 2026 19:12
@eric-engberg
eric-engberg changed the base branch from fix/hotkey-modifiers to develop October 9, 2026 19:12
@eric-engberg
eric-engberg added this pull request to stack #75 October 9, 2026 19:12
@eric-engberg
eric-engberg marked this pull request as ready for review October 9, 2026 19:19
dependabot Bot and others added 16 commits October 13, 2026 14:12
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@8207627...949feb2)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ith 6 updates (sirmalloc#733)

Bumps the dev-dependencies group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@remotion/cli](https://github.com/remotion-dev/remotion) | `4.0.532` | `4.0.534` |
| [eslint](https://github.com/eslint/eslint) | `10.11.0` | `10.12.0` |
| [globals](https://github.com/sindresorhus/globals) | `17.12.0` | `17.13.0` |
| [remotion](https://github.com/remotion-dev/remotion) | `4.0.532` | `4.0.534` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.70.1` | `8.71.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.0` | `5.0.3` |



Updates `@remotion/cli` from 4.0.532 to 4.0.534
- [Release notes](https://github.com/remotion-dev/remotion/releases)
- [Commits](remotion-dev/remotion@v4.0.532...v4.0.534)

Updates `eslint` from 10.11.0 to 10.12.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v10.11.0...v10.12.0)

Updates `globals` from 17.12.0 to 17.13.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](sindresorhus/globals@v17.12.0...v17.13.0)

Updates `remotion` from 4.0.532 to 4.0.534
- [Release notes](https://github.com/remotion-dev/remotion/releases)
- [Commits](remotion-dev/remotion@v4.0.532...v4.0.534)

Updates `typescript-eslint` from 8.70.1 to 8.71.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.1/packages/typescript-eslint)

Updates `vitest` from 5.0.0 to 5.0.3
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.3/packages/vitest)

---
updated-dependencies:
- dependency-name: "@remotion/cli"
  dependency-version: 4.0.534
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: globals
  dependency-version: 17.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: remotion
  dependency-version: 4.0.534
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.71.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 5.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…rmalloc#695)

Claude Code 2.1.251 added rate_limits.spend_limit to the status line payload for developers behind a Claude apps gateway. Parse it and show it with two widgets that read the payload only and never call the usage API.

Closes sirmalloc#572

Co-authored-by: Matthew Breedlove <[email protected]>
…#710)

* fix: draw the text cursor as a bar so trailing spaces show

The label editor quotes the label so a trailing space stays visible, but
the cursor was a highlighted block: over a character, or a highlighted
space after the text when at the end. So a label ending in a space with
the cursor on it ("Weekly:" + block) looked the same as a label with no
space and the cursor past the end ("7d" + block), and "7d" read as "7d ".

Draw the cursor as a bar between characters instead. It never covers a
character, so a trailing space shows as a gap before it, and the end of
the text adds nothing that looks like a space. Custom Text shares the
cursor, so it gets the bar too.

* feat(tui): preview label edits live and blink editor cursors

---------

Co-authored-by: Matthew Breedlove <[email protected]>
…el clash (sirmalloc#708)

* fix(usage): call Session Usage Block Usage, ending its "Session:" label clash

Three widgets printed the label "Session: " and meant two different
things by it. Session Clock and Session Name mean the Claude Code
conversation; Session Usage meant the 5-hour usage window. On one line,
"Session: 42% | Session: 1hr 5m" reads as two facts about the same
session when they aren't.

The 5-hour window already has a name here: Block Timer and Block Reset
Timer call it the block. Session Usage becomes Block Usage, labeled
"Block: ", with the description "Shows the current 5-hour block's usage
percentage". Its type stays session-usage, so saved configs keep
working, and anyone who prefers the old label can set "Session: " back
with the label editor (e). The docs follow; past release notes keep the
name they shipped with.

* fix(usage): label Block Usage "5h: " rather than "Block: "

"Block: " is already Block Timer's label, for the block's elapsed time
("Block: 3hr 45m"), so Block Usage beside it would print two "Block:"
figures. Label it "5h: " instead, which says which window the percent
is of and pairs with Weekly Usage's "Weekly: ". The name stays Block
Usage, and a "Session: " override still sets the old label back.

---------

Co-authored-by: Matthew Breedlove <[email protected]>
…es whole (sirmalloc#704)

* fix(ansi): strip 8-bit color codes as well as ESC [ ones

The ANSI parser reads the 8-bit CSI (0x9b) as an escape sequence, but
stripSgrCodes only matched color codes written with ESC [. Text colored
with \x9b31m kept its colors where they should go: a global foreground
override left a preserveColors widget's own colors in place, and a
Custom Command cut by its max width never got its closing reset, so its
color ran into the separators and widgets after it.

Match both introducers. restoreBackgroundAfterResets shares the pattern,
so an 8-bit reset now gets the background re-applied after it too; its
parameter parsing skips a one-character introducer for those.

* fix(sanitize): keep 8-bit color codes and drop other 8-bit sequences whole

The terminal sanitizer dropped 8-bit C1 controls one character at a time.
For a sequence started by one, that removed only the introducer and left
the rest as text: a command printing \x9b31mred\x9b39m showed "31mred39m",
and an 8-bit hyperlink or title string showed its URL or payload.

Read each 8-bit introducer as the ESC sequence it stands for: CSI (0x9b)
as ESC [, OSC (0x9d) as ESC ], and DCS, SOS, PM and APC likewise, with
ST (0x9c) ending strings. Color codes and safe hyperlinks are kept, in
their 7-bit form, which every terminal reads and the rest of the
renderer parses; any other sequence is dropped whole. Lone 8-bit
controls that start no sequence, such as NEL or a stray ST, are still
dropped.
…irmalloc#701)

* feat(claude-status): optionally link the widget to the status page

Claude Status shows "minor" or "major" when something's wrong, but
the details are on status.claude.com, which you have to look up.
Git Branch and Git Root Dir can already link to the repo or IDE.

Press `l` on Claude Status to make the whole widget an OSC 8 hyperlink
to https://status.claude.com, the site it reads the status from:
label, status and the history strip, and an unknown "?" too, when the
page is most worth a look. It's off by default, like the other links,
and the editor row shows "status link". Terminals without OSC 8
support show the text as before.

* fix(preview): preserve enabled widget hyperlinks

---------

Co-authored-by: Matthew Breedlove <[email protected]>
# 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.
…ith 9 updates (sirmalloc#745)

Bumps the dev-dependencies group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@remotion/cli](https://github.com/remotion-dev/remotion) | `4.0.534` | `4.0.538` |
| [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) | `1.4.2` | `1.4.4` |
| [eslint](https://github.com/eslint/eslint) | `10.12.0` | `10.13.0` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.3.0` | `19.3.1` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.3.0` | `19.3.1` |
| [remotion](https://github.com/remotion-dev/remotion) | `4.0.534` | `4.0.538` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.71.1` | `8.71.2` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.3` | `5.0.5` |
| [zod](https://github.com/colinhacks/zod) | `4.6.5` | `4.6.7` |



Updates `@remotion/cli` from 4.0.534 to 4.0.538
- [Release notes](https://github.com/remotion-dev/remotion/releases)
- [Commits](remotion-dev/remotion@v4.0.534...v4.0.538)

Updates `@types/bun` from 1.4.2 to 1.4.4
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

Updates `eslint` from 10.12.0 to 10.13.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v10.12.0...v10.13.0)

Updates `react` from 19.3.0 to 19.3.1
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.1/packages/react)

Updates `react-dom` from 19.3.0 to 19.3.1
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.1/packages/react-dom)

Updates `remotion` from 4.0.534 to 4.0.538
- [Release notes](https://github.com/remotion-dev/remotion/releases)
- [Commits](remotion-dev/remotion@v4.0.534...v4.0.538)

Updates `typescript-eslint` from 8.71.1 to 8.71.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.2/packages/typescript-eslint)

Updates `vitest` from 5.0.3 to 5.0.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.5/packages/vitest)

Updates `zod` from 4.6.5 to 4.6.7
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](colinhacks/zod@v4.6.5...v4.6.7)

---
updated-dependencies:
- dependency-name: "@remotion/cli"
  dependency-version: 4.0.538
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/bun"
  dependency-version: 1.4.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react
  dependency-version: 19.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-dom
  dependency-version: 19.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: remotion
  dependency-version: 4.0.538
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.71.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 5.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: zod
  dependency-version: 4.6.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…00% (sirmalloc#697)

* feat(usage): let Block Reset Timer hide until session usage hits 100%

* feat(usage): fetch the session percent for Block Reset Timer's under-limit state

* test(usage): compare Block Reset Timer's preview with the state off, not a fixed sample

* fix(usage): read Block Reset Timer's under-limit percent from the usage API

The under-limit hide state read the session percent from the status
payload's rate_limits whenever it had one. That percent comes from the
session's own last API response, so it stops moving while the session
sits idle: with several sessions open, one left idle while another used
up the 5-hour limit kept its timer hidden at the limit. After a reset it
could also keep showing a timer from the old window's 100%.

The state now fetches the usage API's percent even when the payload has
one, and keeps it apart from the merged data as apiSessionUsage, so
Session Usage and every other widget still show the payload's percent.
The API's answer is the account's, cached for 3 minutes and shared by
every session, so this costs at most one request per 3 minutes. When the
fetch fails, the payload's percent stands in, without an error.

* docs(usage): say "block" for the 5-hour window in the under-limit state

The under-limit state's checklist label read "when session usage is
under 100%", but ccstatusline also says "session" for the Claude Code
conversation (Session Clock, Session Cost), and the timers for the
5-hour window say "block". The label now reads "when the 5-hour block is
under 100%", and the docs, comments and test names say the block's
percent too. "Session" stays where it means a Claude Code session, such
as a session left idle. Code names that follow the usage data's
`sessionUsage` field keep it.

* docs(usage): refer to the usage percent without Session Usage's name

Another change renames Session Usage to Block Usage. Say "usage
percent" where the under-limit state's docs and two test comments named
it, so they read right with either name.

* fix(usage): ignore a cached block percent once its block has reset

The usage API's answer is cached for 3 minutes, and kept for as long as
the API keeps failing. Across a reset, under-limit took that answer's
100% for the new block's, so the reset countdown showed at the start of
a fresh block (at 40%, say) until a fetch succeeded. The API's percent
now counts only while its block hasn't reset; otherwise the payload's
percent stands in, as when the fetch fails.

* fix(usage): don't let a stale usage cache decide the under-limit state

While the usage API can't be reached (a 429 backoff, a failed request,
no credentials), fetchUsageData serves the last good cache however old
it is, and nothing tells it apart from a fresh answer: unlike a failed
fetch it carries no error. Block Reset Timer's under-limit state took
that cache's 5-hour percent as the API's current answer, so the
payload's percent never stood in. Through a backoff, which a Retry-After
can stretch to an hour, a cache written at 97% kept the reset countdown
hidden after the payload had reached 100% in the same block.

fetchUsageData now marks the stale fallback it returns, including when a
later call serves it again from memory. The prefetch takes the block's
API percent only from the API or a current cache, so a stale fallback
counts as the API being unreachable, like a failed fetch, and the
payload's percent decides, as the under-limit state's docs describe.

Only a cache that has outlived a refresh is marked: one older than the
3-minute cache lifetime plus the 30-second lock a fetch holds while in
flight. Another session's ordinary refresh serves the cache through
that lock too, seconds past its 3 minutes and as current as the shared
cache ever is, and marking it would let an idle session's frozen
payload percent override the API's for that render.

* fix(usage): keep rejected API percentages out of reset timer fallback

The under-limit hide state preferred apiSessionUsage and fell back to the
merged sessionUsage field. When the status payload omitted its percentage,
that merged field retained the API value even if prefetch rejected it as
stale or from a block that had already reset. An old cached 100% could
therefore show a new block's countdown from a reset-only payload.

Read the fallback percentage directly from the original status payload in
the render context. Continue preferring the API percentage accepted by
prefetch, and treat missing or null payload percentages as unknown so the
timer stays hidden. Keep general usage merging and other widgets unchanged.

Add prefetch-to-render regression coverage for expired and stale cached
100% readings with missing, null, zero, 97%, and 100% payload percentages,
plus an entirely absent payload. Verify that merged usage is preserved
while rejected API values cannot decide visibility. Update widget fixtures
to supply their fallback percentages through the original payload.

Validation:
- Both new regression cases reproduced the erroneous countdown before the fix.
- Targeted prefetch and Block Reset Timer tests: 96 passed.
- bun run lint: passed (TypeScript and ESLint).
- bun test: 3,191 passed; two local proxy listener tests failed because the
  sandbox blocked binding to 127.0.0.1. Both passed outside the sandbox
  (the status-service file: 30 passed; the usage proxy timeout test: 1 passed).

---------

Co-authored-by: Matthew Breedlove <[email protected]>
sirmalloc#698)

* fix(renderer): end a merge at a hidden widget that isn't merged onward

A merged widget joined the next widget that rendered anything, skipping over
widgets that rendered nothing. So with Session Usage merged into a widget that
hides itself, the hidden widget took the merge with it to whatever followed:
the separator vanished and, in Powerline, every later segment shifted one
theme color. A merge now carries past an empty widget only when that widget is
merged onward too; otherwise it ends there and the usual boundary returns. The
Powerline, plain and auto-align paths all follow the same rule.

* fix(renderer): end a merge at a hidden widget in the next-line counters too

The Powerline renderer now ends a merge at a widget that renders nothing
and isn't merged onward: A (merged), an empty unmerged widget, then B
draws a separator between A and B and gives B its own theme color. The
per-line counters that tell the next line where to pick up the separator
and theme-color cycles still followed the old rule and carried the merge
past every empty widget, so they counted one separator and one color
fewer than the line drew.

On a multi-line Powerline status line that put the next line one step
behind: its first separator repeated the previous glyph and, with
Continue Theme Across Lines on, its first segment reused the color of
the segment before it. Git Branch merged with Git Ahead/Behind hits this
whenever the branch is in sync. The TUI preview uses the same counters,
so it showed the same.

countSeparatorSlots and assignPowerlineThemeSlots now drop the pending
merge when they skip an empty widget that isn't merged onward, matching
the renderer. A merge still carries past empty widgets that are merged
onward themselves.

* refactor(renderer): count separator slots one widget at a time

countSeparatorSlots had grown a branch per kind of widget inside its
loop. Each widget's effect on the count now lives in
advanceSeparatorSlots, which keeps the loop to one call and puts the
rules for separators, flex separators, hidden widgets and shown widgets
side by side. No change in behavior.
@sonarqubecloud

Copy link
Copy Markdown

@eric-engberg
eric-engberg removed this pull request from stack #75 October 27, 2026 04:30
@eric-engberg

Copy link
Copy Markdown
Owner Author

Merged upstream as sirmalloc#700.

eric-engberg added a commit 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.
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.

4 participants