srcodex 0.2.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.
- srcodex/__init__.py +0 -0
- srcodex/backend/__init__.py +0 -0
- srcodex/backend/chat.py +79 -0
- srcodex/backend/main.py +98 -0
- srcodex/backend/services/__init__.py +0 -0
- srcodex/backend/services/claude_service.py +754 -0
- srcodex/backend/services/config_loader.py +113 -0
- srcodex/backend/services/file_access_tools.py +279 -0
- srcodex/backend/services/file_tree.py +480 -0
- srcodex/backend/services/graph_tools.py +874 -0
- srcodex/backend/services/logger_setup.py +91 -0
- srcodex/backend/services/session_manager.py +81 -0
- srcodex/backend/services/status_tracker.py +91 -0
- srcodex/cli.py +255 -0
- srcodex/core/__init__.py +0 -0
- srcodex/core/config.py +113 -0
- srcodex/core/logger.py +23 -0
- srcodex/indexer/__init__.py +0 -0
- srcodex/indexer/cscope_client.py +183 -0
- srcodex/indexer/ctags_compat.py +223 -0
- srcodex/indexer/ctags_parser.py +456 -0
- srcodex/indexer/explorer.py +135 -0
- srcodex/indexer/field_access_analyzer.py +436 -0
- srcodex/indexer/indexer.py +664 -0
- srcodex/indexer/reference_ingestor.py +293 -0
- srcodex/indexer/reference_resolver.py +544 -0
- srcodex/tui/__init__.py +0 -0
- srcodex/tui/app.py +103 -0
- srcodex/tui/app.tcss +24 -0
- srcodex/tui/components/__init__.py +0 -0
- srcodex/tui/components/bars/__init__.py +0 -0
- srcodex/tui/components/bars/chat_header.py +48 -0
- srcodex/tui/components/bars/code_tab_bar.py +157 -0
- srcodex/tui/components/bars/footer_bar.py +128 -0
- srcodex/tui/components/bars/left_tab.py +54 -0
- srcodex/tui/components/logger.py +57 -0
- srcodex/tui/components/panels/__init__.py +0 -0
- srcodex/tui/components/panels/chat_panel.py +523 -0
- srcodex/tui/components/panels/code_panel.py +229 -0
- srcodex/tui/components/panels/side_panel.py +128 -0
- srcodex/tui/components/views/__init__.py +0 -0
- srcodex/tui/components/views/explorer_view.py +20 -0
- srcodex/tui/components/views/search_view.py +148 -0
- srcodex/tui/components/widgets/__init__.py +0 -0
- srcodex/tui/components/widgets/file_browser.py +16 -0
- srcodex/tui/components/widgets/find_box.py +85 -0
- srcodex-0.2.0.dist-info/METADATA +170 -0
- srcodex-0.2.0.dist-info/RECORD +52 -0
- srcodex-0.2.0.dist-info/WHEEL +5 -0
- srcodex-0.2.0.dist-info/entry_points.txt +2 -0
- srcodex-0.2.0.dist-info/licenses/LICENSE +21 -0
- srcodex-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
from textual.widgets import Static, Markdown, ListView, ListItem, Label
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.containers import Vertical, VerticalScroll
|
|
4
|
+
from textual.widgets import Input, TextArea
|
|
5
|
+
from textual.message import Message
|
|
6
|
+
import httpx
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from components.bars.chat_header import ChatHeader
|
|
12
|
+
|
|
13
|
+
backend_path = Path(__file__).parent.parent.parent.parent / "backend"
|
|
14
|
+
sys.path.insert(0, str(backend_path))
|
|
15
|
+
from services.session_manager import SessionManager
|
|
16
|
+
from services.config_loader import get_config
|
|
17
|
+
|
|
18
|
+
class ChatInput(TextArea):
|
|
19
|
+
"""Custom TextArea that sends message on Enter"""
|
|
20
|
+
|
|
21
|
+
class Submit(Message):
|
|
22
|
+
"""Posted when user presses Enter (not Shift+Enter)"""
|
|
23
|
+
def __init__(self, text: str):
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.text = text
|
|
26
|
+
|
|
27
|
+
def on_key(self, event):
|
|
28
|
+
"""Intercept Enter key - use Ctrl+Enter for new line"""
|
|
29
|
+
if event.key == "ctrl+enter":
|
|
30
|
+
# Ctrl+Enter - insert new line manually
|
|
31
|
+
cursor = self.cursor_location
|
|
32
|
+
current_text = self.text
|
|
33
|
+
# Insert newline at cursor position
|
|
34
|
+
line, col = cursor
|
|
35
|
+
lines = current_text.split('\n')
|
|
36
|
+
if line < len(lines):
|
|
37
|
+
lines[line] = lines[line][:col] + '\n' + lines[line][col:]
|
|
38
|
+
self.text = '\n'.join(lines)
|
|
39
|
+
self.cursor_location = (line + 1, 0)
|
|
40
|
+
event.prevent_default()
|
|
41
|
+
event.stop()
|
|
42
|
+
elif event.key == "enter":
|
|
43
|
+
# Plain Enter - submit
|
|
44
|
+
event.prevent_default()
|
|
45
|
+
event.stop()
|
|
46
|
+
text = self.text.strip()
|
|
47
|
+
if text:
|
|
48
|
+
self.post_message(self.Submit(text))
|
|
49
|
+
self.text = ""
|
|
50
|
+
|
|
51
|
+
class ChatSettingsMenu(Vertical):
|
|
52
|
+
"""Dropdown menu for chat settings"""
|
|
53
|
+
|
|
54
|
+
DEFAULT_CSS = """
|
|
55
|
+
ChatSettingsMenu {
|
|
56
|
+
width: 20;
|
|
57
|
+
height: auto;
|
|
58
|
+
background: transparent;
|
|
59
|
+
layer: overlay;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ChatSettingsMenu ListView {
|
|
63
|
+
height: auto;
|
|
64
|
+
width: 100%;
|
|
65
|
+
background: transparent;
|
|
66
|
+
border: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ChatSettingsMenu ListItem {
|
|
70
|
+
height: 1;
|
|
71
|
+
padding: 0 1;
|
|
72
|
+
background: transparent;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ChatSettingsMenu ListItem:hover {
|
|
76
|
+
background: $boost;
|
|
77
|
+
}
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def compose(self):
|
|
81
|
+
yield ListView(
|
|
82
|
+
ListItem(Label("New Session"), id="new-session"),
|
|
83
|
+
ListItem(Label("Clear History"), id="clear-history"),
|
|
84
|
+
ListItem(Label("Export Chat"), id="export-chat"),
|
|
85
|
+
ListItem(Label("Change Model"), id="change-model"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ChatPanel(Vertical):
|
|
90
|
+
""" AI chat panel (right) - Claude (LLM) conversation interface"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, **kwargs):
|
|
93
|
+
super().__init__(**kwargs)
|
|
94
|
+
self.session_input_tokens = 0
|
|
95
|
+
self.session_output_tokens = 0
|
|
96
|
+
self.session_cache_read_tokens = 0
|
|
97
|
+
self.session_cache_write_tokens = 0
|
|
98
|
+
self.last_query_input_tokens = 0
|
|
99
|
+
self.last_query_output_tokens = 0
|
|
100
|
+
self.last_query_cache_read = 0
|
|
101
|
+
self.last_query_cache_write = 0
|
|
102
|
+
self.settings_menu_visible = False
|
|
103
|
+
|
|
104
|
+
config = get_config()
|
|
105
|
+
self.session_manager = SessionManager(str(config.project_root))
|
|
106
|
+
|
|
107
|
+
self.conversation_history = self.session_manager.load_session()
|
|
108
|
+
self.session_loaded = len(self.conversation_history) > 0
|
|
109
|
+
|
|
110
|
+
# Load persistent metrics
|
|
111
|
+
metadata = self.session_manager.load_metadata()
|
|
112
|
+
self.query_count = metadata.get("query_count", 0)
|
|
113
|
+
self.total_user_input = metadata.get("total_user_input", 0)
|
|
114
|
+
self.total_output = metadata.get("total_output", 0)
|
|
115
|
+
self.total_files_accessed = metadata.get("total_files_accessed", 0)
|
|
116
|
+
self.total_savings_percentage = metadata.get("total_savings_percentage", 0.0)
|
|
117
|
+
|
|
118
|
+
# Load cache tokens from session
|
|
119
|
+
self.session_cache_read_tokens = metadata.get("cache_read", 0)
|
|
120
|
+
self.session_cache_write_tokens = metadata.get("cache_write", 0)
|
|
121
|
+
|
|
122
|
+
DEFAULT_CSS = """
|
|
123
|
+
ChatPanel {
|
|
124
|
+
width: 100%;
|
|
125
|
+
height: 100%;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#settings-menu {
|
|
129
|
+
offset: 55 1;
|
|
130
|
+
display: none;
|
|
131
|
+
border: white;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#settings-menu.visible {
|
|
135
|
+
display: block;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#conversation-scroll {
|
|
139
|
+
height: 1fr;
|
|
140
|
+
border: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#conversation-history {
|
|
144
|
+
width: 100%;
|
|
145
|
+
padding: 1;
|
|
146
|
+
background: transparent;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Remove markdown header coloring only */
|
|
150
|
+
#conversation-history MarkdownH1,
|
|
151
|
+
#conversation-history MarkdownH2,
|
|
152
|
+
#conversation-history MarkdownH3,
|
|
153
|
+
#conversation-history MarkdownH4,
|
|
154
|
+
#conversation-history MarkdownH5,
|
|
155
|
+
#conversation-history MarkdownH6 {
|
|
156
|
+
color: $text;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#conversation-history MarkdownHorizontalRule {
|
|
160
|
+
display: none;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
ChatPanel Scrollbar {
|
|
164
|
+
width: 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#chat-input-container {
|
|
168
|
+
width: 100%;
|
|
169
|
+
height: auto;
|
|
170
|
+
padding: 0;
|
|
171
|
+
border: grey;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#chat-input {
|
|
175
|
+
width: 1fr;
|
|
176
|
+
height: auto;
|
|
177
|
+
min-height: 3;
|
|
178
|
+
border: none;
|
|
179
|
+
margin: 0;
|
|
180
|
+
padding: 0;
|
|
181
|
+
background: transparent;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#chat-input:focus {
|
|
185
|
+
background: transparent;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#chat-input > .text-area--cursor-line {
|
|
189
|
+
background: transparent;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#status-line {
|
|
193
|
+
height: 1;
|
|
194
|
+
width: 100%;
|
|
195
|
+
background: transparent;
|
|
196
|
+
color: $accent;
|
|
197
|
+
text-align: right;
|
|
198
|
+
padding-right: 1;
|
|
199
|
+
display: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#status-line.visible {
|
|
203
|
+
display: block;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#status-line.completed {
|
|
207
|
+
color: $success;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#token-counter {
|
|
211
|
+
height: 2;
|
|
212
|
+
width: 100%;
|
|
213
|
+
background: transparent;
|
|
214
|
+
color: $text-muted;
|
|
215
|
+
text-align: right;
|
|
216
|
+
padding-right: 1;
|
|
217
|
+
}
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def compose(self):
|
|
221
|
+
"""Build the chat panel UI"""
|
|
222
|
+
yield ChatHeader()
|
|
223
|
+
yield ChatSettingsMenu(id="settings-menu")
|
|
224
|
+
with VerticalScroll(id="conversation-scroll"):
|
|
225
|
+
yield Markdown("", id="conversation-history")
|
|
226
|
+
yield Static("", id="status-line")
|
|
227
|
+
yield Static("", id="token-counter")
|
|
228
|
+
with Vertical(id="chat-input-container"):
|
|
229
|
+
yield ChatInput(id="chat-input", show_line_numbers=False)
|
|
230
|
+
|
|
231
|
+
def on_mount(self) -> None:
|
|
232
|
+
"""When panel is mounted, show welcome message"""
|
|
233
|
+
conversation = self.query_one("#conversation-history", Markdown)
|
|
234
|
+
|
|
235
|
+
if self.session_loaded:
|
|
236
|
+
markdown_text = self._build_conversation_markdown()
|
|
237
|
+
else:
|
|
238
|
+
markdown_text = "*Type below and press Enter to chat. Ctrl+L to clear history.*\n\n"
|
|
239
|
+
|
|
240
|
+
conversation.update(markdown_text)
|
|
241
|
+
self.set_timer(0.3, self._scroll_to_bottom)
|
|
242
|
+
|
|
243
|
+
# Display cached token values on startup
|
|
244
|
+
self._update_token_display()
|
|
245
|
+
|
|
246
|
+
def _build_conversation_markdown(self) -> str:
|
|
247
|
+
"""Build markdown string from conversation history"""
|
|
248
|
+
if not self.conversation_history:
|
|
249
|
+
return "*Ask a question below (Enter to send, Ctrl+L to clear)*"
|
|
250
|
+
|
|
251
|
+
lines = []
|
|
252
|
+
for msg in self.conversation_history:
|
|
253
|
+
if msg["role"] == "user":
|
|
254
|
+
lines.append(f"> **You:** {msg['content']}")
|
|
255
|
+
lines.append("")
|
|
256
|
+
elif msg["role"] == "assistant":
|
|
257
|
+
lines.append(f"**Claude:**")
|
|
258
|
+
lines.append(msg['content'])
|
|
259
|
+
lines.append("")
|
|
260
|
+
|
|
261
|
+
return "\n".join(lines)
|
|
262
|
+
|
|
263
|
+
def on_chat_header_settings_clicked(self, event: ChatHeader.SettingsClicked):
|
|
264
|
+
"""Handle settings button click - toggle menu"""
|
|
265
|
+
menu = self.query_one("#settings-menu", ChatSettingsMenu)
|
|
266
|
+
if self.settings_menu_visible:
|
|
267
|
+
menu.remove_class("visible")
|
|
268
|
+
self.settings_menu_visible = False
|
|
269
|
+
else:
|
|
270
|
+
menu.add_class("visible")
|
|
271
|
+
self.settings_menu_visible = True
|
|
272
|
+
|
|
273
|
+
def on_list_view_selected(self, event: ListView.Selected):
|
|
274
|
+
"""Handle menu item selection"""
|
|
275
|
+
menu = self.query_one("#settings-menu", ChatSettingsMenu)
|
|
276
|
+
menu.remove_class("visible")
|
|
277
|
+
self.settings_menu_visible = False
|
|
278
|
+
|
|
279
|
+
if event.item.id == "clear-history":
|
|
280
|
+
self._clear_history()
|
|
281
|
+
|
|
282
|
+
def _clear_history(self):
|
|
283
|
+
"""Clear conversation history"""
|
|
284
|
+
self.conversation_history = []
|
|
285
|
+
self.session_manager.clear_session()
|
|
286
|
+
self.session_input_tokens = 0
|
|
287
|
+
self.session_output_tokens = 0
|
|
288
|
+
self.session_cache_read_tokens = 0
|
|
289
|
+
self.session_cache_write_tokens = 0
|
|
290
|
+
self.last_query_input_tokens = 0
|
|
291
|
+
self.last_query_output_tokens = 0
|
|
292
|
+
self.last_query_cache_read = 0
|
|
293
|
+
self.last_query_cache_write = 0
|
|
294
|
+
|
|
295
|
+
# Reset persistent footer metrics
|
|
296
|
+
self.query_count = 0
|
|
297
|
+
self.total_user_input = 0
|
|
298
|
+
self.total_output = 0
|
|
299
|
+
self.total_files_accessed = 0
|
|
300
|
+
self.total_savings_percentage = 0.0
|
|
301
|
+
|
|
302
|
+
self._update_token_display()
|
|
303
|
+
self._update_footer()
|
|
304
|
+
self._refresh_conversation()
|
|
305
|
+
|
|
306
|
+
def on_key(self, event):
|
|
307
|
+
"""Handle keyboard shortcuts"""
|
|
308
|
+
if event.key == "ctrl+l":
|
|
309
|
+
self._clear_history()
|
|
310
|
+
event.prevent_default()
|
|
311
|
+
event.stop()
|
|
312
|
+
|
|
313
|
+
async def on_chat_input_submit(self, event: ChatInput.Submit):
|
|
314
|
+
"""Handle message submission from ChatInput"""
|
|
315
|
+
self.conversation_history.append({
|
|
316
|
+
"role": "user",
|
|
317
|
+
"content": event.text
|
|
318
|
+
})
|
|
319
|
+
self._refresh_conversation()
|
|
320
|
+
await self.send_message(event.text)
|
|
321
|
+
|
|
322
|
+
async def send_message(self, user_message: str):
|
|
323
|
+
"""Send message to Claude backend"""
|
|
324
|
+
conversation = self.query_one("#conversation-history", Markdown)
|
|
325
|
+
token_counter = self.query_one("#token-counter", Static)
|
|
326
|
+
status_line = self.query_one("#status-line", Static)
|
|
327
|
+
|
|
328
|
+
# Show status line and start timer
|
|
329
|
+
status_line.add_class("visible")
|
|
330
|
+
self.status_start_time = 0
|
|
331
|
+
self.current_status_text = "Analyzing..."
|
|
332
|
+
self._update_status_display()
|
|
333
|
+
|
|
334
|
+
# Start update timer (update every second)
|
|
335
|
+
self.status_timer = self.set_interval(1.0, self._update_status_timer)
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
async with httpx.AsyncClient() as client:
|
|
339
|
+
async with client.stream(
|
|
340
|
+
"POST",
|
|
341
|
+
"http://localhost:8000/api/chat/stream",
|
|
342
|
+
json={
|
|
343
|
+
"message": user_message,
|
|
344
|
+
"conversation_history": self.conversation_history
|
|
345
|
+
},
|
|
346
|
+
timeout=None
|
|
347
|
+
) as response:
|
|
348
|
+
response.raise_for_status()
|
|
349
|
+
|
|
350
|
+
full_response = ""
|
|
351
|
+
async for line in response.aiter_lines():
|
|
352
|
+
if not line.strip():
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
data = json.loads(line)
|
|
357
|
+
|
|
358
|
+
if data["type"] == "status":
|
|
359
|
+
# Update status line
|
|
360
|
+
self.current_status_text = data["content"]
|
|
361
|
+
self.status_start_time = data.get("elapsed", 0)
|
|
362
|
+
|
|
363
|
+
# Mark as completed if status is "Complete"
|
|
364
|
+
status_line = self.query_one("#status-line", Static)
|
|
365
|
+
if data["content"] == "Complete":
|
|
366
|
+
status_line.add_class("completed")
|
|
367
|
+
else:
|
|
368
|
+
status_line.remove_class("completed")
|
|
369
|
+
|
|
370
|
+
self._update_status_display()
|
|
371
|
+
|
|
372
|
+
elif data["type"] == "text":
|
|
373
|
+
full_response += data["content"]
|
|
374
|
+
|
|
375
|
+
elif data["type"] == "tokens":
|
|
376
|
+
self.last_query_input_tokens = data["input"]
|
|
377
|
+
self.last_query_output_tokens = data["output"]
|
|
378
|
+
self.last_query_cache_read = data.get("cache_read", 0)
|
|
379
|
+
self.last_query_cache_write = data.get("cache_write", 0)
|
|
380
|
+
|
|
381
|
+
self.session_input_tokens += data["input"]
|
|
382
|
+
self.session_output_tokens += data["output"]
|
|
383
|
+
self.session_cache_read_tokens += data.get("cache_read", 0)
|
|
384
|
+
self.session_cache_write_tokens += data.get("cache_write", 0)
|
|
385
|
+
|
|
386
|
+
# Update persistent metrics
|
|
387
|
+
self.query_count += 1
|
|
388
|
+
self.total_user_input += data.get("user_input_only", 0)
|
|
389
|
+
self.total_output += data["output"]
|
|
390
|
+
self.total_files_accessed += data.get("files_accessed", 0)
|
|
391
|
+
# Recalculate average savings
|
|
392
|
+
if self.query_count > 0:
|
|
393
|
+
self.total_savings_percentage = data.get("savings_percentage", 0.0)
|
|
394
|
+
|
|
395
|
+
self._update_token_display()
|
|
396
|
+
self._update_footer()
|
|
397
|
+
|
|
398
|
+
except json.JSONDecodeError:
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
# Stop status timer but KEEP status line visible with final state
|
|
402
|
+
if hasattr(self, 'status_timer'):
|
|
403
|
+
self.status_timer.stop()
|
|
404
|
+
# Status line stays visible showing "Complete | Xs"
|
|
405
|
+
|
|
406
|
+
self.conversation_history.append({
|
|
407
|
+
"role": "assistant",
|
|
408
|
+
"content": full_response
|
|
409
|
+
})
|
|
410
|
+
self._refresh_conversation()
|
|
411
|
+
self.set_timer(0.5, self._scroll_to_bottom)
|
|
412
|
+
|
|
413
|
+
max_messages = 10
|
|
414
|
+
if len(self.conversation_history) > max_messages:
|
|
415
|
+
self.conversation_history = self.conversation_history[-max_messages:]
|
|
416
|
+
|
|
417
|
+
self.session_manager.save_session(
|
|
418
|
+
messages=self.conversation_history,
|
|
419
|
+
metadata={
|
|
420
|
+
"total_tokens": self.session_input_tokens + self.session_output_tokens,
|
|
421
|
+
"input_tokens": self.session_input_tokens,
|
|
422
|
+
"output_tokens": self.session_output_tokens,
|
|
423
|
+
"cache_read": self.session_cache_read_tokens,
|
|
424
|
+
"cache_write": self.session_cache_write_tokens,
|
|
425
|
+
"query_count": self.query_count,
|
|
426
|
+
"total_user_input": self.total_user_input,
|
|
427
|
+
"total_output": self.total_output,
|
|
428
|
+
"total_files_accessed": self.total_files_accessed,
|
|
429
|
+
"total_savings_percentage": self.total_savings_percentage
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
except httpx.HTTPError as e:
|
|
434
|
+
self.conversation_history.append({
|
|
435
|
+
"role": "assistant",
|
|
436
|
+
"content": f"**Error:** {type(e).__name__}: {str(e)}"
|
|
437
|
+
})
|
|
438
|
+
self._refresh_conversation()
|
|
439
|
+
except Exception as e:
|
|
440
|
+
self.conversation_history.append({
|
|
441
|
+
"role": "assistant",
|
|
442
|
+
"content": f"**Error:** {type(e).__name__}: {str(e)}"
|
|
443
|
+
})
|
|
444
|
+
self._refresh_conversation()
|
|
445
|
+
|
|
446
|
+
def _refresh_conversation(self):
|
|
447
|
+
"""Rebuild and update the conversation display"""
|
|
448
|
+
conversation = self.query_one("#conversation-history", Markdown)
|
|
449
|
+
conversation.update(self._build_conversation_markdown())
|
|
450
|
+
self.call_after_refresh(self._scroll_to_bottom)
|
|
451
|
+
self.set_timer(0.2, self._scroll_to_bottom)
|
|
452
|
+
|
|
453
|
+
def _scroll_to_bottom(self):
|
|
454
|
+
"""Scroll conversation to bottom"""
|
|
455
|
+
try:
|
|
456
|
+
scroll = self.query_one("#conversation-scroll", VerticalScroll)
|
|
457
|
+
scroll.scroll_end(duration=0)
|
|
458
|
+
scroll.scroll_to(y=scroll.max_scroll_y, animate=False)
|
|
459
|
+
except:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
def _update_token_display(self):
|
|
463
|
+
"""Update the token counter display (2 lines)"""
|
|
464
|
+
token_counter = self.query_one("#token-counter", Static)
|
|
465
|
+
|
|
466
|
+
# Only show cache tokens with yellow color
|
|
467
|
+
line1 = f"Last: [yellow]💰{self.last_query_cache_read:,} cache read, 💰{self.last_query_cache_write:,} cache write[/yellow]"
|
|
468
|
+
line2 = f"Session: [yellow]💰{self.session_cache_read_tokens:,} cache read, 💰{self.session_cache_write_tokens:,} cache write[/yellow]"
|
|
469
|
+
|
|
470
|
+
token_counter.update(f"{line1}\n{line2}")
|
|
471
|
+
|
|
472
|
+
def _update_status_display(self):
|
|
473
|
+
"""Update the status line with current status and elapsed time"""
|
|
474
|
+
if not hasattr(self, 'current_status_text') or not self.current_status_text:
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
status_line = self.query_one("#status-line", Static)
|
|
478
|
+
elapsed = self.status_start_time
|
|
479
|
+
|
|
480
|
+
# Format time: "5s" or "1m 30s"
|
|
481
|
+
if elapsed < 60:
|
|
482
|
+
time_str = f"{elapsed}s"
|
|
483
|
+
else:
|
|
484
|
+
minutes = elapsed // 60
|
|
485
|
+
seconds = elapsed % 60
|
|
486
|
+
time_str = f"{minutes}m {seconds}s"
|
|
487
|
+
|
|
488
|
+
# Different format for completed vs active
|
|
489
|
+
if self.current_status_text == "Complete":
|
|
490
|
+
# Vary completion message based on duration
|
|
491
|
+
if elapsed < 5:
|
|
492
|
+
verb = "Completed"
|
|
493
|
+
elif elapsed < 15:
|
|
494
|
+
verb = "Finished"
|
|
495
|
+
elif elapsed < 30:
|
|
496
|
+
verb = "Churned"
|
|
497
|
+
elif elapsed < 60:
|
|
498
|
+
verb = "Cooked"
|
|
499
|
+
else:
|
|
500
|
+
verb = "Grinded"
|
|
501
|
+
status_line.update(f"{verb} in {time_str}")
|
|
502
|
+
else:
|
|
503
|
+
status_line.update(f"{self.current_status_text} | {time_str}")
|
|
504
|
+
|
|
505
|
+
def _update_status_timer(self):
|
|
506
|
+
"""Called every second to update elapsed time"""
|
|
507
|
+
if hasattr(self, 'status_start_time'):
|
|
508
|
+
self.status_start_time += 1
|
|
509
|
+
self._update_status_display()
|
|
510
|
+
|
|
511
|
+
def _update_footer(self):
|
|
512
|
+
"""Update the footer bar with persistent stats"""
|
|
513
|
+
try:
|
|
514
|
+
from components.bars.footer_bar import FooterBar
|
|
515
|
+
footer = self.app.query_one(FooterBar)
|
|
516
|
+
footer.update_stats(
|
|
517
|
+
self.query_count,
|
|
518
|
+
self.total_user_input,
|
|
519
|
+
self.total_output,
|
|
520
|
+
self.total_savings_percentage
|
|
521
|
+
)
|
|
522
|
+
except Exception:
|
|
523
|
+
pass
|