Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]

* fix(server): a starting server no longer unlinks and re-binds a session socket that a live server still owns — it probes the path first and refuses to start when someone is listening, even if that server is too busy to answer a health probe; only missing, stale or non-socket paths are cleaned up. A server already running a session likewise rejects a second new-session request instead of re-initializing over live state
* fix(chrome): the chrome a client had visible is parked when that client detaches, so a server with nobody attached stops refreshing the cross-session list once a second; attaching re-activates it and rebuilds the list on the spot
* perf(plugins): `get_session_list` no longer feeds its result back into Screen — a plugin-initiated read used to trigger a `SessionUpdate` broadcast to every plugin (including the caller), a self-sustaining loop whose cost grew with the square of the live session count
* feat(input): `Cmd+K` / `Super+k` opens the existing `❯_ Quick cmd` mini-console in every mode including LOCK; the keybind messages the active compact-bar so keyboard and click share one runner, geometry and pane-title contract, with matching Alacritty CSI-u translation and help

## [0.47.3] - 2026-08-06
Expand Down
25 changes: 16 additions & 9 deletions default-plugins/session-manager/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1857,16 +1857,11 @@ impl State {
}
}
fn handle_session_rail_key(&mut self, key: KeyWithModifier) -> bool {
// Bare arrows are deliberately NOT handled here. Session switching by
// arrow lives only in the ^T tab-mode keybinds (vc_rail_nav pipe) and
// the always-on Super chords; a focused rail consuming raw arrows made
// LOCK mode switch sessions, since LOCK routes keys to the focused pane.
match key.bare_key {
BareKey::Down if key.has_no_modifiers() => {
// Operator contract: arrow = immediate switch, no Enter confirm.
self.switch_session_relative(1);
true
},
BareKey::Up if key.has_no_modifiers() => {
self.switch_session_relative(-1);
true
},
BareKey::Enter if key.has_no_modifiers() => {
self.handle_session_rail_selection();
true
Expand Down Expand Up @@ -3287,6 +3282,18 @@ mod rail_tests {
}
}

/// Product key-contract v3: bare arrows switch sessions ONLY through the
/// ^T tab-mode keybinds (vc_rail_nav pipe). A focused rail pane must not
/// consume them — LOCK routes raw keys to the focused pane, so a rail
/// arrow handler becomes a hidden mode-proof session switcher.
#[test]
fn rail_ignores_bare_arrow_keys() {
let mut state = State::default();
state.sessions.session_ui_infos = vec![session("solo", true)];
assert!(!state.handle_session_rail_key(KeyWithModifier::new(BareKey::Up)));
assert!(!state.handle_session_rail_key(KeyWithModifier::new(BareKey::Down)));
}

fn session_launched_at(name: &str, is_current_session: bool, secs: u64) -> SessionUiInfo {
SessionUiInfo {
creation_time: Duration::from_secs(secs),
Expand Down
28 changes: 27 additions & 1 deletion docs/VC_FRAME_OPERATOR_SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ once from the session snapshot it already owns and sends a small scalar message
only to the status-bar plugin/client pairs viewing active tabs. When a client
switches tabs, the server sends an exact plugin/client deactivation signal to
the status bar it left; sampling does not rely on the tab-global `Visible`
event, which cannot distinguish multiple clients in one session. Per-tab
event, which cannot distinguish multiple clients in one session. A client that
detaches is covered by the same transition: the chrome it had visible is parked
as the client leaves, so a server with nobody attached holds no chrome that
keeps polling for cross-session state. Attaching re-activates that chrome
through the ordinary active-target path, and the session list it shows is
rebuilt on the spot. Per-tab
status bars never subscribe to the full cross-session `SessionUpdate`, and
unrelated `CustomMessage` consumers are not awakened. Host resource sampling
also runs only in active status-bar instances, and clipboard timers cannot
Expand All @@ -72,6 +77,27 @@ resolve to that built-in plugin. An unrelated `file:` or remote plugin is not
treated as the built-in status bar merely because its filename looks similar;
custom replacements must implement and wire their own sampling lifecycle.

## Session Socket Ownership

A session name maps to one socket file, and exactly one live server may own it.
A starting server probes that path before binding: if a process is listening
there — even one too busy to answer a health probe — the newcomer refuses to
start and says so in the log instead of unlinking the file and binding over it.
Only a path that nothing is listening on (missing, stale after a crash, or not
a socket at all) is cleaned up and re-bound.

The rule exists because stealing a socket does not stop the previous server: it
keeps running, unreachable, with zero clients, and nothing ever reaps it. A
server that already runs a session also rejects a second new-session request
rather than re-initializing over live state; the caller sees the refusal and
can attach to the existing session instead.

The probe is a Unix-only mechanism, because only there is the session path a
socket that a second server can unlink and rebind. Off Unix the path is a
marker file and the listener is a named pipe whose name the OS refuses to hand
out twice, so the bind itself already decides ownership — the marker is written
only after it succeeds.

## Key Contract

The shipped defaults promise one navigation language — one modifier per
Expand Down
Loading
Loading