fix(hh): instant sandbox render + working Ctrl-C for the owner
The owner's keystrokes and terminal output used to round-trip through the server, so output lagged (appeared only on the next keypress) and Ctrl-C got queued behind a flood of outgoing output frames (e.g. 'tree') — so it never interrupted and the command seemed to hang. - Owner writes drive keystrokes straight to the local PTY (instant; Ctrl-C is never starved). Remote drivers still relay via the server. - Owner renders its sandbox locally from the PTY and ignores its own echoed data/status frames (broker.is_none gate); others still render from echoes. - Coalesce PTY output bursts into one frame (no flood). - select! is biased on keyboard input; tick 120ms -> 50ms for snappier redraws. Verified live: echo renders with no extra key; sleep+Ctrl-C interrupts cleanly. 9 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1445b1151a
commit
d6595935d3
|
|
@ -349,7 +349,7 @@ pub async fn run(session: Session, theme: Theme) -> Result<()> {
|
||||||
|
|
||||||
let mut app = App::new(session.username.clone());
|
let mut app = App::new(session.username.clone());
|
||||||
let mut events = EventStream::new();
|
let mut events = EventStream::new();
|
||||||
let mut tick = tokio::time::interval(Duration::from_millis(120));
|
let mut tick = tokio::time::interval(Duration::from_millis(50));
|
||||||
|
|
||||||
let result = loop {
|
let result = loop {
|
||||||
if let Err(e) = term.draw(|f| ui::draw(f, &app, &theme)) {
|
if let Err(e) = term.draw(|f| ui::draw(f, &app, &theme)) {
|
||||||
|
|
@ -370,6 +370,7 @@ pub async fn run(session: Session, theme: Theme) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
biased; // keyboard first, so Ctrl-C / Esc are never starved by output floods
|
||||||
maybe = events.next() => {
|
maybe = events.next() => {
|
||||||
match maybe {
|
match maybe {
|
||||||
Some(Ok(Event::Key(k))) if k.kind == KeyEventKind::Press => {
|
Some(Ok(Event::Key(k))) if k.kind == KeyEventKind::Press => {
|
||||||
|
|
@ -387,7 +388,13 @@ pub async fn run(session: Session, theme: Theme) -> Result<()> {
|
||||||
if k.code == KeyCode::Esc {
|
if k.code == KeyCode::Esc {
|
||||||
app.driving = false;
|
app.driving = false;
|
||||||
} else if let Some(bytes) = key_to_pty(k.code, k.modifiers) {
|
} else if let Some(bytes) = key_to_pty(k.code, k.modifiers) {
|
||||||
send_frame(&out_tx, &session.room, json!({"_sbx":"input","b64": STANDARD.encode(&bytes)}));
|
if let Some(sb) = &mut broker {
|
||||||
|
// I own the sandbox: write straight to the PTY — instant,
|
||||||
|
// and Ctrl-C can't be queued behind outgoing output.
|
||||||
|
let _ = sb.write_input(&bytes);
|
||||||
|
} else {
|
||||||
|
send_frame(&out_tx, &session.room, json!({"_sbx":"input","b64": STANDARD.encode(&bytes)}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match k.code {
|
match k.code {
|
||||||
|
|
@ -418,6 +425,14 @@ pub async fn run(session: Session, theme: Theme) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Net::Ft(f)) => handle_ft(f, &mut app, &mut active_send, &out_tx, &session.room, &downloads),
|
Some(Net::Ft(f)) => handle_ft(f, &mut app, &mut active_send, &out_tx, &session.room, &downloads),
|
||||||
|
// The broker renders its sandbox locally from the PTY, so it
|
||||||
|
// ignores its own echoed status/data; everyone else uses them.
|
||||||
|
Some(Net::SbxData(b)) => {
|
||||||
|
if broker.is_none() {
|
||||||
|
if let Some(v) = &mut app.sandbox { v.parser.process(&b); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Net::SbxStatus { .. }) if broker.is_some() => {}
|
||||||
Some(n) => app.apply(n),
|
Some(n) => app.apply(n),
|
||||||
None => break Ok(()),
|
None => break Ok(()),
|
||||||
}
|
}
|
||||||
|
|
@ -429,6 +444,12 @@ pub async fn run(session: Session, theme: Theme) -> Result<()> {
|
||||||
broker_meta = Some((backend, name));
|
broker_meta = Some((backend, name));
|
||||||
announced_dims = Some((rows, cols));
|
announced_dims = Some((rows, cols));
|
||||||
launching = false;
|
launching = false;
|
||||||
|
// Local sandbox view — broker renders straight from the PTY.
|
||||||
|
app.sandbox = Some(SbxView {
|
||||||
|
parser: vt100::Parser::new(rows.max(1), cols.max(1), 0),
|
||||||
|
backend: backend.label().to_string(),
|
||||||
|
});
|
||||||
|
app.sys(format!("⛧ sandbox summoned ({}) — /drive to take the shell", backend.label()));
|
||||||
app.owner = Some(app.me.clone());
|
app.owner = Some(app.me.clone());
|
||||||
app.drivers.clear();
|
app.drivers.clear();
|
||||||
app.drivers.insert(app.me.clone());
|
app.drivers.insert(app.me.clone());
|
||||||
|
|
@ -444,7 +465,14 @@ pub async fn run(session: Session, theme: Theme) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pty = pty_rx.recv() => {
|
pty = pty_rx.recv() => {
|
||||||
if let Some(bytes) = pty {
|
if let Some(mut bytes) = pty {
|
||||||
|
// Coalesce a burst (e.g. `tree`) into one frame: fewer round-trips,
|
||||||
|
// no flood. Render locally now so the owner sees output instantly.
|
||||||
|
while let Ok(more) = pty_rx.try_recv() {
|
||||||
|
bytes.extend_from_slice(&more);
|
||||||
|
if bytes.len() > 256 * 1024 { break; }
|
||||||
|
}
|
||||||
|
if let Some(v) = &mut app.sandbox { v.parser.process(&bytes); }
|
||||||
send_frame(&out_tx, &session.room, json!({"_sbx":"data","b64": STANDARD.encode(&bytes)}));
|
send_frame(&out_tx, &session.room, json!({"_sbx":"data","b64": STANDARD.encode(&bytes)}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user