virtuai-cli 0.7.1__tar.gz → 0.7.3__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.7.1 → virtuai_cli-0.7.3}/PKG-INFO +1 -1
  2. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/pyproject.toml +1 -1
  3. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/__init__.py +1 -1
  4. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/tui.py +42 -6
  5. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/widgets.py +84 -1
  6. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
  7. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/README.md +0 -0
  8. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/setup.cfg +0 -0
  9. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/__init__.py +0 -0
  10. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/ask.py +0 -0
  11. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/command.py +0 -0
  12. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/history.py +0 -0
  13. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/sse.py +0 -0
  14. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/config.py +0 -0
  15. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/executor.py +0 -0
  16. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/main.py +0 -0
  17. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/runner.py +0 -0
  18. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli/security.py +0 -0
  19. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
  20. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
  21. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
  22. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/requires.txt +0 -0
  23. {virtuai_cli-0.7.1 → virtuai_cli-0.7.3}/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.7.1
3
+ Version: 0.7.3
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.7.1"
7
+ version = "0.7.3"
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.7.1"
2
+ __version__ = "0.7.3"
@@ -379,17 +379,53 @@ class ChatApp(App):
379
379
  )
380
380
  await scroll.mount(Static(header))
381
381
 
382
+ loaded_session = self.session_id
382
383
  for msg in messages:
383
384
  mtype = (msg.get("message_type") or "").lower()
384
385
  content = msg.get("content") or ""
386
+ steps = msg.get("steps") or []
385
387
  if mtype in ("user", "human"):
386
388
  await scroll.mount(UserBubble(content))
387
- else:
388
- turn = AssistantTurn(show_thinking=False)
389
- await scroll.mount(turn)
390
- if content:
391
- await turn.append_token(content)
392
- await turn.mark_final()
389
+ continue
390
+
391
+ turn = AssistantTurn(show_thinking=False)
392
+ await scroll.mount(turn)
393
+
394
+ replayed_visible = False
395
+ if isinstance(steps, list) and steps:
396
+ # Replay the original event stream through the same handler
397
+ # the live stream uses, so tool cards, file diffs, todos,
398
+ # and subagent traces all re-render identically to when
399
+ # the turn first ran. Skip lifecycle events that mutate
400
+ # app state (session_id) or close the stream.
401
+ for step in steps:
402
+ if not isinstance(step, dict):
403
+ continue
404
+ etype = step.get("type")
405
+ if etype in ("start", "done", "complete"):
406
+ continue
407
+ try:
408
+ await self._handle_event(step, turn)
409
+ if etype in (
410
+ "token", "text_segment", "tool_start", "tool_end",
411
+ "todos", "subagent_start", "subagent_end",
412
+ "memory_updated", "skill_loaded",
413
+ ):
414
+ replayed_visible = True
415
+ except Exception:
416
+ # One bad step shouldn't break the whole replay.
417
+ continue
418
+
419
+ # Fall back to the persisted final text if there were no steps
420
+ # (older messages saved before steps tracking) or the steps
421
+ # rendered nothing visible.
422
+ if not replayed_visible and content:
423
+ await turn.append_token(content)
424
+ await turn.mark_final()
425
+
426
+ # Replay's `start` events were skipped, but in case anything else
427
+ # touched session_id, restore it to the conversation we loaded.
428
+ self.session_id = loaded_session
393
429
  scroll.scroll_end(animate=False)
394
430
  self._set_status(f"loaded {self.session_id}")
395
431
 
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import difflib
11
11
  import json
12
12
  import random
13
+ import re
13
14
  import time
14
15
  from typing import Any, Optional
15
16
 
@@ -83,6 +84,88 @@ class ChatInput(TextArea):
83
84
  # rather than a generic ToolCallCard.
84
85
  _FILE_DISPLAYS = {"file", "file_written"}
85
86
 
87
+
88
+ # ─── Artifact-tag normalization ──────────────────────────────────────────────
89
+ # The agent emits rich content (mermaid diagrams, code, HTML, SVG) wrapped in
90
+ # <artifact type="..." title="..." language="...">…</artifact> XML tags so the
91
+ # web UI can render them as artifact cards. Terminals can't render mermaid or
92
+ # HTML visually, so we rewrite those tags into fenced markdown code blocks
93
+ # (with a header line) — that way Rich Markdown renders the source as a
94
+ # syntax-highlighted code block instead of either silently swallowing the
95
+ # whole block (HTML-tag handling) or interpreting `[Label]` in mermaid bodies
96
+ # as empty markdown links (which is what reduces "F1[FASE 1] --> F2[…]" to a
97
+ # bare "F1 --> F2 -->").
98
+
99
+ _ARTIFACT_OPEN_RE = re.compile(r"<artifact([^>]*)>", re.IGNORECASE)
100
+ _ARTIFACT_CLOSE_RE = re.compile(r"</artifact\s*>", re.IGNORECASE)
101
+ _ATTR_RE = re.compile(r"(\w+)=(?:\"([^\"]*?)\"|(\S+))")
102
+
103
+
104
+ def _parse_artifact_attrs(raw: str) -> dict[str, str]:
105
+ return {m.group(1): (m.group(2) if m.group(2) is not None else m.group(3) or "")
106
+ for m in _ATTR_RE.finditer(raw)}
107
+
108
+
109
+ def _artifact_header(attrs: dict[str, str], suffix: str = "") -> str:
110
+ atype = (attrs.get("type") or "code").strip()
111
+ title = (attrs.get("title") or "Artifact").strip()
112
+ icon = {
113
+ "mermaid": "📈",
114
+ "html": "🌐",
115
+ "svg": "🎨",
116
+ "code": "📄",
117
+ "markdown": "📝",
118
+ "pdf": "📕",
119
+ }.get(atype.lower(), "📎")
120
+ return f"\n\n**{icon} {atype} · {title}**{suffix}\n"
121
+
122
+
123
+ def _artifact_lang(attrs: dict[str, str]) -> str:
124
+ lang = (attrs.get("language") or attrs.get("lang") or "").strip()
125
+ if lang:
126
+ return lang
127
+ atype = (attrs.get("type") or "").strip().lower()
128
+ return {"mermaid": "mermaid", "html": "html", "svg": "xml"}.get(atype, "text")
129
+
130
+
131
+ def normalize_artifacts(text: str) -> str:
132
+ """Rewrite <artifact>…</artifact> blocks into fenced code blocks.
133
+
134
+ Handles both completed pairs (full body rendered inside a fence) and a
135
+ trailing unclosed `<artifact …>` left over while the model is still
136
+ streaming the body (renders with a "(generating…)" marker so the user
137
+ sees the artifact appear progressively).
138
+ """
139
+ if "<artifact" not in text.lower():
140
+ return text
141
+
142
+ out: list[str] = []
143
+ pos = 0
144
+ while True:
145
+ open_m = _ARTIFACT_OPEN_RE.search(text, pos)
146
+ if not open_m:
147
+ out.append(text[pos:])
148
+ break
149
+ # Emit prose before the opening tag.
150
+ out.append(text[pos:open_m.start()])
151
+ attrs = _parse_artifact_attrs(open_m.group(1))
152
+ lang = _artifact_lang(attrs)
153
+ body_start = open_m.end()
154
+ close_m = _ARTIFACT_CLOSE_RE.search(text, body_start)
155
+ if close_m:
156
+ body = text[body_start:close_m.start()].strip("\n")
157
+ out.append(_artifact_header(attrs))
158
+ out.append(f"```{lang}\n{body}\n```\n")
159
+ pos = close_m.end()
160
+ else:
161
+ # Trailing open tag mid-stream — render what we have so far and stop.
162
+ body = text[body_start:]
163
+ out.append(_artifact_header(attrs, suffix=" *(generating…)*"))
164
+ out.append(f"```{lang}\n{body}\n```\n")
165
+ pos = len(text)
166
+ break
167
+ return "".join(out)
168
+
86
169
  _WRITE_TOOLS = {"write_file", "create_file", "fs_write"}
87
170
 
88
171
 
@@ -129,7 +212,7 @@ class TextSegment(Static):
129
212
 
130
213
  def append(self, chunk: str) -> None:
131
214
  self._buf += chunk
132
- self.update(Markdown(self._buf))
215
+ self.update(Markdown(normalize_artifacts(self._buf)))
133
216
 
134
217
  @property
135
218
  def content(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 0.7.1
3
+ Version: 0.7.3
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