Skip to content

feat: let a provider signal name the pane it came from, and keep the user's statusline - #7

Merged
whackur merged 9 commits into
code0xff:devfrom
whackur:dev
Jul 30, 2026
Merged

feat: let a provider signal name the pane it came from, and keep the user's statusline#7
whackur merged 9 commits into
code0xff:devfrom
whackur:dev

Conversation

@whackur

@whackur whackur commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Follows #5 (closed by #6). Two features plus the CI and test-isolation fixes found while verifying them.

Recovery reaches a pane you opened by hand

<leader> t opens a bare shell, so a claude started inside it had no launch command, no plugin association, and no recovery — which is the most common way these CLIs actually get started.

The pane token is already injected into every pane's environment (pty_spawn.rs) and inherited by the provider's own hook and statusline helpers. A signal carrying that token therefore proves which pane it came from, and that is now grounds to watch the pane:

  • new PluginCommand::WatchPane { v, token }, PROTOCOL_VERSION 1 → 2, carrying no generation (a plugin cannot honestly claim a spawn it was never told about)
  • new guard rule in src/plugin/guard_watch.rs: unknown token → switch off → watched by another → not alive. It charges no budget; re-asking for a pane already owned is approved
  • new per-[[plugin]] switch watch_on_signal, default false
  • plugin-side de-duplication (MAX_PENDING, REQUEST_COOLDOWN), and the signal that justified the request is held and replayed after PaneOpened, since the signal arrives before the pane and no history is replayed

The safety property from #5arbitrary shell panes must never be auto-detected and controlled by default — is kept, but by proof rather than by enumeration: a plain shell never produces a provider signal, so it is never watched.

A late-adopted pane can never be relaunched. It has no launch command, so a relaunch would restart the shell rather than the provider. New refusal reason, and an exited watched pane with no launch command takes the closing path instead of being held for PENDING_RELAUNCH_TTL. Live-pane resume is the only recovery such panes get.

Installing no longer costs you your statusline

Claude Code's statusLine is a single value, so installing took the user's over and printed the plugin's own line instead — the doc comment claiming otherwise was false.

The displaced command was already recorded for uninstall; it now runs as well, with the bytes Claude Code sent passed through untouched (re-serialising would change key order and number formatting, so the delegated command would not see what it was actually sent). Executed via sh -c because that is how Claude Code documents statusLine; 2s budget, stderr discarded, our own binary never re-executed. Every failure path degrades to the plugin's own line — a statusline that errors is worse than a plain one.

Worth knowing: merge_into returns Some(Value::Null) when there was no prior statusLine, so the sidecar can hold null; absence is not the only empty case.

Fixes found while verifying the above

  • snapshot worker never settled for a non-repository path. It kept changed set, so it took MIN_READ_INTERVAL and reported the same error once a second forever. The tree watch covers any readable directory, so a repository appearing later is reported rather than polled for.
  • Test fixtures shared one preferences file. session_state pointed every test at /nonexistent/nightcrow/viewer.json assuming it was unwritable. As root — the default in a container — create_dir_all makes it, so tests shared one accent on disk and the wrap test read a value that made its own write a no-op. This is why that test failed 4/5 locally while passing on CI, where the runner is not root.
  • CI ran one test binary. cargo test --locked without --workspace never ran the recovery plugin's tests at all, and clippy only saw the root package. Now both cover the workspace.
  • Actions moved off the deprecated Node 20 runtime (checkout/setup-node v4 → v7).

Verification

Run in an ubuntu:24.04 container matching the CI runner image (Image: ubuntu-24.04), with the exact CI commands:

cargo build --locked --workspace          Finished
cargo test  --locked --workspace          1326 passed; 0 failed   (root)
                                           269 passed; 0 failed   (nightcrow-recovery)
cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
                                          0 errors, 0 warnings
cargo fmt --all --check                   clean

The three previously failing runtime::snapshot tests went from 5/5 failing to 0/5 over five full-suite iterations at --cpus=2; the accent test now passes all five iterations inside a single container, which is the condition that exposed it.

0dedef7 was also verified on its own, so the history bisects: 1326 + 253 passing, clippy and fmt clean.

Not addressed

  • registry.rs::status still reports "opted-in panes" and will show 0 for a watch_on_signal plugin. Cosmetic.
  • A displaced statusLine's padding/refreshInterval are still lost at install time (pre-existing).
  • A user script whose path contains nightcrow-recovery is treated as ours and falls back. Safe direction, but silent.

whackur added 9 commits July 30, 2026 22:51
A path that is not a repository kept `changed` set, so the worker took the
fast interval and reported the same error once a second forever. The tree
watch is installed for any readable directory, so a repository appearing
later is reported rather than polled for; nothing needs the fast rate.
The fixture pointed every test at /nonexistent/nightcrow/viewer.json on
the assumption it is unwritable. Running as root — the default in a
container — create_dir_all makes it, so tests shared one accent on disk
and the wrap test read a value that made its own write a no-op. The wrap
test now reads the accent back instead of waiting for an announcement it
may not be owed.
checkout and setup-node v4 bundle Node 20, which the runners now force
onto Node 24. v7 targets it directly.
A pane opened by hand runs a bare shell, so a provider started inside it
was invisible to any plugin and got no recovery at all. The pane token is
injected into every pane's environment and inherited by the provider's
own helpers, so a signal carrying it proves where it came from: that is
now grounds to watch the pane, judged by the guard like any other
request. Off by default via [[plugin]] watch_on_signal — a plain shell
never produces a provider signal, so it is never watched, but the switch
stays the operator's. Such a pane can never be relaunched: it has no
launch command, so a relaunch would restart the shell, not the provider.
Claude Code's statusLine is a single value, so installing took the user's
over and printed our own line instead — the doc comment claiming otherwise
was simply wrong. The displaced command was already recorded for uninstall;
run it as well, with the bytes Claude Code sent us untouched so it sees
what it would have seen. Any failure falls back to our line: a statusline
that errors is worse than a plain one.
cargo test --locked ran one binary, so the recovery plugin's tests never
ran in CI at all; clippy only saw the root package.
…reads

The new no-repository test counted from the moment the watch came up, so
the read that start owes it was counted as the once-a-second cadence the
test denies. Failed on macOS only, where nothing else delayed that read.
Same fix the two absence tests next door already use.
@whackur
whackur merged commit c2db542 into code0xff:dev Jul 30, 2026
8 of 10 checks passed
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.

1 participant