Merge remote-tracking branch 'origin/tui-watchdog-timer-countdown' into repair/collab-stack-refresh-20260402

This commit is contained in:
Friel
2026-04-02 11:51:01 +00:00
3 changed files with 107 additions and 2 deletions

View File

@@ -13,6 +13,9 @@ pub(super) use crate::app_event_sender::AppEventSender;
pub(super) use crate::bottom_pane::LocalImageAttachment;
pub(super) use crate::bottom_pane::MentionBinding;
pub(super) use crate::chatwidget::realtime::RealtimeConversationPhase;
pub(super) use crate::history_cell::SubagentPanelAgent;
pub(super) use crate::history_cell::SubagentPanelState;
pub(super) use crate::history_cell::SubagentStatusCell;
pub(super) use crate::history_cell::UserHistoryCell;
pub(super) use crate::model_catalog::ModelCatalog;
pub(super) use crate::test_backend::VT100Backend;
@@ -203,6 +206,7 @@ pub(super) use std::collections::BTreeMap;
pub(super) use std::collections::HashMap;
pub(super) use std::collections::HashSet;
pub(super) use std::path::PathBuf;
pub(super) use std::sync::Mutex as StdMutex;
pub(super) use tempfile::NamedTempFile;
pub(super) use tempfile::tempdir;
pub(super) use tokio::sync::mpsc::error::TryRecvError;

View File

@@ -210,6 +210,7 @@ pub(super) async fn make_chatwidget_manual(
pending_turn_copyable_output: None,
running_commands: HashMap::new(),
collab_agent_metadata: HashMap::new(),
subagent_panel: None,
pending_collab_spawn_requests: HashMap::new(),
suppressed_exec_calls: HashSet::new(),
skills_all: Vec::new(),
@@ -286,9 +287,8 @@ pub(super) async fn make_chatwidget_manual(
external_editor_state: ExternalEditorState::Closed,
realtime_conversation: RealtimeConversationUiState::default(),
last_rendered_user_message_event: None,
last_non_retry_error: None,
subagent_panel: None,
last_replayed_agent_inbox_message: None,
last_non_retry_error: None,
};
widget.set_model(&resolved_model);
(widget, rx, op_rx)

View File

@@ -111,6 +111,107 @@ async fn turn_started_uses_runtime_context_window_before_first_token_count() {
"expected /status to avoid raw config context window, got: {context_line}"
);
}
#[tokio::test]
async fn subagent_panel_is_not_flushed_into_transcript_history() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let state = Arc::new(StdMutex::new(SubagentPanelState {
started_at: Instant::now(),
total_agents: 1,
running_count: 0,
running_agents: vec![SubagentPanelAgent {
ordinal: 1,
name: "user-request-derisk-implement".to_string(),
status: AgentStatus::PendingInit,
is_watchdog: true,
watchdog_countdown_started_at: Some(Instant::now()),
preview: "watchdog idle".to_string(),
latest_update_at: Instant::now(),
}],
}));
chat.on_subagent_panel_updated(Arc::new(SubagentStatusCell::new(
Arc::clone(&state),
/*animations_enabled*/ true,
)));
chat.add_to_history(history_cell::new_error_event("follow-up cell".to_string()));
let inserted = drain_insert_history(&mut rx);
assert_eq!(
inserted.len(),
1,
"subagent panel should remain transient and not be inserted into transcript history"
);
let rendered = lines_to_single_string(&inserted[0]);
assert!(rendered.contains("follow-up cell"));
assert!(!rendered.contains("Subagents"));
assert!(
chat.subagent_panel
.as_ref()
.is_some_and(|panel| panel.matches_state(&state)),
"subagent panel should stay mounted after other history cells are inserted"
);
}
#[tokio::test]
async fn subagent_panel_mounts_while_placeholder_active_cell_exists_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.active_cell = Some(ChatWidget::placeholder_session_header_cell(
chat.config_ref(),
));
chat.bottom_pane.set_composer_text(
"show current subagent state".to_string(),
Vec::new(),
Vec::new(),
);
let state = Arc::new(StdMutex::new(SubagentPanelState {
started_at: Instant::now(),
total_agents: 1,
running_count: 0,
running_agents: vec![SubagentPanelAgent {
ordinal: 1,
name: "watchdog-agent".to_string(),
status: AgentStatus::PendingInit,
is_watchdog: true,
watchdog_countdown_started_at: Some(Instant::now()),
preview: "watchdog idle".to_string(),
latest_update_at: Instant::now(),
}],
}));
chat.on_subagent_panel_updated(Arc::new(SubagentStatusCell::new(
Arc::clone(&state),
/*animations_enabled*/ false,
)));
assert!(
chat.active_cell
.as_ref()
.is_some_and(|cell| cell.as_any().is::<history_cell::SessionHeaderHistoryCell>()),
"placeholder session header should remain the active cell"
);
assert!(
chat.subagent_panel
.as_ref()
.is_some_and(|panel| panel.matches_state(&state)),
"subagent panel should mount even when another active cell already exists"
);
let width = 80;
let height = chat.desired_height(width);
let mut terminal =
ratatui::Terminal::new(VT100Backend::new(width, height)).expect("create terminal");
terminal.set_viewport_area(Rect::new(0, 0, width, height));
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("render chat with placeholder header and subagent panel");
assert_chatwidget_snapshot!(
"subagent_panel_mounts_while_placeholder_active_cell_exists",
terminal.backend().vt100().screen().contents()
);
}
#[tokio::test]
async fn helpers_are_available_and_do_not_panic() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();