virtuai-cli 0.7.5__tar.gz → 0.7.6__tar.gz
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.
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/PKG-INFO +1 -1
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/pyproject.toml +1 -1
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/__init__.py +1 -1
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/chat/tui.py +4 -1
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/chat/widgets.py +42 -2
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/README.md +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/setup.cfg +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/chat/__init__.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/chat/ask.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/chat/command.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/chat/history.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/chat/sse.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/config.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/executor.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/main.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/runner.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli/security.py +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli.egg-info/requires.txt +0 -0
- {virtuai_cli-0.7.5 → virtuai_cli-0.7.6}/src/virtuai_cli.egg-info/top_level.txt +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""VirtuAI local CLI."""
|
|
2
|
-
__version__ = "0.7.
|
|
2
|
+
__version__ = "0.7.6"
|
|
@@ -176,10 +176,13 @@ class ChatApp(App):
|
|
|
176
176
|
self._runner_task = asyncio.create_task(self._run_ws())
|
|
177
177
|
|
|
178
178
|
def _tick_status(self) -> None:
|
|
179
|
+
# Only repaint when something is actually animating — otherwise we'd
|
|
180
|
+
# force a Textual reflow 10x/sec across the whole scrollback, which
|
|
181
|
+
# makes long conversations laggy.
|
|
179
182
|
moving = self._streaming or self._connection_state in ("connecting", "reconnecting")
|
|
180
183
|
if moving:
|
|
181
184
|
self._spinner_idx = (self._spinner_idx + 1) % len(self._SPINNER)
|
|
182
|
-
|
|
185
|
+
self._refresh_status_bar()
|
|
183
186
|
|
|
184
187
|
def _refresh_status_bar(self) -> None:
|
|
185
188
|
try:
|
|
@@ -200,19 +200,52 @@ class UserBubble(Static):
|
|
|
200
200
|
|
|
201
201
|
|
|
202
202
|
class TextSegment(Static):
|
|
203
|
-
"""A streaming markdown segment of assistant text.
|
|
203
|
+
"""A streaming markdown segment of assistant text.
|
|
204
|
+
|
|
205
|
+
Renders are throttled to ~20 Hz: the markdown AST is rebuilt from the
|
|
206
|
+
full buffer on every paint (Rich/mistletoe parse from scratch), so
|
|
207
|
+
rendering once per token on a long response is O(n²) and dominates CPU.
|
|
208
|
+
With throttling, bursts of tokens coalesce into a single render and any
|
|
209
|
+
final pending render is flushed via mark_final()/flush().
|
|
210
|
+
"""
|
|
204
211
|
|
|
205
212
|
DEFAULT_CSS = """
|
|
206
213
|
TextSegment { margin: 0 0 0 2; }
|
|
207
214
|
"""
|
|
208
215
|
|
|
216
|
+
_RENDER_INTERVAL = 0.05 # seconds (= 20 fps)
|
|
217
|
+
|
|
209
218
|
def __init__(self) -> None:
|
|
210
219
|
super().__init__("")
|
|
211
220
|
self._buf = ""
|
|
221
|
+
self._last_render_t: float = 0.0
|
|
222
|
+
self._render_pending: bool = False
|
|
212
223
|
|
|
213
224
|
def append(self, chunk: str) -> None:
|
|
214
225
|
self._buf += chunk
|
|
215
|
-
self.
|
|
226
|
+
if self._render_pending:
|
|
227
|
+
# A flush is already scheduled and will pick up the new content.
|
|
228
|
+
return
|
|
229
|
+
now = time.monotonic()
|
|
230
|
+
elapsed = now - self._last_render_t
|
|
231
|
+
if elapsed >= self._RENDER_INTERVAL:
|
|
232
|
+
self._flush()
|
|
233
|
+
else:
|
|
234
|
+
self._render_pending = True
|
|
235
|
+
self.set_timer(self._RENDER_INTERVAL - elapsed, self._flush)
|
|
236
|
+
|
|
237
|
+
def flush(self) -> None:
|
|
238
|
+
"""Force a render now — used to finalize the segment at end-of-stream."""
|
|
239
|
+
self._flush()
|
|
240
|
+
|
|
241
|
+
def _flush(self) -> None:
|
|
242
|
+
self._render_pending = False
|
|
243
|
+
self._last_render_t = time.monotonic()
|
|
244
|
+
try:
|
|
245
|
+
self.update(Markdown(normalize_artifacts(self._buf)))
|
|
246
|
+
except Exception:
|
|
247
|
+
# Widget might be unmounted by the time a scheduled flush fires.
|
|
248
|
+
pass
|
|
216
249
|
|
|
217
250
|
@property
|
|
218
251
|
def content(self) -> str:
|
|
@@ -585,4 +618,11 @@ class AssistantTurn(Vertical):
|
|
|
585
618
|
|
|
586
619
|
async def mark_final(self) -> None:
|
|
587
620
|
await self._stop_thinking()
|
|
621
|
+
# Force any throttled TextSegment to do its final render so the user
|
|
622
|
+
# doesn't see the last sub-50ms of tokens missing after stream-end.
|
|
623
|
+
for seg in self.query(TextSegment):
|
|
624
|
+
try:
|
|
625
|
+
seg.flush()
|
|
626
|
+
except Exception:
|
|
627
|
+
pass
|
|
588
628
|
self._final = True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|