1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//! Tokio task body for the provider spawn.
//!
//! Contains the unwinding wrapper and panic recovery used
//! by [`super::chat_spawn::spawn_provider_task`].
use super::worktree::WorktreeState;
use super::worktree_result::{handle_worktree_result, run_prompt};
use crate::provider::ProviderRegistry;
use crate::session::{ImageAttachment, Session, SessionEvent};
use futures::FutureExt;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{Notify, mpsc};
/// Run the provider inside a panic-catching wrapper.
///
/// Delegates to [`run_prompt`] then handles worktree result
/// and sends the outcome through `result_tx`.
///
/// `cancel` is a shared [`Notify`] that, when triggered (e.g. by the TUI
/// when the user submits a steering message mid-stream), aborts the
/// in-flight provider call so the partial assistant content already
/// streamed via `event_tx` can be treated as a completed turn and the
/// new user message dispatched immediately.
pub(super) async fn run_spawned_task(
mut session: Session,
prompt: String,
images: Vec<ImageAttachment>,
event_tx: mpsc::Sender<SessionEvent>,
registry: Arc<ProviderRegistry>,
original_dir: Option<PathBuf>,
result_tx: mpsc::Sender<anyhow::Result<Session>>,
worktree: Option<WorktreeState>,
prompt_for_pr: String,
cancel: Arc<Notify>,
) {
// Clone so we can restore `metadata.directory` on the cancel branch
// without racing with `run_prompt`, which also consumes `original_dir`.
let original_dir_for_cancel = original_dir.clone();
let result = std::panic::AssertUnwindSafe(async {
tokio::select! {
biased;
_ = cancel.notified() => {
tracing::info!("Provider turn interrupted by user steering");
// `run_prompt` mutates `session` in place as assistant
// tokens/tool calls stream in via SessionEvent, and it's
// also what appends the *user* message to `session.messages`
// at turn start. Return the live `session` so the partial
// turn (including the user prompt that triggered it) is
// preserved in history; returning a pre-turn snapshot here
// would silently drop the user's message.
session.metadata.directory = original_dir_for_cancel;
Ok(session.clone())
}
r = run_prompt(
&mut session,
&prompt,
images,
event_tx,
registry,
original_dir,
) => r,
}
})
.catch_unwind()
.await;
match result {
Ok(inner) => {
handle_worktree_result(&inner, worktree, Some(&prompt_for_pr)).await;
let _ = result_tx.send(inner).await;
}
Err(panic_err) => {
let msg = extract_panic_message(&panic_err);
tracing::error!(error = %msg, "Provider task panicked");
let _ = result_tx
.send(Err(anyhow::anyhow!("Provider task panicked: {msg}")))
.await;
}
}
}
/// Extract a readable message from a panic payload.
fn extract_panic_message(err: &Box<dyn std::any::Any + Send>) -> &str {
err.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| err.downcast_ref::<&str>().copied())
.unwrap_or("unknown panic")
}