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
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:
+15
-6
@@ -334,7 +334,7 @@ impl App {
|
||||
self.connected = true;
|
||||
self.chat_scroll = 0;
|
||||
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::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::Backspace => Some(vec![0x7f]),
|
||||
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::Down => Some(b"\x1b[B".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)
|
||||
&& matches!(k.code, KeyCode::Char('x'))
|
||||
&& !app.driving
|
||||
{
|
||||
// Panic kill switch (sandbox owner): revoke every
|
||||
// non-owner driver, interrupt whatever is running in
|
||||
// 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 {
|
||||
let owner = app.me.clone();
|
||||
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");
|
||||
}
|
||||
} else if app.driving {
|
||||
if k.code == KeyCode::Esc {
|
||||
app.driving = false;
|
||||
} else if k.code == KeyCode::PageUp {
|
||||
// Esc is NOT a release key here — vim & friends need it,
|
||||
// so it's forwarded to the PTY like any other key (see
|
||||
// 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
|
||||
// drive: PgUp/PgDn aren't forwarded to the PTY anyway.
|
||||
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");
|
||||
} else if app.can_drive() {
|
||||
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 {
|
||||
app.sys("you don't have drive permission — the owner can /grant you");
|
||||
}
|
||||
|
||||
+5
-1
@@ -199,7 +199,7 @@ fn help_clusters(theme: &Theme) -> Vec<HelpCluster> {
|
||||
"list local VirtualBox VMs · a VM's snapshots",
|
||||
),
|
||||
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 {
|
||||
@@ -273,6 +273,10 @@ fn help_clusters(theme: &Theme) -> Vec<HelpCluster> {
|
||||
kv("Enter", "send chat message"),
|
||||
kv("F1 · /help", "toggle this help"),
|
||||
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 (driving)",
|
||||
|
||||
Reference in New Issue
Block a user