chore: merge develop into work/companion
# Conflicts: # crates/tui/src/app.rs
This commit is contained in:
+125
-1
@@ -1,4 +1,4 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -292,6 +292,8 @@ pub struct App {
|
||||
/// Selected Internal Worker transcript/task view. `None` is the parent (`main`)
|
||||
/// view; the stable session identity survives projection reordering.
|
||||
selected_internal_worker_session_id: Option<String>,
|
||||
/// Terminal child-session fences, reset only by an authoritative snapshot.
|
||||
removed_internal_workers: HashMap<String, u64>,
|
||||
pub scroll: Scroll,
|
||||
pub mode: Mode,
|
||||
pub cache: FileCache,
|
||||
@@ -371,6 +373,7 @@ impl App {
|
||||
run_error_messages: Vec::new(),
|
||||
internal_workers: Vec::new(),
|
||||
selected_internal_worker_session_id: None,
|
||||
removed_internal_workers: HashMap::new(),
|
||||
scroll: Scroll::default(),
|
||||
mode: Mode::Normal,
|
||||
cache: FileCache::new(),
|
||||
@@ -1410,10 +1413,16 @@ impl App {
|
||||
revision,
|
||||
event,
|
||||
} => self.apply_internal_worker_event(worker, revision, *event),
|
||||
Event::InternalWorkerRemoved { worker, revision } => {
|
||||
self.remove_internal_worker(worker, revision)
|
||||
}
|
||||
Event::Status { status } => {
|
||||
self.rewind_refresh_fence = false;
|
||||
self.set_worker_status(status);
|
||||
}
|
||||
// Command telemetry is an operational Web Console surface. The
|
||||
// TUI continues to render the final Bash ToolResult from history.
|
||||
Event::Command { .. } => {}
|
||||
Event::Completions { kind, entries } => {
|
||||
// Apply only if the popup is still on the same
|
||||
// (kind, prefix) the request was issued for; an
|
||||
@@ -2112,6 +2121,7 @@ impl App {
|
||||
if self.selected_internal_worker_index().is_none() {
|
||||
self.selected_internal_worker_session_id = None;
|
||||
}
|
||||
self.removed_internal_workers.clear();
|
||||
}
|
||||
|
||||
fn update_internal_worker_view_from_snapshot(
|
||||
@@ -2175,6 +2185,12 @@ impl App {
|
||||
revision: u64,
|
||||
event: Event,
|
||||
) {
|
||||
if self
|
||||
.removed_internal_workers
|
||||
.contains_key(&worker.session_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let index = self
|
||||
.internal_workers
|
||||
.iter()
|
||||
@@ -2199,6 +2215,32 @@ impl App {
|
||||
let _ = target.app.handle_worker_event(event);
|
||||
}
|
||||
|
||||
fn remove_internal_worker(&mut self, worker: InternalWorkerRef, revision: u64) {
|
||||
let session_id = worker.session_id;
|
||||
let Some(index) = self
|
||||
.internal_workers
|
||||
.iter()
|
||||
.position(|candidate| candidate.worker.session_id == session_id)
|
||||
else {
|
||||
if self.selected_internal_worker_session_id.as_deref() == Some(session_id.as_str()) {
|
||||
self.selected_internal_worker_session_id = None;
|
||||
}
|
||||
self.removed_internal_workers
|
||||
.entry(session_id)
|
||||
.and_modify(|current| *current = (*current).max(revision))
|
||||
.or_insert(revision);
|
||||
return;
|
||||
};
|
||||
if revision <= self.internal_workers[index].revision {
|
||||
return;
|
||||
}
|
||||
self.internal_workers.remove(index);
|
||||
if self.selected_internal_worker_session_id.as_deref() == Some(session_id.as_str()) {
|
||||
self.selected_internal_worker_session_id = None;
|
||||
}
|
||||
self.removed_internal_workers.insert(session_id, revision);
|
||||
}
|
||||
|
||||
fn restore_snapshot(
|
||||
&mut self,
|
||||
entries: &[serde_json::Value],
|
||||
@@ -3622,6 +3664,7 @@ mod completion_flow_tests {
|
||||
finished: false,
|
||||
},
|
||||
],
|
||||
commands: Vec::new(),
|
||||
},
|
||||
internal_workers: Vec::new(),
|
||||
});
|
||||
@@ -3876,6 +3919,87 @@ mod completion_flow_tests {
|
||||
assert_eq!(app.task_pane_scroll, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_internal_worker_removal_drops_descendants_and_fences_late_events() {
|
||||
let mut app = App::new("parent".into());
|
||||
let worker = InternalWorkerRef {
|
||||
session_id: "child-session".into(),
|
||||
name: "child".into(),
|
||||
parent_session_id: Some("parent-session".into()),
|
||||
kind: protocol::InternalWorkerKind::SubWorker,
|
||||
};
|
||||
let nested = InternalWorkerRef {
|
||||
session_id: "grandchild-session".into(),
|
||||
name: "grandchild".into(),
|
||||
parent_session_id: Some("child-session".into()),
|
||||
kind: protocol::InternalWorkerKind::SubWorker,
|
||||
};
|
||||
app.handle_worker_event(Event::InternalWorker {
|
||||
worker: worker.clone(),
|
||||
revision: 2,
|
||||
event: Box::new(Event::InternalWorker {
|
||||
worker: nested,
|
||||
revision: 1,
|
||||
event: Box::new(Event::TextDone {
|
||||
text: "nested".into(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
assert_eq!(app.internal_workers.len(), 1);
|
||||
assert_eq!(app.internal_workers[0].app.internal_workers.len(), 1);
|
||||
app.cycle_worker_view();
|
||||
assert_eq!(app.selected_worker_view().worker_name, "child");
|
||||
|
||||
app.handle_worker_event(Event::InternalWorkerRemoved {
|
||||
worker: worker.clone(),
|
||||
revision: 3,
|
||||
});
|
||||
app.handle_worker_event(Event::InternalWorker {
|
||||
worker,
|
||||
revision: 4,
|
||||
event: Box::new(Event::TextDone {
|
||||
text: "late".into(),
|
||||
}),
|
||||
});
|
||||
|
||||
assert!(app.internal_workers.is_empty());
|
||||
assert_eq!(app.selected_worker_view().worker_name, "parent");
|
||||
app.handle_worker_event(Event::Snapshot {
|
||||
greeting: test_greeting(),
|
||||
entries: Vec::new(),
|
||||
status: WorkerStatus::Idle,
|
||||
in_flight: Default::default(),
|
||||
internal_workers: Vec::new(),
|
||||
});
|
||||
assert!(app.internal_workers.is_empty());
|
||||
assert!(app.removed_internal_workers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_internal_worker_removal_keeps_newer_projection() {
|
||||
let mut app = App::new("parent".into());
|
||||
let worker = InternalWorkerRef {
|
||||
session_id: "child-session".into(),
|
||||
name: "child".into(),
|
||||
parent_session_id: Some("parent-session".into()),
|
||||
kind: protocol::InternalWorkerKind::SubWorker,
|
||||
};
|
||||
app.handle_worker_event(Event::InternalWorker {
|
||||
worker: worker.clone(),
|
||||
revision: 4,
|
||||
event: Box::new(Event::TextDone {
|
||||
text: "current".into(),
|
||||
}),
|
||||
});
|
||||
app.handle_worker_event(Event::InternalWorkerRemoved {
|
||||
worker,
|
||||
revision: 3,
|
||||
});
|
||||
|
||||
assert_eq!(app.internal_workers.len(), 1);
|
||||
assert_eq!(app.internal_workers[0].revision, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_authoritatively_replaces_internal_worker_views() {
|
||||
let mut app = App::new("parent".into());
|
||||
|
||||
Reference in New Issue
Block a user