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 if !self.start_tree_navigation(
86 pending,
87 TreeSummaryChoice::NoSummary,
88 None,
89 ) {
90 self.tree_ui = Some(TreeUiState::Selector(selector));
91 }
92 return None;
93 }
94
95 self.tree_ui = Some(TreeUiState::SummaryPrompt(TreeSummaryPromptState {
96 pending,
97 selected: 0,
98 }));
99 return None;
100 }
101 _ => {}
102 }
103
104 self.tree_ui = Some(TreeUiState::Selector(selector));
105 }
106 TreeUiState::SummaryPrompt(mut prompt) => {
107 match key.key_type {
108 KeyType::Up if prompt.selected > 0 => {
109 prompt.selected -= 1;
110 }
111 KeyType::Down
112 if prompt.selected < TreeSummaryChoice::all().len().saturating_sub(1) =>
113 {
114 prompt.selected += 1;
115 }
116 KeyType::Esc | KeyType::CtrlC => {
117 self.status_message = Some("Tree navigation cancelled".to_string());
118 self.tree_ui = None;
119 return None;
120 }
121 KeyType::Enter => {
122 let choice = TreeSummaryChoice::all()[prompt.selected];
123 match choice {
124 TreeSummaryChoice::NoSummary | TreeSummaryChoice::Summarize => {
125 let pending = prompt.pending.clone();
126 if !self.start_tree_navigation(pending, choice, None) {
127 self.tree_ui = Some(TreeUiState::SummaryPrompt(prompt));
128 }
129 return None;
130 }
131 TreeSummaryChoice::SummarizeWithCustomPrompt => {
132 self.tree_ui =
133 Some(TreeUiState::CustomPrompt(TreeCustomPromptState {
134 pending: prompt.pending,
135 instructions: String::new(),
136 }));
137 return None;
138 }
139 }
140 }
141 _ => {}
142 }
143 self.tree_ui = Some(TreeUiState::SummaryPrompt(prompt));
144 }
145 TreeUiState::CustomPrompt(mut custom) => {
146 match key.key_type {
147 KeyType::Esc | KeyType::CtrlC => {
148 self.tree_ui = Some(TreeUiState::SummaryPrompt(TreeSummaryPromptState {
149 pending: custom.pending,
150 selected: 2,
151 }));
152 return None;
153 }
154 KeyType::Backspace => {
155 custom.instructions.pop();
156 }
157 KeyType::Enter => {
158 let pending = custom.pending.clone();
159 let instructions = if custom.instructions.trim().is_empty() {
160 None
161 } else {
162 Some(custom.instructions.clone())
163 };
164 if !self.start_tree_navigation(
165 pending,
166 TreeSummaryChoice::SummarizeWithCustomPrompt,
167 instructions,
168 ) {
169 self.tree_ui = Some(TreeUiState::CustomPrompt(custom));
170 }
171 return None;
172 }
173 KeyType::Runes => {
174 for ch in key.runes.iter().copied() {
175 custom.instructions.push(ch);
176 }
177 }
178 _ => {}
179 }
180 self.tree_ui = Some(TreeUiState::CustomPrompt(custom));
181 }
182 }
183
184 None
185 }
186
187 pub fn handle_branch_picker_key(&mut self, key: &KeyMsg) -> Option<Cmd> {
189 let picker = self.branch_picker.as_mut()?;
190
191 match key.key_type {
192 KeyType::Up => picker.select_prev(),
193 KeyType::Down => picker.select_next(),
194 KeyType::PgUp => picker.select_page_up(),
195 KeyType::PgDown => picker.select_page_down(),
196 KeyType::Runes if key.runes == ['k'] => picker.select_prev(),
197 KeyType::Runes if key.runes == ['j'] => picker.select_next(),
198 KeyType::Enter => {
199 if let Some(branch) = picker.selected_branch().cloned() {
200 if self.switch_to_branch_leaf(&branch.leaf_id) {
201 self.branch_picker = None;
202 }
203 return None;
204 }
205 self.branch_picker = None;
206 }
207 KeyType::Esc | KeyType::CtrlC => {
208 self.branch_picker = None;
209 self.status_message = Some("Branch picker cancelled".to_string());
210 }
211 KeyType::Runes if key.runes == ['q'] => {
212 self.branch_picker = None;
213 }
214 _ => {} }
216
217 None
218 }
219
220 fn switch_to_branch_leaf(&mut self, leaf_id: &str) -> bool {
222 let Ok(session_guard) = self.session.try_lock() else {
223 self.status_message = Some("Session busy; try again".to_string());
224 return false;
225 };
226 let session_id = session_guard.header.id.clone();
227 let old_leaf_id = session_guard.leaf_id.clone();
228 drop(session_guard);
229
230 let pending = PendingTreeNavigation {
231 session_id,
232 old_leaf_id,
233 selected_entry_id: leaf_id.to_string(),
234 new_leaf_id: Some(leaf_id.to_string()),
235 editor_text: None,
236 entries_to_summarize: Vec::new(),
237 summary_from_id: String::new(),
238 api_key_present: false,
239 };
240 self.start_tree_navigation(pending, TreeSummaryChoice::NoSummary, None)
241 }
242
243 pub fn open_branch_picker(&mut self) {
245 if self.agent_state != AgentState::Idle {
246 self.status_message = Some("Cannot switch branches while processing".to_string());
247 return;
248 }
249
250 let Ok(session_guard) = self.session.try_lock() else {
251 self.status_message = Some("Session busy; try again".to_string());
252 return;
253 };
254 let branches = session_guard.sibling_branches().map(|(_, b)| b);
255 drop(session_guard);
256
257 match branches {
258 Some(branches) if branches.len() > 1 => {
259 let mut picker = BranchPickerOverlay::new(branches);
260 picker.max_visible = super::overlay_max_visible(self.term_height);
261 self.branch_picker = Some(picker);
262 }
263 _ => {
264 self.status_message =
265 Some("No branches to pick (use /fork to create one)".to_string());
266 }
267 }
268 }
269
270 pub fn cycle_sibling_branch(&mut self, forward: bool) {
272 if self.agent_state != AgentState::Idle {
273 self.status_message = Some("Cannot switch branches while processing".to_string());
274 return;
275 }
276
277 let Ok(session_guard) = self.session.try_lock() else {
278 self.status_message = Some("Session busy; try again".to_string());
279 return;
280 };
281 let target = session_guard.sibling_branches().and_then(|(_, branches)| {
282 if branches.len() <= 1 {
283 return None;
284 }
285 let current_idx = branches.iter().position(|b| b.is_current)?;
286 let next_idx = if forward {
287 (current_idx + 1) % branches.len()
288 } else {
289 current_idx.checked_sub(1).unwrap_or(branches.len() - 1)
290 };
291 Some(branches[next_idx].leaf_id.clone())
292 });
293 drop(session_guard);
294
295 if let Some(leaf_id) = target {
296 self.switch_to_branch_leaf(&leaf_id);
297 } else {
298 self.status_message = Some("No sibling branches (use /fork to create one)".to_string());
299 }
300 }
301
302 #[allow(clippy::too_many_lines)]
303 pub(super) fn start_tree_navigation(
304 &mut self,
305 pending: PendingTreeNavigation,
306 choice: TreeSummaryChoice,
307 custom_instructions: Option<String>,
308 ) -> bool {
309 let summary_requested = matches!(
310 choice,
311 TreeSummaryChoice::Summarize | TreeSummaryChoice::SummarizeWithCustomPrompt
312 );
313
314 if !summary_requested && self.extensions.is_none() {
317 let Ok(mut session_guard) = self.session.try_lock() else {
318 self.status_message = Some("Session busy; try again".to_string());
319 return false;
320 };
321
322 if let Some(target_id) = &pending.new_leaf_id {
323 if !session_guard.navigate_to(target_id) {
324 self.status_message = Some(format!("Branch target not found: {target_id}"));
325 return false;
326 }
327 } else {
328 session_guard.reset_leaf();
329 }
330
331 let (messages, usage) = conversation_from_session(&session_guard);
332 let agent_messages = session_guard.to_messages_for_current_path();
333 let status_leaf = pending
334 .new_leaf_id
335 .clone()
336 .unwrap_or_else(|| "root".to_string());
337 drop(session_guard);
338
339 self.spawn_save_session();
340
341 if let Ok(mut agent_guard) = self.agent.try_lock() {
342 agent_guard.replace_messages(agent_messages);
343 }
344
345 self.messages = messages;
346 self.message_render_cache.clear();
347 self.total_usage = usage;
348 self.current_response.clear();
349 self.current_thinking.clear();
350 self.agent_state = AgentState::Idle;
351 self.current_tool = None;
352 self.abort_handle = None;
353 self.status_message = Some(format!("Switched to {status_leaf}"));
354 if let Err(message) = self.sync_runtime_selection_from_session_header() {
355 self.status_message = Some(message);
356 }
357 self.scroll_to_bottom();
358
359 if let Some(text) = pending.editor_text {
360 self.input.set_value(&text);
361 }
362 self.input.focus();
363
364 return true;
365 }
366
367 let event_tx = self.event_tx.clone();
368 let session = Arc::clone(&self.session);
369 let agent = Arc::clone(&self.agent);
370 let extensions = self.extensions.clone();
371 let reserve_tokens = self.config.branch_summary_reserve_tokens();
372 let runtime_handle = self.runtime_handle.clone();
373
374 let Ok(agent_guard) = self.agent.try_lock() else {
375 self.status_message = Some("Agent busy; try again".to_string());
376 self.agent_state = AgentState::Idle;
377 return false;
378 };
379 let provider = agent_guard.provider();
380 let key_opt = agent_guard.stream_options().api_key.clone();
381
382 self.tree_ui = None;
383 self.agent_state = AgentState::Processing;
384 self.status_message = Some("Switching branches...".to_string());
385
386 runtime_handle.spawn(async move {
387 let cx = Cx::for_request();
388
389 let from_id_for_event = pending
390 .old_leaf_id
391 .clone()
392 .unwrap_or_else(|| "root".to_string());
393 let to_id_for_event = pending
394 .new_leaf_id
395 .clone()
396 .unwrap_or_else(|| "root".to_string());
397
398 if let Some(manager) = extensions.clone() {
399 let cancelled = manager
400 .dispatch_cancellable_event(
401 ExtensionEventName::SessionBeforeSwitch,
402 Some(json!({
403 "fromId": from_id_for_event.clone(),
404 "toId": to_id_for_event.clone(),
405 "sessionId": pending.session_id.clone(),
406 })),
407 EXTENSION_EVENT_TIMEOUT_MS,
408 )
409 .await
410 .unwrap_or(false);
411 if cancelled {
412 let _ = crate::interactive::enqueue_pi_event(
413 &event_tx,
414 &asupersync::Cx::current().unwrap_or_else(asupersync::Cx::for_request),
415 PiMsg::System("Session switch cancelled by extension".to_string()),
416 )
417 .await;
418 return;
419 }
420 }
421
422 let summary_skipped =
423 summary_requested && key_opt.is_none() && !pending.entries_to_summarize.is_empty();
424 let summary_text = if !summary_requested || pending.entries_to_summarize.is_empty() {
425 None
426 } else if let Some(key) = key_opt.as_deref() {
427 match crate::compaction::summarize_entries(
428 &pending.entries_to_summarize,
429 provider,
430 key,
431 reserve_tokens,
432 custom_instructions.as_deref(),
433 )
434 .await
435 {
436 Ok(summary) => summary,
437 Err(err) => {
438 let _ = crate::interactive::enqueue_pi_event(
439 &event_tx,
440 &cx,
441 PiMsg::AgentError(format!("Branch summary failed: {err}")),
442 )
443 .await;
444 return;
445 }
446 }
447 } else {
448 None
449 };
450
451 let mut summary_entry_payload: Option<Value> = None;
452 let mut summary_entry_id: Option<String> = None;
453
454 let messages_for_agent = {
455 let mut guard = match session.lock(&cx).await {
456 Ok(guard) => guard,
457 Err(err) => {
458 let _ = crate::interactive::enqueue_pi_event(
459 &event_tx,
460 &cx,
461 PiMsg::AgentError(format!("Failed to lock session: {err}")),
462 )
463 .await;
464 return;
465 }
466 };
467
468 if let Some(target_id) = &pending.new_leaf_id {
469 if !guard.navigate_to(target_id) {
470 let _ = crate::interactive::enqueue_pi_event(
471 &event_tx,
472 &asupersync::Cx::current().unwrap_or_else(asupersync::Cx::for_request),
473 PiMsg::AgentError(format!("Branch target not found: {target_id}")),
474 )
475 .await;
476 return;
477 }
478 } else {
479 guard.reset_leaf();
480 }
481
482 if let Some(summary_text) = summary_text {
483 let summary_clone = summary_text.clone();
484 guard.append_branch_summary(
485 pending.summary_from_id.clone(),
486 summary_text,
487 None,
488 None,
489 );
490 summary_entry_id = guard.leaf_id.clone();
491 let mut summary_entry = serde_json::Map::new();
492 summary_entry.insert(
493 "type".to_string(),
494 Value::String("branch_summary".to_string()),
495 );
496 summary_entry.insert(
497 "fromId".to_string(),
498 Value::String(pending.summary_from_id.clone()),
499 );
500 summary_entry.insert("summary".to_string(), Value::String(summary_clone));
501 summary_entry.insert("fromHook".to_string(), Value::Bool(false));
502 summary_entry_payload = Some(Value::Object(summary_entry));
503 }
504
505 let _ = guard.save().await;
506 guard.to_messages_for_current_path()
507 };
508
509 {
510 let mut agent_guard = match agent.lock(&cx).await {
511 Ok(guard) => guard,
512 Err(err) => {
513 let _ = crate::interactive::enqueue_pi_event(
514 &event_tx,
515 &cx,
516 PiMsg::AgentError(format!("Failed to lock agent: {err}")),
517 )
518 .await;
519 return;
520 }
521 };
522 agent_guard.replace_messages(messages_for_agent);
523 }
524
525 let (messages, usage) = {
526 let guard = match session.lock(&cx).await {
527 Ok(guard) => guard,
528 Err(err) => {
529 let _ = crate::interactive::enqueue_pi_event(
530 &event_tx,
531 &cx,
532 PiMsg::AgentError(format!("Failed to lock session: {err}")),
533 )
534 .await;
535 return;
536 }
537 };
538 conversation_from_session(&guard)
539 };
540
541 let status = if summary_skipped {
542 Some(format!(
543 "Switched to {to_id_for_event} (no summary: missing API key)"
544 ))
545 } else {
546 Some(format!("Switched to {to_id_for_event}"))
547 };
548
549 let _ = crate::interactive::enqueue_pi_event(
550 &event_tx,
551 &asupersync::Cx::current().unwrap_or_else(asupersync::Cx::for_request),
552 PiMsg::ConversationReset {
553 messages,
554 usage,
555 status,
556 },
557 )
558 .await;
559
560 if let Some(text) = pending.editor_text {
561 let _ = crate::interactive::enqueue_pi_event(
562 &event_tx,
563 &asupersync::Cx::current().unwrap_or_else(asupersync::Cx::for_request),
564 PiMsg::SetEditorText(text),
565 )
566 .await;
567 }
568
569 if let Some(manager) = extensions {
570 let new_leaf_id = summary_entry_id
571 .clone()
572 .or_else(|| pending.new_leaf_id.clone());
573 let old_leaf_value = pending
574 .old_leaf_id
575 .clone()
576 .map_or(Value::Null, Value::String);
577 let new_leaf_value = new_leaf_id.clone().map_or(Value::Null, Value::String);
578 let mut tree_payload = serde_json::Map::new();
579 tree_payload.insert("newLeafId".to_string(), new_leaf_value);
580 tree_payload.insert("oldLeafId".to_string(), old_leaf_value);
581 if let Some(summary_entry) = summary_entry_payload {
582 tree_payload.insert("summaryEntry".to_string(), summary_entry);
583 }
584
585 let _ = manager
586 .dispatch_event(
587 ExtensionEventName::SessionSwitch,
588 Some(json!({
589 "fromId": from_id_for_event,
590 "toId": to_id_for_event,
591 "sessionId": pending.session_id,
592 })),
593 )
594 .await;
595 let _ = manager
596 .dispatch_event(
597 ExtensionEventName::SessionTree,
598 Some(Value::Object(tree_payload)),
599 )
600 .await;
601 }
602 });
603 true
604 }
605}