virtuai-cli 0.8.0__tar.gz → 0.8.2__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.8.0 → virtuai_cli-0.8.2}/PKG-INFO +1 -1
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/pyproject.toml +1 -1
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/__init__.py +1 -1
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/tui.py +5 -2
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/widgets.py +27 -5
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/runner.py +9 -1
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/README.md +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/setup.cfg +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/__init__.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/ask.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/command.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/history.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/sse.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/config.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/executor.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/main.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/security.py +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/requires.txt +0 -0
- {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/top_level.txt +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""VirtuAI local CLI."""
|
|
2
|
-
__version__ = "0.8.
|
|
2
|
+
__version__ = "0.8.2"
|
|
@@ -718,6 +718,7 @@ class ChatApp(App):
|
|
|
718
718
|
async def _stream_response(self, message: str, turn: AssistantTurn) -> None:
|
|
719
719
|
self._streaming = True
|
|
720
720
|
self._refresh_status_bar()
|
|
721
|
+
cancelled = False
|
|
721
722
|
# Snapshot + clear pending attachments BEFORE the request so a new
|
|
722
723
|
# /attach during streaming doesn't bleed into this turn or the next.
|
|
723
724
|
attachments = list(self._pending_attachments)
|
|
@@ -742,7 +743,9 @@ class ChatApp(App):
|
|
|
742
743
|
await self._handle_event(event, turn)
|
|
743
744
|
self._follow_bottom()
|
|
744
745
|
except asyncio.CancelledError:
|
|
745
|
-
#
|
|
746
|
+
# User hit Esc (or app is shutting down). Skip the expensive
|
|
747
|
+
# mark_final flush — see widgets.AssistantTurn.mark_final.
|
|
748
|
+
cancelled = True
|
|
746
749
|
raise
|
|
747
750
|
except Exception as exc:
|
|
748
751
|
try:
|
|
@@ -754,7 +757,7 @@ class ChatApp(App):
|
|
|
754
757
|
self._streaming = False
|
|
755
758
|
self._refresh_status_bar()
|
|
756
759
|
try:
|
|
757
|
-
await turn.mark_final()
|
|
760
|
+
await turn.mark_final(cancelled=cancelled)
|
|
758
761
|
except Exception:
|
|
759
762
|
pass
|
|
760
763
|
try:
|
|
@@ -220,6 +220,7 @@ class TextSegment(Static):
|
|
|
220
220
|
self._buf = ""
|
|
221
221
|
self._last_render_t: float = 0.0
|
|
222
222
|
self._render_pending: bool = False
|
|
223
|
+
self._render_timer: Any = None
|
|
223
224
|
|
|
224
225
|
def append(self, chunk: str) -> None:
|
|
225
226
|
self._buf += chunk
|
|
@@ -232,14 +233,28 @@ class TextSegment(Static):
|
|
|
232
233
|
self._flush()
|
|
233
234
|
else:
|
|
234
235
|
self._render_pending = True
|
|
235
|
-
self.
|
|
236
|
+
self._render_timer = self.set_timer(
|
|
237
|
+
self._RENDER_INTERVAL - elapsed, self._flush
|
|
238
|
+
)
|
|
236
239
|
|
|
237
240
|
def flush(self) -> None:
|
|
238
241
|
"""Force a render now — used to finalize the segment at end-of-stream."""
|
|
239
242
|
self._flush()
|
|
240
243
|
|
|
244
|
+
def cancel_pending(self) -> None:
|
|
245
|
+
"""Drop any scheduled render — used on stream cancel so the cancel
|
|
246
|
+
doesn't trigger another expensive Markdown reparse a moment later."""
|
|
247
|
+
if self._render_timer is not None:
|
|
248
|
+
try:
|
|
249
|
+
self._render_timer.stop()
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
self._render_timer = None
|
|
253
|
+
self._render_pending = False
|
|
254
|
+
|
|
241
255
|
def _flush(self) -> None:
|
|
242
256
|
self._render_pending = False
|
|
257
|
+
self._render_timer = None
|
|
243
258
|
self._last_render_t = time.monotonic()
|
|
244
259
|
try:
|
|
245
260
|
self.update(Markdown(normalize_artifacts(self._buf)))
|
|
@@ -616,13 +631,20 @@ class AssistantTurn(Vertical):
|
|
|
616
631
|
await self.mount(self._todo)
|
|
617
632
|
self._todo.set_items(items)
|
|
618
633
|
|
|
619
|
-
async def mark_final(self) -> None:
|
|
634
|
+
async def mark_final(self, *, cancelled: bool = False) -> None:
|
|
620
635
|
await self._stop_thinking()
|
|
621
|
-
#
|
|
622
|
-
#
|
|
636
|
+
# On graceful completion: do a final synchronous flush so the last
|
|
637
|
+
# sub-50ms of tokens definitely render. On cancel: SKIP the flush —
|
|
638
|
+
# building the markdown AST from a multi-KB buffer is a 100-500ms
|
|
639
|
+
# blocking spike on the event loop right when the user wants to
|
|
640
|
+
# type again, and trying to "polish" the last fragment of an
|
|
641
|
+
# interrupted response isn't worth the lag.
|
|
623
642
|
for seg in self.query(TextSegment):
|
|
624
643
|
try:
|
|
625
|
-
|
|
644
|
+
if cancelled:
|
|
645
|
+
seg.cancel_pending()
|
|
646
|
+
else:
|
|
647
|
+
seg.flush()
|
|
626
648
|
except Exception:
|
|
627
649
|
pass
|
|
628
650
|
self._final = True
|
|
@@ -40,6 +40,7 @@ def _audit(workdir: Path, command: str, exit_code: int, elapsed: float) -> None:
|
|
|
40
40
|
pass
|
|
41
41
|
|
|
42
42
|
_RECONNECT_DELAYS = [1, 2, 4, 8, 16, 30] # backoff steps
|
|
43
|
+
_RECV_TIMEOUT = 60.0 # seconds of silence before declaring the connection stale
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def _default_on_status(level: str, msg: str) -> None:
|
|
@@ -139,7 +140,14 @@ async def run_session(
|
|
|
139
140
|
# JSON pings every 20 s to keep the connection alive (cli_api/router.py).
|
|
140
141
|
ssl_ctx = _ssl_context() if ws_url.startswith("wss://") else None
|
|
141
142
|
async with websockets.connect(url, ping_interval=None, ssl=ssl_ctx) as ws:
|
|
142
|
-
|
|
143
|
+
while True:
|
|
144
|
+
try:
|
|
145
|
+
raw = await asyncio.wait_for(ws.recv(), timeout=_RECV_TIMEOUT)
|
|
146
|
+
except asyncio.TimeoutError:
|
|
147
|
+
# No frame received within the timeout window — the server
|
|
148
|
+
# sends a JSON ping every ~20 s, so 60 s of silence means the
|
|
149
|
+
# TCP connection is stale without a clean close.
|
|
150
|
+
raise ConnectionError("No data received for 60 s — stale connection")
|
|
143
151
|
try:
|
|
144
152
|
frame = json.loads(raw)
|
|
145
153
|
await _handle_frame(frame, workdir, ws, on_status)
|
|
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
|