feat: let a provider signal name the pane it came from, and keep the user's statusline - #7
Merged
Conversation
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.
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.
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> topens a bare shell, so aclaudestarted 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:PluginCommand::WatchPane { v, token },PROTOCOL_VERSION1 → 2, carrying nogeneration(a plugin cannot honestly claim a spawn it was never told about)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[[plugin]]switchwatch_on_signal, default falseMAX_PENDING,REQUEST_COOLDOWN), and the signal that justified the request is held and replayed afterPaneOpened, since the signal arrives before the pane and no history is replayedThe safety property from #5 — arbitrary 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
statusLineis 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 -cbecause that is how Claude Code documentsstatusLine; 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_intoreturnsSome(Value::Null)when there was no priorstatusLine, so the sidecar can holdnull; absence is not the only empty case.Fixes found while verifying the above
snapshotworker never settled for a non-repository path. It keptchangedset, so it tookMIN_READ_INTERVALand 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.session_statepointed every test at/nonexistent/nightcrow/viewer.jsonassuming it was unwritable. As root — the default in a container —create_dir_allmakes 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.cargo test --lockedwithout--workspacenever ran the recovery plugin's tests at all, and clippy only saw the root package. Now both cover the workspace.checkout/setup-nodev4 → v7).Verification
Run in an
ubuntu:24.04container matching the CI runner image (Image: ubuntu-24.04), with the exact CI commands:The three previously failing
runtime::snapshottests 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.0dedef7was also verified on its own, so the history bisects: 1326 + 253 passing, clippy and fmt clean.Not addressed
registry.rs::statusstill reports "opted-in panes" and will show0for awatch_on_signalplugin. Cosmetic.statusLine'spadding/refreshIntervalare still lost at install time (pre-existing).nightcrow-recoveryis treated as ours and falls back. Safe direction, but silent.