voidx 1.0.0__py3-none-any.whl
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.
- voidx/__init__.py +3 -0
- voidx/agent/__init__.py +0 -0
- voidx/agent/agents.py +439 -0
- voidx/agent/attachments.py +235 -0
- voidx/agent/graph.py +463 -0
- voidx/agent/graph_components/__init__.py +1 -0
- voidx/agent/graph_components/compaction.py +268 -0
- voidx/agent/graph_components/permissions.py +139 -0
- voidx/agent/graph_components/run_loop.py +532 -0
- voidx/agent/graph_components/runtime.py +14 -0
- voidx/agent/graph_components/streaming.py +351 -0
- voidx/agent/graph_components/subagent.py +278 -0
- voidx/agent/graph_components/tool_execution.py +208 -0
- voidx/agent/runtime_context.py +368 -0
- voidx/agent/slash.py +466 -0
- voidx/agent/slash_components/__init__.py +1 -0
- voidx/agent/slash_components/code_ide.py +68 -0
- voidx/agent/slash_components/lsp.py +105 -0
- voidx/agent/slash_components/mcp.py +332 -0
- voidx/agent/slash_components/model.py +419 -0
- voidx/agent/slash_components/runtime.py +55 -0
- voidx/agent/slash_components/skills.py +94 -0
- voidx/agent/state.py +32 -0
- voidx/agent/task_state.py +278 -0
- voidx/agent/tool_filters.py +27 -0
- voidx/config.py +707 -0
- voidx/llm/__init__.py +0 -0
- voidx/llm/catalog.py +188 -0
- voidx/llm/compaction.py +267 -0
- voidx/llm/context.py +43 -0
- voidx/llm/instruction.py +220 -0
- voidx/llm/provider.py +312 -0
- voidx/llm/usage.py +341 -0
- voidx/lsp/__init__.py +30 -0
- voidx/lsp/client.py +259 -0
- voidx/lsp/config.py +172 -0
- voidx/lsp/detector.py +512 -0
- voidx/lsp/errors.py +19 -0
- voidx/lsp/manager.py +280 -0
- voidx/lsp/schema.py +179 -0
- voidx/lsp/service.py +103 -0
- voidx/main.py +154 -0
- voidx/mcp/__init__.py +33 -0
- voidx/mcp/client.py +458 -0
- voidx/mcp/manager.py +267 -0
- voidx/mcp/schema.py +112 -0
- voidx/mcp/tool.py +122 -0
- voidx/mcp_servers/__init__.py +1 -0
- voidx/mcp_servers/web.py +104 -0
- voidx/memory/__init__.py +0 -0
- voidx/memory/context_frames.py +188 -0
- voidx/memory/model_profiles.py +98 -0
- voidx/memory/runtime_state.py +240 -0
- voidx/memory/session.py +272 -0
- voidx/memory/store.py +245 -0
- voidx/memory/transcript.py +137 -0
- voidx/permission/__init__.py +28 -0
- voidx/permission/engine.py +430 -0
- voidx/permission/evaluate.py +114 -0
- voidx/permission/sandbox.py +280 -0
- voidx/permission/schema.py +24 -0
- voidx/permission/service.py +314 -0
- voidx/permission/wildcard.py +34 -0
- voidx/skills/__init__.py +18 -0
- voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
- voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
- voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
- voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
- voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
- voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
- voidx/skills/policy.py +97 -0
- voidx/skills/registry.py +162 -0
- voidx/skills/schema.py +47 -0
- voidx/skills/service.py +199 -0
- voidx/tools/__init__.py +0 -0
- voidx/tools/agent.py +81 -0
- voidx/tools/base.py +86 -0
- voidx/tools/bash.py +105 -0
- voidx/tools/file_ops.py +193 -0
- voidx/tools/lsp.py +155 -0
- voidx/tools/registry.py +104 -0
- voidx/tools/repomap.py +238 -0
- voidx/tools/search.py +162 -0
- voidx/tools/task_status.py +57 -0
- voidx/tools/task_tracker.py +81 -0
- voidx/tools/todo.py +82 -0
- voidx/tools/web_content.py +357 -0
- voidx/tools/web_mcp.py +107 -0
- voidx/tools/webfetch.py +155 -0
- voidx/tools/websearch.py +276 -0
- voidx/ui/__init__.py +0 -0
- voidx/ui/app.py +1033 -0
- voidx/ui/app_components/__init__.py +1 -0
- voidx/ui/app_components/clipboard_image.py +245 -0
- voidx/ui/app_components/commands.py +18 -0
- voidx/ui/app_components/controls.py +29 -0
- voidx/ui/app_components/file_picker.py +115 -0
- voidx/ui/app_components/formatting.py +187 -0
- voidx/ui/app_components/git_changes.py +51 -0
- voidx/ui/app_components/rendering.py +1169 -0
- voidx/ui/browse.py +160 -0
- voidx/ui/capture.py +169 -0
- voidx/ui/code_ide.py +251 -0
- voidx/ui/commands.py +83 -0
- voidx/ui/console.py +381 -0
- voidx/ui/console_components/__init__.py +1 -0
- voidx/ui/console_components/formatting.py +96 -0
- voidx/ui/console_components/streaming.py +253 -0
- voidx/ui/diff.py +331 -0
- voidx/ui/dock.py +372 -0
- voidx/ui/dock_components/__init__.py +1 -0
- voidx/ui/dock_components/formatting.py +123 -0
- voidx/ui/dock_components/nodes.py +401 -0
- voidx/ui/dock_components/state.py +51 -0
- voidx/ui/event_components/__init__.py +1 -0
- voidx/ui/event_components/schema.py +249 -0
- voidx/ui/events.py +341 -0
- voidx/ui/session_changes.py +163 -0
- voidx/ui/startup.py +161 -0
- voidx/ui/transcript.py +148 -0
- voidx/ui/tree.py +316 -0
- voidx-1.0.0.dist-info/METADATA +59 -0
- voidx-1.0.0.dist-info/RECORD +126 -0
- voidx-1.0.0.dist-info/WHEEL +5 -0
- voidx-1.0.0.dist-info/entry_points.txt +2 -0
- voidx-1.0.0.dist-info/top_level.txt +1 -0
voidx/ui/console.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Rich console — smooth streaming, status indicators, Claude Code style."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from typing import Callable, Iterator
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.markdown import Markdown
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from voidx.ui.console_components.formatting import (
|
|
15
|
+
_capture_ansi,
|
|
16
|
+
_done_spin,
|
|
17
|
+
_event_tool_id,
|
|
18
|
+
_fmt_args,
|
|
19
|
+
_fmt_args_short,
|
|
20
|
+
_next_spin,
|
|
21
|
+
_pop_event_tool_id,
|
|
22
|
+
_title,
|
|
23
|
+
fmt_args,
|
|
24
|
+
)
|
|
25
|
+
from voidx.ui.dock import dock
|
|
26
|
+
from voidx.ui.events import (
|
|
27
|
+
AnsiAppended,
|
|
28
|
+
DiffAppended,
|
|
29
|
+
ErrorAppended,
|
|
30
|
+
MarkdownAppended,
|
|
31
|
+
StatusUpdated,
|
|
32
|
+
ToolFinished,
|
|
33
|
+
ToolResultAppended,
|
|
34
|
+
ToolStarted,
|
|
35
|
+
ui_events,
|
|
36
|
+
)
|
|
37
|
+
from voidx.ui.console_components.streaming import StreamingRenderer
|
|
38
|
+
|
|
39
|
+
CommandOutputSink = Callable[[str], None]
|
|
40
|
+
CommandOutputWidth = int | Callable[[], int]
|
|
41
|
+
|
|
42
|
+
_command_output_sink: ContextVar[CommandOutputSink | None] = ContextVar(
|
|
43
|
+
"command_output_sink",
|
|
44
|
+
default=None,
|
|
45
|
+
)
|
|
46
|
+
_command_output_width: ContextVar[CommandOutputWidth] = ContextVar(
|
|
47
|
+
"command_output_width",
|
|
48
|
+
default=80,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _capture_width() -> int:
|
|
53
|
+
width = _command_output_width.get()
|
|
54
|
+
if callable(width):
|
|
55
|
+
try:
|
|
56
|
+
return max(int(width()), 20)
|
|
57
|
+
except Exception:
|
|
58
|
+
return 80
|
|
59
|
+
return max(int(width), 20)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class VoidConsole:
|
|
63
|
+
"""Thin wrapper with voidx-specific rendering primitives."""
|
|
64
|
+
|
|
65
|
+
_TOOL_GERUND: dict[str, str] = {
|
|
66
|
+
"read": "reading", "write": "writing", "edit": "editing",
|
|
67
|
+
"glob": "finding", "grep": "searching", "bash": "running",
|
|
68
|
+
"agent": "delegating", "webfetch": "fetching", "websearch": "searching",
|
|
69
|
+
"todo": "updating", "task_status": "checking", "repo_map": "mapping",
|
|
70
|
+
"lsp_diagnostics": "checking", "lsp_symbols": "indexing",
|
|
71
|
+
"lsp_definition": "locating", "lsp_references": "finding",
|
|
72
|
+
"lsp_format": "formatting",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_AGENT_GERUND: dict[str, str] = {
|
|
76
|
+
"orchestrator": "thinking",
|
|
77
|
+
"explore": "exploring",
|
|
78
|
+
"plan": "planning",
|
|
79
|
+
"implement": "implementing",
|
|
80
|
+
"review": "reviewing",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
self._console = Console()
|
|
85
|
+
self._debug = True
|
|
86
|
+
self._pending_tools: dict[str, list[dict[str, object]]] = {}
|
|
87
|
+
self._event_tool_ids: dict[str, list[str]] = {}
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def console(self) -> Console:
|
|
91
|
+
return self._console
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def width(self) -> int:
|
|
95
|
+
return self._console.width
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def debug(self) -> bool:
|
|
99
|
+
return self._debug
|
|
100
|
+
|
|
101
|
+
def set_debug(self, value: bool) -> None:
|
|
102
|
+
self._debug = value
|
|
103
|
+
|
|
104
|
+
@contextmanager
|
|
105
|
+
def capture_command_output(
|
|
106
|
+
self,
|
|
107
|
+
sink: CommandOutputSink,
|
|
108
|
+
*,
|
|
109
|
+
width: CommandOutputWidth = 80,
|
|
110
|
+
) -> Iterator[None]:
|
|
111
|
+
sink_token = _command_output_sink.set(sink)
|
|
112
|
+
width_token = _command_output_width.set(width)
|
|
113
|
+
try:
|
|
114
|
+
yield
|
|
115
|
+
finally:
|
|
116
|
+
_command_output_width.reset(width_token)
|
|
117
|
+
_command_output_sink.reset(sink_token)
|
|
118
|
+
|
|
119
|
+
def _emit_command_output(self, render: Callable[[Console], None]) -> bool:
|
|
120
|
+
sink = _command_output_sink.get()
|
|
121
|
+
if sink is None:
|
|
122
|
+
return False
|
|
123
|
+
text = _capture_ansi(_capture_width(), render)
|
|
124
|
+
if text:
|
|
125
|
+
sink(text)
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
def print(self, *args, **kwargs) -> None:
|
|
129
|
+
if self._emit_command_output(lambda console: console.print(*args, **kwargs)):
|
|
130
|
+
return
|
|
131
|
+
if dock.active and ui_events.is_running:
|
|
132
|
+
text = _capture_ansi(
|
|
133
|
+
self._console.width,
|
|
134
|
+
lambda console: console.print(*args, **kwargs),
|
|
135
|
+
)
|
|
136
|
+
if text:
|
|
137
|
+
ui_events.emit_nowait(AnsiAppended(text=text))
|
|
138
|
+
return
|
|
139
|
+
if dock.active and dock.print(*args, **kwargs):
|
|
140
|
+
return
|
|
141
|
+
self._console.print(*args, **kwargs)
|
|
142
|
+
|
|
143
|
+
def markdown(self, content: str) -> None:
|
|
144
|
+
if self._emit_command_output(lambda console: console.print(Markdown(content))):
|
|
145
|
+
return
|
|
146
|
+
if dock.active and ui_events.is_running:
|
|
147
|
+
ui_events.emit_nowait(MarkdownAppended(content=content))
|
|
148
|
+
return
|
|
149
|
+
if dock.active and dock.capture(lambda console: console.print(Markdown(content))):
|
|
150
|
+
return
|
|
151
|
+
self._console.print(Markdown(content))
|
|
152
|
+
|
|
153
|
+
def thinking(self, text: str) -> None:
|
|
154
|
+
if self._emit_command_output(lambda console: console.print(Text(text, style="dim italic"))):
|
|
155
|
+
return
|
|
156
|
+
if dock.active and ui_events.is_running:
|
|
157
|
+
captured = _capture_ansi(
|
|
158
|
+
self._console.width,
|
|
159
|
+
lambda console: console.print(Text(text, style="dim italic")),
|
|
160
|
+
)
|
|
161
|
+
if captured:
|
|
162
|
+
ui_events.emit_nowait(AnsiAppended(text=captured))
|
|
163
|
+
return
|
|
164
|
+
if dock.active and dock.capture(lambda console: console.print(Text(text, style="dim italic"))):
|
|
165
|
+
return
|
|
166
|
+
self._console.print(Text(text, style="dim italic"))
|
|
167
|
+
|
|
168
|
+
def tool_call(self, tool_name: str, args: dict[str, object]) -> None:
|
|
169
|
+
if not self._debug:
|
|
170
|
+
self._pending_tools.setdefault(tool_name, []).append(args)
|
|
171
|
+
return
|
|
172
|
+
gerund = _title(self._TOOL_GERUND.get(tool_name, tool_name + "ing"))
|
|
173
|
+
if dock.active and ui_events.is_running:
|
|
174
|
+
event_id = _event_tool_id(tool_name)
|
|
175
|
+
self._event_tool_ids.setdefault(tool_name, []).append(event_id)
|
|
176
|
+
ui_events.emit_nowait(ToolStarted(
|
|
177
|
+
tool_call_id=event_id,
|
|
178
|
+
tool_name=tool_name,
|
|
179
|
+
label=gerund,
|
|
180
|
+
args=_fmt_args(args),
|
|
181
|
+
raw_args=args,
|
|
182
|
+
))
|
|
183
|
+
return
|
|
184
|
+
if dock.active:
|
|
185
|
+
dock.start_tool(gerund, _fmt_args(args), tool_name=tool_name, raw_args=args)
|
|
186
|
+
return
|
|
187
|
+
self.print(f" {_next_spin()} [bold]{gerund}[/bold]({_fmt_args(args)})")
|
|
188
|
+
|
|
189
|
+
def tool_done(self, tool_name: str, elapsed: float, ok: bool = True) -> None:
|
|
190
|
+
icon = _done_spin() if ok else "[red]●[/red]"
|
|
191
|
+
style = "green" if ok else "red"
|
|
192
|
+
label = _title(tool_name)
|
|
193
|
+
if not self._debug:
|
|
194
|
+
pending = self._pending_tools.get(tool_name, [])
|
|
195
|
+
args = pending.pop(0) if pending else {}
|
|
196
|
+
if not pending:
|
|
197
|
+
self._pending_tools.pop(tool_name, None)
|
|
198
|
+
elapsed_part = f" [dim]({elapsed:.1f}s)[/dim]" if elapsed >= 2 else ""
|
|
199
|
+
if dock.active and ui_events.is_running:
|
|
200
|
+
event_id = _event_tool_id(tool_name)
|
|
201
|
+
detail = _fmt_args_short(tool_name, args)
|
|
202
|
+
ui_events.emit_nowait(ToolStarted(
|
|
203
|
+
tool_call_id=event_id,
|
|
204
|
+
tool_name=tool_name,
|
|
205
|
+
label=label,
|
|
206
|
+
args=detail,
|
|
207
|
+
raw_args=args,
|
|
208
|
+
))
|
|
209
|
+
ui_events.emit_nowait(ToolFinished(
|
|
210
|
+
tool_call_id=event_id,
|
|
211
|
+
label=label,
|
|
212
|
+
elapsed=elapsed,
|
|
213
|
+
ok=ok,
|
|
214
|
+
))
|
|
215
|
+
return
|
|
216
|
+
if dock.active:
|
|
217
|
+
detail = _fmt_args_short(tool_name, args)
|
|
218
|
+
dock.start_tool(label, detail, tool_name=tool_name, raw_args=args)
|
|
219
|
+
dock.finish_tool(label, elapsed, ok)
|
|
220
|
+
return
|
|
221
|
+
self.print(
|
|
222
|
+
f" {icon} [{style}]{label}[/] [dim]{_fmt_args_short(tool_name, args)}[/]{elapsed_part}"
|
|
223
|
+
)
|
|
224
|
+
return
|
|
225
|
+
if dock.active and ui_events.is_running:
|
|
226
|
+
event_id = _pop_event_tool_id(self._event_tool_ids, tool_name)
|
|
227
|
+
if event_id:
|
|
228
|
+
ui_events.emit_nowait(ToolFinished(
|
|
229
|
+
tool_call_id=event_id,
|
|
230
|
+
label=label,
|
|
231
|
+
elapsed=elapsed,
|
|
232
|
+
ok=ok,
|
|
233
|
+
))
|
|
234
|
+
return
|
|
235
|
+
if dock.active:
|
|
236
|
+
dock.finish_tool(label, elapsed, ok)
|
|
237
|
+
return
|
|
238
|
+
self.print(f" {icon} [{style}]{label}[/{style}] [dim]({elapsed:.1f}s)[/dim]")
|
|
239
|
+
|
|
240
|
+
def tool_result(self, text: str) -> None:
|
|
241
|
+
if dock.active and ui_events.is_running:
|
|
242
|
+
ui_events.emit_nowait(ToolResultAppended(text=text))
|
|
243
|
+
return
|
|
244
|
+
if dock.active:
|
|
245
|
+
dock.append_tool_result(text)
|
|
246
|
+
return
|
|
247
|
+
self.print(text)
|
|
248
|
+
|
|
249
|
+
def error(self, message: str) -> None:
|
|
250
|
+
if self._emit_command_output(
|
|
251
|
+
lambda console: console.print(Panel(message, border_style="red", title="error"))
|
|
252
|
+
):
|
|
253
|
+
return
|
|
254
|
+
if dock.active and ui_events.is_running:
|
|
255
|
+
ui_events.emit_nowait(ErrorAppended(message=message))
|
|
256
|
+
return
|
|
257
|
+
if dock.active:
|
|
258
|
+
dock.append_error(message)
|
|
259
|
+
return
|
|
260
|
+
self._console.print(Panel(message, border_style="red", title="error"))
|
|
261
|
+
|
|
262
|
+
def warn(self, message: str) -> None:
|
|
263
|
+
self.print(f"[yellow]! {message}[/yellow]")
|
|
264
|
+
|
|
265
|
+
def sep(self) -> None:
|
|
266
|
+
w = self._console.width or 80
|
|
267
|
+
self.print("─" * w, style="dim")
|
|
268
|
+
|
|
269
|
+
def step_header(self, n: int, max_n: int, agent: str = "") -> None:
|
|
270
|
+
gerund = _title(self._AGENT_GERUND.get(agent, agent))
|
|
271
|
+
label = f"Agent step {n}/{max_n}" if agent == "orchestrator" else f"{gerund} {n}/{max_n}"
|
|
272
|
+
if dock.active and ui_events.is_running:
|
|
273
|
+
ui_events.emit_nowait(StatusUpdated(
|
|
274
|
+
status_id="agent:-1:progress",
|
|
275
|
+
label=label,
|
|
276
|
+
stage="agent_step",
|
|
277
|
+
))
|
|
278
|
+
return
|
|
279
|
+
if not self._debug:
|
|
280
|
+
return
|
|
281
|
+
if dock.active:
|
|
282
|
+
return
|
|
283
|
+
self.print(f" {_next_spin()} [dim]{gerund} ({n}/{max_n})[/dim]")
|
|
284
|
+
|
|
285
|
+
def diff(self, diff_text: str, title: str = "") -> None:
|
|
286
|
+
from voidx.ui.diff import render_diff
|
|
287
|
+
if self._emit_command_output(lambda console: render_diff(console, diff_text, title)):
|
|
288
|
+
return
|
|
289
|
+
if dock.active and ui_events.is_running:
|
|
290
|
+
ui_events.emit_nowait(DiffAppended(diff_text=diff_text, title=title))
|
|
291
|
+
return
|
|
292
|
+
if dock.active and dock.capture(lambda console: render_diff(console, diff_text, title)):
|
|
293
|
+
return
|
|
294
|
+
render_diff(self._console, diff_text, title)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TreeAwareConsole:
|
|
298
|
+
"""Dual-write console: prints to Rich AND records to an OutputTree node.
|
|
299
|
+
|
|
300
|
+
Used by the main orchestrator agent for real-time output + tree building.
|
|
301
|
+
Sub-agents use CaptureConsole instead.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(self, rich_console: Console, tree, turn_node):
|
|
305
|
+
self._console = rich_console
|
|
306
|
+
self._tree = tree
|
|
307
|
+
self._turn = turn_node
|
|
308
|
+
self._current_tool = None
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def console(self) -> Console:
|
|
312
|
+
return self._console
|
|
313
|
+
|
|
314
|
+
def step_header(self, n: int, max_n: int, agent: str = "") -> None:
|
|
315
|
+
gerund = _title(VoidConsole._AGENT_GERUND.get(agent, agent))
|
|
316
|
+
self._console.print(f" {_next_spin()} [dim]{gerund} ({n}/{max_n})[/dim]")
|
|
317
|
+
|
|
318
|
+
def tool_call(self, tool_name: str, args: dict[str, object]) -> None:
|
|
319
|
+
gerund = _title(VoidConsole._TOOL_GERUND.get(tool_name, tool_name + "ing"))
|
|
320
|
+
dot = _next_spin()
|
|
321
|
+
self._console.print(
|
|
322
|
+
f" {dot} [bold]{gerund}[/bold]({fmt_args(args)})"
|
|
323
|
+
)
|
|
324
|
+
self._current_tool = self._tree.new_node(
|
|
325
|
+
parent=self._turn,
|
|
326
|
+
node_type="tool_call",
|
|
327
|
+
header=f"{dot} [bold]{gerund}[/bold]({fmt_args(args)})",
|
|
328
|
+
status="running",
|
|
329
|
+
collapsed=True,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def tool_done(self, tool_name: str, elapsed: float, ok: bool = True) -> None:
|
|
333
|
+
icon = _done_spin() if ok else "[red]●[/red]"
|
|
334
|
+
style = "green" if ok else "red"
|
|
335
|
+
label = _title(tool_name)
|
|
336
|
+
self._console.print(
|
|
337
|
+
f" {icon} [{style}]{label}[/{style}] [dim]({elapsed:.1f}s)[/dim]"
|
|
338
|
+
)
|
|
339
|
+
if self._current_tool:
|
|
340
|
+
self._current_tool.header += f" [{style}]{label} ({elapsed:.1f}s)[/{style}]"
|
|
341
|
+
self._current_tool.elapsed = elapsed
|
|
342
|
+
self._current_tool.status = "done" if ok else "error"
|
|
343
|
+
self._tree.mark_dirty()
|
|
344
|
+
|
|
345
|
+
def tool_result(self, text: str) -> None:
|
|
346
|
+
self._console.print(text)
|
|
347
|
+
if self._current_tool:
|
|
348
|
+
self._tree.new_node(
|
|
349
|
+
parent=self._current_tool,
|
|
350
|
+
node_type="tool_result",
|
|
351
|
+
body_lines=text.split("\n"),
|
|
352
|
+
collapsed=False,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def diff(self, diff_text: str, title: str = "") -> None:
|
|
356
|
+
from voidx.ui.diff import render_diff
|
|
357
|
+
render_diff(self._console, diff_text, title)
|
|
358
|
+
if self._current_tool:
|
|
359
|
+
lines = diff_text.split("\n")
|
|
360
|
+
if title:
|
|
361
|
+
lines.insert(0, f"[bold]{title}[/bold]")
|
|
362
|
+
self._tree.new_node(
|
|
363
|
+
parent=self._current_tool,
|
|
364
|
+
node_type="tool_result",
|
|
365
|
+
body_lines=lines,
|
|
366
|
+
collapsed=False,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def print(self, *args, **kwargs) -> None:
|
|
370
|
+
self._console.print(*args, **kwargs)
|
|
371
|
+
|
|
372
|
+
def warn(self, message: str) -> None:
|
|
373
|
+
self._console.print(f"[yellow]! {message}[/yellow]")
|
|
374
|
+
|
|
375
|
+
def error(self, message: str) -> None:
|
|
376
|
+
from rich.panel import Panel
|
|
377
|
+
self._console.print(Panel(message, border_style="red", title="error"))
|
|
378
|
+
|
|
379
|
+
def sep(self) -> None:
|
|
380
|
+
w = self._console.width or 80
|
|
381
|
+
self._console.print("─" * w, style="dim")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Implementation parts for console rendering."""
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Shared console formatting helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from io import StringIO
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
_SPIN_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
11
|
+
_spin_idx = 0
|
|
12
|
+
_ORANGE = "#EBCB8B" # Nord yellow-ish
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _next_spin() -> str:
|
|
16
|
+
global _spin_idx
|
|
17
|
+
_spin_idx = (_spin_idx + 1) % len(_SPIN_FRAMES)
|
|
18
|
+
return f"[{_ORANGE}]{_SPIN_FRAMES[_spin_idx]}[/]"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _done_spin() -> str:
|
|
22
|
+
return "[#A3BE8C]●[/#A3BE8C]"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _title(text: str) -> str:
|
|
26
|
+
return text[:1].upper() + text[1:] if text else text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _escape_rich(s: str) -> str:
|
|
30
|
+
return s.replace("[", "\\[").replace("]", "\\]")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _capture_ansi(width: int, render) -> str:
|
|
34
|
+
buffer = StringIO()
|
|
35
|
+
console = Console(
|
|
36
|
+
file=buffer,
|
|
37
|
+
force_terminal=True,
|
|
38
|
+
color_system="truecolor",
|
|
39
|
+
width=width or 80,
|
|
40
|
+
)
|
|
41
|
+
render(console)
|
|
42
|
+
return buffer.getvalue().rstrip("\n")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _event_tool_id(tool_name: str) -> str:
|
|
46
|
+
return f"console:{tool_name}:{time.time_ns()}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _pop_event_tool_id(pending: dict[str, list[str]], tool_name: str) -> str:
|
|
50
|
+
ids = pending.get(tool_name, [])
|
|
51
|
+
event_id = ids.pop(0) if ids else ""
|
|
52
|
+
if not ids:
|
|
53
|
+
pending.pop(tool_name, None)
|
|
54
|
+
return event_id
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _fmt_args(args: dict[str, object]) -> str:
|
|
58
|
+
"""Format tool args Claude Code style: key="value" inside parentheses."""
|
|
59
|
+
parts = []
|
|
60
|
+
for k, v in args.items():
|
|
61
|
+
s = str(v)
|
|
62
|
+
if len(s) > 60:
|
|
63
|
+
s = s[:57] + "..."
|
|
64
|
+
escaped = _escape_rich(s)
|
|
65
|
+
if isinstance(v, str):
|
|
66
|
+
parts.append(f'{k}="[cyan]{escaped}[/cyan]"')
|
|
67
|
+
else:
|
|
68
|
+
parts.append(f"{k}=[cyan]{escaped}[/cyan]")
|
|
69
|
+
return ", ".join(parts)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _fmt_args_short(tool_name: str, args: dict[str, object]) -> str:
|
|
73
|
+
if tool_name in {"read", "write", "edit"}:
|
|
74
|
+
value = args.get("file_path")
|
|
75
|
+
return _escape_rich(str(value)) if value else ""
|
|
76
|
+
if tool_name == "glob":
|
|
77
|
+
value = args.get("pattern")
|
|
78
|
+
return _escape_rich(str(value)) if value else ""
|
|
79
|
+
if tool_name == "grep":
|
|
80
|
+
value = args.get("pattern")
|
|
81
|
+
include = args.get("include")
|
|
82
|
+
suffix = f" in {_escape_rich(str(include))}" if include else ""
|
|
83
|
+
return f"{_escape_rich(str(value))}{suffix}" if value else ""
|
|
84
|
+
if tool_name == "bash":
|
|
85
|
+
value = str(args.get("command", ""))
|
|
86
|
+
shortened = value[:77] + "..." if len(value) > 80 else value
|
|
87
|
+
return _escape_rich(shortened)
|
|
88
|
+
if tool_name == "agent":
|
|
89
|
+
return _escape_rich(str(args.get("agent") or ""))
|
|
90
|
+
if tool_name in {"webfetch", "websearch"}:
|
|
91
|
+
value = args.get("url") or args.get("query")
|
|
92
|
+
return _escape_rich(str(value)) if value else ""
|
|
93
|
+
return ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
fmt_args = _fmt_args
|