Add MCP server (v1 read-only): toggle, per-pane gate, panel UI
This commit is contained in:
parent
6068522ee3
commit
83d8932c98
15 changed files with 1235 additions and 7 deletions
|
|
@ -2,7 +2,7 @@
|
|||
//! through portable-pty, reads its output on a background thread, and
|
||||
//! forwards chunks to the frontend as `pane://{id}/data` events.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
|
@ -52,6 +52,41 @@ pub enum SpawnSpec {
|
|||
/// the SSH prompt.
|
||||
type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;
|
||||
|
||||
/// Per-pane scrollback ring exposed to the MCP server. Capped — we drop the
|
||||
/// oldest bytes when full. `seq` is a monotonic byte counter that wraps at
|
||||
/// u64; the MCP `read_pane` tool uses it for incremental polling and the
|
||||
/// `wait_for_idle` tool uses it to detect silence.
|
||||
pub const PANE_RING_CAPACITY: usize = 256 * 1024;
|
||||
|
||||
pub struct PaneRing {
|
||||
buf: VecDeque<u8>,
|
||||
seq: u64,
|
||||
}
|
||||
|
||||
impl PaneRing {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
buf: VecDeque::with_capacity(PANE_RING_CAPACITY),
|
||||
seq: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, bytes: &[u8]) {
|
||||
for &b in bytes {
|
||||
if self.buf.len() == PANE_RING_CAPACITY {
|
||||
self.buf.pop_front();
|
||||
}
|
||||
self.buf.push_back(b);
|
||||
}
|
||||
self.seq = self.seq.wrapping_add(bytes.len() as u64);
|
||||
}
|
||||
|
||||
/// Snapshot: current contents (oldest-first) + the seq counter.
|
||||
pub fn snapshot(&self) -> (Vec<u8>, u64) {
|
||||
(self.buf.iter().copied().collect(), self.seq)
|
||||
}
|
||||
}
|
||||
|
||||
/// What we keep alive for each spawned PTY.
|
||||
///
|
||||
/// `master` stays in scope to keep the PTY alive; we never write through it
|
||||
|
|
@ -63,6 +98,9 @@ struct PaneHandle {
|
|||
writer: SharedWriter,
|
||||
#[allow(dead_code)]
|
||||
child: Box<dyn portable_pty::Child + Send + Sync>,
|
||||
/// Same Arc the reader thread appends into; the MCP server reads via
|
||||
/// {@link PtyManager::ring}.
|
||||
ring: Arc<Mutex<PaneRing>>,
|
||||
}
|
||||
|
||||
pub struct PtyManager {
|
||||
|
|
@ -127,6 +165,7 @@ impl PtyManager {
|
|||
.take_writer()
|
||||
.context("take_writer failed")?;
|
||||
let writer: SharedWriter = Arc::new(Mutex::new(writer_raw));
|
||||
let ring: Arc<Mutex<PaneRing>> = Arc::new(Mutex::new(PaneRing::new()));
|
||||
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
|
|
@ -136,14 +175,18 @@ impl PtyManager {
|
|||
master: pair.master,
|
||||
writer: writer.clone(),
|
||||
child,
|
||||
ring: ring.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Reader thread: pump bytes -> base64 -> emit. Also handles the
|
||||
// password-prompt autotype state machine if `saved_password` is set.
|
||||
// password-prompt autotype state machine if `saved_password` is set,
|
||||
// and pushes raw bytes into the per-pane scrollback ring for the
|
||||
// MCP server to read.
|
||||
let app_for_reader = app.clone();
|
||||
let event_name = format!("pane://{id}/data");
|
||||
let writer_for_reader = writer.clone();
|
||||
let ring_for_reader = ring.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut buf = [0u8; 8192];
|
||||
let mut pw_state = PasswordState::from(saved_password);
|
||||
|
|
@ -159,6 +202,9 @@ impl PtyManager {
|
|||
// on the renderer; pw_state mutates here.
|
||||
pw_state.observe(&buf[..n], &writer_for_reader, id);
|
||||
|
||||
// Mirror bytes into the scrollback ring (MCP source).
|
||||
ring_for_reader.lock().push(&buf[..n]);
|
||||
|
||||
let chunk_b64 = B64.encode(&buf[..n]);
|
||||
if let Err(e) =
|
||||
app_for_reader.emit(&event_name, DataChunk { b64: chunk_b64 })
|
||||
|
|
@ -217,6 +263,13 @@ impl PtyManager {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Borrow the per-pane scrollback ring. Returns None if the pane has
|
||||
/// been killed. The Arc lets callers hold the ring even after the
|
||||
/// PaneHandle is dropped (reader thread will stop pushing into it).
|
||||
pub fn ring(&self, id: PaneId) -> Option<Arc<Mutex<PaneRing>>> {
|
||||
self.panes.lock().get(&id).map(|p| p.ring.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue