Scope TUI approval requests to their threads (#39372)

## Why

Approval and item IDs can collide across concurrent threads. Tracking pending app-server approvals by ID alone can therefore resolve, submit, or dismiss a request from the wrong thread.

## What changed

- Key command execution, file change, and permissions approvals by both thread ID and approval ID.
- Route approval responses through the originating thread and require the thread to match when processing resolution notifications or dismissing approval prompts.
- Canonicalize parseable thread IDs so equivalent UUID representations still match.

## Testing

- Cover colliding approval IDs across threads, mismatched resolution notifications, prompt dismissal, and response routing for primary and background threads.

GitOrigin-RevId: c2d899a1e4b6a6413c78a0595a5311f5fabb9815
This commit is contained in:
Eric Traut authored and copyberry committed 2026-08-19 02:28:42 +00:00
1 parent fde2156057
commit 8843960ba0
9 files changed
+358 -125

No files matched your search

+13 -1
View File
@@ -1032,7 +1032,19 @@ impl ChatWidget {
pub(crate) fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) {
// A remotely resolved request must not remain user-actionable. It may be
// materialized in the bottom pane or still deferred behind active streaming.
let removed_deferred = self.interrupts.remove_resolved_prompt(request);
let request_thread_id = match request {
ResolvedAppServerRequest::ExecApproval { thread_id, .. }
| ResolvedAppServerRequest::FileChangeApproval { thread_id, .. }
| ResolvedAppServerRequest::PermissionsApproval { thread_id, .. } => {
Some(thread_id.as_str())
}
ResolvedAppServerRequest::UserInput { .. }
| ResolvedAppServerRequest::McpElicitation { .. } => None,
};
let removed_deferred = request_thread_id.is_none_or(|request_thread_id| {
self.thread_id
.is_some_and(|thread_id| thread_id.to_string() == request_thread_id)
}) && self.interrupts.remove_resolved_prompt(request);
let removed_visible = self.bottom_pane.dismiss_app_server_request(request);
if removed_deferred || removed_visible {
self.request_redraw();