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.
- AgentCrew/__init__.py +1 -1
- AgentCrew/app.py +34 -633
- AgentCrew/main.py +55 -3
- AgentCrew/main_docker.py +1 -30
- AgentCrew/modules/agents/local_agent.py +26 -1
- AgentCrew/modules/chat/message/command_processor.py +33 -8
- AgentCrew/modules/chat/message/handler.py +5 -1
- AgentCrew/modules/code_analysis/__init__.py +8 -0
- AgentCrew/modules/code_analysis/parsers/__init__.py +67 -0
- AgentCrew/modules/code_analysis/parsers/base.py +93 -0
- AgentCrew/modules/code_analysis/parsers/cpp_parser.py +127 -0
- AgentCrew/modules/code_analysis/parsers/csharp_parser.py +162 -0
- AgentCrew/modules/code_analysis/parsers/generic_parser.py +63 -0
- AgentCrew/modules/code_analysis/parsers/go_parser.py +154 -0
- AgentCrew/modules/code_analysis/parsers/java_parser.py +103 -0
- AgentCrew/modules/code_analysis/parsers/javascript_parser.py +268 -0
- AgentCrew/modules/code_analysis/parsers/kotlin_parser.py +84 -0
- AgentCrew/modules/code_analysis/parsers/php_parser.py +107 -0
- AgentCrew/modules/code_analysis/parsers/python_parser.py +60 -0
- AgentCrew/modules/code_analysis/parsers/ruby_parser.py +46 -0
- AgentCrew/modules/code_analysis/parsers/rust_parser.py +72 -0
- AgentCrew/modules/code_analysis/service.py +231 -897
- AgentCrew/modules/command_execution/constants.py +2 -2
- AgentCrew/modules/console/completers.py +1 -1
- AgentCrew/modules/console/confirmation_handler.py +4 -4
- AgentCrew/modules/console/console_ui.py +17 -3
- 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/browser_ui.py +643 -0
- AgentCrew/modules/console/conversation_handler.py +34 -1
- AgentCrew/modules/console/diff_display.py +22 -51
- AgentCrew/modules/console/display_handlers.py +142 -26
- AgentCrew/modules/console/tool_display.py +4 -6
- AgentCrew/modules/file_editing/service.py +8 -8
- AgentCrew/modules/file_editing/tool.py +65 -67
- AgentCrew/modules/gui/components/command_handler.py +137 -29
- AgentCrew/modules/gui/components/tool_handlers.py +0 -2
- 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/gui/widgets/diff_widget.py +30 -61
- AgentCrew/modules/llm/constants.py +18 -9
- AgentCrew/modules/memory/context_persistent.py +1 -0
- AgentCrew/modules/memory/tool.py +1 -1
- AgentCrew/setup.py +470 -0
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/METADATA +1 -1
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/RECORD +60 -41
- {agentcrew_ai-0.8.12.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.12.dist-info → agentcrew_ai-0.9.0.dist-info}/entry_points.txt +0 -0
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {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
|