Restore agent tool panes and require explicit session resume

Persist direct-provider tool calls, results, display details, and turn identities so saved conversations rebuild individual panes. Keep restored runs dormant until explicitly resumed and bind queued shell commands to their actual execution blocks.
This commit is contained in:
2026-09-20 17:11:41 -05:00
parent d7fbc09bcf
commit 35ff43de7f
21 changed files with 1011 additions and 88 deletions
@@ -0,0 +1,131 @@
use galaxyui::App;
use super::*;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent::AIAgentActionId;
use crate::terminal::model::block::AgentInteractionMetadata;
struct RecordingEventLoopSender(Arc<FairMutex<Vec<Message>>>);
impl EventLoopSender for RecordingEventLoopSender {
fn send(&self, message: Message) -> Result<(), EventLoopSendError> {
self.0.lock().push(message);
Ok(())
}
}
#[test]
fn queued_agent_command_claims_only_the_block_at_dispatch() {
App::test((), |mut app| async move {
let sessions = app.add_model(|_| Sessions::new_for_test());
let (_events_tx, events_rx) = async_channel::unbounded();
let dispatcher =
app.add_model(|ctx| ModelEventDispatcher::new(events_rx, sessions.clone(), ctx));
let line_editor =
app.add_model(|ctx| LineEditorStatus::new(dispatcher.clone(), sessions.clone(), ctx));
let (_commands_tx, commands_rx) = async_channel::unbounded();
let model = Arc::new(FairMutex::new(TerminalModel::mock(None, None)));
let writes = Arc::new(FairMutex::new(Vec::new()));
let controller = app.add_model(|ctx| {
PtyController::new(
RecordingEventLoopSender(writes.clone()),
dispatcher,
line_editor,
sessions,
commands_rx,
model.clone(),
ctx,
)
});
let conversation_id = AIConversationId::new();
let previous_action = AIAgentActionId::from("previous-command".to_string());
let next_action = AIAgentActionId::from("follow-up-command".to_string());
let previous_block = {
let mut model = model.lock();
model.simulate_long_running_block("echo previous", "previous output");
model
.block_list_mut()
.active_block_mut()
.set_agent_interaction_mode_for_requested_command(
previous_action.clone(),
None,
conversation_id,
);
model.active_block_id().clone()
};
controller.update(&mut app, |controller, ctx| {
controller.write_command(
&ExecuteCommandEvent {
command: "echo follow-up".into(),
session_id: 1.into(),
workflow_id: None,
workflow_command: None,
should_add_command_to_history: false,
source: CommandExecutionSource::AI {
metadata: AgentInteractionMetadata::new_hidden(
next_action.clone(),
conversation_id,
),
},
},
ShellType::Zsh,
ctx,
);
assert_eq!(controller.pending_writes.len(), 1);
controller.cancel_queued_agent_command(&previous_action);
assert_eq!(controller.pending_writes.len(), 1);
let write = controller.pending_writes.pop_front().unwrap();
let PtyWrite::Command {
execution: Some(event),
..
} = &write
else {
panic!("expected queued agent command");
};
let event = event.clone();
controller.pending_writes.push_back(write);
controller.cancel_queued_agent_command(&next_action);
assert!(controller.pending_writes.is_empty());
assert!(!controller.is_user_command_executing);
controller.write_command(&event, ShellType::Zsh, ctx);
});
assert!(writes.lock().is_empty());
{
let mut model = model.lock();
assert_eq!(
model
.block_list()
.active_block()
.requested_command_action_id(),
Some(&previous_action)
);
assert!(model
.block_list()
.block_for_ai_action_id(&next_action)
.is_none());
model.finish_block();
}
// The line editor becomes ready only after the preceding block has finished.
// Dispatch the queued write at that boundary, as the Active event normally does.
controller.update(&mut app, |controller, ctx| {
let write = controller.pending_writes.pop_front().unwrap();
controller.send_write_to_event_loop(write, ctx);
});
let model = model.lock();
let new_block = model
.block_list()
.block_for_ai_action_id(&next_action)
.unwrap();
assert_ne!(new_block.id(), &previous_block);
assert_eq!(new_block.ai_conversation_id(), Some(conversation_id));
assert_eq!(
model
.block_list()
.block_with_id(&previous_block)
.unwrap()
.requested_command_action_id(),
Some(&previous_action)
);
assert_eq!(writes.lock().len(), 1);
});
}