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.
Files changed (23) hide show
  1. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/PKG-INFO +1 -1
  2. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/pyproject.toml +1 -1
  3. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/__init__.py +1 -1
  4. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/tui.py +5 -2
  5. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/widgets.py +27 -5
  6. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/runner.py +9 -1
  7. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
  8. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/README.md +0 -0
  9. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/setup.cfg +0 -0
  10. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/__init__.py +0 -0
  11. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/ask.py +0 -0
  12. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/command.py +0 -0
  13. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/history.py +0 -0
  14. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/chat/sse.py +0 -0
  15. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/config.py +0 -0
  16. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/executor.py +0 -0
  17. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/main.py +0 -0
  18. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli/security.py +0 -0
  19. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
  20. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
  21. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
  22. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/requires.txt +0 -0
  23. {virtuai_cli-0.8.0 → virtuai_cli-0.8.2}/src/virtuai_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "virtuai-cli"
7
- version = "0.8.0"
7
+ version = "0.8.2"
8
8
  description = "Run VirtuAI deep agents on your local machine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,2 +1,2 @@
1
1
  """VirtuAI local CLI."""
2
- __version__ = "0.8.0"
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
- # App may be shutting down don't touch widgets here.
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.set_timer(self._RENDER_INTERVAL - elapsed, self._flush)
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
- # 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.
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
- seg.flush()
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
- async for raw in ws:
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
File without changes
File without changes