agentcrew-ai 0.8.13__py3-none-any.whl → 0.9.1__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 +46 -634
- AgentCrew/main_docker.py +1 -30
- AgentCrew/modules/a2a/common/client/card_resolver.py +27 -8
- AgentCrew/modules/a2a/server.py +5 -0
- AgentCrew/modules/a2a/task_manager.py +1 -0
- AgentCrew/modules/agents/local_agent.py +2 -2
- AgentCrew/modules/chat/message/command_processor.py +33 -8
- AgentCrew/modules/chat/message/conversation.py +18 -1
- AgentCrew/modules/chat/message/handler.py +5 -1
- AgentCrew/modules/code_analysis/service.py +50 -7
- AgentCrew/modules/code_analysis/tool.py +9 -8
- AgentCrew/modules/console/completers.py +5 -1
- AgentCrew/modules/console/console_ui.py +23 -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 +127 -7
- AgentCrew/modules/console/visual_mode/__init__.py +5 -0
- AgentCrew/modules/console/visual_mode/viewer.py +41 -0
- AgentCrew/modules/console/visual_mode/viewer_input_handler.py +315 -0
- AgentCrew/modules/console/visual_mode/viewer_ui.py +608 -0
- AgentCrew/modules/gui/components/command_handler.py +137 -29
- AgentCrew/modules/gui/components/menu_components.py +8 -7
- 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 +78 -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 +4 -4
- AgentCrew/modules/gui/widgets/history_sidebar.py +6 -1
- AgentCrew/modules/llm/constants.py +28 -9
- AgentCrew/modules/mcpclient/service.py +0 -1
- AgentCrew/modules/memory/base_service.py +13 -0
- AgentCrew/modules/memory/chroma_service.py +50 -0
- AgentCrew/setup.py +470 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/METADATA +1 -1
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/RECORD +49 -40
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.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.1.dist-info}/entry_points.txt +0 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""Visual mode UI for displaying raw message content."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING, List, Dict, Any, Optional, Tuple
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
from rich.box import HORIZONTALS, SIMPLE
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.layout import Layout
|
|
14
|
+
|
|
15
|
+
from ..constants import (
|
|
16
|
+
RICH_STYLE_BLUE,
|
|
17
|
+
RICH_STYLE_GREEN,
|
|
18
|
+
RICH_STYLE_GRAY,
|
|
19
|
+
RICH_STYLE_YELLOW_BOLD,
|
|
20
|
+
RICH_STYLE_GREEN_BOLD,
|
|
21
|
+
RICH_STYLE_BLUE_BOLD,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class VisualModeUI:
|
|
29
|
+
"""UI component for visual mode viewer."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, console: Console):
|
|
32
|
+
self.console = console
|
|
33
|
+
self._messages: List[Dict[str, Any]] = []
|
|
34
|
+
self._lines: List[Tuple[str, str, int]] = []
|
|
35
|
+
self._cursor_line = 0
|
|
36
|
+
self._cursor_col = 0
|
|
37
|
+
self._scroll_offset = 0
|
|
38
|
+
self._horizontal_scroll = 0
|
|
39
|
+
self._selection_start: Optional[Tuple[int, int]] = None
|
|
40
|
+
self._selection_end: Optional[Tuple[int, int]] = None
|
|
41
|
+
self._visual_mode = False
|
|
42
|
+
self._live: Optional[Live] = None
|
|
43
|
+
self._layout: Optional[Layout] = None
|
|
44
|
+
self._search_mode = False
|
|
45
|
+
self._search_query = ""
|
|
46
|
+
self._search_matches: List[Tuple[int, int]] = []
|
|
47
|
+
self._current_match_idx = -1
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def total_lines(self) -> int:
|
|
51
|
+
return len(self._lines)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def viewport_height(self) -> int:
|
|
55
|
+
return max(5, self.console.size.height - 10)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def viewport_width(self) -> int:
|
|
59
|
+
return max(20, self.console.size.width - 10)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def current_line_length(self) -> int:
|
|
63
|
+
if 0 <= self._cursor_line < len(self._lines):
|
|
64
|
+
return len(self._lines[self._cursor_line][0])
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
def set_messages(self, messages: List[Dict[str, Any]]):
|
|
68
|
+
self._messages = messages
|
|
69
|
+
self._build_lines()
|
|
70
|
+
self._cursor_line = max(0, self.total_lines - 1)
|
|
71
|
+
self._cursor_col = 0
|
|
72
|
+
self._scroll_offset = max(0, self.total_lines - self.viewport_height)
|
|
73
|
+
self._horizontal_scroll = 0
|
|
74
|
+
self._selection_start = None
|
|
75
|
+
self._selection_end = None
|
|
76
|
+
self._visual_mode = False
|
|
77
|
+
self._search_mode = False
|
|
78
|
+
self._search_query = ""
|
|
79
|
+
self._search_matches = []
|
|
80
|
+
self._current_match_idx = -1
|
|
81
|
+
|
|
82
|
+
def _extract_content(self, message: Dict[str, Any]) -> str:
|
|
83
|
+
content = message.get("content", "")
|
|
84
|
+
if isinstance(content, str):
|
|
85
|
+
return content
|
|
86
|
+
elif isinstance(content, list):
|
|
87
|
+
result = []
|
|
88
|
+
for item in content:
|
|
89
|
+
if isinstance(item, dict):
|
|
90
|
+
if item.get("type") == "text":
|
|
91
|
+
result.append(item.get("text", ""))
|
|
92
|
+
elif isinstance(item, str):
|
|
93
|
+
result.append(item)
|
|
94
|
+
return "\n".join(result)
|
|
95
|
+
return str(content)
|
|
96
|
+
|
|
97
|
+
def _build_lines(self):
|
|
98
|
+
self._lines = []
|
|
99
|
+
for msg_idx, msg in enumerate(self._messages):
|
|
100
|
+
role = msg.get("role", "unknown")
|
|
101
|
+
if role == "tool":
|
|
102
|
+
continue
|
|
103
|
+
agent = msg.get("agent", "")
|
|
104
|
+
content = self._extract_content(msg)
|
|
105
|
+
if not content.strip():
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
header = f"--- {role.upper()}"
|
|
109
|
+
if agent:
|
|
110
|
+
header += f" ({agent})"
|
|
111
|
+
header += " ---"
|
|
112
|
+
self._lines.append((header, role, msg_idx))
|
|
113
|
+
|
|
114
|
+
for line in content.split("\n"):
|
|
115
|
+
self._lines.append((line, role, msg_idx))
|
|
116
|
+
|
|
117
|
+
self._lines.append(("", role, msg_idx))
|
|
118
|
+
|
|
119
|
+
def start_search_mode(self):
|
|
120
|
+
self._search_mode = True
|
|
121
|
+
self._search_query = ""
|
|
122
|
+
self._search_matches = []
|
|
123
|
+
self._current_match_idx = -1
|
|
124
|
+
|
|
125
|
+
def exit_search_mode(self, clear_results: bool = False):
|
|
126
|
+
self._search_mode = False
|
|
127
|
+
if clear_results:
|
|
128
|
+
self._search_query = ""
|
|
129
|
+
self._search_matches = []
|
|
130
|
+
self._current_match_idx = -1
|
|
131
|
+
|
|
132
|
+
def append_search_char(self, char: str):
|
|
133
|
+
self._search_query += char
|
|
134
|
+
self._perform_search()
|
|
135
|
+
|
|
136
|
+
def backspace_search(self):
|
|
137
|
+
if self._search_query:
|
|
138
|
+
self._search_query = self._search_query[:-1]
|
|
139
|
+
self._perform_search()
|
|
140
|
+
|
|
141
|
+
def _perform_search(self):
|
|
142
|
+
self._search_matches = []
|
|
143
|
+
self._current_match_idx = -1
|
|
144
|
+
if not self._search_query:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
pattern = re.escape(self._search_query.lower())
|
|
148
|
+
for line_idx, (line_text, _, _) in enumerate(self._lines):
|
|
149
|
+
for match in re.finditer(pattern, line_text.lower()):
|
|
150
|
+
self._search_matches.append((line_idx, match.start()))
|
|
151
|
+
|
|
152
|
+
if self._search_matches:
|
|
153
|
+
self._current_match_idx = 0
|
|
154
|
+
self._jump_to_match(0)
|
|
155
|
+
|
|
156
|
+
def next_search_match(self):
|
|
157
|
+
if not self._search_matches:
|
|
158
|
+
return
|
|
159
|
+
self._current_match_idx = (self._current_match_idx + 1) % len(
|
|
160
|
+
self._search_matches
|
|
161
|
+
)
|
|
162
|
+
self._jump_to_match(self._current_match_idx)
|
|
163
|
+
|
|
164
|
+
def prev_search_match(self):
|
|
165
|
+
if not self._search_matches:
|
|
166
|
+
return
|
|
167
|
+
self._current_match_idx = (self._current_match_idx - 1) % len(
|
|
168
|
+
self._search_matches
|
|
169
|
+
)
|
|
170
|
+
self._jump_to_match(self._current_match_idx)
|
|
171
|
+
|
|
172
|
+
def _jump_to_match(self, match_idx: int):
|
|
173
|
+
if 0 <= match_idx < len(self._search_matches):
|
|
174
|
+
line_idx, col_idx = self._search_matches[match_idx]
|
|
175
|
+
self._cursor_line = line_idx
|
|
176
|
+
self._cursor_col = col_idx
|
|
177
|
+
self._adjust_scroll()
|
|
178
|
+
self._adjust_horizontal_scroll()
|
|
179
|
+
|
|
180
|
+
def toggle_visual_mode(self):
|
|
181
|
+
if self._visual_mode:
|
|
182
|
+
self._visual_mode = False
|
|
183
|
+
self._selection_start = None
|
|
184
|
+
self._selection_end = None
|
|
185
|
+
else:
|
|
186
|
+
self._visual_mode = True
|
|
187
|
+
self._selection_start = (self._cursor_line, self._cursor_col)
|
|
188
|
+
self._selection_end = (self._cursor_line, self._cursor_col)
|
|
189
|
+
|
|
190
|
+
def update_selection(self):
|
|
191
|
+
if self._visual_mode and self._selection_start is not None:
|
|
192
|
+
self._selection_end = (self._cursor_line, self._cursor_col)
|
|
193
|
+
|
|
194
|
+
def get_selected_text(self) -> str:
|
|
195
|
+
if self._selection_start is None or self._selection_end is None:
|
|
196
|
+
if 0 <= self._cursor_line < len(self._lines):
|
|
197
|
+
return self._lines[self._cursor_line][0]
|
|
198
|
+
return ""
|
|
199
|
+
|
|
200
|
+
start_line, start_col = self._selection_start
|
|
201
|
+
end_line, end_col = self._selection_end
|
|
202
|
+
|
|
203
|
+
if (start_line, start_col) > (end_line, end_col):
|
|
204
|
+
start_line, start_col, end_line, end_col = (
|
|
205
|
+
end_line,
|
|
206
|
+
end_col,
|
|
207
|
+
start_line,
|
|
208
|
+
start_col,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if start_line == end_line:
|
|
212
|
+
line_text = self._lines[start_line][0]
|
|
213
|
+
return line_text[start_col : end_col + 1]
|
|
214
|
+
|
|
215
|
+
result = []
|
|
216
|
+
for i in range(start_line, end_line + 1):
|
|
217
|
+
line_text = self._lines[i][0]
|
|
218
|
+
if i == start_line:
|
|
219
|
+
result.append(line_text[start_col:])
|
|
220
|
+
elif i == end_line:
|
|
221
|
+
result.append(line_text[: end_col + 1])
|
|
222
|
+
else:
|
|
223
|
+
result.append(line_text)
|
|
224
|
+
return "\n".join(result)
|
|
225
|
+
|
|
226
|
+
def move_cursor(self, direction: str) -> bool:
|
|
227
|
+
old_line = self._cursor_line
|
|
228
|
+
old_col = self._cursor_col
|
|
229
|
+
|
|
230
|
+
if direction == "up":
|
|
231
|
+
self._cursor_line = max(0, self._cursor_line - 1)
|
|
232
|
+
self._cursor_col = min(self._cursor_col, self.current_line_length)
|
|
233
|
+
elif direction == "down":
|
|
234
|
+
self._cursor_line = min(self.total_lines - 1, self._cursor_line + 1)
|
|
235
|
+
self._cursor_col = min(self._cursor_col, self.current_line_length)
|
|
236
|
+
elif direction == "left":
|
|
237
|
+
if self._cursor_col > 0:
|
|
238
|
+
self._cursor_col -= 1
|
|
239
|
+
elif self._cursor_line > 0:
|
|
240
|
+
self._cursor_line -= 1
|
|
241
|
+
self._cursor_col = self.current_line_length
|
|
242
|
+
elif direction == "right":
|
|
243
|
+
if self._cursor_col < self.current_line_length:
|
|
244
|
+
self._cursor_col += 1
|
|
245
|
+
elif self._cursor_line < self.total_lines - 1:
|
|
246
|
+
self._cursor_line += 1
|
|
247
|
+
self._cursor_col = 0
|
|
248
|
+
elif direction == "line_start":
|
|
249
|
+
self._cursor_col = 0
|
|
250
|
+
elif direction == "line_end":
|
|
251
|
+
self._cursor_col = max(0, self.current_line_length - 1)
|
|
252
|
+
elif direction == "word_forward":
|
|
253
|
+
self._move_word_forward()
|
|
254
|
+
elif direction == "word_backward":
|
|
255
|
+
self._move_word_backward()
|
|
256
|
+
elif direction == "top":
|
|
257
|
+
self._cursor_line = 0
|
|
258
|
+
self._cursor_col = 0
|
|
259
|
+
elif direction == "bottom":
|
|
260
|
+
self._cursor_line = max(0, self.total_lines - 1)
|
|
261
|
+
self._cursor_col = 0
|
|
262
|
+
elif direction == "page_up":
|
|
263
|
+
self._cursor_line = max(0, self._cursor_line - self.viewport_height)
|
|
264
|
+
self._cursor_col = min(self._cursor_col, self.current_line_length)
|
|
265
|
+
elif direction == "page_down":
|
|
266
|
+
self._cursor_line = min(
|
|
267
|
+
self.total_lines - 1, self._cursor_line + self.viewport_height
|
|
268
|
+
)
|
|
269
|
+
self._cursor_col = min(self._cursor_col, self.current_line_length)
|
|
270
|
+
elif direction == "half_up":
|
|
271
|
+
self._cursor_line = max(0, self._cursor_line - self.viewport_height // 2)
|
|
272
|
+
self._cursor_col = min(self._cursor_col, self.current_line_length)
|
|
273
|
+
elif direction == "half_down":
|
|
274
|
+
self._cursor_line = min(
|
|
275
|
+
self.total_lines - 1, self._cursor_line + self.viewport_height // 2
|
|
276
|
+
)
|
|
277
|
+
self._cursor_col = min(self._cursor_col, self.current_line_length)
|
|
278
|
+
|
|
279
|
+
self._adjust_scroll()
|
|
280
|
+
self._adjust_horizontal_scroll()
|
|
281
|
+
self.update_selection()
|
|
282
|
+
return old_line != self._cursor_line or old_col != self._cursor_col
|
|
283
|
+
|
|
284
|
+
def _move_word_forward(self):
|
|
285
|
+
if self._cursor_line >= len(self._lines):
|
|
286
|
+
return
|
|
287
|
+
line = self._lines[self._cursor_line][0]
|
|
288
|
+
col = self._cursor_col
|
|
289
|
+
|
|
290
|
+
while col < len(line) and line[col].isalnum():
|
|
291
|
+
col += 1
|
|
292
|
+
while col < len(line) and not line[col].isalnum():
|
|
293
|
+
col += 1
|
|
294
|
+
|
|
295
|
+
if col >= len(line) and self._cursor_line < self.total_lines - 1:
|
|
296
|
+
self._cursor_line += 1
|
|
297
|
+
self._cursor_col = 0
|
|
298
|
+
else:
|
|
299
|
+
self._cursor_col = min(col, max(0, len(line) - 1))
|
|
300
|
+
|
|
301
|
+
def _move_word_backward(self):
|
|
302
|
+
if self._cursor_line >= len(self._lines):
|
|
303
|
+
return
|
|
304
|
+
line = self._lines[self._cursor_line][0]
|
|
305
|
+
col = self._cursor_col
|
|
306
|
+
|
|
307
|
+
if col == 0 and self._cursor_line > 0:
|
|
308
|
+
self._cursor_line -= 1
|
|
309
|
+
line = self._lines[self._cursor_line][0]
|
|
310
|
+
col = len(line)
|
|
311
|
+
|
|
312
|
+
while col > 0 and not line[col - 1].isalnum():
|
|
313
|
+
col -= 1
|
|
314
|
+
while col > 0 and line[col - 1].isalnum():
|
|
315
|
+
col -= 1
|
|
316
|
+
|
|
317
|
+
self._cursor_col = col
|
|
318
|
+
|
|
319
|
+
def _adjust_scroll(self):
|
|
320
|
+
if self._cursor_line < self._scroll_offset:
|
|
321
|
+
self._scroll_offset = self._cursor_line
|
|
322
|
+
elif self._cursor_line >= self._scroll_offset + self.viewport_height:
|
|
323
|
+
self._scroll_offset = self._cursor_line - self.viewport_height + 1
|
|
324
|
+
|
|
325
|
+
def _adjust_horizontal_scroll(self):
|
|
326
|
+
visible_width = self.viewport_width - 6
|
|
327
|
+
if self._cursor_col < self._horizontal_scroll:
|
|
328
|
+
self._horizontal_scroll = self._cursor_col
|
|
329
|
+
elif self._cursor_col >= self._horizontal_scroll + visible_width:
|
|
330
|
+
self._horizontal_scroll = self._cursor_col - visible_width + 1
|
|
331
|
+
|
|
332
|
+
def _create_header(self) -> Panel:
|
|
333
|
+
header_table = Table.grid(expand=True)
|
|
334
|
+
header_table.add_column(justify="left", ratio=1)
|
|
335
|
+
header_table.add_column(justify="center", ratio=2)
|
|
336
|
+
header_table.add_column(justify="right", ratio=1)
|
|
337
|
+
|
|
338
|
+
mode_text = Text()
|
|
339
|
+
if self._search_mode:
|
|
340
|
+
mode_text.append("-- SEARCH --", style="bold cyan")
|
|
341
|
+
elif self._visual_mode:
|
|
342
|
+
mode_text.append("-- VISUAL --", style="bold yellow")
|
|
343
|
+
else:
|
|
344
|
+
mode_text.append("-- NORMAL --", style="bold green")
|
|
345
|
+
|
|
346
|
+
position = (
|
|
347
|
+
f"L{self._cursor_line + 1}:C{self._cursor_col + 1} [{self.total_lines}]"
|
|
348
|
+
)
|
|
349
|
+
title = "Visual Mode - Raw Content Viewer"
|
|
350
|
+
|
|
351
|
+
header_table.add_row(
|
|
352
|
+
Text(title, style=RICH_STYLE_YELLOW_BOLD),
|
|
353
|
+
mode_text,
|
|
354
|
+
Text(position, style=RICH_STYLE_GRAY),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return Panel(header_table, box=SIMPLE, style=RICH_STYLE_BLUE)
|
|
358
|
+
|
|
359
|
+
def _is_position_selected(self, line: int, col: int) -> bool:
|
|
360
|
+
if self._selection_start is None or self._selection_end is None:
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
start_line, start_col = self._selection_start
|
|
364
|
+
end_line, end_col = self._selection_end
|
|
365
|
+
|
|
366
|
+
if (start_line, start_col) > (end_line, end_col):
|
|
367
|
+
start_line, start_col, end_line, end_col = (
|
|
368
|
+
end_line,
|
|
369
|
+
end_col,
|
|
370
|
+
start_line,
|
|
371
|
+
start_col,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if line < start_line or line > end_line:
|
|
375
|
+
return False
|
|
376
|
+
if line == start_line and line == end_line:
|
|
377
|
+
return start_col <= col <= end_col
|
|
378
|
+
if line == start_line:
|
|
379
|
+
return col >= start_col
|
|
380
|
+
if line == end_line:
|
|
381
|
+
return col <= end_col
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
def _is_search_match(self, line: int, col: int) -> bool:
|
|
385
|
+
if not self._search_query or not self._search_matches:
|
|
386
|
+
return False
|
|
387
|
+
query_len = len(self._search_query)
|
|
388
|
+
for match_line, match_col in self._search_matches:
|
|
389
|
+
if line == match_line and match_col <= col < match_col + query_len:
|
|
390
|
+
return True
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
def _create_content_panel(self) -> Panel:
|
|
394
|
+
content = Text()
|
|
395
|
+
end_line = min(self._scroll_offset + self.viewport_height, self.total_lines)
|
|
396
|
+
visible_width = self.viewport_width - 6
|
|
397
|
+
|
|
398
|
+
search_set = set()
|
|
399
|
+
if self._search_query and self._search_matches:
|
|
400
|
+
query_len = len(self._search_query)
|
|
401
|
+
for match_line, match_col in self._search_matches:
|
|
402
|
+
for c in range(match_col, match_col + query_len):
|
|
403
|
+
search_set.add((match_line, c))
|
|
404
|
+
|
|
405
|
+
sel_start = None
|
|
406
|
+
sel_end = None
|
|
407
|
+
if self._selection_start is not None and self._selection_end is not None:
|
|
408
|
+
s_line, s_col = self._selection_start
|
|
409
|
+
e_line, e_col = self._selection_end
|
|
410
|
+
if (s_line, s_col) > (e_line, e_col):
|
|
411
|
+
s_line, s_col, e_line, e_col = e_line, e_col, s_line, s_col
|
|
412
|
+
sel_start = (s_line, s_col)
|
|
413
|
+
sel_end = (e_line, e_col)
|
|
414
|
+
|
|
415
|
+
for i in range(self._scroll_offset, end_line):
|
|
416
|
+
line_text, role, _ = self._lines[i]
|
|
417
|
+
is_header = line_text.startswith("---") and line_text.endswith("---")
|
|
418
|
+
|
|
419
|
+
if is_header:
|
|
420
|
+
if role == "user":
|
|
421
|
+
base_style = RICH_STYLE_BLUE_BOLD
|
|
422
|
+
elif role == "assistant":
|
|
423
|
+
base_style = RICH_STYLE_GREEN_BOLD
|
|
424
|
+
else:
|
|
425
|
+
base_style = RICH_STYLE_YELLOW_BOLD
|
|
426
|
+
else:
|
|
427
|
+
base_style = "white"
|
|
428
|
+
|
|
429
|
+
line_num = f"{i + 1:4d} "
|
|
430
|
+
content.append(line_num, style=RICH_STYLE_GRAY)
|
|
431
|
+
|
|
432
|
+
visible_text = line_text[
|
|
433
|
+
self._horizontal_scroll : self._horizontal_scroll + visible_width
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
if not visible_text and i == self._cursor_line and self._cursor_col == 0:
|
|
437
|
+
content.append(" ", style="reverse")
|
|
438
|
+
else:
|
|
439
|
+
segment_start = 0
|
|
440
|
+
current_style = None
|
|
441
|
+
|
|
442
|
+
for col_idx, char in enumerate(visible_text):
|
|
443
|
+
actual_col = self._horizontal_scroll + col_idx
|
|
444
|
+
is_cursor = (
|
|
445
|
+
i == self._cursor_line and actual_col == self._cursor_col
|
|
446
|
+
)
|
|
447
|
+
is_selected = False
|
|
448
|
+
if sel_start and sel_end:
|
|
449
|
+
if sel_start[0] == sel_end[0] == i:
|
|
450
|
+
is_selected = sel_start[1] <= actual_col <= sel_end[1]
|
|
451
|
+
elif sel_start[0] == i:
|
|
452
|
+
is_selected = actual_col >= sel_start[1]
|
|
453
|
+
elif sel_end[0] == i:
|
|
454
|
+
is_selected = actual_col <= sel_end[1]
|
|
455
|
+
elif sel_start[0] < i < sel_end[0]:
|
|
456
|
+
is_selected = True
|
|
457
|
+
is_match = (i, actual_col) in search_set
|
|
458
|
+
|
|
459
|
+
if is_cursor:
|
|
460
|
+
char_style = "reverse"
|
|
461
|
+
elif is_selected:
|
|
462
|
+
char_style = "on blue"
|
|
463
|
+
elif is_match:
|
|
464
|
+
char_style = "on yellow black"
|
|
465
|
+
else:
|
|
466
|
+
char_style = base_style
|
|
467
|
+
|
|
468
|
+
if char_style != current_style:
|
|
469
|
+
if current_style is not None and segment_start < col_idx:
|
|
470
|
+
content.append(
|
|
471
|
+
visible_text[segment_start:col_idx], style=current_style
|
|
472
|
+
)
|
|
473
|
+
segment_start = col_idx
|
|
474
|
+
current_style = char_style
|
|
475
|
+
|
|
476
|
+
if current_style is not None and segment_start < len(visible_text):
|
|
477
|
+
content.append(visible_text[segment_start:], style=current_style)
|
|
478
|
+
|
|
479
|
+
if i == self._cursor_line and self._cursor_col >= len(line_text):
|
|
480
|
+
if self._cursor_col == self._horizontal_scroll + len(visible_text):
|
|
481
|
+
content.append(" ", style="reverse")
|
|
482
|
+
|
|
483
|
+
content.append("\n")
|
|
484
|
+
|
|
485
|
+
scroll_info = ""
|
|
486
|
+
if self._scroll_offset > 0:
|
|
487
|
+
scroll_info += f"↑ {self._scroll_offset} more "
|
|
488
|
+
remaining = self.total_lines - end_line
|
|
489
|
+
if remaining > 0:
|
|
490
|
+
scroll_info += f"↓ {remaining} more"
|
|
491
|
+
if self._horizontal_scroll > 0:
|
|
492
|
+
scroll_info += f" ← {self._horizontal_scroll}"
|
|
493
|
+
|
|
494
|
+
return Panel(
|
|
495
|
+
content,
|
|
496
|
+
box=HORIZONTALS,
|
|
497
|
+
subtitle=Text(scroll_info, style=RICH_STYLE_GRAY) if scroll_info else None,
|
|
498
|
+
border_style=RICH_STYLE_GREEN,
|
|
499
|
+
height=self.viewport_height + 2,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def _create_search_bar(self) -> Panel:
|
|
503
|
+
search_text = Text()
|
|
504
|
+
search_text.append("/", style="bold cyan")
|
|
505
|
+
search_text.append(self._search_query, style="white")
|
|
506
|
+
search_text.append("█", style="blink")
|
|
507
|
+
|
|
508
|
+
if self._search_matches:
|
|
509
|
+
match_info = f" [{self._current_match_idx + 1}/{len(self._search_matches)}]"
|
|
510
|
+
search_text.append(match_info, style=RICH_STYLE_GRAY)
|
|
511
|
+
elif self._search_query:
|
|
512
|
+
search_text.append(" [no matches]", style="red")
|
|
513
|
+
|
|
514
|
+
return Panel(search_text, box=SIMPLE, border_style="cyan")
|
|
515
|
+
|
|
516
|
+
def _create_help_panel(self) -> Panel:
|
|
517
|
+
help_table = Table.grid(expand=True)
|
|
518
|
+
help_table.add_column(justify="left", ratio=1)
|
|
519
|
+
help_table.add_column(justify="left", ratio=1)
|
|
520
|
+
help_table.add_column(justify="left", ratio=1)
|
|
521
|
+
|
|
522
|
+
if self._search_mode:
|
|
523
|
+
nav_text = Text()
|
|
524
|
+
nav_text.append("Enter", style="bold")
|
|
525
|
+
nav_text.append(": confirm ")
|
|
526
|
+
nav_text.append("Esc", style="bold")
|
|
527
|
+
nav_text.append(": cancel")
|
|
528
|
+
|
|
529
|
+
action_text = Text()
|
|
530
|
+
action_text.append("n/N", style="bold")
|
|
531
|
+
action_text.append(": next/prev match")
|
|
532
|
+
|
|
533
|
+
exit_text = Text()
|
|
534
|
+
exit_text.append("Backspace", style="bold")
|
|
535
|
+
exit_text.append(": delete char")
|
|
536
|
+
else:
|
|
537
|
+
nav_text = Text()
|
|
538
|
+
nav_text.append("h/j/k/l", style="bold")
|
|
539
|
+
nav_text.append(": move ")
|
|
540
|
+
nav_text.append("w/b", style="bold")
|
|
541
|
+
nav_text.append(": word ")
|
|
542
|
+
nav_text.append("0/$", style="bold")
|
|
543
|
+
nav_text.append(": line start/end")
|
|
544
|
+
|
|
545
|
+
action_text = Text()
|
|
546
|
+
action_text.append("v", style="bold")
|
|
547
|
+
action_text.append(": visual ")
|
|
548
|
+
action_text.append("y", style="bold")
|
|
549
|
+
action_text.append(": yank ")
|
|
550
|
+
action_text.append("/", style="bold")
|
|
551
|
+
action_text.append(": search")
|
|
552
|
+
|
|
553
|
+
exit_text = Text()
|
|
554
|
+
exit_text.append("gg/G", style="bold")
|
|
555
|
+
exit_text.append(": top/bottom ")
|
|
556
|
+
exit_text.append("q/Esc", style="bold")
|
|
557
|
+
exit_text.append(": quit")
|
|
558
|
+
|
|
559
|
+
help_table.add_row(nav_text, action_text, exit_text)
|
|
560
|
+
|
|
561
|
+
return Panel(help_table, box=SIMPLE, border_style=RICH_STYLE_GRAY)
|
|
562
|
+
|
|
563
|
+
def _create_layout(self) -> Layout:
|
|
564
|
+
layout = Layout()
|
|
565
|
+
layout.split_column(
|
|
566
|
+
Layout(name="header", size=3),
|
|
567
|
+
Layout(name="content"),
|
|
568
|
+
Layout(name="search", size=3),
|
|
569
|
+
Layout(name="help", size=3),
|
|
570
|
+
)
|
|
571
|
+
return layout
|
|
572
|
+
|
|
573
|
+
def _update_layout(self):
|
|
574
|
+
if self._layout:
|
|
575
|
+
self._layout["header"].update(self._create_header())
|
|
576
|
+
self._layout["content"].update(self._create_content_panel())
|
|
577
|
+
if self._search_mode:
|
|
578
|
+
self._layout["search"].update(self._create_search_bar())
|
|
579
|
+
self._layout["search"].visible = True
|
|
580
|
+
else:
|
|
581
|
+
self._layout["search"].update("")
|
|
582
|
+
self._layout["search"].visible = False
|
|
583
|
+
self._layout["help"].update(self._create_help_panel())
|
|
584
|
+
|
|
585
|
+
def render(self):
|
|
586
|
+
if self._live and self._layout:
|
|
587
|
+
self._update_layout()
|
|
588
|
+
self._live.refresh()
|
|
589
|
+
|
|
590
|
+
def start_live(self):
|
|
591
|
+
self.console.clear()
|
|
592
|
+
self._layout = self._create_layout()
|
|
593
|
+
self._update_layout()
|
|
594
|
+
|
|
595
|
+
self._live = Live(
|
|
596
|
+
self._layout,
|
|
597
|
+
console=self.console,
|
|
598
|
+
auto_refresh=False,
|
|
599
|
+
screen=True,
|
|
600
|
+
)
|
|
601
|
+
self._live.start()
|
|
602
|
+
self._live.refresh()
|
|
603
|
+
|
|
604
|
+
def stop_live(self):
|
|
605
|
+
if self._live:
|
|
606
|
+
self._live.stop()
|
|
607
|
+
self._live = None
|
|
608
|
+
self._layout = None
|