virtuai-cli 0.7.2__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.
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/PKG-INFO +1 -1
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/pyproject.toml +1 -1
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/__init__.py +1 -1
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/widgets.py +84 -1
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/README.md +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/setup.cfg +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/__init__.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/ask.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/command.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/history.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/sse.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/chat/tui.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/config.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/executor.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/main.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/runner.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli/security.py +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/src/virtuai_cli.egg-info/requires.txt +0 -0
- {virtuai_cli-0.7.2 → virtuai_cli-0.7.3}/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.3"
|
|
@@ -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:
|
|
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
|
|
File without changes
|