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.
- AgentCrew/__init__.py +1 -1
- AgentCrew/main.py +55 -3
- AgentCrew/modules/agents/local_agent.py +25 -0
- 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/confirmation_handler.py +4 -4
- AgentCrew/modules/console/console_ui.py +20 -1
- AgentCrew/modules/console/conversation_browser.py +557 -0
- AgentCrew/modules/console/diff_display.py +22 -51
- AgentCrew/modules/console/display_handlers.py +22 -22
- 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/tool_handlers.py +0 -2
- AgentCrew/modules/gui/widgets/diff_widget.py +30 -61
- AgentCrew/modules/llm/constants.py +5 -5
- AgentCrew/modules/memory/context_persistent.py +1 -0
- AgentCrew/modules/memory/tool.py +1 -1
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/METADATA +1 -1
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/RECORD +38 -24
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/WHEEL +1 -1
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/entry_points.txt +0 -0
- {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/licenses/LICENSE +0 -0
- {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 =
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
138
|
+
parsed_blocks = DiffDisplay.parse_search_replace_blocks(blocks)
|
|
139
139
|
|
|
140
|
-
if not
|
|
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
|
|
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.
|
|
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
|