agentcrew-ai 0.8.13__py3-none-any.whl → 0.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. AgentCrew/__init__.py +1 -1
  2. AgentCrew/app.py +46 -634
  3. AgentCrew/main_docker.py +1 -30
  4. AgentCrew/modules/a2a/common/client/card_resolver.py +27 -8
  5. AgentCrew/modules/a2a/server.py +5 -0
  6. AgentCrew/modules/a2a/task_manager.py +1 -0
  7. AgentCrew/modules/agents/local_agent.py +2 -2
  8. AgentCrew/modules/chat/message/command_processor.py +33 -8
  9. AgentCrew/modules/chat/message/conversation.py +18 -1
  10. AgentCrew/modules/chat/message/handler.py +5 -1
  11. AgentCrew/modules/code_analysis/service.py +50 -7
  12. AgentCrew/modules/code_analysis/tool.py +9 -8
  13. AgentCrew/modules/console/completers.py +5 -1
  14. AgentCrew/modules/console/console_ui.py +23 -11
  15. AgentCrew/modules/console/conversation_browser/__init__.py +9 -0
  16. AgentCrew/modules/console/conversation_browser/browser.py +84 -0
  17. AgentCrew/modules/console/conversation_browser/browser_input_handler.py +279 -0
  18. AgentCrew/modules/console/{conversation_browser.py → conversation_browser/browser_ui.py} +249 -163
  19. AgentCrew/modules/console/conversation_handler.py +34 -1
  20. AgentCrew/modules/console/display_handlers.py +127 -7
  21. AgentCrew/modules/console/visual_mode/__init__.py +5 -0
  22. AgentCrew/modules/console/visual_mode/viewer.py +41 -0
  23. AgentCrew/modules/console/visual_mode/viewer_input_handler.py +315 -0
  24. AgentCrew/modules/console/visual_mode/viewer_ui.py +608 -0
  25. AgentCrew/modules/gui/components/command_handler.py +137 -29
  26. AgentCrew/modules/gui/components/menu_components.py +8 -7
  27. AgentCrew/modules/gui/themes/README.md +30 -14
  28. AgentCrew/modules/gui/themes/__init__.py +2 -1
  29. AgentCrew/modules/gui/themes/atom_light.yaml +1287 -0
  30. AgentCrew/modules/gui/themes/catppuccin.yaml +1276 -0
  31. AgentCrew/modules/gui/themes/dracula.yaml +1262 -0
  32. AgentCrew/modules/gui/themes/nord.yaml +1267 -0
  33. AgentCrew/modules/gui/themes/saigontech.yaml +1268 -0
  34. AgentCrew/modules/gui/themes/style_provider.py +78 -264
  35. AgentCrew/modules/gui/themes/theme_loader.py +379 -0
  36. AgentCrew/modules/gui/themes/unicorn.yaml +1276 -0
  37. AgentCrew/modules/gui/widgets/configs/global_settings.py +4 -4
  38. AgentCrew/modules/gui/widgets/history_sidebar.py +6 -1
  39. AgentCrew/modules/llm/constants.py +28 -9
  40. AgentCrew/modules/mcpclient/service.py +0 -1
  41. AgentCrew/modules/memory/base_service.py +13 -0
  42. AgentCrew/modules/memory/chroma_service.py +50 -0
  43. AgentCrew/setup.py +470 -0
  44. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/METADATA +1 -1
  45. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/RECORD +49 -40
  46. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/WHEEL +1 -1
  47. AgentCrew/modules/gui/themes/atom_light.py +0 -1365
  48. AgentCrew/modules/gui/themes/catppuccin.py +0 -1404
  49. AgentCrew/modules/gui/themes/dracula.py +0 -1372
  50. AgentCrew/modules/gui/themes/nord.py +0 -1365
  51. AgentCrew/modules/gui/themes/saigontech.py +0 -1359
  52. AgentCrew/modules/gui/themes/unicorn.py +0 -1372
  53. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/entry_points.txt +0 -0
  54. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/licenses/LICENSE +0 -0
  55. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,8 @@
1
- """Conversation browser with split-panel interface.
2
- Provides Rich-based UI for listing and loading conversations with preview.
3
- """
1
+ """Conversation browser UI rendering components."""
4
2
 
5
3
  from __future__ import annotations
6
4
 
7
- from typing import List, Dict, Any, Optional, Callable, Tuple, Tuple
5
+ from typing import List, Dict, Any, Optional, Callable, Tuple
8
6
  from datetime import datetime
9
7
 
10
8
  from rich.console import Console, Group
@@ -14,10 +12,11 @@ from rich.text import Text
14
12
  from rich.layout import Layout
15
13
  from rich.rule import Rule
16
14
  from rich.box import ROUNDED
15
+ from rich.live import Live
17
16
 
18
17
  from loguru import logger
19
18
 
20
- from .constants import (
19
+ from ..constants import (
21
20
  RICH_STYLE_YELLOW,
22
21
  RICH_STYLE_YELLOW_BOLD,
23
22
  RICH_STYLE_BLUE,
@@ -28,8 +27,8 @@ from .constants import (
28
27
  )
29
28
 
30
29
 
31
- class ConversationBrowser:
32
- """Interactive conversation browser with split-panel layout."""
30
+ class ConversationBrowserUI:
31
+ """Handles UI rendering for the conversation browser."""
33
32
 
34
33
  def __init__(
35
34
  self,
@@ -38,28 +37,125 @@ class ConversationBrowser:
38
37
  Callable[[str], List[Dict[str, Any]]]
39
38
  ] = None,
40
39
  ):
41
- """Initialize the conversation browser.
42
-
43
- Args:
44
- console: Rich console for rendering
45
- get_conversation_history: Optional callback to fetch full conversation history
46
- """
47
40
  self.console = console
48
41
  self.conversations: List[Dict[str, Any]] = []
42
+ self._all_conversations: List[Dict[str, Any]] = []
49
43
  self.selected_index = 0
50
44
  self.scroll_offset = 0
51
- self.max_list_items = 50
52
- self._running = False
53
45
  self._get_conversation_history = get_conversation_history
54
46
  self._preview_cache: Dict[str, Tuple[List[Dict[str, Any]], int]] = {}
55
- self._g_pressed = False
47
+ self.selected_items: set[int] = set()
48
+ self._live: Optional[Live] = None
49
+ self._layout: Optional[Layout] = None
50
+ self._search_query: str = ""
51
+ self._search_mode: bool = False
52
+
53
+ @property
54
+ def max_list_items(self) -> int:
55
+ return self.console.height - 9
56
56
 
57
57
  def set_conversations(self, conversations: List[Dict[str, Any]]):
58
58
  """Set the conversations list to browse."""
59
+ self._all_conversations = conversations
59
60
  self.conversations = conversations
60
61
  self.selected_index = 0
61
62
  self.scroll_offset = 0
62
63
  self._preview_cache.clear()
64
+ self.selected_items.clear()
65
+ self._search_query = ""
66
+ self._search_mode = False
67
+
68
+ @property
69
+ def search_mode(self) -> bool:
70
+ return self._search_mode
71
+
72
+ @property
73
+ def search_query(self) -> str:
74
+ return self._search_query
75
+
76
+ def start_search_mode(self):
77
+ """Enter search mode, preserving previous search query."""
78
+ self._search_mode = True
79
+
80
+ def exit_search_mode(self, clear_filter: bool = False):
81
+ """Exit search mode."""
82
+ self._search_mode = False
83
+ if clear_filter:
84
+ self._search_query = ""
85
+ self.conversations = self._all_conversations
86
+ self.selected_index = 0
87
+ self.scroll_offset = 0
88
+ self.selected_items.clear()
89
+
90
+ def update_search_query(self, query: str):
91
+ """Update search query and filter conversations."""
92
+ self._search_query = query
93
+ self._filter_conversations()
94
+
95
+ def append_search_char(self, char: str):
96
+ """Append a character to search query."""
97
+ self._search_query += char
98
+ self._filter_conversations()
99
+
100
+ def backspace_search(self):
101
+ """Remove last character from search query."""
102
+ if self._search_query:
103
+ self._search_query = self._search_query[:-1]
104
+ self._filter_conversations()
105
+
106
+ def _filter_conversations(self):
107
+ """Filter conversations based on search query."""
108
+ if not self._search_query:
109
+ self.conversations = self._all_conversations
110
+ else:
111
+ query_lower = self._search_query.lower()
112
+ self.conversations = [
113
+ c
114
+ for c in self._all_conversations
115
+ if query_lower in c.get("title", "").lower()
116
+ ]
117
+ self.selected_index = 0
118
+ self.scroll_offset = 0
119
+ self.selected_items.clear()
120
+
121
+ def toggle_selection(self, index: Optional[int] = None) -> bool:
122
+ """Toggle selection state of an item. Returns True if state changed."""
123
+ idx = index if index is not None else self.selected_index
124
+ if idx < 0 or idx >= len(self.conversations):
125
+ return False
126
+ if idx in self.selected_items:
127
+ self.selected_items.discard(idx)
128
+ else:
129
+ self.selected_items.add(idx)
130
+ return True
131
+
132
+ def clear_selections(self):
133
+ """Clear all selected items."""
134
+ self.selected_items.clear()
135
+
136
+ def get_selected_conversation_ids(self) -> List[str]:
137
+ """Get IDs of all selected conversations."""
138
+ ids = []
139
+ for idx in sorted(self.selected_items):
140
+ if 0 <= idx < len(self.conversations):
141
+ convo_id = self.conversations[idx].get("id")
142
+ if convo_id:
143
+ ids.append(convo_id)
144
+ return ids
145
+
146
+ def remove_conversations(self, indices: List[int]):
147
+ """Remove conversations at specified indices and update UI state."""
148
+ for idx in sorted(indices, reverse=True):
149
+ if 0 <= idx < len(self.conversations):
150
+ convo_id = self.conversations[idx].get("id")
151
+ if convo_id:
152
+ self._preview_cache.pop(convo_id, None)
153
+ del self.conversations[idx]
154
+ self.selected_items.clear()
155
+ if self.selected_index >= len(self.conversations):
156
+ self.selected_index = max(0, len(self.conversations) - 1)
157
+ if self.scroll_offset > 0 and self.scroll_offset >= len(self.conversations):
158
+ self.scroll_offset = max(0, len(self.conversations) - self.max_list_items)
63
159
 
64
160
  def _format_timestamp(self, timestamp) -> str:
65
161
  """Format timestamp for display."""
@@ -87,8 +183,12 @@ class ConversationBrowser:
87
183
  header_table.add_column("right", justify="right", ratio=1)
88
184
 
89
185
  left_text = Text()
90
- left_text.append("📚 ", style="bold")
186
+ left_text.append("\U0001f4da ", style="bold")
91
187
  left_text.append(f"{len(self.conversations)} ", style=RICH_STYLE_GREEN_BOLD)
188
+ if self._search_query:
189
+ left_text.append(
190
+ f"/ {len(self._all_conversations)} ", style=RICH_STYLE_GRAY
191
+ )
92
192
  left_text.append("conversations", style=RICH_STYLE_GRAY)
93
193
 
94
194
  center_text = Text()
@@ -134,7 +234,7 @@ class ConversationBrowser:
134
234
  )
135
235
  table.add_column("#", width=5, justify="right", no_wrap=True)
136
236
  table.add_column("Title", no_wrap=True, overflow="ellipsis")
137
- table.add_column("Date", width=16, justify="right", no_wrap=True)
237
+ table.add_column("Date", width=10, justify="right", no_wrap=True)
138
238
 
139
239
  visible_count = min(
140
240
  self.max_list_items, len(self.conversations) - self.scroll_offset
@@ -143,31 +243,53 @@ class ConversationBrowser:
143
243
  for i in range(visible_count):
144
244
  idx = self.scroll_offset + i
145
245
  convo = self.conversations[idx]
146
- is_selected = idx == self.selected_index
246
+ is_cursor = idx == self.selected_index
247
+ is_marked = idx in self.selected_items
147
248
 
148
249
  index_text = f"{idx + 1}"
149
- title = convo.get("title", "Untitled")
250
+ title = convo.get("title", "Untitled").replace("\n", " ")
150
251
  timestamp = self._format_timestamp(convo.get("timestamp"))
151
252
 
152
- if is_selected:
253
+ mark_indicator = "\u25cf " if is_marked else " "
254
+ cursor_indicator = "\u25b8" if is_cursor else " "
255
+
256
+ if is_cursor and is_marked:
257
+ table.add_row(
258
+ Text(index_text, style="bold magenta"),
259
+ Text(
260
+ f"{mark_indicator}{cursor_indicator}{title}",
261
+ style="bold magenta",
262
+ ),
263
+ Text(timestamp, style="magenta"),
264
+ )
265
+ elif is_cursor:
153
266
  table.add_row(
154
267
  Text(index_text, style=RICH_STYLE_GREEN_BOLD),
155
- Text(f"▸ {title}", style=RICH_STYLE_GREEN_BOLD),
268
+ Text(
269
+ f"{mark_indicator}{cursor_indicator}{title}",
270
+ style=RICH_STYLE_GREEN_BOLD,
271
+ ),
156
272
  Text(timestamp, style=RICH_STYLE_GREEN),
157
273
  )
274
+ elif is_marked:
275
+ table.add_row(
276
+ Text(index_text, style="magenta"),
277
+ Text(f"{mark_indicator} {title}", style="magenta"),
278
+ Text(timestamp, style="magenta"),
279
+ )
158
280
  else:
159
281
  table.add_row(
160
282
  Text(index_text, style=RICH_STYLE_GRAY),
161
- Text(f" {title}", style=RICH_STYLE_BLUE),
283
+ Text(f"{mark_indicator} {title}", style=RICH_STYLE_BLUE),
162
284
  Text(timestamp, style=RICH_STYLE_GRAY),
163
285
  )
164
286
 
165
287
  scroll_parts = []
166
288
  if self.scroll_offset > 0:
167
- scroll_parts.append(f"{self.scroll_offset}")
289
+ scroll_parts.append(f"\u2191{self.scroll_offset}")
168
290
  remaining = len(self.conversations) - self.scroll_offset - visible_count
169
291
  if remaining > 0:
170
- scroll_parts.append(f"{remaining}")
292
+ scroll_parts.append(f"\u2193{remaining}")
171
293
 
172
294
  subtitle = None
173
295
  if scroll_parts:
@@ -266,7 +388,7 @@ class ConversationBrowser:
266
388
  preview_lines = []
267
389
 
268
390
  title = convo.get("title", "Untitled")
269
- preview_lines.append(Text(f"📌 {title}", style=RICH_STYLE_YELLOW_BOLD))
391
+ preview_lines.append(Text(f"\U0001f4cc {title}", style=RICH_STYLE_YELLOW_BOLD))
270
392
 
271
393
  convo_id = convo.get("id", "unknown")
272
394
  timestamp = self._format_timestamp(convo.get("timestamp"))
@@ -275,7 +397,7 @@ class ConversationBrowser:
275
397
  meta_table.add_column("key", style=RICH_STYLE_GRAY)
276
398
  meta_table.add_column("value", style=RICH_STYLE_WHITE)
277
399
 
278
- display_id = convo_id[:24] + "" if len(convo_id) > 24 else convo_id
400
+ display_id = convo_id[:24] + "\u2026" if len(convo_id) > 24 else convo_id
279
401
  meta_table.add_row("ID:", display_id)
280
402
  meta_table.add_row("Created:", timestamp)
281
403
 
@@ -297,13 +419,13 @@ class ConversationBrowser:
297
419
  max_content_len = 120
298
420
  content_display = content.replace("\n", " ").strip()
299
421
  if len(content_display) > max_content_len:
300
- content_display = content_display[:max_content_len] + ""
422
+ content_display = content_display[:max_content_len] + "\u2026"
301
423
 
302
424
  preview_lines.append(Text(""))
303
425
 
304
426
  if role == "user":
305
427
  user_header = Text()
306
- user_header.append("👤 ", style="bold")
428
+ user_header.append("\U0001f464 ", style="bold")
307
429
  user_header.append("User", style=RICH_STYLE_BLUE)
308
430
  preview_lines.append(user_header)
309
431
  preview_lines.append(
@@ -311,7 +433,7 @@ class ConversationBrowser:
311
433
  )
312
434
  else:
313
435
  assistant_header = Text()
314
- assistant_header.append("🤖 ", style="bold")
436
+ assistant_header.append("\U0001f916 ", style="bold")
315
437
  assistant_header.append("Assistant", style=RICH_STYLE_GREEN)
316
438
  preview_lines.append(assistant_header)
317
439
  preview_lines.append(
@@ -326,7 +448,9 @@ class ConversationBrowser:
326
448
  preview_lines.append(Text(""))
327
449
  preview_lines.append(Rule(style=RICH_STYLE_GRAY))
328
450
  preview_lines.append(
329
- Text(f" … and {remaining} more messages", style=RICH_STYLE_GRAY)
451
+ Text(
452
+ f" \u2026 and {remaining} more messages", style=RICH_STYLE_GRAY
453
+ )
330
454
  )
331
455
  else:
332
456
  basic_preview = convo.get("preview", "No preview available")
@@ -342,6 +466,9 @@ class ConversationBrowser:
342
466
 
343
467
  def _create_help_panel(self) -> Panel:
344
468
  """Create the help panel with keyboard shortcuts."""
469
+ if self._search_mode:
470
+ return self._create_search_bar()
471
+
345
472
  help_table = Table(
346
473
  show_header=False,
347
474
  box=None,
@@ -353,9 +480,9 @@ class ConversationBrowser:
353
480
  help_table.add_column("section3", justify="right", ratio=1)
354
481
 
355
482
  nav_text = Text()
356
- nav_text.append("↑/k ", style=RICH_STYLE_GREEN_BOLD)
483
+ nav_text.append("\u2191/k ", style=RICH_STYLE_GREEN_BOLD)
357
484
  nav_text.append("Up ", style=RICH_STYLE_GRAY)
358
- nav_text.append("↓/j ", style=RICH_STYLE_GREEN_BOLD)
485
+ nav_text.append("\u2193/j ", style=RICH_STYLE_GREEN_BOLD)
359
486
  nav_text.append("Down ", style=RICH_STYLE_GRAY)
360
487
  nav_text.append("gg ", style=RICH_STYLE_GREEN_BOLD)
361
488
  nav_text.append("Top ", style=RICH_STYLE_GRAY)
@@ -365,14 +492,20 @@ class ConversationBrowser:
365
492
  action_text = Text()
366
493
  action_text.append("Enter/l ", style=RICH_STYLE_GREEN_BOLD)
367
494
  action_text.append("Load ", style=RICH_STYLE_GRAY)
368
- action_text.append("Esc/q ", style=RICH_STYLE_GREEN_BOLD)
369
- action_text.append("Exit", style=RICH_STYLE_GRAY)
495
+ action_text.append("v ", style=RICH_STYLE_GREEN_BOLD)
496
+ action_text.append("Select ", style=RICH_STYLE_GRAY)
497
+ action_text.append("dd ", style=RICH_STYLE_GREEN_BOLD)
498
+ action_text.append("Delete", style=RICH_STYLE_GRAY)
370
499
 
371
500
  page_text = Text()
372
- page_text.append("PgUp/Ctrl+U ", style=RICH_STYLE_GREEN_BOLD)
373
- page_text.append("Page Up ", style=RICH_STYLE_GRAY)
374
- page_text.append("PgDn/Ctrl+D ", style=RICH_STYLE_GREEN_BOLD)
375
- page_text.append("Page Down", style=RICH_STYLE_GRAY)
501
+ page_text.append("/ ", style=RICH_STYLE_GREEN_BOLD)
502
+ page_text.append("Search ", style=RICH_STYLE_GRAY)
503
+ page_text.append("Esc/q ", style=RICH_STYLE_GREEN_BOLD)
504
+ page_text.append("Exit", style=RICH_STYLE_GRAY)
505
+ if self.selected_items:
506
+ page_text.append(
507
+ f" ({len(self.selected_items)} selected)", style="magenta"
508
+ )
376
509
 
377
510
  help_table.add_row(nav_text, action_text, page_text)
378
511
 
@@ -382,8 +515,38 @@ class ConversationBrowser:
382
515
  box=ROUNDED,
383
516
  )
384
517
 
385
- def _render(self):
386
- """Render the split-panel interface."""
518
+ def _create_search_bar(self) -> Panel:
519
+ """Create the search bar panel."""
520
+ search_text = Text()
521
+ search_text.append("/ ", style=RICH_STYLE_GREEN_BOLD)
522
+ search_text.append(self._search_query, style=RICH_STYLE_WHITE)
523
+ search_text.append("\u2588", style="blink bold cyan")
524
+
525
+ help_text = Text()
526
+ help_text.append(" Enter ", style=RICH_STYLE_GREEN_BOLD)
527
+ help_text.append("Confirm ", style=RICH_STYLE_GRAY)
528
+ help_text.append("Esc ", style=RICH_STYLE_GREEN_BOLD)
529
+ help_text.append("Cancel", style=RICH_STYLE_GRAY)
530
+
531
+ search_table = Table(
532
+ show_header=False,
533
+ box=None,
534
+ padding=0,
535
+ expand=True,
536
+ )
537
+ search_table.add_column("search", justify="left", ratio=2)
538
+ search_table.add_column("help", justify="right", ratio=1)
539
+ search_table.add_row(search_text, help_text)
540
+
541
+ return Panel(
542
+ search_table,
543
+ border_style="cyan",
544
+ box=ROUNDED,
545
+ title=Text("Search ", style=RICH_STYLE_YELLOW_BOLD),
546
+ )
547
+
548
+ def _create_layout(self) -> Layout:
549
+ """Create the layout structure."""
387
550
  layout = Layout()
388
551
  layout.split_column(
389
552
  Layout(name="header", size=3),
@@ -395,16 +558,52 @@ class ConversationBrowser:
395
558
  Layout(name="list", ratio=1, minimum_size=40),
396
559
  Layout(name="preview", ratio=1, minimum_size=40),
397
560
  )
398
-
399
- layout["header"].update(self._create_header())
400
- layout["list"].update(self._create_list_panel())
401
- layout["preview"].update(self._create_preview_panel())
402
- layout["help"].update(self._create_help_panel())
403
-
561
+ return layout
562
+
563
+ def _update_layout(self):
564
+ """Update layout panels with current content."""
565
+ if self._layout is None:
566
+ return
567
+ self._layout["header"].update(self._create_header())
568
+ self._layout["list"].update(self._create_list_panel())
569
+ self._layout["preview"].update(self._create_preview_panel())
570
+ self._layout["help"].update(self._create_help_panel())
571
+
572
+ def start_live(self):
573
+ """Start live display mode."""
404
574
  self.console.clear()
405
- self.console.print(layout)
406
-
407
- def _handle_navigation(self, direction: str) -> bool:
575
+ self._layout = self._create_layout()
576
+ self._update_layout()
577
+ self._live = Live(
578
+ self._layout,
579
+ console=self.console,
580
+ refresh_per_second=10,
581
+ screen=True,
582
+ )
583
+ self._live.start()
584
+
585
+ def stop_live(self):
586
+ """Stop live display mode."""
587
+ if self._live:
588
+ self._live.stop()
589
+ self._live = None
590
+ self._layout = None
591
+
592
+ def render(self):
593
+ """Update the display with current state."""
594
+ if self._live and self._layout:
595
+ self._update_layout()
596
+ self._live.refresh()
597
+ else:
598
+ layout = self._create_layout()
599
+ layout["header"].update(self._create_header())
600
+ layout["list"].update(self._create_list_panel())
601
+ layout["preview"].update(self._create_preview_panel())
602
+ layout["help"].update(self._create_help_panel())
603
+ self.console.clear()
604
+ self.console.print(layout)
605
+
606
+ def handle_navigation(self, direction: str) -> bool:
408
607
  """Handle navigation (up/down/top/bottom). Returns True if selection changed."""
409
608
  if not self.conversations:
410
609
  return False
@@ -442,116 +641,3 @@ class ConversationBrowser:
442
641
  def get_selected_conversation_index(self) -> int:
443
642
  """Get the 1-based index of the currently selected conversation."""
444
643
  return self.selected_index + 1
445
-
446
- def show(self) -> Optional[str]:
447
- """Show the interactive conversation browser.
448
-
449
- Returns:
450
- The ID of the selected conversation, or None if cancelled.
451
- """
452
- if not self.conversations:
453
- self.console.print(
454
- Text("No conversations available.", style=RICH_STYLE_YELLOW)
455
- )
456
- return None
457
-
458
- self._running = True
459
- self._g_pressed = False
460
- selected_id = None
461
-
462
- from prompt_toolkit import PromptSession
463
- from prompt_toolkit.key_binding import KeyBindings
464
- from prompt_toolkit.keys import Keys
465
-
466
- self._render()
467
-
468
- kb = KeyBindings()
469
-
470
- @kb.add(Keys.Up)
471
- @kb.add("k")
472
- def _(event):
473
- self._g_pressed = False
474
- if self._handle_navigation("up"):
475
- self._render()
476
-
477
- @kb.add(Keys.Down)
478
- @kb.add("j")
479
- def _(event):
480
- self._g_pressed = False
481
- if self._handle_navigation("down"):
482
- self._render()
483
-
484
- @kb.add(Keys.ControlP)
485
- def _(event):
486
- self._g_pressed = False
487
- if self._handle_navigation("up"):
488
- self._render()
489
-
490
- @kb.add(Keys.ControlN)
491
- def _(event):
492
- self._g_pressed = False
493
- if self._handle_navigation("down"):
494
- self._render()
495
-
496
- @kb.add("g")
497
- def _(event):
498
- if self._g_pressed:
499
- self._g_pressed = False
500
- if self._handle_navigation("top"):
501
- self._render()
502
- else:
503
- self._g_pressed = True
504
-
505
- @kb.add("G")
506
- def _(event):
507
- self._g_pressed = False
508
- if self._handle_navigation("bottom"):
509
- self._render()
510
-
511
- @kb.add(Keys.ControlU)
512
- @kb.add(Keys.PageUp)
513
- def _(event):
514
- self._g_pressed = False
515
- if self._handle_navigation("page_up"):
516
- self._render()
517
-
518
- @kb.add(Keys.ControlD)
519
- @kb.add(Keys.PageDown)
520
- def _(event):
521
- self._g_pressed = False
522
- if self._handle_navigation("page_down"):
523
- self._render()
524
-
525
- @kb.add(Keys.Enter)
526
- @kb.add("l")
527
- def _(event):
528
- nonlocal selected_id
529
- self._g_pressed = False
530
- selected_id = self.get_selected_conversation_id()
531
- event.app.exit()
532
-
533
- @kb.add(Keys.Escape)
534
- @kb.add("q")
535
- def _(event):
536
- self._g_pressed = False
537
- event.app.exit()
538
-
539
- @kb.add(Keys.ControlC)
540
- def _(event):
541
- self._g_pressed = False
542
- event.app.exit()
543
-
544
- @kb.add(Keys.Any)
545
- def _(event):
546
- self._g_pressed = False
547
-
548
- try:
549
- session = PromptSession(key_bindings=kb)
550
- session.prompt("")
551
- except (KeyboardInterrupt, EOFError):
552
- pass
553
- except Exception as e:
554
- logger.error(f"Error in conversation browser: {e}")
555
-
556
- self._running = False
557
- return selected_id
@@ -4,7 +4,7 @@ Manages conversation loading, listing, and display functionality.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
7
- from typing import List, Dict, Any
7
+ from typing import List, Dict, Any, Optional
8
8
  from rich.text import Text
9
9
 
10
10
  from .constants import RICH_STYLE_YELLOW, RICH_STYLE_RED
@@ -20,10 +20,16 @@ class ConversationHandler:
20
20
 
21
21
  def __init__(self, console_ui: ConsoleUI):
22
22
  """Initialize the conversation handler."""
23
+ self._console_ui = console_ui
23
24
  self.console = console_ui.console
24
25
  self.display_handlers = console_ui.display_handlers
25
26
  self._cached_conversations = []
26
27
 
28
+ @property
29
+ def _message_handler(self):
30
+ """Get message handler from console UI."""
31
+ return self._console_ui.message_handler
32
+
27
33
  def handle_load_conversation(self, load_arg: str, message_handler):
28
34
  """
29
35
  Handle loading a conversation by number or ID.
@@ -92,3 +98,30 @@ class ConversationHandler:
92
98
  def get_cached_conversations(self):
93
99
  """Get the cached conversations list."""
94
100
  return self._cached_conversations
101
+
102
+ def get_conversation_history(
103
+ self, conversation_id: str
104
+ ) -> Optional[List[Dict[str, Any]]]:
105
+ """Get conversation history for preview in browser."""
106
+ if self._message_handler.persistent_service:
107
+ return self._message_handler.persistent_service.get_conversation_history(
108
+ conversation_id
109
+ )
110
+ return None
111
+
112
+ def delete_conversations(self, conversation_ids: List[str]) -> bool:
113
+ """Delete conversations by their IDs.
114
+
115
+ Args:
116
+ conversation_ids: List of conversation IDs to delete
117
+
118
+ Returns:
119
+ True if all deletions were successful
120
+ """
121
+ if not conversation_ids:
122
+ return False
123
+ success = True
124
+ for convo_id in conversation_ids:
125
+ if not self._message_handler.delete_conversation_by_id(convo_id):
126
+ success = False
127
+ return success