agentcrew-ai 0.8.12__py3-none-any.whl → 0.9.0__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 (66) hide show
  1. AgentCrew/__init__.py +1 -1
  2. AgentCrew/app.py +34 -633
  3. AgentCrew/main.py +55 -3
  4. AgentCrew/main_docker.py +1 -30
  5. AgentCrew/modules/agents/local_agent.py +26 -1
  6. AgentCrew/modules/chat/message/command_processor.py +33 -8
  7. AgentCrew/modules/chat/message/handler.py +5 -1
  8. AgentCrew/modules/code_analysis/__init__.py +8 -0
  9. AgentCrew/modules/code_analysis/parsers/__init__.py +67 -0
  10. AgentCrew/modules/code_analysis/parsers/base.py +93 -0
  11. AgentCrew/modules/code_analysis/parsers/cpp_parser.py +127 -0
  12. AgentCrew/modules/code_analysis/parsers/csharp_parser.py +162 -0
  13. AgentCrew/modules/code_analysis/parsers/generic_parser.py +63 -0
  14. AgentCrew/modules/code_analysis/parsers/go_parser.py +154 -0
  15. AgentCrew/modules/code_analysis/parsers/java_parser.py +103 -0
  16. AgentCrew/modules/code_analysis/parsers/javascript_parser.py +268 -0
  17. AgentCrew/modules/code_analysis/parsers/kotlin_parser.py +84 -0
  18. AgentCrew/modules/code_analysis/parsers/php_parser.py +107 -0
  19. AgentCrew/modules/code_analysis/parsers/python_parser.py +60 -0
  20. AgentCrew/modules/code_analysis/parsers/ruby_parser.py +46 -0
  21. AgentCrew/modules/code_analysis/parsers/rust_parser.py +72 -0
  22. AgentCrew/modules/code_analysis/service.py +231 -897
  23. AgentCrew/modules/command_execution/constants.py +2 -2
  24. AgentCrew/modules/console/completers.py +1 -1
  25. AgentCrew/modules/console/confirmation_handler.py +4 -4
  26. AgentCrew/modules/console/console_ui.py +17 -3
  27. AgentCrew/modules/console/conversation_browser/__init__.py +9 -0
  28. AgentCrew/modules/console/conversation_browser/browser.py +84 -0
  29. AgentCrew/modules/console/conversation_browser/browser_input_handler.py +279 -0
  30. AgentCrew/modules/console/conversation_browser/browser_ui.py +643 -0
  31. AgentCrew/modules/console/conversation_handler.py +34 -1
  32. AgentCrew/modules/console/diff_display.py +22 -51
  33. AgentCrew/modules/console/display_handlers.py +142 -26
  34. AgentCrew/modules/console/tool_display.py +4 -6
  35. AgentCrew/modules/file_editing/service.py +8 -8
  36. AgentCrew/modules/file_editing/tool.py +65 -67
  37. AgentCrew/modules/gui/components/command_handler.py +137 -29
  38. AgentCrew/modules/gui/components/tool_handlers.py +0 -2
  39. AgentCrew/modules/gui/themes/README.md +30 -14
  40. AgentCrew/modules/gui/themes/__init__.py +2 -1
  41. AgentCrew/modules/gui/themes/atom_light.yaml +1287 -0
  42. AgentCrew/modules/gui/themes/catppuccin.yaml +1276 -0
  43. AgentCrew/modules/gui/themes/dracula.yaml +1262 -0
  44. AgentCrew/modules/gui/themes/nord.yaml +1267 -0
  45. AgentCrew/modules/gui/themes/saigontech.yaml +1268 -0
  46. AgentCrew/modules/gui/themes/style_provider.py +76 -264
  47. AgentCrew/modules/gui/themes/theme_loader.py +379 -0
  48. AgentCrew/modules/gui/themes/unicorn.yaml +1276 -0
  49. AgentCrew/modules/gui/widgets/configs/global_settings.py +3 -4
  50. AgentCrew/modules/gui/widgets/diff_widget.py +30 -61
  51. AgentCrew/modules/llm/constants.py +18 -9
  52. AgentCrew/modules/memory/context_persistent.py +1 -0
  53. AgentCrew/modules/memory/tool.py +1 -1
  54. AgentCrew/setup.py +470 -0
  55. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/METADATA +1 -1
  56. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/RECORD +60 -41
  57. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/WHEEL +1 -1
  58. AgentCrew/modules/gui/themes/atom_light.py +0 -1365
  59. AgentCrew/modules/gui/themes/catppuccin.py +0 -1404
  60. AgentCrew/modules/gui/themes/dracula.py +0 -1372
  61. AgentCrew/modules/gui/themes/nord.py +0 -1365
  62. AgentCrew/modules/gui/themes/saigontech.py +0 -1359
  63. AgentCrew/modules/gui/themes/unicorn.py +0 -1372
  64. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/entry_points.txt +0 -0
  65. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/licenses/LICENSE +0 -0
  66. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,643 @@
1
+ """Conversation browser UI rendering components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List, Dict, Any, Optional, Callable, Tuple
6
+ from datetime import datetime
7
+
8
+ from rich.console import Console, Group
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+ from rich.text import Text
12
+ from rich.layout import Layout
13
+ from rich.rule import Rule
14
+ from rich.box import ROUNDED
15
+ from rich.live import Live
16
+
17
+ from loguru import logger
18
+
19
+ from ..constants import (
20
+ RICH_STYLE_YELLOW,
21
+ RICH_STYLE_YELLOW_BOLD,
22
+ RICH_STYLE_BLUE,
23
+ RICH_STYLE_GREEN_BOLD,
24
+ RICH_STYLE_GREEN,
25
+ RICH_STYLE_GRAY,
26
+ RICH_STYLE_WHITE,
27
+ )
28
+
29
+
30
+ class ConversationBrowserUI:
31
+ """Handles UI rendering for the conversation browser."""
32
+
33
+ def __init__(
34
+ self,
35
+ console: Console,
36
+ get_conversation_history: Optional[
37
+ Callable[[str], List[Dict[str, Any]]]
38
+ ] = None,
39
+ ):
40
+ self.console = console
41
+ self.conversations: List[Dict[str, Any]] = []
42
+ self._all_conversations: List[Dict[str, Any]] = []
43
+ self.selected_index = 0
44
+ self.scroll_offset = 0
45
+ self._get_conversation_history = get_conversation_history
46
+ self._preview_cache: Dict[str, Tuple[List[Dict[str, Any]], int]] = {}
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
+
57
+ def set_conversations(self, conversations: List[Dict[str, Any]]):
58
+ """Set the conversations list to browse."""
59
+ self._all_conversations = conversations
60
+ self.conversations = conversations
61
+ self.selected_index = 0
62
+ self.scroll_offset = 0
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)
159
+
160
+ def _format_timestamp(self, timestamp) -> str:
161
+ """Format timestamp for display."""
162
+ if isinstance(timestamp, (int, float)):
163
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M")
164
+ if isinstance(timestamp, str):
165
+ try:
166
+ dt = datetime.fromisoformat(timestamp)
167
+ return dt.strftime("%Y-%m-%d %H:%M")
168
+ except (ValueError, TypeError):
169
+ return timestamp
170
+ return str(timestamp) if timestamp else "Unknown"
171
+
172
+ def _create_header(self) -> Panel:
173
+ """Create the header panel with title and info."""
174
+ header_table = Table(
175
+ show_header=False,
176
+ show_edge=False,
177
+ expand=True,
178
+ box=None,
179
+ padding=0,
180
+ )
181
+ header_table.add_column("left", justify="left", ratio=1)
182
+ header_table.add_column("center", justify="center", ratio=2)
183
+ header_table.add_column("right", justify="right", ratio=1)
184
+
185
+ left_text = Text()
186
+ left_text.append("\U0001f4da ", style="bold")
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
+ )
192
+ left_text.append("conversations", style=RICH_STYLE_GRAY)
193
+
194
+ center_text = Text()
195
+ center_text.append("Conversation History", style=RICH_STYLE_YELLOW_BOLD)
196
+
197
+ right_text = Text()
198
+ if self.conversations:
199
+ right_text.append(f"{self.selected_index + 1}", style=RICH_STYLE_GREEN_BOLD)
200
+ right_text.append(f"/{len(self.conversations)}", style=RICH_STYLE_GRAY)
201
+
202
+ header_table.add_row(left_text, center_text, right_text)
203
+
204
+ return Panel(
205
+ header_table,
206
+ border_style="cyan",
207
+ box=ROUNDED,
208
+ padding=(0, 1),
209
+ )
210
+
211
+ def _create_list_panel(self, panel_height: Optional[int] = None) -> Panel:
212
+ """Create the left panel with conversation list."""
213
+ if not self.conversations:
214
+ empty_content = Group(
215
+ Text(""),
216
+ Text(" No conversations found", style=RICH_STYLE_GRAY),
217
+ Text(""),
218
+ Text(" Start chatting to create one!", style=RICH_STYLE_YELLOW),
219
+ )
220
+ return Panel(
221
+ empty_content,
222
+ title=Text("Conversations ", style=RICH_STYLE_YELLOW_BOLD),
223
+ border_style="blue",
224
+ box=ROUNDED,
225
+ )
226
+
227
+ table = Table(
228
+ show_header=True,
229
+ show_footer=False,
230
+ expand=True,
231
+ box=None,
232
+ padding=(0, 1),
233
+ header_style=RICH_STYLE_YELLOW_BOLD,
234
+ )
235
+ table.add_column("#", width=5, justify="right", no_wrap=True)
236
+ table.add_column("Title", no_wrap=True, overflow="ellipsis")
237
+ table.add_column("Date", width=10, justify="right", no_wrap=True)
238
+
239
+ visible_count = min(
240
+ self.max_list_items, len(self.conversations) - self.scroll_offset
241
+ )
242
+
243
+ for i in range(visible_count):
244
+ idx = self.scroll_offset + i
245
+ convo = self.conversations[idx]
246
+ is_cursor = idx == self.selected_index
247
+ is_marked = idx in self.selected_items
248
+
249
+ index_text = f"{idx + 1}"
250
+ title = convo.get("title", "Untitled").replace("\n", " ")
251
+ timestamp = self._format_timestamp(convo.get("timestamp"))
252
+
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:
266
+ table.add_row(
267
+ Text(index_text, style=RICH_STYLE_GREEN_BOLD),
268
+ Text(
269
+ f"{mark_indicator}{cursor_indicator}{title}",
270
+ style=RICH_STYLE_GREEN_BOLD,
271
+ ),
272
+ Text(timestamp, style=RICH_STYLE_GREEN),
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
+ )
280
+ else:
281
+ table.add_row(
282
+ Text(index_text, style=RICH_STYLE_GRAY),
283
+ Text(f"{mark_indicator} {title}", style=RICH_STYLE_BLUE),
284
+ Text(timestamp, style=RICH_STYLE_GRAY),
285
+ )
286
+
287
+ scroll_parts = []
288
+ if self.scroll_offset > 0:
289
+ scroll_parts.append(f"\u2191{self.scroll_offset}")
290
+ remaining = len(self.conversations) - self.scroll_offset - visible_count
291
+ if remaining > 0:
292
+ scroll_parts.append(f"\u2193{remaining}")
293
+
294
+ subtitle = None
295
+ if scroll_parts:
296
+ subtitle = Text(" ".join(scroll_parts), style=RICH_STYLE_GRAY)
297
+
298
+ return Panel(
299
+ table,
300
+ title=Text("Conversations ", style=RICH_STYLE_YELLOW_BOLD),
301
+ subtitle=subtitle,
302
+ border_style="blue",
303
+ box=ROUNDED,
304
+ )
305
+
306
+ def _get_conversation_preview_messages(
307
+ self, convo_id: str
308
+ ) -> tuple[List[Dict[str, Any]], int]:
309
+ """Get first 4 user-assistant exchanges for preview.
310
+
311
+ Returns:
312
+ Tuple of (preview_messages, total_filtered_messages)
313
+ """
314
+ if convo_id in self._preview_cache:
315
+ return self._preview_cache[convo_id]
316
+
317
+ if not self._get_conversation_history:
318
+ return [], 0
319
+
320
+ try:
321
+ history = self._get_conversation_history(convo_id)
322
+ if not history:
323
+ return [], 0
324
+
325
+ all_messages = []
326
+ for msg in history:
327
+ if not isinstance(msg, dict):
328
+ continue
329
+ role = msg.get("role")
330
+ if role in ["user", "assistant"]:
331
+ content = msg.get("content", "")
332
+ if isinstance(content, str) and content.strip():
333
+ if content.startswith("Memories related to the user request:"):
334
+ continue
335
+ if content.startswith("Content of "):
336
+ continue
337
+ all_messages.append({"role": role, "content": content})
338
+ elif isinstance(content, list):
339
+ text_content = ""
340
+ for block in content:
341
+ if isinstance(block, dict) and block.get("type") == "text":
342
+ text_content = block.get("text", "")
343
+ break
344
+ if text_content.strip():
345
+ if text_content.startswith(
346
+ "Memories related to the user request:"
347
+ ):
348
+ continue
349
+ if text_content.startswith("Content of "):
350
+ continue
351
+ all_messages.append({"role": role, "content": text_content})
352
+
353
+ preview_messages = []
354
+ exchanges = 0
355
+ max_exchanges = 4
356
+
357
+ for msg in all_messages:
358
+ preview_messages.append(msg)
359
+ if msg.get("role") == "assistant":
360
+ exchanges += 1
361
+ if exchanges >= max_exchanges:
362
+ break
363
+
364
+ total = len(all_messages)
365
+ result = (preview_messages, total)
366
+ self._preview_cache[convo_id] = result
367
+ return result
368
+
369
+ except Exception as e:
370
+ logger.warning(f"Error fetching conversation preview: {e}")
371
+ return [], 0
372
+
373
+ def _create_preview_panel(self, panel_height: Optional[int] = None) -> Panel:
374
+ """Create the right panel with conversation preview."""
375
+ if not self.conversations or self.selected_index >= len(self.conversations):
376
+ empty_content = Group(
377
+ Text(""),
378
+ Text(" Select a conversation to preview", style=RICH_STYLE_GRAY),
379
+ )
380
+ return Panel(
381
+ empty_content,
382
+ title=Text("Preview ", style=RICH_STYLE_YELLOW_BOLD),
383
+ border_style="green",
384
+ box=ROUNDED,
385
+ )
386
+
387
+ convo = self.conversations[self.selected_index]
388
+ preview_lines = []
389
+
390
+ title = convo.get("title", "Untitled")
391
+ preview_lines.append(Text(f"\U0001f4cc {title}", style=RICH_STYLE_YELLOW_BOLD))
392
+
393
+ convo_id = convo.get("id", "unknown")
394
+ timestamp = self._format_timestamp(convo.get("timestamp"))
395
+
396
+ meta_table = Table(show_header=False, box=None, padding=0, expand=True)
397
+ meta_table.add_column("key", style=RICH_STYLE_GRAY)
398
+ meta_table.add_column("value", style=RICH_STYLE_WHITE)
399
+
400
+ display_id = convo_id[:24] + "\u2026" if len(convo_id) > 24 else convo_id
401
+ meta_table.add_row("ID:", display_id)
402
+ meta_table.add_row("Created:", timestamp)
403
+
404
+ preview_lines.append(Text(""))
405
+ preview_lines.append(meta_table)
406
+ preview_lines.append(Text(""))
407
+ preview_lines.append(Rule(title="Messages", style=RICH_STYLE_GRAY))
408
+
409
+ messages, total_messages = self._get_conversation_preview_messages(convo_id)
410
+
411
+ if messages:
412
+ exchange_count = 0
413
+ i = 0
414
+ while i < len(messages) and exchange_count < 4:
415
+ msg = messages[i]
416
+ role = msg.get("role", "unknown")
417
+ content = msg.get("content", "")
418
+
419
+ max_content_len = 120
420
+ content_display = content.replace("\n", " ").strip()
421
+ if len(content_display) > max_content_len:
422
+ content_display = content_display[:max_content_len] + "\u2026"
423
+
424
+ preview_lines.append(Text(""))
425
+
426
+ if role == "user":
427
+ user_header = Text()
428
+ user_header.append("\U0001f464 ", style="bold")
429
+ user_header.append("User", style=RICH_STYLE_BLUE)
430
+ preview_lines.append(user_header)
431
+ preview_lines.append(
432
+ Text(f" {content_display}", style=RICH_STYLE_WHITE)
433
+ )
434
+ else:
435
+ assistant_header = Text()
436
+ assistant_header.append("\U0001f916 ", style="bold")
437
+ assistant_header.append("Assistant", style=RICH_STYLE_GREEN)
438
+ preview_lines.append(assistant_header)
439
+ preview_lines.append(
440
+ Text(f" {content_display}", style=RICH_STYLE_WHITE)
441
+ )
442
+ exchange_count += 1
443
+
444
+ i += 1
445
+
446
+ remaining = total_messages - len(messages)
447
+ if remaining > 0:
448
+ preview_lines.append(Text(""))
449
+ preview_lines.append(Rule(style=RICH_STYLE_GRAY))
450
+ preview_lines.append(
451
+ Text(
452
+ f" \u2026 and {remaining} more messages", style=RICH_STYLE_GRAY
453
+ )
454
+ )
455
+ else:
456
+ basic_preview = convo.get("preview", "No preview available")
457
+ preview_lines.append(Text(""))
458
+ preview_lines.append(Text(f" {basic_preview}", style=RICH_STYLE_WHITE))
459
+
460
+ return Panel(
461
+ Group(*preview_lines),
462
+ title=Text("Preview ", style=RICH_STYLE_YELLOW_BOLD),
463
+ border_style="green",
464
+ box=ROUNDED,
465
+ )
466
+
467
+ def _create_help_panel(self) -> Panel:
468
+ """Create the help panel with keyboard shortcuts."""
469
+ if self._search_mode:
470
+ return self._create_search_bar()
471
+
472
+ help_table = Table(
473
+ show_header=False,
474
+ box=None,
475
+ padding=0,
476
+ expand=True,
477
+ )
478
+ help_table.add_column("section1", justify="left", ratio=1)
479
+ help_table.add_column("section2", justify="center", ratio=1)
480
+ help_table.add_column("section3", justify="right", ratio=1)
481
+
482
+ nav_text = Text()
483
+ nav_text.append("\u2191/k ", style=RICH_STYLE_GREEN_BOLD)
484
+ nav_text.append("Up ", style=RICH_STYLE_GRAY)
485
+ nav_text.append("\u2193/j ", style=RICH_STYLE_GREEN_BOLD)
486
+ nav_text.append("Down ", style=RICH_STYLE_GRAY)
487
+ nav_text.append("gg ", style=RICH_STYLE_GREEN_BOLD)
488
+ nav_text.append("Top ", style=RICH_STYLE_GRAY)
489
+ nav_text.append("G ", style=RICH_STYLE_GREEN_BOLD)
490
+ nav_text.append("End", style=RICH_STYLE_GRAY)
491
+
492
+ action_text = Text()
493
+ action_text.append("Enter/l ", style=RICH_STYLE_GREEN_BOLD)
494
+ action_text.append("Load ", 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)
499
+
500
+ page_text = Text()
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
+ )
509
+
510
+ help_table.add_row(nav_text, action_text, page_text)
511
+
512
+ return Panel(
513
+ help_table,
514
+ border_style="yellow",
515
+ box=ROUNDED,
516
+ )
517
+
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."""
550
+ layout = Layout()
551
+ layout.split_column(
552
+ Layout(name="header", size=3),
553
+ Layout(name="main"),
554
+ Layout(name="help", size=3),
555
+ )
556
+
557
+ layout["main"].split_row(
558
+ Layout(name="list", ratio=1, minimum_size=40),
559
+ Layout(name="preview", ratio=1, minimum_size=40),
560
+ )
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."""
574
+ self.console.clear()
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:
607
+ """Handle navigation (up/down/top/bottom). Returns True if selection changed."""
608
+ if not self.conversations:
609
+ return False
610
+
611
+ old_index = self.selected_index
612
+
613
+ if direction == "up" and self.selected_index > 0:
614
+ self.selected_index -= 1
615
+ elif direction == "down" and self.selected_index < len(self.conversations) - 1:
616
+ self.selected_index += 1
617
+ elif direction == "top":
618
+ self.selected_index = 0
619
+ elif direction == "bottom":
620
+ self.selected_index = len(self.conversations) - 1
621
+ elif direction == "page_up":
622
+ self.selected_index = max(0, self.selected_index - self.max_list_items)
623
+ elif direction == "page_down":
624
+ self.selected_index = min(
625
+ len(self.conversations) - 1, self.selected_index + self.max_list_items
626
+ )
627
+
628
+ if self.selected_index < self.scroll_offset:
629
+ self.scroll_offset = self.selected_index
630
+ elif self.selected_index >= self.scroll_offset + self.max_list_items:
631
+ self.scroll_offset = self.selected_index - self.max_list_items + 1
632
+
633
+ return self.selected_index != old_index
634
+
635
+ def get_selected_conversation_id(self) -> Optional[str]:
636
+ """Get the ID of the currently selected conversation."""
637
+ if 0 <= self.selected_index < len(self.conversations):
638
+ return self.conversations[self.selected_index].get("id")
639
+ return None
640
+
641
+ def get_selected_conversation_index(self) -> int:
642
+ """Get the 1-based index of the currently selected conversation."""
643
+ return self.selected_index + 1
@@ -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