fix(ux): forward Esc and Ctrl-X to the sandbox shell while driving
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run

Two interactive shell apps were unusable in the shared sandbox because the
TUI swallowed their core keys:

- Esc released drive mode and was never mapped in key_to_pty, so vim could
  never leave insert mode. Esc now forwards 0x1b to the PTY; F2 (already a
  drive toggle) is the release key.
- Ctrl-X (owner kill switch) was intercepted globally, so nano could never
  quit. It's now gated on !app.driving — while you hold the shell Ctrl-X
  reaches the PTY (0x18, nano's quit); release with F2 to arm the kill
  switch.

Updated the on-screen hints and /help KEYS cluster to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-06 22:53:45 -07:00
parent b8a06077a4
commit 764c827d07
2 changed files with 20 additions and 7 deletions
+15 -6
View File
@@ -334,7 +334,7 @@ impl App {
self.connected = true; self.connected = true;
self.chat_scroll = 0; self.chat_scroll = 0;
self.sys(format!("joined as {}", self.me)); self.sys(format!("joined as {}", self.me));
self.sys("/sbx launch <docker|multipass|vbox> · /drive (Esc releases) · /ai start · /ai <question> · /send <user> <file> · /sendroom <file> · /pw show password · PgUp/PgDn scroll chat · ctrl-q quit"); self.sys("/sbx launch <docker|multipass|vbox> · /drive (F2 releases) · /ai start · /ai <question> · /send <user> <file> · /sendroom <file> · /pw show password · PgUp/PgDn scroll chat · ctrl-q quit");
} }
Net::Message(l) => self.push_line(l), Net::Message(l) => self.push_line(l),
Net::Roster { users, capacity } => { Net::Roster { users, capacity } => {
@@ -495,6 +495,9 @@ fn key_to_pty(code: KeyCode, mods: KeyModifiers) -> Option<Vec<u8>> {
KeyCode::Enter => Some(vec![b'\r']), KeyCode::Enter => Some(vec![b'\r']),
KeyCode::Backspace => Some(vec![0x7f]), KeyCode::Backspace => Some(vec![0x7f]),
KeyCode::Tab => Some(vec![b'\t']), KeyCode::Tab => Some(vec![b'\t']),
// Esc must reach the shell (vim/less/etc. depend on it). Drive is released
// with F2, not Esc, so this never collides with leaving the shell.
KeyCode::Esc => Some(vec![0x1b]),
KeyCode::Up => Some(b"\x1b[A".to_vec()), KeyCode::Up => Some(b"\x1b[A".to_vec()),
KeyCode::Down => Some(b"\x1b[B".to_vec()), KeyCode::Down => Some(b"\x1b[B".to_vec()),
KeyCode::Right => Some(b"\x1b[C".to_vec()), KeyCode::Right => Some(b"\x1b[C".to_vec()),
@@ -892,11 +895,15 @@ pub async fn run(params: net::ConnParams, mut session: Session, mut theme: Theme
} }
if k.modifiers.contains(KeyModifiers::CONTROL) if k.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(k.code, KeyCode::Char('x')) && matches!(k.code, KeyCode::Char('x'))
&& !app.driving
{ {
// Panic kill switch (sandbox owner): revoke every // Panic kill switch (sandbox owner): revoke every
// non-owner driver, interrupt whatever is running in // non-owner driver, interrupt whatever is running in
// the PTY, and re-broadcast the locked-down ACL. Cuts a // the PTY, and re-broadcast the locked-down ACL. Cuts a
// runaway agent (or human) off mid-command. // runaway agent (or human) off mid-command. Gated on
// `!app.driving` so that while YOU hold the shell, Ctrl-X
// reaches the PTY instead (nano's quit; key_to_pty sends
// 0x18) — release with F2 first to arm the kill switch.
if let Some(sb) = &mut broker { if let Some(sb) = &mut broker {
let owner = app.me.clone(); let owner = app.me.clone();
app.drivers.retain(|u| *u == owner); app.drivers.retain(|u| *u == owner);
@@ -1037,9 +1044,11 @@ pub async fn run(params: net::ConnParams, mut session: Session, mut theme: Theme
app.sys("you don't have drive permission — the owner can /grant you"); app.sys("you don't have drive permission — the owner can /grant you");
} }
} else if app.driving { } else if app.driving {
if k.code == KeyCode::Esc { // Esc is NOT a release key here — vim & friends need it,
app.driving = false; // so it's forwarded to the PTY like any other key (see
} else if k.code == KeyCode::PageUp { // key_to_pty). Press F2 (handled above) to release the
// shell back to chat.
if k.code == KeyCode::PageUp {
// Scroll the shared shell's scrollback without releasing the // Scroll the shared shell's scrollback without releasing the
// drive: PgUp/PgDn aren't forwarded to the PTY anyway. // drive: PgUp/PgDn aren't forwarded to the PTY anyway.
app.sbx_scroll = (app.sbx_scroll + sbx_page(&app)).min(2000); app.sbx_scroll = (app.sbx_scroll + sbx_page(&app)).min(2000);
@@ -1513,7 +1522,7 @@ fn handle_command(
app.sys("no sandbox running — /sbx launch first"); app.sys("no sandbox running — /sbx launch first");
} else if app.can_drive() { } else if app.can_drive() {
app.driving = true; app.driving = true;
app.sys("⛧ drive mode ON — type into the shell · press Esc to release"); app.sys("⛧ drive mode ON — type into the shell (Esc reaches vim etc.) · press F2 to release");
} else { } else {
app.sys("you don't have drive permission — the owner can /grant you"); app.sys("you don't have drive permission — the owner can /grant you");
} }
+5 -1
View File
@@ -199,7 +199,7 @@ fn help_clusters(theme: &Theme) -> Vec<HelpCluster> {
"list local VirtualBox VMs · a VM's snapshots", "list local VirtualBox VMs · a VM's snapshots",
), ),
kv("/sbx gui <vm> [yes]", "alias of /sbx launch vbox gui <vm> [yes]"), kv("/sbx gui <vm> [yes]", "alias of /sbx launch vbox gui <vm> [yes]"),
kv("/drive · F2", "type into the shared shell (Esc releases)"), kv("/drive · F2", "type into the shared shell (F2 releases; Esc reaches vim)"),
], ],
}, },
HelpCluster { HelpCluster {
@@ -273,6 +273,10 @@ fn help_clusters(theme: &Theme) -> Vec<HelpCluster> {
kv("Enter", "send chat message"), kv("Enter", "send chat message"),
kv("F1 · /help", "toggle this help"), kv("F1 · /help", "toggle this help"),
kv("Ctrl-C (while driving)", "interrupt the running command"), kv("Ctrl-C (while driving)", "interrupt the running command"),
kv(
"Ctrl-X (owner, not driving)",
"kill switch — revoke all drive + interrupt the shell (while driving it reaches the shell, e.g. nano)",
),
kv("PgUp / PgDn", "scroll chat · Home/End = oldest/live"), kv("PgUp / PgDn", "scroll chat · Home/End = oldest/live"),
kv( kv(
"PgUp / PgDn (driving)", "PgUp / PgDn (driving)",