agentcrew-ai 0.8.13__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.
- AgentCrew/__init__.py +1 -1
- AgentCrew/app.py +34 -633
- AgentCrew/main_docker.py +1 -30
- AgentCrew/modules/agents/local_agent.py +2 -2
- AgentCrew/modules/chat/message/command_processor.py +33 -8
- AgentCrew/modules/chat/message/handler.py +5 -1
- AgentCrew/modules/console/completers.py +1 -1
- AgentCrew/modules/console/console_ui.py +6 -11
- AgentCrew/modules/console/conversation_browser/__init__.py +9 -0
- AgentCrew/modules/console/conversation_browser/browser.py +84 -0
- AgentCrew/modules/console/conversation_browser/browser_input_handler.py +279 -0
- AgentCrew/modules/console/{conversation_browser.py → conversation_browser/browser_ui.py} +249 -163
- AgentCrew/modules/console/conversation_handler.py +34 -1
- AgentCrew/modules/console/display_handlers.py +123 -7
- AgentCrew/modules/gui/components/command_handler.py +137 -29
- AgentCrew/modules/gui/themes/README.md +30 -14
- AgentCrew/modules/gui/themes/__init__.py +2 -1
- AgentCrew/modules/gui/themes/atom_light.yaml +1287 -0
- AgentCrew/modules/gui/themes/catppuccin.yaml +1276 -0
- AgentCrew/modules/gui/themes/dracula.yaml +1262 -0
- AgentCrew/modules/gui/themes/nord.yaml +1267 -0
- AgentCrew/modules/gui/themes/saigontech.yaml +1268 -0
- AgentCrew/modules/gui/themes/style_provider.py +76 -264
- AgentCrew/modules/gui/themes/theme_loader.py +379 -0
- AgentCrew/modules/gui/themes/unicorn.yaml +1276 -0
- AgentCrew/modules/gui/widgets/configs/global_settings.py +3 -4
- AgentCrew/modules/llm/constants.py +13 -4
- AgentCrew/setup.py +470 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/METADATA +1 -1
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/RECORD +34 -29
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/WHEEL +1 -1
- AgentCrew/modules/gui/themes/atom_light.py +0 -1365
- AgentCrew/modules/gui/themes/catppuccin.py +0 -1404
- AgentCrew/modules/gui/themes/dracula.py +0 -1372
- AgentCrew/modules/gui/themes/nord.py +0 -1365
- AgentCrew/modules/gui/themes/saigontech.py +0 -1359
- AgentCrew/modules/gui/themes/unicorn.py +0 -1372
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/entry_points.txt +0 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
"""Conversation browser
|
|
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
|
|
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
|
|
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
|
|
32
|
-
"""
|
|
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.
|
|
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("
|
|
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=
|
|
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
|
-
|
|
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
|
|
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(
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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] + "
|
|
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("
|
|
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("
|
|
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(
|
|
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("
|
|
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("
|
|
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("
|
|
369
|
-
action_text.append("
|
|
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("
|
|
373
|
-
page_text.append("
|
|
374
|
-
page_text.append("
|
|
375
|
-
page_text.append("
|
|
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
|
|
386
|
-
"""
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
layout
|
|
402
|
-
|
|
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.
|
|
406
|
-
|
|
407
|
-
|
|
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
|