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.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
@@ -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)",
|
||||||
|
|||||||
Reference in New Issue
Block a user