fix closed terminal feedback loop + add some addons
This commit is contained in:
@@ -17,6 +17,9 @@
|
|||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@xterm/addon-clipboard": "^0.2.0",
|
"@xterm/addon-clipboard": "^0.2.0",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-unicode-graphemes": "^0.4.0",
|
||||||
|
"@xterm/addon-unicode11": "^0.9.0",
|
||||||
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
Generated
+1370
-2753
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||||
|
import { UnicodeGraphemesAddon } from "@xterm/addon-unicode-graphemes";
|
||||||
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||||
import { Terminal as XTerm } from "@xterm/xterm";
|
import { Terminal as XTerm } from "@xterm/xterm";
|
||||||
import { Eraser, PlugZap, RotateCcw, Square, Terminal } from "lucide-react";
|
import { Eraser, PlugZap, RotateCcw, Square, Terminal } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -274,7 +277,12 @@ export function TerminalPage({
|
|||||||
scrollbarSliderHoverBackground: "#6b7c8dcc",
|
scrollbarSliderHoverBackground: "#6b7c8dcc",
|
||||||
scrollbarSliderActiveBackground: "#8294a6",
|
scrollbarSliderActiveBackground: "#8294a6",
|
||||||
},
|
},
|
||||||
|
allowProposedApi: true, // this for unicode11 addon. FOR SOME reason xtermjs doesnt tell me this in the readme.
|
||||||
});
|
});
|
||||||
|
terminal.loadAddon(new Unicode11Addon());
|
||||||
|
terminal.unicode.activeVersion = "11";
|
||||||
|
terminal.loadAddon(new UnicodeGraphemesAddon());
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
const fit = new FitAddon();
|
const fit = new FitAddon();
|
||||||
terminal.loadAddon(new ClipboardAddon());
|
terminal.loadAddon(new ClipboardAddon());
|
||||||
terminal.loadAddon(fit);
|
terminal.loadAddon(fit);
|
||||||
|
|||||||
@@ -545,6 +545,7 @@
|
|||||||
fitted canvas, so keep it on the same surface as the terminal theme. */
|
fitted canvas, so keep it on the same surface as the terminal theme. */
|
||||||
background-color: #0b1117;
|
background-color: #0b1117;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-surface .xterm-scrollable-element > .scrollbar,
|
.terminal-surface .xterm-scrollable-element > .scrollbar,
|
||||||
|
|||||||
+53
-14
@@ -56,7 +56,7 @@ impl TerminalState {
|
|||||||
let contents = screen.rows(0, cols).next().unwrap_or_default();
|
let contents = screen.rows(0, cols).next().unwrap_or_default();
|
||||||
physical_rows.push((
|
physical_rows.push((
|
||||||
screen.rows_formatted(0, cols).next().unwrap_or_default(),
|
screen.rows_formatted(0, cols).next().unwrap_or_default(),
|
||||||
UnicodeWidthStr::width(contents.as_str()).min(usize::from(cols)),
|
UnicodeWidthStr::width_cjk(contents.as_str()).min(usize::from(cols)),
|
||||||
screen.row_wrapped(0),
|
screen.row_wrapped(0),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ impl TerminalState {
|
|||||||
.rows_formatted(0, cols)
|
.rows_formatted(0, cols)
|
||||||
.nth(usize::from(row))
|
.nth(usize::from(row))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
UnicodeWidthStr::width(contents.as_str()).min(usize::from(cols)),
|
UnicodeWidthStr::width_cjk(contents.as_str()).min(usize::from(cols)),
|
||||||
screen.row_wrapped(row),
|
screen.row_wrapped(row),
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -173,16 +173,17 @@ impl TerminalManager {
|
|||||||
let active = Arc::downgrade(&self.active);
|
let active = Arc::downgrade(&self.active);
|
||||||
let events = self.events.clone();
|
let events = self.events.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(err) =
|
let result = run_terminal(&config, &terminal_id, rows, cols, cancel_rx, relay_rx).await;
|
||||||
run_terminal(&config, &terminal_id, rows, cols, cancel_rx, relay_rx).await
|
// Inventory is authoritative. Remove the stopped worker before
|
||||||
{
|
// notifying the control session, which may immediately resync it.
|
||||||
|
remove_completed(&active, terminal_id.as_str());
|
||||||
|
if let Err(err) = result {
|
||||||
warn!(terminal_id = %terminal_id, error = %err, "terminal worker failed");
|
warn!(terminal_id = %terminal_id, error = %err, "terminal worker failed");
|
||||||
let _ = events.send(TerminalManagerEvent {
|
let _ = events.send(TerminalManagerEvent {
|
||||||
terminal_id: terminal_id.clone(),
|
terminal_id: terminal_id.clone(),
|
||||||
error: err.to_string(),
|
error: err.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
remove_completed(&active, terminal_id.as_str());
|
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -196,20 +197,21 @@ impl TerminalManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resume(&self, terminal_id: &TerminalId, relay_token: String) -> Result<()> {
|
pub fn resume(&self, terminal_id: &TerminalId, relay_token: String) -> Result<()> {
|
||||||
let active = self.active.lock().expect("terminal manager poisoned");
|
let mut active = self.active.lock().expect("terminal manager poisoned");
|
||||||
let session = active
|
let session = active
|
||||||
.get(terminal_id.as_str())
|
.get(terminal_id.as_str())
|
||||||
.with_context(|| format!("terminal session {terminal_id} is not active"))?;
|
.with_context(|| format!("terminal session {terminal_id} is not active"))?;
|
||||||
session
|
if session.relay_credentials.send(relay_token).is_err() {
|
||||||
.relay_credentials
|
active.remove(terminal_id.as_str());
|
||||||
.send(relay_token)
|
anyhow::bail!("terminal session {terminal_id} has stopped");
|
||||||
.map_err(|_| anyhow::anyhow!("terminal session {terminal_id} has stopped"))
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sessions(&self) -> Vec<AgentTerminalSession> {
|
pub fn sessions(&self) -> Vec<AgentTerminalSession> {
|
||||||
self.active
|
let mut active = self.active.lock().expect("terminal manager poisoned");
|
||||||
.lock()
|
active.retain(|_, terminal| !terminal.relay_credentials.is_closed());
|
||||||
.expect("terminal manager poisoned")
|
active
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(terminal_id, active)| {
|
.filter_map(|(terminal_id, active)| {
|
||||||
TerminalId::new(terminal_id.clone())
|
TerminalId::new(terminal_id.clone())
|
||||||
@@ -605,6 +607,27 @@ fn validate_size(rows: u16, cols: u16) -> Result<()> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn manager_with_stopped_terminal(terminal_id: &str) -> TerminalManager {
|
||||||
|
let (cancel, cancel_rx) = oneshot::channel();
|
||||||
|
drop(cancel_rx);
|
||||||
|
let (relay_credentials, relay_rx) = mpsc::unbounded_channel();
|
||||||
|
drop(relay_rx);
|
||||||
|
let (events, event_rx) = mpsc::unbounded_channel();
|
||||||
|
drop(event_rx);
|
||||||
|
TerminalManager {
|
||||||
|
active: Arc::new(Mutex::new(HashMap::from([(
|
||||||
|
terminal_id.to_string(),
|
||||||
|
ActiveTerminal {
|
||||||
|
cancel,
|
||||||
|
relay_credentials,
|
||||||
|
created_at_unix: 42,
|
||||||
|
},
|
||||||
|
)]))),
|
||||||
|
max_sessions: 2,
|
||||||
|
events,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn terminal_url_uses_dedicated_agent_path() {
|
fn terminal_url_uses_dedicated_agent_path() {
|
||||||
let id = TerminalId::new("term-1").expect("terminal id");
|
let id = TerminalId::new("term-1").expect("terminal id");
|
||||||
@@ -615,6 +638,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_resume_removes_stopped_terminal_from_inventory() {
|
||||||
|
let manager = manager_with_stopped_terminal("stopped-terminal");
|
||||||
|
let terminal_id = TerminalId::new("stopped-terminal").expect("terminal id");
|
||||||
|
|
||||||
|
assert!(manager.resume(&terminal_id, "replacement".into()).is_err());
|
||||||
|
assert!(manager.sessions().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inventory_prunes_terminal_with_stopped_worker() {
|
||||||
|
let manager = manager_with_stopped_terminal("stopped-terminal");
|
||||||
|
|
||||||
|
assert!(manager.sessions().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn snapshot_reconstructs_screen_content_and_cursor() {
|
fn snapshot_reconstructs_screen_content_and_cursor() {
|
||||||
let mut state = TerminalState::new(24, 80);
|
let mut state = TerminalState::new(24, 80);
|
||||||
|
|||||||
Reference in New Issue
Block a user