mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Dismiss stale app-server requests after remote resolution (#15134)
Dismiss stale TUI app-server approvals after remote resolution When an approval, user-input prompt, or elicitation request is resolved by another client, the TUI now dismisses the matching local UI instead of leaving stale prompts behind and emitting a misleading local cancellation. This change teaches pending app-server request tracking to map `serverRequest/resolved` notifications back to the concrete request type and stable request key, then propagates that resolved request into TUI prompt state. Approval, request-user-input, and MCP elicitation overlays now drop the resolved current or queued request quietly, advance to the next queued request when present, and avoid emitting abort/cancel events for stale UI. The latest update also retires matching prompts while they are still deferred behind active streaming and suppresses buffered active-thread requests whose app-server request id has already been resolved before drain. `ChatWidget` removes a resolved request from both the deferred interrupt queue and the materialized bottom-pane stack, while active-thread request handling verifies the app-server request is still pending before showing a prompt. Lifecycle events such as exec begin/end remain queued so approved work can still render normally. Tests cover resolved-request mapping, overlay dismissal behavior, deferred prompt pruning for same-turn user input, exec approval IDs, lifecycle-event retention, and the buffered active-thread ordering regression. Validation: - `just fmt` - `git diff --check` - `cargo test -p codex-tui resolved_buffered_approval_does_not_become_actionable_after_drain` - `cargo test -p codex-tui enqueue_primary_thread_session_replays_buffered_approval_after_attach` - `cargo test -p codex-tui chatwidget::interrupts` - `just fix -p codex-tui` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
ba36415a30
commit
bc969b6516
@@ -1,5 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::app::app_server_requests::ResolvedAppServerRequest;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
@@ -86,6 +87,13 @@ impl InterruptManager {
|
||||
self.queue.push_back(QueuedInterrupt::PatchEnd(ev));
|
||||
}
|
||||
|
||||
pub(crate) fn remove_resolved_prompt(&mut self, request: &ResolvedAppServerRequest) -> bool {
|
||||
let original_len = self.queue.len();
|
||||
self.queue
|
||||
.retain(|queued| !queued.matches_resolved_prompt(request));
|
||||
self.queue.len() != original_len
|
||||
}
|
||||
|
||||
pub(crate) fn flush_all(&mut self, chat: &mut ChatWidget) {
|
||||
while let Some(q) = self.queue.pop_front() {
|
||||
match q {
|
||||
@@ -103,3 +111,144 @@ impl InterruptManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueuedInterrupt {
|
||||
fn matches_resolved_prompt(&self, request: &ResolvedAppServerRequest) -> bool {
|
||||
match self {
|
||||
QueuedInterrupt::ExecApproval(ev) => {
|
||||
matches!(request, ResolvedAppServerRequest::ExecApproval { id }
|
||||
if ev.effective_approval_id() == id.as_str())
|
||||
}
|
||||
QueuedInterrupt::ApplyPatchApproval(ev) => {
|
||||
matches!(request, ResolvedAppServerRequest::FileChangeApproval { id }
|
||||
if ev.call_id == id.as_str())
|
||||
}
|
||||
QueuedInterrupt::Elicitation(ev) => {
|
||||
matches!(request, ResolvedAppServerRequest::McpElicitation {
|
||||
server_name,
|
||||
request_id,
|
||||
} if ev.server_name == server_name.as_str() && &ev.id == request_id)
|
||||
}
|
||||
QueuedInterrupt::RequestPermissions(ev) => {
|
||||
matches!(request, ResolvedAppServerRequest::PermissionsApproval { id }
|
||||
if ev.call_id == id.as_str())
|
||||
}
|
||||
QueuedInterrupt::RequestUserInput(ev) => {
|
||||
matches!(request, ResolvedAppServerRequest::UserInput { call_id }
|
||||
if ev.call_id == call_id.as_str())
|
||||
}
|
||||
QueuedInterrupt::ExecBegin(_)
|
||||
| QueuedInterrupt::ExecEnd(_)
|
||||
| QueuedInterrupt::McpBegin(_)
|
||||
| QueuedInterrupt::McpEnd(_)
|
||||
| QueuedInterrupt::PatchEnd(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_protocol::approvals::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecCommandBeginEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn user_input(call_id: &str, turn_id: &str) -> RequestUserInputEvent {
|
||||
RequestUserInputEvent {
|
||||
call_id: call_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
questions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_approval(call_id: &str, approval_id: Option<&str>) -> ExecApprovalRequestEvent {
|
||||
ExecApprovalRequestEvent {
|
||||
call_id: call_id.to_string(),
|
||||
approval_id: approval_id.map(str::to_string),
|
||||
turn_id: "turn".to_string(),
|
||||
command: vec!["true".to_string()],
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
reason: None,
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_begin(call_id: &str) -> ExecCommandBeginEvent {
|
||||
ExecCommandBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
process_id: None,
|
||||
turn_id: "turn".to_string(),
|
||||
command: vec!["true".to_string()],
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
parsed_cmd: Vec::new(),
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_resolved_prompt_removes_matching_user_input_only() {
|
||||
let mut manager = InterruptManager::new();
|
||||
manager.push_user_input(user_input("call-a", "turn"));
|
||||
manager.push_user_input(user_input("call-b", "turn"));
|
||||
|
||||
assert!(
|
||||
manager.remove_resolved_prompt(&ResolvedAppServerRequest::UserInput {
|
||||
call_id: "call-b".to_string(),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(manager.queue.len(), 1);
|
||||
let Some(QueuedInterrupt::RequestUserInput(remaining)) = manager.queue.front() else {
|
||||
panic!("expected remaining queued user input");
|
||||
};
|
||||
assert_eq!(remaining.call_id, "call-a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_resolved_prompt_matches_exec_approval_id() {
|
||||
let mut manager = InterruptManager::new();
|
||||
manager.push_exec_approval(exec_approval("call", Some("approval")));
|
||||
|
||||
assert!(
|
||||
!manager.remove_resolved_prompt(&ResolvedAppServerRequest::ExecApproval {
|
||||
id: "call".to_string(),
|
||||
})
|
||||
);
|
||||
assert_eq!(manager.queue.len(), 1);
|
||||
|
||||
assert!(
|
||||
manager.remove_resolved_prompt(&ResolvedAppServerRequest::ExecApproval {
|
||||
id: "approval".to_string(),
|
||||
})
|
||||
);
|
||||
assert!(manager.queue.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_resolved_prompt_keeps_lifecycle_events() {
|
||||
let mut manager = InterruptManager::new();
|
||||
manager.push_exec_begin(exec_begin("call"));
|
||||
|
||||
assert!(
|
||||
!manager.remove_resolved_prompt(&ResolvedAppServerRequest::ExecApproval {
|
||||
id: "call".to_string(),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(manager.queue.len(), 1);
|
||||
assert!(matches!(
|
||||
manager.queue.front(),
|
||||
Some(QueuedInterrupt::ExecBegin(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user