agentcrew-ai 0.8.12__py3-none-any.whl → 0.8.13__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 (38) hide show
  1. AgentCrew/__init__.py +1 -1
  2. AgentCrew/main.py +55 -3
  3. AgentCrew/modules/agents/local_agent.py +25 -0
  4. AgentCrew/modules/code_analysis/__init__.py +8 -0
  5. AgentCrew/modules/code_analysis/parsers/__init__.py +67 -0
  6. AgentCrew/modules/code_analysis/parsers/base.py +93 -0
  7. AgentCrew/modules/code_analysis/parsers/cpp_parser.py +127 -0
  8. AgentCrew/modules/code_analysis/parsers/csharp_parser.py +162 -0
  9. AgentCrew/modules/code_analysis/parsers/generic_parser.py +63 -0
  10. AgentCrew/modules/code_analysis/parsers/go_parser.py +154 -0
  11. AgentCrew/modules/code_analysis/parsers/java_parser.py +103 -0
  12. AgentCrew/modules/code_analysis/parsers/javascript_parser.py +268 -0
  13. AgentCrew/modules/code_analysis/parsers/kotlin_parser.py +84 -0
  14. AgentCrew/modules/code_analysis/parsers/php_parser.py +107 -0
  15. AgentCrew/modules/code_analysis/parsers/python_parser.py +60 -0
  16. AgentCrew/modules/code_analysis/parsers/ruby_parser.py +46 -0
  17. AgentCrew/modules/code_analysis/parsers/rust_parser.py +72 -0
  18. AgentCrew/modules/code_analysis/service.py +231 -897
  19. AgentCrew/modules/command_execution/constants.py +2 -2
  20. AgentCrew/modules/console/confirmation_handler.py +4 -4
  21. AgentCrew/modules/console/console_ui.py +20 -1
  22. AgentCrew/modules/console/conversation_browser.py +557 -0
  23. AgentCrew/modules/console/diff_display.py +22 -51
  24. AgentCrew/modules/console/display_handlers.py +22 -22
  25. AgentCrew/modules/console/tool_display.py +4 -6
  26. AgentCrew/modules/file_editing/service.py +8 -8
  27. AgentCrew/modules/file_editing/tool.py +65 -67
  28. AgentCrew/modules/gui/components/tool_handlers.py +0 -2
  29. AgentCrew/modules/gui/widgets/diff_widget.py +30 -61
  30. AgentCrew/modules/llm/constants.py +5 -5
  31. AgentCrew/modules/memory/context_persistent.py +1 -0
  32. AgentCrew/modules/memory/tool.py +1 -1
  33. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/METADATA +1 -1
  34. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/RECORD +38 -24
  35. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/WHEEL +1 -1
  36. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/entry_points.txt +0 -0
  37. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/licenses/LICENSE +0 -0
  38. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ definitions for the CommandExecutionService.
10
10
  # ==============================================================================
11
11
 
12
12
  # Maximum number of commands that can run concurrently (application-wide)
13
- MAX_CONCURRENT_COMMANDS = 3
13
+ MAX_CONCURRENT_COMMANDS = 10
14
14
 
15
15
  # Maximum lifetime for a single command execution (seconds)
16
16
  MAX_COMMAND_LIFETIME = 600
@@ -19,7 +19,7 @@ MAX_COMMAND_LIFETIME = 600
19
19
  MAX_OUTPUT_LINES = 300
20
20
 
21
21
  # Maximum number of commands allowed per minute (application-wide rate limit)
22
- MAX_COMMANDS_PER_MINUTE = 10
22
+ MAX_COMMANDS_PER_MINUTE = 50
23
23
 
24
24
  # Default timeout for command execution (seconds)
25
25
  DEFAULT_TIMEOUT = 5
@@ -125,7 +125,7 @@ class ConfirmationHandler:
125
125
 
126
126
  self.input_handler._start_input_thread()
127
127
 
128
- def _display_write_or_edit_file_diff(self, tool_use, file_path, blocks_text):
128
+ def _display_write_or_edit_file_diff(self, tool_use, file_path, blocks):
129
129
  """Display split diff view for write_or_edit_file tool."""
130
130
  header = Text("📝 File Edit ", style=RICH_STYLE_YELLOW)
131
131
  header.append(file_path, style=RICH_STYLE_BLUE_BOLD)
@@ -135,15 +135,15 @@ class ConfirmationHandler:
135
135
  Panel(header, box=HORIZONTALS, border_style=RICH_STYLE_YELLOW)
136
136
  )
137
137
 
138
- blocks = DiffDisplay.parse_search_replace_blocks(blocks_text)
138
+ parsed_blocks = DiffDisplay.parse_search_replace_blocks(blocks)
139
139
 
140
- if not blocks:
140
+ if not parsed_blocks:
141
141
  self.console.print(
142
142
  Text("No valid search/replace blocks found", style=RICH_STYLE_RED)
143
143
  )
144
144
  return
145
145
 
146
- for block in blocks:
146
+ for block in parsed_blocks:
147
147
  diff_table = DiffDisplay.create_split_diff_table(
148
148
  block["search"], block["replace"], max_width=self.console.width - 4
149
149
  )
@@ -71,6 +71,14 @@ class ConsoleUI(Observer):
71
71
  self.conversation_handler = ConversationHandler(self)
72
72
  self.command_handlers = CommandHandlers(self)
73
73
 
74
+ def _get_conversation_history(self, conversation_id: str):
75
+ """Get conversation history for preview in browser."""
76
+ if self.message_handler.persistent_service:
77
+ return self.message_handler.persistent_service.get_conversation_history(
78
+ conversation_id
79
+ )
80
+ return None
81
+
74
82
  def listen(self, event: str, data: Any = None):
75
83
  """
76
84
  Update method required by the Observer interface. Handles events from the MessageHandler.
@@ -454,7 +462,18 @@ class ConsoleUI(Observer):
454
462
  self.conversation_handler.update_cached_conversations(
455
463
  conversations
456
464
  )
457
- self.display_handlers.display_conversations(conversations)
465
+ self.input_handler._stop_input_thread()
466
+ try:
467
+ selected_id = self.display_handlers.display_conversations(
468
+ conversations,
469
+ get_history_callback=self._get_conversation_history,
470
+ )
471
+ if selected_id:
472
+ self.conversation_handler.handle_load_conversation(
473
+ selected_id, self.message_handler
474
+ )
475
+ finally:
476
+ self.input_handler._start_input_thread()
458
477
  continue
459
478
 
460
479
  # Handle load command directly
@@ -0,0 +1,557 @@
1
+ """Conversation browser with split-panel interface.
2
+ Provides Rich-based UI for listing and loading conversations with preview.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import List, Dict, Any, Optional, Callable, Tuple, Tuple
8
+ from datetime import datetime
9
+
10
+ from rich.console import Console, Group
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+ from rich.layout import Layout
15
+ from rich.rule import Rule
16
+ from rich.box import ROUNDED
17
+
18
+ from loguru import logger
19
+
20
+ from .constants import (
21
+ RICH_STYLE_YELLOW,
22
+ RICH_STYLE_YELLOW_BOLD,
23
+ RICH_STYLE_BLUE,
24
+ RICH_STYLE_GREEN_BOLD,
25
+ RICH_STYLE_GREEN,
26
+ RICH_STYLE_GRAY,
27
+ RICH_STYLE_WHITE,
28
+ )
29
+
30
+
31
+ class ConversationBrowser:
32
+ """Interactive conversation browser with split-panel layout."""
33
+
34
+ def __init__(
35
+ self,
36
+ console: Console,
37
+ get_conversation_history: Optional[
38
+ Callable[[str], List[Dict[str, Any]]]
39
+ ] = None,
40
+ ):
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
+ self.console = console
48
+ self.conversations: List[Dict[str, Any]] = []
49
+ self.selected_index = 0
50
+ self.scroll_offset = 0
51
+ self.max_list_items = 50
52
+ self._running = False
53
+ self._get_conversation_history = get_conversation_history
54
+ self._preview_cache: Dict[str, Tuple[List[Dict[str, Any]], int]] = {}
55
+ self._g_pressed = False
56
+
57
+ def set_conversations(self, conversations: List[Dict[str, Any]]):
58
+ """Set the conversations list to browse."""
59
+ self.conversations = conversations
60
+ self.selected_index = 0
61
+ self.scroll_offset = 0
62
+ self._preview_cache.clear()
63
+
64
+ def _format_timestamp(self, timestamp) -> str:
65
+ """Format timestamp for display."""
66
+ if isinstance(timestamp, (int, float)):
67
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M")
68
+ if isinstance(timestamp, str):
69
+ try:
70
+ dt = datetime.fromisoformat(timestamp)
71
+ return dt.strftime("%Y-%m-%d %H:%M")
72
+ except (ValueError, TypeError):
73
+ return timestamp
74
+ return str(timestamp) if timestamp else "Unknown"
75
+
76
+ def _create_header(self) -> Panel:
77
+ """Create the header panel with title and info."""
78
+ header_table = Table(
79
+ show_header=False,
80
+ show_edge=False,
81
+ expand=True,
82
+ box=None,
83
+ padding=0,
84
+ )
85
+ header_table.add_column("left", justify="left", ratio=1)
86
+ header_table.add_column("center", justify="center", ratio=2)
87
+ header_table.add_column("right", justify="right", ratio=1)
88
+
89
+ left_text = Text()
90
+ left_text.append("📚 ", style="bold")
91
+ left_text.append(f"{len(self.conversations)} ", style=RICH_STYLE_GREEN_BOLD)
92
+ left_text.append("conversations", style=RICH_STYLE_GRAY)
93
+
94
+ center_text = Text()
95
+ center_text.append("Conversation History", style=RICH_STYLE_YELLOW_BOLD)
96
+
97
+ right_text = Text()
98
+ if self.conversations:
99
+ right_text.append(f"{self.selected_index + 1}", style=RICH_STYLE_GREEN_BOLD)
100
+ right_text.append(f"/{len(self.conversations)}", style=RICH_STYLE_GRAY)
101
+
102
+ header_table.add_row(left_text, center_text, right_text)
103
+
104
+ return Panel(
105
+ header_table,
106
+ border_style="cyan",
107
+ box=ROUNDED,
108
+ padding=(0, 1),
109
+ )
110
+
111
+ def _create_list_panel(self, panel_height: Optional[int] = None) -> Panel:
112
+ """Create the left panel with conversation list."""
113
+ if not self.conversations:
114
+ empty_content = Group(
115
+ Text(""),
116
+ Text(" No conversations found", style=RICH_STYLE_GRAY),
117
+ Text(""),
118
+ Text(" Start chatting to create one!", style=RICH_STYLE_YELLOW),
119
+ )
120
+ return Panel(
121
+ empty_content,
122
+ title=Text("Conversations ", style=RICH_STYLE_YELLOW_BOLD),
123
+ border_style="blue",
124
+ box=ROUNDED,
125
+ )
126
+
127
+ table = Table(
128
+ show_header=True,
129
+ show_footer=False,
130
+ expand=True,
131
+ box=None,
132
+ padding=(0, 1),
133
+ header_style=RICH_STYLE_YELLOW_BOLD,
134
+ )
135
+ table.add_column("#", width=5, justify="right", no_wrap=True)
136
+ table.add_column("Title", no_wrap=True, overflow="ellipsis")
137
+ table.add_column("Date", width=16, justify="right", no_wrap=True)
138
+
139
+ visible_count = min(
140
+ self.max_list_items, len(self.conversations) - self.scroll_offset
141
+ )
142
+
143
+ for i in range(visible_count):
144
+ idx = self.scroll_offset + i
145
+ convo = self.conversations[idx]
146
+ is_selected = idx == self.selected_index
147
+
148
+ index_text = f"{idx + 1}"
149
+ title = convo.get("title", "Untitled")
150
+ timestamp = self._format_timestamp(convo.get("timestamp"))
151
+
152
+ if is_selected:
153
+ table.add_row(
154
+ Text(index_text, style=RICH_STYLE_GREEN_BOLD),
155
+ Text(f"▸ {title}", style=RICH_STYLE_GREEN_BOLD),
156
+ Text(timestamp, style=RICH_STYLE_GREEN),
157
+ )
158
+ else:
159
+ table.add_row(
160
+ Text(index_text, style=RICH_STYLE_GRAY),
161
+ Text(f" {title}", style=RICH_STYLE_BLUE),
162
+ Text(timestamp, style=RICH_STYLE_GRAY),
163
+ )
164
+
165
+ scroll_parts = []
166
+ if self.scroll_offset > 0:
167
+ scroll_parts.append(f"↑{self.scroll_offset}")
168
+ remaining = len(self.conversations) - self.scroll_offset - visible_count
169
+ if remaining > 0:
170
+ scroll_parts.append(f"↓{remaining}")
171
+
172
+ subtitle = None
173
+ if scroll_parts:
174
+ subtitle = Text(" ".join(scroll_parts), style=RICH_STYLE_GRAY)
175
+
176
+ return Panel(
177
+ table,
178
+ title=Text("Conversations ", style=RICH_STYLE_YELLOW_BOLD),
179
+ subtitle=subtitle,
180
+ border_style="blue",
181
+ box=ROUNDED,
182
+ )
183
+
184
+ def _get_conversation_preview_messages(
185
+ self, convo_id: str
186
+ ) -> tuple[List[Dict[str, Any]], int]:
187
+ """Get first 4 user-assistant exchanges for preview.
188
+
189
+ Returns:
190
+ Tuple of (preview_messages, total_filtered_messages)
191
+ """
192
+ if convo_id in self._preview_cache:
193
+ return self._preview_cache[convo_id]
194
+
195
+ if not self._get_conversation_history:
196
+ return [], 0
197
+
198
+ try:
199
+ history = self._get_conversation_history(convo_id)
200
+ if not history:
201
+ return [], 0
202
+
203
+ all_messages = []
204
+ for msg in history:
205
+ if not isinstance(msg, dict):
206
+ continue
207
+ role = msg.get("role")
208
+ if role in ["user", "assistant"]:
209
+ content = msg.get("content", "")
210
+ if isinstance(content, str) and content.strip():
211
+ if content.startswith("Memories related to the user request:"):
212
+ continue
213
+ if content.startswith("Content of "):
214
+ continue
215
+ all_messages.append({"role": role, "content": content})
216
+ elif isinstance(content, list):
217
+ text_content = ""
218
+ for block in content:
219
+ if isinstance(block, dict) and block.get("type") == "text":
220
+ text_content = block.get("text", "")
221
+ break
222
+ if text_content.strip():
223
+ if text_content.startswith(
224
+ "Memories related to the user request:"
225
+ ):
226
+ continue
227
+ if text_content.startswith("Content of "):
228
+ continue
229
+ all_messages.append({"role": role, "content": text_content})
230
+
231
+ preview_messages = []
232
+ exchanges = 0
233
+ max_exchanges = 4
234
+
235
+ for msg in all_messages:
236
+ preview_messages.append(msg)
237
+ if msg.get("role") == "assistant":
238
+ exchanges += 1
239
+ if exchanges >= max_exchanges:
240
+ break
241
+
242
+ total = len(all_messages)
243
+ result = (preview_messages, total)
244
+ self._preview_cache[convo_id] = result
245
+ return result
246
+
247
+ except Exception as e:
248
+ logger.warning(f"Error fetching conversation preview: {e}")
249
+ return [], 0
250
+
251
+ def _create_preview_panel(self, panel_height: Optional[int] = None) -> Panel:
252
+ """Create the right panel with conversation preview."""
253
+ if not self.conversations or self.selected_index >= len(self.conversations):
254
+ empty_content = Group(
255
+ Text(""),
256
+ Text(" Select a conversation to preview", style=RICH_STYLE_GRAY),
257
+ )
258
+ return Panel(
259
+ empty_content,
260
+ title=Text("Preview ", style=RICH_STYLE_YELLOW_BOLD),
261
+ border_style="green",
262
+ box=ROUNDED,
263
+ )
264
+
265
+ convo = self.conversations[self.selected_index]
266
+ preview_lines = []
267
+
268
+ title = convo.get("title", "Untitled")
269
+ preview_lines.append(Text(f"📌 {title}", style=RICH_STYLE_YELLOW_BOLD))
270
+
271
+ convo_id = convo.get("id", "unknown")
272
+ timestamp = self._format_timestamp(convo.get("timestamp"))
273
+
274
+ meta_table = Table(show_header=False, box=None, padding=0, expand=True)
275
+ meta_table.add_column("key", style=RICH_STYLE_GRAY)
276
+ meta_table.add_column("value", style=RICH_STYLE_WHITE)
277
+
278
+ display_id = convo_id[:24] + "…" if len(convo_id) > 24 else convo_id
279
+ meta_table.add_row("ID:", display_id)
280
+ meta_table.add_row("Created:", timestamp)
281
+
282
+ preview_lines.append(Text(""))
283
+ preview_lines.append(meta_table)
284
+ preview_lines.append(Text(""))
285
+ preview_lines.append(Rule(title="Messages", style=RICH_STYLE_GRAY))
286
+
287
+ messages, total_messages = self._get_conversation_preview_messages(convo_id)
288
+
289
+ if messages:
290
+ exchange_count = 0
291
+ i = 0
292
+ while i < len(messages) and exchange_count < 4:
293
+ msg = messages[i]
294
+ role = msg.get("role", "unknown")
295
+ content = msg.get("content", "")
296
+
297
+ max_content_len = 120
298
+ content_display = content.replace("\n", " ").strip()
299
+ if len(content_display) > max_content_len:
300
+ content_display = content_display[:max_content_len] + "…"
301
+
302
+ preview_lines.append(Text(""))
303
+
304
+ if role == "user":
305
+ user_header = Text()
306
+ user_header.append("👤 ", style="bold")
307
+ user_header.append("User", style=RICH_STYLE_BLUE)
308
+ preview_lines.append(user_header)
309
+ preview_lines.append(
310
+ Text(f" {content_display}", style=RICH_STYLE_WHITE)
311
+ )
312
+ else:
313
+ assistant_header = Text()
314
+ assistant_header.append("🤖 ", style="bold")
315
+ assistant_header.append("Assistant", style=RICH_STYLE_GREEN)
316
+ preview_lines.append(assistant_header)
317
+ preview_lines.append(
318
+ Text(f" {content_display}", style=RICH_STYLE_WHITE)
319
+ )
320
+ exchange_count += 1
321
+
322
+ i += 1
323
+
324
+ remaining = total_messages - len(messages)
325
+ if remaining > 0:
326
+ preview_lines.append(Text(""))
327
+ preview_lines.append(Rule(style=RICH_STYLE_GRAY))
328
+ preview_lines.append(
329
+ Text(f" … and {remaining} more messages", style=RICH_STYLE_GRAY)
330
+ )
331
+ else:
332
+ basic_preview = convo.get("preview", "No preview available")
333
+ preview_lines.append(Text(""))
334
+ preview_lines.append(Text(f" {basic_preview}", style=RICH_STYLE_WHITE))
335
+
336
+ return Panel(
337
+ Group(*preview_lines),
338
+ title=Text("Preview ", style=RICH_STYLE_YELLOW_BOLD),
339
+ border_style="green",
340
+ box=ROUNDED,
341
+ )
342
+
343
+ def _create_help_panel(self) -> Panel:
344
+ """Create the help panel with keyboard shortcuts."""
345
+ help_table = Table(
346
+ show_header=False,
347
+ box=None,
348
+ padding=0,
349
+ expand=True,
350
+ )
351
+ help_table.add_column("section1", justify="left", ratio=1)
352
+ help_table.add_column("section2", justify="center", ratio=1)
353
+ help_table.add_column("section3", justify="right", ratio=1)
354
+
355
+ nav_text = Text()
356
+ nav_text.append("↑/k ", style=RICH_STYLE_GREEN_BOLD)
357
+ nav_text.append("Up ", style=RICH_STYLE_GRAY)
358
+ nav_text.append("↓/j ", style=RICH_STYLE_GREEN_BOLD)
359
+ nav_text.append("Down ", style=RICH_STYLE_GRAY)
360
+ nav_text.append("gg ", style=RICH_STYLE_GREEN_BOLD)
361
+ nav_text.append("Top ", style=RICH_STYLE_GRAY)
362
+ nav_text.append("G ", style=RICH_STYLE_GREEN_BOLD)
363
+ nav_text.append("End", style=RICH_STYLE_GRAY)
364
+
365
+ action_text = Text()
366
+ action_text.append("Enter/l ", style=RICH_STYLE_GREEN_BOLD)
367
+ 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)
370
+
371
+ 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)
376
+
377
+ help_table.add_row(nav_text, action_text, page_text)
378
+
379
+ return Panel(
380
+ help_table,
381
+ border_style="yellow",
382
+ box=ROUNDED,
383
+ )
384
+
385
+ def _render(self):
386
+ """Render the split-panel interface."""
387
+ layout = Layout()
388
+ layout.split_column(
389
+ Layout(name="header", size=3),
390
+ Layout(name="main"),
391
+ Layout(name="help", size=3),
392
+ )
393
+
394
+ layout["main"].split_row(
395
+ Layout(name="list", ratio=1, minimum_size=40),
396
+ Layout(name="preview", ratio=1, minimum_size=40),
397
+ )
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
+
404
+ self.console.clear()
405
+ self.console.print(layout)
406
+
407
+ def _handle_navigation(self, direction: str) -> bool:
408
+ """Handle navigation (up/down/top/bottom). Returns True if selection changed."""
409
+ if not self.conversations:
410
+ return False
411
+
412
+ old_index = self.selected_index
413
+
414
+ if direction == "up" and self.selected_index > 0:
415
+ self.selected_index -= 1
416
+ elif direction == "down" and self.selected_index < len(self.conversations) - 1:
417
+ self.selected_index += 1
418
+ elif direction == "top":
419
+ self.selected_index = 0
420
+ elif direction == "bottom":
421
+ self.selected_index = len(self.conversations) - 1
422
+ elif direction == "page_up":
423
+ self.selected_index = max(0, self.selected_index - self.max_list_items)
424
+ elif direction == "page_down":
425
+ self.selected_index = min(
426
+ len(self.conversations) - 1, self.selected_index + self.max_list_items
427
+ )
428
+
429
+ if self.selected_index < self.scroll_offset:
430
+ self.scroll_offset = self.selected_index
431
+ elif self.selected_index >= self.scroll_offset + self.max_list_items:
432
+ self.scroll_offset = self.selected_index - self.max_list_items + 1
433
+
434
+ return self.selected_index != old_index
435
+
436
+ def get_selected_conversation_id(self) -> Optional[str]:
437
+ """Get the ID of the currently selected conversation."""
438
+ if 0 <= self.selected_index < len(self.conversations):
439
+ return self.conversations[self.selected_index].get("id")
440
+ return None
441
+
442
+ def get_selected_conversation_index(self) -> int:
443
+ """Get the 1-based index of the currently selected conversation."""
444
+ 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