virtuai-cli 0.7.0__tar.gz → 0.7.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.7.0 → virtuai_cli-0.7.2}/PKG-INFO +1 -1
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/pyproject.toml +1 -1
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/__init__.py +1 -1
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/chat/tui.py +78 -11
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/README.md +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/setup.cfg +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/chat/__init__.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/chat/ask.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/chat/command.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/chat/history.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/chat/sse.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/chat/widgets.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/config.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/executor.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/main.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/runner.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli/security.py +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/src/virtuai_cli.egg-info/requires.txt +0 -0
- {virtuai_cli-0.7.0 → virtuai_cli-0.7.2}/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.2"
|
|
@@ -26,6 +26,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
26
26
|
("/clear", "start a fresh conversation"),
|
|
27
27
|
("/new", "alias for /clear"),
|
|
28
28
|
("/copy", "copy the last assistant response to the clipboard"),
|
|
29
|
+
("/select", "toggle terminal-native text selection (also F2)"),
|
|
29
30
|
("/history", "list this agent's recent conversations"),
|
|
30
31
|
("/load", "load a past conversation: /load <session_id>"),
|
|
31
32
|
("/models", "list models available for this agent"),
|
|
@@ -75,6 +76,7 @@ class ChatApp(App):
|
|
|
75
76
|
Binding("ctrl+c", "quit", "Quit", show=True),
|
|
76
77
|
Binding("ctrl+l", "clear_conversation", "New chat", show=True),
|
|
77
78
|
Binding("ctrl+y", "copy_last", "Copy last", show=True, priority=True),
|
|
79
|
+
Binding("f2", "toggle_select_mode", "Select", show=True, priority=True),
|
|
78
80
|
Binding("tab", "complete_slash", "Complete", show=False, priority=True),
|
|
79
81
|
]
|
|
80
82
|
|
|
@@ -106,6 +108,7 @@ class ChatApp(App):
|
|
|
106
108
|
self._stream_task: Optional[asyncio.Task] = None
|
|
107
109
|
self._runner_task: Optional[asyncio.Task] = None
|
|
108
110
|
self._current_turn: Optional[AssistantTurn] = None
|
|
111
|
+
self._select_mode: bool = False
|
|
109
112
|
|
|
110
113
|
# ── Layout ────────────────────────────────────────────────────────────
|
|
111
114
|
def compose(self) -> ComposeResult:
|
|
@@ -272,12 +275,15 @@ class ChatApp(App):
|
|
|
272
275
|
await self._load_session(arg)
|
|
273
276
|
elif head == "/copy":
|
|
274
277
|
self.action_copy_last()
|
|
278
|
+
elif head == "/select":
|
|
279
|
+
self.action_toggle_select_mode()
|
|
275
280
|
elif head == "/help":
|
|
276
281
|
await self._append(Static(
|
|
277
282
|
"[b]Commands[/b]\n"
|
|
278
283
|
" /help this list\n"
|
|
279
284
|
" /clear, /new start a fresh conversation\n"
|
|
280
285
|
" /copy copy the last assistant response to clipboard\n"
|
|
286
|
+
" /select toggle terminal-native text selection\n"
|
|
281
287
|
" /history list this agent's recent conversations\n"
|
|
282
288
|
" /load <id> load a past conversation by session_id\n"
|
|
283
289
|
" /models list models available for this agent\n"
|
|
@@ -290,13 +296,14 @@ class ChatApp(App):
|
|
|
290
296
|
" Esc cancel current response\n"
|
|
291
297
|
" Ctrl+L new conversation\n"
|
|
292
298
|
" Ctrl+Y copy last assistant response\n"
|
|
299
|
+
" F2 toggle select mode (mouse selection on/off)\n"
|
|
293
300
|
" Tab autocomplete slash command\n"
|
|
294
301
|
"\n"
|
|
295
|
-
"[b]Selecting text[/b]\n"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
302
|
+
"[b]Selecting text with the mouse[/b]\n"
|
|
303
|
+
" Press [b]F2[/b] (or run [b]/select[/b]) to release the TUI's mouse\n"
|
|
304
|
+
" capture. Then drag to highlight any text and use your\n"
|
|
305
|
+
" terminal's copy shortcut: [b]Cmd+C[/b] on macOS, [b]Ctrl+Shift+C[/b]\n"
|
|
306
|
+
" on Linux/Windows. Press F2 again to return to normal mode."
|
|
300
307
|
))
|
|
301
308
|
else:
|
|
302
309
|
await self._append(Static(f"[red]Unknown command: {head}[/red] (try /help)"))
|
|
@@ -372,17 +379,53 @@ class ChatApp(App):
|
|
|
372
379
|
)
|
|
373
380
|
await scroll.mount(Static(header))
|
|
374
381
|
|
|
382
|
+
loaded_session = self.session_id
|
|
375
383
|
for msg in messages:
|
|
376
384
|
mtype = (msg.get("message_type") or "").lower()
|
|
377
385
|
content = msg.get("content") or ""
|
|
386
|
+
steps = msg.get("steps") or []
|
|
378
387
|
if mtype in ("user", "human"):
|
|
379
388
|
await scroll.mount(UserBubble(content))
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
386
429
|
scroll.scroll_end(animate=False)
|
|
387
430
|
self._set_status(f"loaded {self.session_id}")
|
|
388
431
|
|
|
@@ -432,6 +475,30 @@ class ChatApp(App):
|
|
|
432
475
|
self._stream_task.cancel()
|
|
433
476
|
self._set_status("response cancelled")
|
|
434
477
|
|
|
478
|
+
def action_toggle_select_mode(self) -> None:
|
|
479
|
+
"""Pause / resume Textual's mouse capture so the terminal handles
|
|
480
|
+
click-drag text selection (and the terminal's native copy shortcut:
|
|
481
|
+
Cmd+C on macOS, Ctrl+Shift+C on Linux/Windows).
|
|
482
|
+
"""
|
|
483
|
+
driver = getattr(self, "_driver", None)
|
|
484
|
+
if driver is None:
|
|
485
|
+
self._set_status("can't toggle: no driver attached")
|
|
486
|
+
return
|
|
487
|
+
try:
|
|
488
|
+
if not self._select_mode:
|
|
489
|
+
driver._disable_mouse_support()
|
|
490
|
+
self._select_mode = True
|
|
491
|
+
self._set_status(
|
|
492
|
+
"SELECT MODE — drag to highlight, then Cmd+C (macOS) or "
|
|
493
|
+
"Ctrl+Shift+C (Linux). F2 to resume."
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
driver._enable_mouse_support()
|
|
497
|
+
self._select_mode = False
|
|
498
|
+
self._set_status("select mode off")
|
|
499
|
+
except Exception as exc:
|
|
500
|
+
self._set_status(f"toggle error: {exc}")
|
|
501
|
+
|
|
435
502
|
def action_copy_last(self) -> None:
|
|
436
503
|
"""Copy the most recent assistant turn's text to the system clipboard."""
|
|
437
504
|
turns = list(self.query(AssistantTurn))
|
|
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
|