Fleet terminal: fix tmux nesting-refusal + frontend resize storm
Root cause (from the server trace: 69 banner bytes then immediate "to_browser
ended" = pty_exit): tmux spawned and exited instantly, writing its "sessions
should be nested with care" warning to stderr (not the PTY). The operator runs
the daemon inside their own tmux, so $TMUX was inherited and the spawned tmux
refused to nest → browser saw nothing.
- daemon (v0.2.2): spawn tmux on a DEDICATED socket (`tmux -L clawmates
new-session -A -s main`) and `env_remove("TMUX")`, so it can never collide with
or be refused by the operator's tmux.
- NodeTerminalApp: debounce the ResizeObserver (150ms). The pull-out animates
open, firing the observer on every pixel — previously ~80 resize frames per
open, each fit()+SIGWINCH. Now one resize after layout settles.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
27f5d05f96
commit
95bd022d07
Generated
+1
-1
@@ -737,7 +737,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clawmates-node"
|
name = "clawmates-node"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"cm-sandbox",
|
"cm-sandbox",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clawmates-node"
|
name = "clawmates-node"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -475,10 +475,16 @@ fn tailscale_up(authkey: &str) {
|
|||||||
fn terminal_command() -> CommandBuilder {
|
fn terminal_command() -> CommandBuilder {
|
||||||
let mut cmd = if has_tmux() {
|
let mut cmd = if has_tmux() {
|
||||||
let mut c = CommandBuilder::new("tmux");
|
let mut c = CommandBuilder::new("tmux");
|
||||||
|
// A DEDICATED server socket (-L) so we never collide with — or get refused
|
||||||
|
// by — the operator's own tmux. Without this, running the daemon inside a
|
||||||
|
// tmux makes the spawned tmux refuse to nest and exit instantly (writing
|
||||||
|
// its warning to stderr, not the PTY → the browser sees nothing).
|
||||||
|
c.arg("-L");
|
||||||
|
c.arg("clawmates");
|
||||||
c.arg("new-session");
|
c.arg("new-session");
|
||||||
c.arg("-A");
|
c.arg("-A");
|
||||||
c.arg("-s");
|
c.arg("-s");
|
||||||
c.arg("clawmates");
|
c.arg("main");
|
||||||
c
|
c
|
||||||
} else {
|
} else {
|
||||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_owned());
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_owned());
|
||||||
@@ -487,6 +493,8 @@ fn terminal_command() -> CommandBuilder {
|
|||||||
c
|
c
|
||||||
};
|
};
|
||||||
cmd.env("TERM", "xterm-256color");
|
cmd.env("TERM", "xterm-256color");
|
||||||
|
// Clear any inherited $TMUX so the new tmux doesn't think it's nested.
|
||||||
|
cmd.env_remove("TMUX");
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
if let Ok(home) = std::env::var("HOME") {
|
||||||
cmd.cwd(home);
|
cmd.cwd(home);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function NodeShell({ nodeId }: { nodeId: string }) {
|
|||||||
let term: import("@xterm/xterm").Terminal | null = null;
|
let term: import("@xterm/xterm").Terminal | null = null;
|
||||||
let ws: WebSocket | null = null;
|
let ws: WebSocket | null = null;
|
||||||
let ro: ResizeObserver | null = null;
|
let ro: ResizeObserver | null = null;
|
||||||
|
let resizeT: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const [{ Terminal }, { FitAddon }] = await Promise.all([
|
const [{ Terminal }, { FitAddon }] = await Promise.all([
|
||||||
@@ -83,12 +84,18 @@ function NodeShell({ nodeId }: { nodeId: string }) {
|
|||||||
term.onData((d) => {
|
term.onData((d) => {
|
||||||
if (ws?.readyState === WebSocket.OPEN) ws.send(new TextEncoder().encode(d));
|
if (ws?.readyState === WebSocket.OPEN) ws.send(new TextEncoder().encode(d));
|
||||||
});
|
});
|
||||||
ro = new ResizeObserver(() => sendResize());
|
// Debounce: the pull-out animates open, firing the observer on every pixel.
|
||||||
|
// Without this we'd spam dozens of resize frames (and SIGWINCH the PTY).
|
||||||
|
ro = new ResizeObserver(() => {
|
||||||
|
clearTimeout(resizeT);
|
||||||
|
resizeT = setTimeout(sendResize, 150);
|
||||||
|
});
|
||||||
ro.observe(hostRef.current);
|
ro.observe(hostRef.current);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
clearTimeout(resizeT);
|
||||||
ro?.disconnect();
|
ro?.disconnect();
|
||||||
ws?.close();
|
ws?.close();
|
||||||
term?.dispose();
|
term?.dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user