diff --git a/src/runtime/moon.pkg b/src/runtime/moon.pkg index f5f10b6f..12318e91 100644 --- a/src/runtime/moon.pkg +++ b/src/runtime/moon.pkg @@ -1,5 +1,7 @@ import { "choir/src/sys", + "choir/src/exec", + "choir/src/workspace", "choir/src/types", "choir/src/config", "choir/src/phase", diff --git a/src/runtime/pane_watch.mbt b/src/runtime/pane_watch.mbt index d098051a..f8cdca95 100644 --- a/src/runtime/pane_watch.mbt +++ b/src/runtime/pane_watch.mbt @@ -4,12 +4,15 @@ pub(all) struct PaneWatchHost { } ///| -async fn native_dump_pane_screen( - _session : String, - _pane_id : String, -) -> String { - @async.sleep(0) - "" +async fn native_dump_pane_screen(session : String, pane_id : String) -> String { + let (status, output) = @exec.capture_command_stdout( + @workspace.zellij_dump_screen_command(session, pane_id), + ) + if status == 0 { + output + } else { + "" + } } ///| @@ -29,7 +32,7 @@ pub(all) struct WatchObservation { ///| pub(all) struct WatchState { session : String - pane_id : String + pane_id : String? mut observation : WatchObservation } @@ -61,11 +64,18 @@ pub fn PaneWatcher::watch_pane( agent_id : String, target : @types.LocalTarget, now : Int64, - pane_id? : String = "", + pane_id? : String? = None, ) -> Unit { - let resolved = pane_id.trim().to_owned() - if resolved == "" { - return + let resolved = match pane_id { + Some(value) => { + let trimmed = value.trim().to_owned() + if trimmed == "" { + None + } else { + Some(trimmed) + } + } + None => None } self.watches.set(agent_id, { session: target.session, @@ -74,6 +84,17 @@ pub fn PaneWatcher::watch_pane( }) } +///| +pub fn PaneWatcher::pane_id_for_agent( + self : PaneWatcher, + agent_id : String, +) -> String? { + match self.watches.get(agent_id) { + Some(watch) => watch.pane_id + None => None + } +} + ///| pub fn PaneWatcher::stop_subscribe( self : PaneWatcher, @@ -91,7 +112,11 @@ pub async fn PaneWatcher::current_screen( Some(watch) => watch None => return None } - let screen = (self.host.dump_pane_screen)(watch.session, watch.pane_id) + let pane_id = match watch.pane_id { + Some(value) => value + None => return None + } + let screen = (self.host.dump_pane_screen)(watch.session, pane_id) if screen.trim().to_owned() == "" { None } else { @@ -143,7 +168,11 @@ pub async fn PaneWatcher::observe_idle( Some(watch) => watch None => return None } - let screen = (self.host.dump_pane_screen)(watch.session, watch.pane_id) + let pane_id = match watch.pane_id { + Some(value) => value + None => return None + } + let screen = (self.host.dump_pane_screen)(watch.session, pane_id) if screen.trim().to_owned() == "" { return None } diff --git a/src/runtime/pane_watch_test.mbt b/src/runtime/pane_watch_test.mbt index ab6ab673..3a3234d5 100644 --- a/src/runtime/pane_watch_test.mbt +++ b/src/runtime/pane_watch_test.mbt @@ -46,7 +46,7 @@ async test "PaneWatcher::observe_idle dumps the resolved pane id through host sp pane_watch_dump_host(["worker ready"], dump_args), ) - pw.watch_pane("agent-x", pane_watch_target(), 0L, pane_id="terminal_4") + pw.watch_pane("agent-x", pane_watch_target(), 0L, pane_id=Some("terminal_4")) assert_true(pw.observe_idle("agent-x", 10L) is None) @debug.assert_eq(dump_args, ["sess", "terminal_4"]) @@ -62,7 +62,12 @@ async test "PaneWatcher::observe_idle reports unchanged dump-screen idle and res ), ) - pw.watch_pane("agent-idle", pane_watch_target(), 100L, pane_id="terminal_9") + pw.watch_pane( + "agent-idle", + pane_watch_target(), + 100L, + pane_id=Some("terminal_9"), + ) assert_true(pw.observe_idle("agent-idle", 110L) is None) match pw.observe_idle("agent-idle", 230L) { @@ -86,15 +91,16 @@ async test "PaneWatcher::observe_idle reports unchanged dump-screen idle and res } ///| -async test "PaneWatcher::watch_pane skips unresolved pane ids" { +async test "PaneWatcher::observe_idle skips unresolved pane ids" { let dump_args : Array[String] = [] let pw = PaneWatcher::with_host( pane_watch_dump_host(["should not be read"], dump_args), ) - pw.watch_pane("agent-missing", pane_watch_target(), 0L, pane_id="") + pw.watch_pane("agent-missing", pane_watch_target(), 0L, pane_id=None) - @debug.assert_eq(pw.watched_agents(), []) + @debug.assert_eq(pw.watched_agents(), ["agent-missing"]) + @debug.assert_eq(pw.pane_id_for_agent("agent-missing"), None) assert_true(pw.observe_idle("agent-missing", 999L) is None) @debug.assert_eq(dump_args, []) } diff --git a/src/server/handler_disconnect.mbt b/src/server/handler_disconnect.mbt index e9cba7d3..b82facce 100644 --- a/src/server/handler_disconnect.mbt +++ b/src/server/handler_disconnect.mbt @@ -1,21 +1,39 @@ ///| /// Register a pane for idle watchdog monitoring. -/// Reads the agent's persisted zellij pane id and records it for dump-screen -/// polling. Missing sidecars leave the agent unwatched. -pub fn ServerState::watch_pane(self : ServerState, agent_id : String) -> Unit { +/// Resolves the agent's live zellij pane id from the current list-panes +/// snapshot and records it for dump-screen polling. Missing tabs remain +/// tracked with no pane id so later poller ticks can retry resolution. +pub async fn ServerState::watch_pane( + self : ServerState, + agent_id : String, +) -> Unit { let agent = self.registry.get(agent_id) catch { _ => return } let target = match agent.terminal_target { Some(t) => t None => return } - let pane_id = (self.read_file)( - @workspace.agent_pane_id_file_path(self.config.project_dir, agent_id), - ) - .trim() - .to_owned() + let snapshot = (self.list_panes_snapshot_capture)(target.session) + let pane_id = match @tools.resolve_zellij_pane_id(target, snapshot) { + Some(value) => Some("terminal_" + value.to_string()) + None => None + } self.pane_watcher.watch_pane(agent_id, target, @poller.now_sec(), pane_id~) } +///| +async fn ServerState::ensure_watched_pane_resolved( + self : ServerState, + agent_id : String, +) -> Bool { + match self.pane_watcher.pane_id_for_agent(agent_id) { + Some(_) => true + None => { + self.watch_pane(agent_id) + self.pane_watcher.pane_id_for_agent(agent_id) is Some(_) + } + } +} + ///| /// Stop the zellij subscribe process for an agent and clean up state. fn ServerState::stop_subscribe(self : ServerState, agent_id : String) -> Unit { @@ -120,11 +138,6 @@ fn ServerState::clear_agent_runtime_tracking( agent_id : String, ) -> Unit { self.stop_subscribe(agent_id) - ignore( - (self.delete_file)( - @workspace.agent_pane_id_file_path(self.config.project_dir, agent_id), - ), - ) self.pending_disconnects.remove(agent_id) self.agent_pids.remove(agent_id) self.disconnect_contexts.remove(agent_id) @@ -1117,6 +1130,9 @@ pub async fn ServerState::check_idle_agents( Some(_) => () None => continue } + if !self.ensure_watched_pane_resolved(agent_id) { + continue + } // Read and parse the pane snapshot once for all idle decisions this tick. match self.pane_watcher.observe_idle(agent_id, now) { @@ -1188,7 +1204,6 @@ async test "check_idle_agents skips supervisor roles before reading panes" { "-" + @env.now().reinterpret_as_int64().to_string() ignore(@sys.create_dir_all(project_dir + "/.choir/kv")) - ignore(@sys.create_dir_all(project_dir + "/.choir/pane-ids")) let reads : Array[String] = [] let host : @runtime.PaneWatchHost = { dump_pane_screen: async fn(_session, _pane_id) { @@ -1210,6 +1225,11 @@ async test "check_idle_agents skips supervisor roles before reading panes" { @types.Role::TL => "main.tl" _ => "main.supervisor" } + let target : @types.LocalTarget = { + kind: @types.TargetKind::ZellijPane, + session: "s", + name: agent_id, + } ignore( state.register_agent({ agent_id: @types.AgentId(agent_id), @@ -1218,23 +1238,18 @@ async test "check_idle_agents skips supervisor roles before reading panes" { parent: None, workspace: project_dir, branch: "main", - terminal_target: Some({ - kind: @types.TargetKind::ZellijPane, - session: "s", - name: agent_id, - }), + terminal_target: Some(target), host: @types.Host::Local, spawn_depth: 0, state: @types.AgentState::Active, }), ) - ignore( - @sys.write_file_sync( - @workspace.agent_pane_id_file_path(project_dir, agent_id), - "terminal_" + agent_id.length().to_string(), - ), + state.pane_watcher.watch_pane( + agent_id, + target, + 0L, + pane_id=Some("terminal_" + agent_id.length().to_string()), ) - state.watch_pane(agent_id) } let calls : Array[@workspace.Command] = [] state.check_idle_agents(1000L, 0L, async fn(cmds) { diff --git a/src/server/handler_test.mbt b/src/server/handler_test.mbt index bfb4e7e3..7e5a9f31 100644 --- a/src/server/handler_test.mbt +++ b/src/server/handler_test.mbt @@ -7690,20 +7690,20 @@ async test "soft transport disconnect still escalates when idle watchdog expires }, } let state = ServerState::new( - { - ..@types.Config::default(), - project_dir, - terminal_session: "test-session", - }, - disconnect_confirmation_provider([], [pane_name]), - ).with_pane_watcher(@runtime.PaneWatcher::with_host(host)) - ignore(@sys.create_dir_all(project_dir + "/.choir/pane-ids")) - ignore( - @sys.write_file_sync( - @workspace.agent_pane_id_file_path(project_dir, agent_id), - "terminal_4", - ), - ) + { + ..@types.Config::default(), + project_dir, + terminal_session: "test-session", + }, + disconnect_confirmation_provider([], [pane_name]), + ) + .with_pane_watcher(@runtime.PaneWatcher::with_host(host)) + .with_list_panes_snapshot_capture(async fn(_session : String) { + @async.sleep(0) + @tools.parse_zellij_list_panes_json( + "[{\"id\":4,\"tab_name\":\"watchdog-leaf-pane\",\"pane_name\":\"watchdog-leaf-pane\"}]", + ) + }) ignore(state.register_agent(disconnect_confirmation_parent(parent_id))) ignore( state.register_agent({ @@ -8644,12 +8644,11 @@ fn worker_handoff_test_state_with_read_log( } base .with_pane_watcher(@runtime.PaneWatcher::with_host(host)) - .with_read_file(fn(path : String) { - if path.contains("/.choir/pane-ids/") { - "terminal_test" - } else { - @sys.read_file(path) - } + .with_list_panes_snapshot_capture(async fn(_session : String) { + @async.sleep(0) + @tools.parse_zellij_list_panes_json( + "[{\"id\":41,\"tab_name\":\"worker-pane\",\"pane_name\":\"worker-pane\"},{\"id\":42,\"tab_name\":\"leaf-pane\",\"pane_name\":\"leaf-pane\"}]", + ) }) } @@ -8678,12 +8677,11 @@ fn worker_handoff_test_state_with_snapshots( } base .with_pane_watcher(@runtime.PaneWatcher::with_host(host)) - .with_read_file(fn(path : String) { - if path.contains("/.choir/pane-ids/") { - "terminal_test" - } else { - @sys.read_file(path) - } + .with_list_panes_snapshot_capture(async fn(_session : String) { + @async.sleep(0) + @tools.parse_zellij_list_panes_json( + "[{\"id\":41,\"tab_name\":\"worker-pane\",\"pane_name\":\"worker-pane\"},{\"id\":42,\"tab_name\":\"leaf-pane\",\"pane_name\":\"leaf-pane\"}]", + ) }) } @@ -8743,6 +8741,139 @@ fn worker_handoff_dev( } } +///| +async test "ServerState::watch_pane resolves pane id from list-panes snapshot" { + let base = handler_auto_close_test_state("watch-pane-resolve") + let dumps : Array[String] = [] + let list_calls : Array[String] = [] + let host : @runtime.PaneWatchHost = { + dump_pane_screen: async fn(session, pane_id) { + @async.sleep(0) + dumps.push(session + ":" + pane_id) + "resolved screen" + }, + } + let state = base + .with_pane_watcher(@runtime.PaneWatcher::with_host(host)) + .with_list_panes_snapshot_capture(async fn(session : String) { + @async.sleep(0) + list_calls.push(session) + @tools.parse_zellij_list_panes_json( + "[{\"id\":77,\"tab_name\":\"worker-pane\",\"pane_name\":\"worker-pane\"}]", + ) + }) + let parent_id = "main.parent" + let worker_id = "main.resolve-worker" + ignore(state.register_agent(worker_handoff_parent(parent_id))) + ignore( + state.register_agent( + worker_handoff_worker(worker_id, parent_id, @types.AgentState::Active), + ), + ) + + state.watch_pane(worker_id) + let sent : Array[String] = [] + let start = @poller.now_sec() + state.check_idle_agents( + start, + 300L, + async fn(cmds) { worker_handoff_capture_runner(sent, cmds) }, + worker_no_handoff_idle_sec=120L, + ) + + @debug.assert_eq(list_calls, ["test-session"]) + @debug.assert_eq(dumps, ["test-session:terminal_77"]) + assert_eq( + worker_handoff_count_sent(sent, "[WORKER STALLED — no handoff]"), + 0, + ) + ignore(@sys.rm_rf(state.config.project_dir)) +} + +///| +async test "check_idle_agents retries unresolved pane id before observing idle" { + let base = handler_auto_close_test_state("watch-pane-lazy-retry") + let dumps : Array[String] = [] + let list_calls : Array[String] = [] + let snapshots = [ + "[]", "[]", "[{\"id\":88,\"tab_name\":\"worker-pane\",\"pane_name\":\"worker-pane\"}]", + ] + let mut snapshot_index = 0 + let host : @runtime.PaneWatchHost = { + dump_pane_screen: async fn(session, pane_id) { + @async.sleep(0) + dumps.push(session + ":" + pane_id) + "lazy retry screen" + }, + } + let state = base + .with_pane_watcher(@runtime.PaneWatcher::with_host(host)) + .with_list_panes_snapshot_capture(async fn(session : String) { + @async.sleep(0) + list_calls.push(session) + let idx = if snapshot_index < snapshots.length() { + snapshot_index + } else { + snapshots.length() - 1 + } + snapshot_index = snapshot_index + 1 + @tools.parse_zellij_list_panes_json(snapshots[idx]) + }) + let parent_id = "main.parent" + let worker_id = "main.lazy-worker" + ignore(state.register_agent(worker_handoff_parent(parent_id))) + ignore( + state.register_agent( + worker_handoff_worker(worker_id, parent_id, @types.AgentState::Active), + ), + ) + + state.watch_pane(worker_id) + let sent : Array[String] = [] + let start = @poller.now_sec() + state.check_idle_agents( + start + 121L, + 300L, + async fn(cmds) { worker_handoff_capture_runner(sent, cmds) }, + worker_no_handoff_idle_sec=120L, + ) + @debug.assert_eq(dumps, []) + assert_eq( + worker_handoff_count_sent(sent, "[WORKER STALLED — no handoff]"), + 0, + ) + + state.check_idle_agents( + start + 242L, + 300L, + async fn(cmds) { worker_handoff_capture_runner(sent, cmds) }, + worker_no_handoff_idle_sec=120L, + ) + @debug.assert_eq(dumps, ["test-session:terminal_88"]) + assert_eq( + worker_handoff_count_sent(sent, "[WORKER STALLED — no handoff]"), + 0, + ) + + state.check_idle_agents( + start + 363L, + 300L, + async fn(cmds) { worker_handoff_capture_runner(sent, cmds) }, + worker_no_handoff_idle_sec=120L, + ) + @debug.assert_eq(list_calls, ["test-session", "test-session", "test-session"]) + @debug.assert_eq(dumps, [ + "test-session:terminal_88", "test-session:terminal_88", + ]) + assert_eq( + worker_handoff_count_sent( + sent, "[WORKER STALLED — no handoff] lazy-worker", + ), + 1, + ) + ignore(@sys.rm_rf(state.config.project_dir)) +} + ///| async fn worker_handoff_capture_runner( sent : Array[String], @@ -10050,7 +10181,7 @@ async test "check_idle_agents reuses one pane dump for handoff and watchdog chec ) @debug.assert_eq(reads, [ - "test-session:terminal_test", "test-session:terminal_test", + "test-session:terminal_41", "test-session:terminal_41", ]) } diff --git a/src/server/state.mbt b/src/server/state.mbt index 52794ffa..f8565c02 100644 --- a/src/server/state.mbt +++ b/src/server/state.mbt @@ -8,6 +8,13 @@ fn default_read_file(path : String) -> String { @sys.read_file(path) } +///| +async fn default_list_panes_snapshot_capture( + session : String, +) -> @tools.ZellijListPanesSnapshot { + @tools.capture_zellij_list_panes_snapshot(session) +} + ///| fn default_write_file_atomic(path : String, content : String) -> Bool { @sys.write_file_atomic(path, content) @@ -313,6 +320,7 @@ pub struct ServerState { recent_poller_event_emissions : Map[String, Map[String, RecentEmission]] goal_judge : GoalJudgeState pane_watcher : @runtime.PaneWatcher + list_panes_snapshot_capture : async (String) -> @tools.ZellijListPanesSnapshot recovery_provider : @runtime.RecoveryProvider otlp_exporter : OtlpSpanExporter? append_file : (String, String) -> Bool @@ -438,6 +446,7 @@ pub fn ServerState::new( recent_poller_event_emissions: {}, goal_judge: GoalJudgeState::new(), pane_watcher: @runtime.PaneWatcher::new(), + list_panes_snapshot_capture: default_list_panes_snapshot_capture, recovery_provider, otlp_exporter: otlp_span_exporter_for_config( config, @@ -645,6 +654,14 @@ pub fn ServerState::with_pane_watcher( { ..self, pane_watcher, } } +///| +pub fn ServerState::with_list_panes_snapshot_capture( + self : ServerState, + list_panes_snapshot_capture : async (String) -> @tools.ZellijListPanesSnapshot, +) -> ServerState { + { ..self, list_panes_snapshot_capture, } +} + ///| pub fn ServerState::with_read_file( self : ServerState, diff --git a/src/tools/dispatch_helpers.mbt b/src/tools/dispatch_helpers.mbt index d8b0c8b4..a009369d 100644 --- a/src/tools/dispatch_helpers.mbt +++ b/src/tools/dispatch_helpers.mbt @@ -46,19 +46,9 @@ pub async fn run_strict( record_spawn_launch_diagnostic? : (SpawnLaunchDiagnostic) -> Unit = fn(_) { () }, - capture_pane_id? : Bool = false, - record_spawn_pane_id? : async (String, String) -> Unit = async fn(_, _) { - @async.sleep(0) - }, ) -> Result[Unit, @types.ChoirError] { let (statuses, spawn_launch_captured_outputs) = run_commands_with_spawn_launch_capture( - spawn_launch_bindings, - cmds, - runner, - capture, - record_spawn_launch_diagnostic, - capture_pane_id~, - record_spawn_pane_id~, + spawn_launch_bindings, cmds, runner, capture, record_spawn_launch_diagnostic, ) catch { e => return Err(@types.ChoirError::transport_error(e.to_string())) } diff --git a/src/tools/fork_wave.mbt b/src/tools/fork_wave.mbt index a2622456..d09a6368 100644 --- a/src/tools/fork_wave.mbt +++ b/src/tools/fork_wave.mbt @@ -690,10 +690,6 @@ pub async fn interpret_fork_wave_spawn_effect_phase( runner, capture, record_spawn_launch_diagnostic, - capture_pane_id=true, - record_spawn_pane_id=async fn(agent_id, pane_id) { - record_spawn_pane_id_sidecar(project_dir, runner, agent_id, pane_id) - }, ) for index, output in captured_outputs { spawn_launch_captured_outputs.set(index, output) diff --git a/src/tools/spawn.mbt b/src/tools/spawn.mbt index 8158dd47..f22999f7 100644 --- a/src/tools/spawn.mbt +++ b/src/tools/spawn.mbt @@ -588,10 +588,6 @@ pub async fn interpret_spawn_worker( capture~, spawn_launch_bindings~, record_spawn_launch_diagnostic~, - capture_pane_id=true, - record_spawn_pane_id=async fn(agent_id, pane_id) { - record_spawn_pane_id_sidecar(project_dir, runner, agent_id, pane_id) - }, ) { Ok(_) => { match registry { @@ -840,10 +836,6 @@ pub async fn interpret_spawn_gemini( capture~, spawn_launch_bindings~, record_spawn_launch_diagnostic~, - capture_pane_id=true, - record_spawn_pane_id=async fn(agent_id, pane_id) { - record_spawn_pane_id_sidecar(project_dir, runner, agent_id, pane_id) - }, ) { Ok(_) => match seed_warning { diff --git a/src/tools/spawn_diagnostics.mbt b/src/tools/spawn_diagnostics.mbt index c815d493..79fba472 100644 --- a/src/tools/spawn_diagnostics.mbt +++ b/src/tools/spawn_diagnostics.mbt @@ -19,30 +19,6 @@ pub(all) enum SpawnLaunchDiagnostic { SpawnCommandsSucceeded(agent_id~ : String, launch_count~ : Int) } derive(Debug, Eq) -///| -async fn noop_record_spawn_pane_id( - _agent_id : String, - _pane_id : String, -) -> Unit { - @async.sleep(0) -} - -///| -pub async fn record_spawn_pane_id_sidecar( - project_dir : String, - runner : async (Array[@workspace.Command]) -> Array[Int], - agent_id : String, - pane_id : String, -) -> Unit { - ignore( - runner([ - @workspace.write_agent_pane_id_command(project_dir, agent_id, pane_id), - ]) catch { - _ => [] - }, - ) -} - ///| fn spawn_diag_command_arg_value(args : Array[String], flag : String) -> String? { for i = 0; i + 1 < args.length(); i = i + 1 { @@ -289,8 +265,6 @@ pub async fn run_commands_with_spawn_launch_capture( runner : async (Array[@workspace.Command]) -> Array[Int], capture : async (@workspace.Command) -> (Int, String), record : (SpawnLaunchDiagnostic) -> Unit, - capture_pane_id? : Bool = false, - record_spawn_pane_id? : async (String, String) -> Unit = noop_record_spawn_pane_id, ) -> (Array[Int], Map[Int, String]) { if bindings.length() == 0 { return (runner(cmds), {}) @@ -318,46 +292,11 @@ pub async fn run_commands_with_spawn_launch_capture( return (statuses, captured_outputs) } } - let before_list_panes = match - ( - capture_pane_id, - spawn_diag_command_arg_value(cmds[i].args, "--session"), - ) { - (true, Some(session)) => { - let (before_status, before_output) = capture( - @workspace.zellij_list_panes_command(session), - ) - if before_status == 0 { - Some((session, before_output)) - } else { - None - } - } - _ => None - } let (status, output) = capture(cmds[i]) statuses.push(status) let redacted_output = spawn_launch_sanitized_stderr_tail(output) captured_outputs.set(i, redacted_output) if status == 0 { - match before_list_panes { - Some((session, before_output)) => { - let (after_status, after_output) = capture( - @workspace.zellij_list_panes_command(session), - ) - if after_status == 0 { - match - @workspace.pane_id_from_list_panes_diff( - before_output, after_output, - ) { - Some(pane_id) => - record_spawn_pane_id(binding.agent_id, pane_id) - None => () - } - } - } - None => () - } success_counts.set( binding.agent_id, success_counts.get(binding.agent_id).unwrap_or(0) + 1, diff --git a/src/workspace/launch.mbt b/src/workspace/launch.mbt index 9b938dad..4eac26d3 100644 --- a/src/workspace/launch.mbt +++ b/src/workspace/launch.mbt @@ -57,14 +57,6 @@ pub fn agent_pid_file_path(project_dir : String, agent_id : String) -> String { project_dir + "/.choir/pids/" + @types.sanitize_agent_id(agent_id) } -///| -pub fn agent_pane_id_file_path( - project_dir : String, - agent_id : String, -) -> String { - project_dir + "/.choir/pane-ids/" + @types.sanitize_agent_id(agent_id) -} - ///| /// Pure parse of an agent pidfile body (see [`agent_pid_file_path`]): the /// recorded setsid process-group id. Returns `None` for missing/empty, diff --git a/src/workspace/multiplexer.mbt b/src/workspace/multiplexer.mbt index d6f4c8d9..4d1acb9c 100644 --- a/src/workspace/multiplexer.mbt +++ b/src/workspace/multiplexer.mbt @@ -152,15 +152,6 @@ pub fn zellij_list_panes_json_command(session : String) -> Command { } } -///| -pub fn zellij_list_panes_command(session : String) -> Command { - { - program: "env", - args: ["NO_COLOR=1", "zellij", "--session", session, "action", "list-panes"], - workdir: None, - } -} - ///| pub fn zellij_dump_screen_command( session : String, @@ -175,76 +166,6 @@ pub fn zellij_dump_screen_command( } } -///| -pub fn write_agent_pane_id_command( - project_dir : String, - agent_id : String, - pane_id : String, -) -> Command { - { - program: "sh", - args: [ - "-c", - "mkdir -p \"$1\" && printf '%s' \"$2\" > \"$3\"", - "choir-write-pane-id", - project_dir + "/.choir/pane-ids", - pane_id, - agent_pane_id_file_path(project_dir, agent_id), - ], - workdir: None, - } -} - -///| -fn list_panes_whitespace_fields(line : String) -> Array[String] { - let fields : Array[String] = [] - let trimmed = line.trim().to_owned() - let mut start = 0 - let mut i = 0 - while i <= trimmed.length() { - if i == trimmed.length() || trimmed[i] == ' ' || trimmed[i] == '\t' { - if i > start { - fields.push(trimmed[start:i].to_owned()) - } - start = i + 1 - } - i = i + 1 - } - fields -} - -///| -fn terminal_pane_ids_from_list_panes(output : String) -> Map[String, Bool] { - let ids : Map[String, Bool] = {} - let text = strip_ansi_csi_sgr(output) - for raw in text.split("\n") { - let fields = list_panes_whitespace_fields(raw.to_owned()) - if fields.length() >= 2 && - fields[0].has_prefix("terminal_") && - fields[1] == "terminal" { - ids.set(fields[0], true) - } - } - ids -} - -///| -pub fn pane_id_from_list_panes_diff(before : String, after : String) -> String? { - let before_ids = terminal_pane_ids_from_list_panes(before) - let after_ids = terminal_pane_ids_from_list_panes(after) - let diff : Array[String] = [] - for id, _ in after_ids { - if !before_ids.contains(id) { - diff.push(id) - } - } - if diff.length() == 1 { - Some(diff[0]) - } else { - None - } -} - ///| /// Layout loaded into choir-managed zellij sessions. Opt-in: only loads when /// the layout file is present under the user's zellij config dir diff --git a/src/workspace/workspace_test.mbt b/src/workspace/workspace_test.mbt index e667c599..b188bb66 100644 --- a/src/workspace/workspace_test.mbt +++ b/src/workspace/workspace_test.mbt @@ -425,49 +425,6 @@ test "agent_pid_file_path sanitizes agent ids as flat filenames" { ) } -///| -test "agent_pane_id_file_path mirrors pid sidecar sanitization" { - assert_eq( - @workspace.agent_pane_id_file_path( - "/project", "../../evil\\agent\u0000pane", - ), - "/project/.choir/pane-ids/.._.._evil_agent_pane", - ) -} - -///| -test "pane_id_from_list_panes_diff returns single new terminal pane id" { - let before = "PANE_ID TYPE TITLE\nterminal_1 terminal root\nplugin_2 plugin status\n" - let after = before + "terminal_4 terminal worker-a\n" - - @debug.assert_eq( - @workspace.pane_id_from_list_panes_diff(before, after), - Some("terminal_4"), - ) -} - -///| -test "pane_id_from_list_panes_diff rejects zero ambiguous and non-terminal diffs" { - let before = "PANE_ID TYPE TITLE\nterminal_1 terminal root\n" - let ambiguous_after = before + - "terminal_2 terminal a\nterminal_3 terminal b\n" - let non_terminal_after = before + - "plugin_4 plugin status\npane_5 terminal malformed\n" - - @debug.assert_eq( - @workspace.pane_id_from_list_panes_diff(before, before), - None, - ) - @debug.assert_eq( - @workspace.pane_id_from_list_panes_diff(before, ambiguous_after), - None, - ) - @debug.assert_eq( - @workspace.pane_id_from_list_panes_diff(before, non_terminal_after), - None, - ) -} - ///| test "agy_stderr_log_path sanitizes agent ids as flat filenames" { assert_eq(