1use super::*;
2
3impl PiApp {
4 #[allow(clippy::too_many_lines)]
5 pub(super) fn handle_tree_ui_key(&mut self, key: &KeyMsg) -> Option<Cmd> {
6 let tree_ui = self.tree_ui.take()?;
7
8 match tree_ui {
9 TreeUiState::Selector(mut selector) => {
10 match key.key_type {
11 KeyType::Up => selector.move_selection(-1),
12 KeyType::Down => selector.move_selection(1),
13 KeyType::CtrlU => {
14 selector.user_only = !selector.user_only;
15 if let Ok(session_guard) = self.session.try_lock() {
16 selector.rebuild(&session_guard);
17 }
18 }
19 KeyType::CtrlO => {
20 selector.show_all = !selector.show_all;
21 if let Ok(session_guard) = self.session.try_lock() {
22 selector.rebuild(&session_guard);
23 }
24 }
25 KeyType::Esc | KeyType::CtrlC => {
26 self.status_message = Some("Tree navigation cancelled".to_string());
27 self.tree_ui = None;
28 return None;
29 }
30 KeyType::Enter => {
31 if selector.rows.is_empty() {
32 self.tree_ui = None;
33 return None;
34 }
35
36 let selected = selector.rows[selector.selected].clone();
37 selector.last_selected_id = Some(selected.id.clone());
38
39 let (new_leaf_id, editor_text) = if let Some(text) = selected.resubmit_text
40 {
41 (selected.parent_id.clone(), Some(text))
42 } else {
43 (Some(selected.id.clone()), None)
44 };
45
46 if selector.current_leaf_id.as_deref() == new_leaf_id.as_deref() {
48 self.status_message = Some("Already on that branch".to_string());
49 self.tree_ui = None;
50 return None;
51 }
52
53 let Ok(session_guard) = self.session.try_lock() else {
54 self.status_message = Some("Session busy; try again".to_string());
55 self.tree_ui = None;
56 return None;
57 };
58
59 let old_leaf_id = session_guard.leaf_id.clone();
60 let (entries_to_summarize, summary_from_id) = collect_tree_branch_entries(
61 &session_guard,
62 old_leaf_id.as_deref(),
63 new_leaf_id.as_deref(),
64 );
65 let session_id = session_guard.header.id.clone();
66 drop(session_guard);
67
68 let api_key_present = self.agent.try_lock().is_ok_and(|agent_guard| {
69 agent_guard.stream_options().api_key.is_some()
70 });
71
72 let pending = PendingTreeNavigation {
73 session_id,
74 old_leaf_id,
75 selected_entry_id: selected.id,
76 new_leaf_id,
77 editor_text,
78 entries_to_summarize,
79 summary_from_id,
80 api_key_present,
81 };
82
83 if pending.entries_to_summarize.is_empty() {
84 self.start_tree_navigation(pending, TreeSummaryChoice::NoSummary, None);
86 return None;
87 }
88
89 self.tree_ui = Some(TreeUiState::SummaryPrompt(TreeSummaryPromptState {
90 pending,
91 selected: 0,
92 }));
93 return None;
94 }
95 _ => {}
96 }
97
98 self.tree_ui = Some(TreeUiState::Selector(selector));
99 }
100 TreeUiState::SummaryPrompt(mut prompt) => {
101 match key.key_type {
102 KeyType::Up => {
103 if prompt.selected > 0 {
104 prompt.selected -= 1;
105 }
106 }
107 KeyType::Down => {
108 if prompt.selected < TreeSummaryChoice::all().len().saturating_sub(1) {
109 prompt.selected += 1;
110 }
111 }
112 KeyType::Esc | KeyType::CtrlC => {
113 self.status_message = Some("Tree navigation cancelled".to_string());
114 self.tree_ui = None;
115 return None;
116 }
117 KeyType::Enter => {
118 let choice = TreeSummaryChoice::all()[prompt.selected];
119 match choice {
120 TreeSummaryChoice::NoSummary | TreeSummaryChoice::Summarize => {
121 let pending = prompt.pending;
122 self.start_tree_navigation(pending, choice, None);
123 return None;
124 }
125 TreeSummaryChoice::SummarizeWithCustomPrompt => {
126 self.tree_ui =
127 Some(TreeUiState::CustomPrompt(TreeCustomPromptState {
128 pending: prompt.pending,
129 instructions: String::new(),
130 }));
131 return None;
132 }
133 }
134 }
135 _ => {}
136 }
137 self.tree_ui = Some(TreeUiState::SummaryPrompt(prompt));
138 }
139 TreeUiState::CustomPrompt(mut custom) => {
140 match key.key_type {
141 KeyType::Esc | KeyType::CtrlC => {
142 self.tree_ui = Some(TreeUiState::SummaryPrompt(TreeSummaryPromptState {
143 pending: custom.pending,
144 selected: 2,
145 }));
146 return None;
147 }
148 KeyType::Backspace => {
149 custom.instructions.pop();
150 }
151 KeyType::Enter => {
152 let pending = custom.pending;
153 let instructions = if custom.instructions.trim().is_empty() {
154 None
155 } else {
156 Some(custom.instructions)
157 };
158 self.start_tree_navigation(
159 pending,
160 TreeSummaryChoice::SummarizeWithCustomPrompt,
161 instructions,
162 );
163 return None;
164 }
165 KeyType::Runes => {
166 for ch in key.runes.iter().copied() {
167 custom.instructions.push(ch);
168 }
169 }
170 _ => {}
171 }
172 self.tree_ui = Some(TreeUiState::CustomPrompt(custom));
173 }
174 }
175
176 None
177 }
178
179 pub fn handle_branch_picker_key(&mut self, key: &KeyMsg) -> Option<Cmd> {
181 let picker = self.branch_picker.as_mut()?;
182
183 match key.key_type {
184 KeyType::Up => picker.select_prev(),
185 KeyType::Down => picker.select_next(),
186 KeyType::Runes if key.runes == ['k'] => picker.select_prev(),
187 KeyType::Runes if key.runes == ['j'] => picker.select_next(),
188 KeyType::Enter => {
189 if let Some(branch) = picker.selected_branch().cloned() {
190 self.branch_picker = None;
191 return self.switch_to_branch_leaf(&branch.leaf_id);
192 }
193 self.branch_picker = None;
194 }
195 KeyType::Esc | KeyType::CtrlC => {
196 self.branch_picker = None;
197 self.status_message = Some("Branch picker cancelled".to_string());
198 }
199 KeyType::Runes if key.runes == ['q'] => {
200 self.branch_picker = None;
201 }
202 _ => {} }
204
205 None
206 }
207
208 fn switch_to_branch_leaf(&mut self, leaf_id: &str) -> Option<Cmd> {
210 let (session_id, old_leaf_id) = self
211 .session
212 .try_lock()
213 .ok()
214 .map(|g| (g.header.id.clone(), g.leaf_id.clone()))
215 .unwrap_or_default();
216
217 let pending = PendingTreeNavigation {
218 session_id,
219 old_leaf_id,
220 selected_entry_id: leaf_id.to_string(),
221 new_leaf_id: Some(leaf_id.to_string()),
222 editor_text: None,
223 entries_to_summarize: Vec::new(),
224 summary_from_id: String::new(),
225 api_key_present: false,
226 };
227 self.start_tree_navigation(pending, TreeSummaryChoice::NoSummary, None);
228 None
229 }
230
231 pub fn open_branch_picker(&mut self) {
233 if self.agent_state != AgentState::Idle {
234 self.status_message = Some("Cannot switch branches while processing".to_string());
235 return;
236 }
237
238 let branches = self
239 .session
240 .try_lock()
241 .ok()
242 .and_then(|guard| guard.sibling_branches().map(|(_, b)| b));
243
244 match branches {
245 Some(branches) if branches.len() > 1 => {
246 self.branch_picker = Some(BranchPickerOverlay::new(branches));
247 }
248 _ => {
249 self.status_message =
250 Some("No branches to pick (use /fork to create one)".to_string());
251 }
252 }
253 }
254
255 pub fn cycle_sibling_branch(&mut self, forward: bool) {
257 if self.agent_state != AgentState::Idle {
258 self.status_message = Some("Cannot switch branches while processing".to_string());
259 return;
260 }
261
262 let target = self.session.try_lock().ok().and_then(|guard| {
263 let (_, branches) = guard.sibling_branches()?;
264 if branches.len() <= 1 {
265 return None;
266 }
267 let current_idx = branches.iter().position(|b| b.is_current)?;
268 let next_idx = if forward {
269 (current_idx + 1) % branches.len()
270 } else {
271 current_idx.checked_sub(1).unwrap_or(branches.len() - 1)
272 };
273 Some(branches[next_idx].leaf_id.clone())
274 });
275
276 if let Some(leaf_id) = target {
277 self.switch_to_branch_leaf(&leaf_id);
278 } else {
279 self.status_message = Some("No sibling branches (use /fork to create one)".to_string());
280 }
281 }
282
283 #[allow(clippy::too_many_lines)]
284 pub(super) fn start_tree_navigation(
285 &mut self,
286 pending: PendingTreeNavigation,
287 choice: TreeSummaryChoice,
288 custom_instructions: Option<String>,
289 ) {
290 let summary_requested = matches!(
291 choice,
292 TreeSummaryChoice::Summarize | TreeSummaryChoice::SummarizeWithCustomPrompt
293 );
294
295 if !summary_requested && self.extensions.is_none() {
298 let Ok(mut session_guard) = self.session.try_lock() else {
299 self.status_message = Some("Session busy; try again".to_string());
300 return;
301 };
302
303 if let Some(target_id) = &pending.new_leaf_id {
304 if !session_guard.navigate_to(target_id) {
305 self.status_message = Some(format!("Branch target not found: {target_id}"));
306 return;
307 }
308 } else {
309 session_guard.reset_leaf();
310 }
311
312 let (messages, usage) = conversation_from_session(&session_guard);
313 let agent_messages = session_guard.to_messages_for_current_path();
314 let status_leaf = pending
315 .new_leaf_id
316 .clone()
317 .unwrap_or_else(|| "root".to_string());
318 drop(session_guard);
319
320 self.spawn_save_session();
321
322 if let Ok(mut agent_guard) = self.agent.try_lock() {
323 agent_guard.replace_messages(agent_messages);
324 }
325
326 self.messages = messages;
327 self.message_render_cache.clear();
328 self.total_usage = usage;
329 self.current_response.clear();
330 self.current_thinking.clear();
331 self.agent_state = AgentState::Idle;
332 self.current_tool = None;
333 self.abort_handle = None;
334 self.status_message = Some(format!("Switched to {status_leaf}"));
335 self.scroll_to_bottom();
336
337 if let Some(text) = pending.editor_text {
338 self.input.set_value(&text);
339 }
340 self.input.focus();
341
342 return;
343 }
344
345 let event_tx = self.event_tx.clone();
346 let session = Arc::clone(&self.session);
347 let agent = Arc::clone(&self.agent);
348 let extensions = self.extensions.clone();
349 let reserve_tokens = self.config.branch_summary_reserve_tokens();
350 let runtime_handle = self.runtime_handle.clone();
351
352 let Ok(agent_guard) = self.agent.try_lock() else {
353 self.status_message = Some("Agent busy; try again".to_string());
354 self.agent_state = AgentState::Idle;
355 return;
356 };
357 let provider = agent_guard.provider();
358 let key_opt = agent_guard.stream_options().api_key.clone();
359
360 self.tree_ui = None;
361 self.agent_state = AgentState::Processing;
362 self.status_message = Some("Switching branches...".to_string());
363
364 runtime_handle.spawn(async move {
365 let cx = Cx::for_request();
366
367 let from_id_for_event = pending
368 .old_leaf_id
369 .clone()
370 .unwrap_or_else(|| "root".to_string());
371 let to_id_for_event = pending
372 .new_leaf_id
373 .clone()
374 .unwrap_or_else(|| "root".to_string());
375
376 if let Some(manager) = extensions.clone() {
377 let cancelled = manager
378 .dispatch_cancellable_event(
379 ExtensionEventName::SessionBeforeSwitch,
380 Some(json!({
381 "fromId": from_id_for_event.clone(),
382 "toId": to_id_for_event.clone(),
383 "sessionId": pending.session_id.clone(),
384 })),
385 EXTENSION_EVENT_TIMEOUT_MS,
386 )
387 .await
388 .unwrap_or(false);
389 if cancelled {
390 let _ = event_tx.try_send(PiMsg::System(
391 "Session switch cancelled by extension".to_string(),
392 ));
393 return;
394 }
395 }
396
397 let summary_skipped =
398 summary_requested && key_opt.is_none() && !pending.entries_to_summarize.is_empty();
399 let summary_text = if !summary_requested || pending.entries_to_summarize.is_empty() {
400 None
401 } else if let Some(key) = key_opt.as_deref() {
402 match crate::compaction::summarize_entries(
403 &pending.entries_to_summarize,
404 provider,
405 key,
406 reserve_tokens,
407 custom_instructions.as_deref(),
408 )
409 .await
410 {
411 Ok(summary) => summary,
412 Err(err) => {
413 let _ = event_tx
414 .try_send(PiMsg::AgentError(format!("Branch summary failed: {err}")));
415 return;
416 }
417 }
418 } else {
419 None
420 };
421
422 let messages_for_agent = {
423 let mut guard = match session.lock(&cx).await {
424 Ok(guard) => guard,
425 Err(err) => {
426 let _ = event_tx
427 .try_send(PiMsg::AgentError(format!("Failed to lock session: {err}")));
428 return;
429 }
430 };
431
432 if let Some(target_id) = &pending.new_leaf_id {
433 if !guard.navigate_to(target_id) {
434 let _ = event_tx.try_send(PiMsg::AgentError(format!(
435 "Branch target not found: {target_id}"
436 )));
437 return;
438 }
439 } else {
440 guard.reset_leaf();
441 }
442
443 if let Some(summary_text) = summary_text {
444 guard.append_branch_summary(
445 pending.summary_from_id.clone(),
446 summary_text,
447 None,
448 None,
449 );
450 }
451
452 let _ = guard.save().await;
453 guard.to_messages_for_current_path()
454 };
455
456 {
457 let mut agent_guard = match agent.lock(&cx).await {
458 Ok(guard) => guard,
459 Err(err) => {
460 let _ = event_tx
461 .try_send(PiMsg::AgentError(format!("Failed to lock agent: {err}")));
462 return;
463 }
464 };
465 agent_guard.replace_messages(messages_for_agent);
466 }
467
468 let (messages, usage) = {
469 let guard = match session.lock(&cx).await {
470 Ok(guard) => guard,
471 Err(err) => {
472 let _ = event_tx
473 .try_send(PiMsg::AgentError(format!("Failed to lock session: {err}")));
474 return;
475 }
476 };
477 conversation_from_session(&guard)
478 };
479
480 let status = if summary_skipped {
481 Some(format!(
482 "Switched to {to_id_for_event} (no summary: missing API key)"
483 ))
484 } else {
485 Some(format!("Switched to {to_id_for_event}"))
486 };
487
488 let _ = event_tx.try_send(PiMsg::ConversationReset {
489 messages,
490 usage,
491 status,
492 });
493
494 if let Some(text) = pending.editor_text {
495 let _ = event_tx.try_send(PiMsg::SetEditorText(text));
496 }
497
498 if let Some(manager) = extensions {
499 let _ = manager
500 .dispatch_event(
501 ExtensionEventName::SessionSwitch,
502 Some(json!({
503 "fromId": from_id_for_event,
504 "toId": to_id_for_event,
505 "sessionId": pending.session_id,
506 })),
507 )
508 .await;
509 }
510 });
511 }
512}