channels: add open/close RPCs for dynamic pane management

Add open/close to the channel capnp schema. The tmux daemon implements
open by finding a pane by name (pane title or window name) and
attaching pipe-pane; close detaches and removes from state.

Tool handlers channel_open and channel_close added to the tool
registry.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-04 18:25:20 -04:00 committed by Kent Overstreet
parent a14e85afe1
commit e8e9386856
3 changed files with 153 additions and 1 deletions

View file

@ -10,7 +10,7 @@ use super::Tool;
// ── Tool registry ──────────────────────────────────────────────
pub fn tools() -> [Tool; 4] {
pub fn tools() -> [Tool; 6] {
[
Tool { name: "channel_list",
description: "List all available channels and their status (connected, unread count).",
@ -28,6 +28,14 @@ pub fn tools() -> [Tool; 4] {
description: "Get pending channel notifications (unread signals). Does not consume messages — use channel_recv for that.",
parameters_json: r#"{"type":"object","properties":{}}"#,
handler: |_a, _v| Box::pin(async { channel_notifications().await }) },
Tool { name: "channel_open",
description: "Open a channel — start monitoring. For tmux: finds the pane by name and attaches pipe-pane.",
parameters_json: r#"{"type":"object","properties":{"label":{"type":"string","description":"Channel label / tmux pane name"}},"required":["label"]}"#,
handler: |_a, v| Box::pin(async move { channel_open(&v).await }) },
Tool { name: "channel_close",
description: "Close a channel — stop monitoring and clean up.",
parameters_json: r#"{"type":"object","properties":{"channel":{"type":"string","description":"Channel path (e.g. tmux.ktest)"}},"required":["channel"]}"#,
handler: |_a, v| Box::pin(async move { channel_close(&v).await }) },
]
}
@ -107,6 +115,35 @@ async fn channel_notifications() -> Result<String> {
}
}
async fn channel_open(args: &serde_json::Value) -> Result<String> {
let label = args.get("label").and_then(|v| v.as_str())
.context("label is required")?
.to_string();
let prefix = label.split('.').next().unwrap_or("tmux");
let sock = daemon_sock(prefix)?;
tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all().build().unwrap();
let local = tokio::task::LocalSet::new();
local.block_on(&rt, rpc_open(&sock, &label))
}).await?
.map_err(|e| anyhow::anyhow!("{}", e))
}
async fn channel_close(args: &serde_json::Value) -> Result<String> {
let channel = args.get("channel").and_then(|v| v.as_str())
.context("channel is required")?
.to_string();
let sock = daemon_sock(&channel)?;
tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all().build().unwrap();
let local = tokio::task::LocalSet::new();
local.block_on(&rt, rpc_close(&sock, &channel))
}).await?
.map_err(|e| anyhow::anyhow!("{}", e))
}
// ── Socket helpers ─────────────────────────────────────────────
fn channels_dir() -> std::path::PathBuf {
@ -195,6 +232,22 @@ async fn rpc_list(sock: &std::path::Path) -> Option<Vec<(String, bool, u32)>> {
Some(result)
}
async fn rpc_open(sock: &std::path::Path, label: &str) -> Result<String, String> {
let client = rpc_connect(sock).await?;
let mut req = client.open_request();
req.get().set_label(label);
req.send().promise.await.map_err(|e| format!("open failed: {e}"))?;
Ok(format!("opened channel tmux.{}", label))
}
async fn rpc_close(sock: &std::path::Path, channel: &str) -> Result<String, String> {
let client = rpc_connect(sock).await?;
let mut req = client.close_request();
req.get().set_channel(channel);
req.send().promise.await.map_err(|e| format!("close failed: {e}"))?;
Ok(format!("closed channel {}", channel))
}
// ── Fetch all channels ─────────────────────────────────────────
/// Fetch channel status from all daemon sockets.