herdr phase 2: Live Pane tab (xterm.js → node's herdr TUI)
The killer UX feature: click a mission's Live Pane tab and watch the
actual Herdr TUI on the target node in the browser — cursor, colors,
tool output, all live. WebRTC DataChannel direct where the browser
can reach the node peer-to-peer, WS-relayed fallback otherwise
(same auto-negotiation the INFRA node terminal already uses).
Zero new deployment infra — reuses the existing terminal_ticket +
terminal_ws + PTY-over-control-channel machinery. The one primitive
we grew: PtyTarget::Command variant so the node can spawn an
arbitrary program (\`herdr\`) in the PTY instead of the login shell.
Node daemon (clawmates-node):
- PtyTarget grows a Command { argv } variant
- spawn_command_pty resolves bare names against user + system bin
dirs (matches how tool_update finds claude/kimi)
- PtyTarget::from_frame reads the `command` array from the pty_open
frame; precedence Command > Container > Host
cm-api:
- NodeHub::open_pty grows an optional command argv; when set, the
frame carries it and the daemon spawns the program directly.
- routes::nodes::TermCtrl gains a `command: Vec<String>`; the
fallback branch threads it through.
Frontend:
- core.ts::webrtcConnector takes an optional commandOverride
that ships inside the fallback frame
- nodeHerdrConnector(nodeId) — mints the standard ticket + WS URL
but overrides command to ["herdr"]
- MissionCanvas grows a "pane" tab, visible only when
runtime_kind='local_herdr'. LivePane subcomponent uses xterm.js
(already a workspace dep) via useResilientTerminal, shows a
connecting/relayed/direct pill in the corner.
To watch a mission live: pick "On a fleet node (Herdr)" + target
node in the wizard, launch, click Pane tab → node's Herdr TUI
appears. Navigate to the mission workspace in the Herdr sidebar
(mouse or prefix+w) to zoom into the mission's pane.
Focus-a-specific-pane-directly is a later enhancement — Herdr has
no CLI arg for it yet, so operator navigates the sidebar for now.
Verified: cargo check --workspace + tsc --noEmit both green.
This commit is contained in:
@@ -293,6 +293,10 @@ struct TermCtrl {
|
||||
candidate: Option<String>,
|
||||
sdp_mid: Option<String>,
|
||||
sdp_mline_index: Option<u16>,
|
||||
/// When present on `fallback`, spawns this argv in the PTY instead of
|
||||
/// the login shell (used by the Herdr Live Pane).
|
||||
#[serde(default)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
/// The terminal WS is BOTH the WebRTC signaling channel and the fallback data
|
||||
@@ -335,8 +339,15 @@ async fn bridge_terminal(hub: Arc<NodeHub>, node_id: NodeId, socket: WebSocket)
|
||||
let rows = c.rows.unwrap_or(24);
|
||||
match c.kind.as_str() {
|
||||
"resize" => hub.terminal_resize(node_id, sid, cols, rows).await,
|
||||
// Host shell (no container) — the Infra node terminal.
|
||||
"fallback" => hub.open_pty(node_id, sid, cols, rows, None, None).await,
|
||||
// Host shell by default; `command` override wins.
|
||||
"fallback" => {
|
||||
let cmd = if c.command.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(c.command.as_slice())
|
||||
};
|
||||
hub.open_pty(node_id, sid, cols, rows, None, None, cmd).await
|
||||
}
|
||||
"webrtc_offer" => {
|
||||
hub.webrtc_offer(
|
||||
node_id,
|
||||
|
||||
@@ -402,7 +402,7 @@ async fn bridge_node(
|
||||
match c.kind.as_str() {
|
||||
"resize" => hub.terminal_resize(node_id, sid, cols, rows).await,
|
||||
"fallback" => {
|
||||
hub.open_pty(node_id, sid, cols, rows, Some(&container), Some(&session))
|
||||
hub.open_pty(node_id, sid, cols, rows, Some(&container), Some(&session), None)
|
||||
.await
|
||||
}
|
||||
"webrtc_offer" => {
|
||||
|
||||
Reference in New Issue
Block a user