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.
Files changed (126) hide show
  1. voidx/__init__.py +3 -0
  2. voidx/agent/__init__.py +0 -0
  3. voidx/agent/agents.py +439 -0
  4. voidx/agent/attachments.py +235 -0
  5. voidx/agent/graph.py +463 -0
  6. voidx/agent/graph_components/__init__.py +1 -0
  7. voidx/agent/graph_components/compaction.py +268 -0
  8. voidx/agent/graph_components/permissions.py +139 -0
  9. voidx/agent/graph_components/run_loop.py +532 -0
  10. voidx/agent/graph_components/runtime.py +14 -0
  11. voidx/agent/graph_components/streaming.py +351 -0
  12. voidx/agent/graph_components/subagent.py +278 -0
  13. voidx/agent/graph_components/tool_execution.py +208 -0
  14. voidx/agent/runtime_context.py +368 -0
  15. voidx/agent/slash.py +466 -0
  16. voidx/agent/slash_components/__init__.py +1 -0
  17. voidx/agent/slash_components/code_ide.py +68 -0
  18. voidx/agent/slash_components/lsp.py +105 -0
  19. voidx/agent/slash_components/mcp.py +332 -0
  20. voidx/agent/slash_components/model.py +419 -0
  21. voidx/agent/slash_components/runtime.py +55 -0
  22. voidx/agent/slash_components/skills.py +94 -0
  23. voidx/agent/state.py +32 -0
  24. voidx/agent/task_state.py +278 -0
  25. voidx/agent/tool_filters.py +27 -0
  26. voidx/config.py +707 -0
  27. voidx/llm/__init__.py +0 -0
  28. voidx/llm/catalog.py +188 -0
  29. voidx/llm/compaction.py +267 -0
  30. voidx/llm/context.py +43 -0
  31. voidx/llm/instruction.py +220 -0
  32. voidx/llm/provider.py +312 -0
  33. voidx/llm/usage.py +341 -0
  34. voidx/lsp/__init__.py +30 -0
  35. voidx/lsp/client.py +259 -0
  36. voidx/lsp/config.py +172 -0
  37. voidx/lsp/detector.py +512 -0
  38. voidx/lsp/errors.py +19 -0
  39. voidx/lsp/manager.py +280 -0
  40. voidx/lsp/schema.py +179 -0
  41. voidx/lsp/service.py +103 -0
  42. voidx/main.py +154 -0
  43. voidx/mcp/__init__.py +33 -0
  44. voidx/mcp/client.py +458 -0
  45. voidx/mcp/manager.py +267 -0
  46. voidx/mcp/schema.py +112 -0
  47. voidx/mcp/tool.py +122 -0
  48. voidx/mcp_servers/__init__.py +1 -0
  49. voidx/mcp_servers/web.py +104 -0
  50. voidx/memory/__init__.py +0 -0
  51. voidx/memory/context_frames.py +188 -0
  52. voidx/memory/model_profiles.py +98 -0
  53. voidx/memory/runtime_state.py +240 -0
  54. voidx/memory/session.py +272 -0
  55. voidx/memory/store.py +245 -0
  56. voidx/memory/transcript.py +137 -0
  57. voidx/permission/__init__.py +28 -0
  58. voidx/permission/engine.py +430 -0
  59. voidx/permission/evaluate.py +114 -0
  60. voidx/permission/sandbox.py +280 -0
  61. voidx/permission/schema.py +24 -0
  62. voidx/permission/service.py +314 -0
  63. voidx/permission/wildcard.py +34 -0
  64. voidx/skills/__init__.py +18 -0
  65. voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
  66. voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
  67. voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
  68. voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
  69. voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
  70. voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
  71. voidx/skills/policy.py +97 -0
  72. voidx/skills/registry.py +162 -0
  73. voidx/skills/schema.py +47 -0
  74. voidx/skills/service.py +199 -0
  75. voidx/tools/__init__.py +0 -0
  76. voidx/tools/agent.py +81 -0
  77. voidx/tools/base.py +86 -0
  78. voidx/tools/bash.py +105 -0
  79. voidx/tools/file_ops.py +193 -0
  80. voidx/tools/lsp.py +155 -0
  81. voidx/tools/registry.py +104 -0
  82. voidx/tools/repomap.py +238 -0
  83. voidx/tools/search.py +162 -0
  84. voidx/tools/task_status.py +57 -0
  85. voidx/tools/task_tracker.py +81 -0
  86. voidx/tools/todo.py +82 -0
  87. voidx/tools/web_content.py +357 -0
  88. voidx/tools/web_mcp.py +107 -0
  89. voidx/tools/webfetch.py +155 -0
  90. voidx/tools/websearch.py +276 -0
  91. voidx/ui/__init__.py +0 -0
  92. voidx/ui/app.py +1033 -0
  93. voidx/ui/app_components/__init__.py +1 -0
  94. voidx/ui/app_components/clipboard_image.py +245 -0
  95. voidx/ui/app_components/commands.py +18 -0
  96. voidx/ui/app_components/controls.py +29 -0
  97. voidx/ui/app_components/file_picker.py +115 -0
  98. voidx/ui/app_components/formatting.py +187 -0
  99. voidx/ui/app_components/git_changes.py +51 -0
  100. voidx/ui/app_components/rendering.py +1169 -0
  101. voidx/ui/browse.py +160 -0
  102. voidx/ui/capture.py +169 -0
  103. voidx/ui/code_ide.py +251 -0
  104. voidx/ui/commands.py +83 -0
  105. voidx/ui/console.py +381 -0
  106. voidx/ui/console_components/__init__.py +1 -0
  107. voidx/ui/console_components/formatting.py +96 -0
  108. voidx/ui/console_components/streaming.py +253 -0
  109. voidx/ui/diff.py +331 -0
  110. voidx/ui/dock.py +372 -0
  111. voidx/ui/dock_components/__init__.py +1 -0
  112. voidx/ui/dock_components/formatting.py +123 -0
  113. voidx/ui/dock_components/nodes.py +401 -0
  114. voidx/ui/dock_components/state.py +51 -0
  115. voidx/ui/event_components/__init__.py +1 -0
  116. voidx/ui/event_components/schema.py +249 -0
  117. voidx/ui/events.py +341 -0
  118. voidx/ui/session_changes.py +163 -0
  119. voidx/ui/startup.py +161 -0
  120. voidx/ui/transcript.py +148 -0
  121. voidx/ui/tree.py +316 -0
  122. voidx-1.0.0.dist-info/METADATA +59 -0
  123. voidx-1.0.0.dist-info/RECORD +126 -0
  124. voidx-1.0.0.dist-info/WHEEL +5 -0
  125. voidx-1.0.0.dist-info/entry_points.txt +2 -0
  126. voidx-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,401 @@
1
+ """Output node mutation mixin for BottomInputDock."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from rich.markup import escape
9
+
10
+ from voidx.ui.dock_components.formatting import (
11
+ _ansi_line,
12
+ _clean,
13
+ _markdown_lines,
14
+ _short_path,
15
+ _short_value,
16
+ _strip_ansi_trailing_space,
17
+ _tail_lines,
18
+ )
19
+ from voidx.ui.tree import OutputNode
20
+
21
+
22
+ class DockNodeMixin:
23
+ def append_startup(
24
+ self,
25
+ *,
26
+ model: str,
27
+ provider: str,
28
+ workspace: str,
29
+ session_title: str,
30
+ is_new: bool,
31
+ profile_configured: bool = True,
32
+ ) -> OutputNode | None:
33
+ from voidx.ui.startup import render_startup_lines
34
+
35
+ lines = render_startup_lines(
36
+ self._width(),
37
+ model=model,
38
+ provider=provider,
39
+ workspace=workspace,
40
+ session_title=session_title,
41
+ is_new=is_new,
42
+ )
43
+ if not profile_configured:
44
+ lines.extend([
45
+ "[yellow]No profile configured — chat is disabled until you set one up.[/yellow]",
46
+ "[dim] Use [cyan]/model new[/cyan] to create a profile interactively[/dim]",
47
+ ])
48
+ if not lines:
49
+ return None
50
+ node = self._tree.new_node(
51
+ parent=self._tree.root,
52
+ node_type="startup",
53
+ header=lines[0],
54
+ body_lines=lines[1:],
55
+ collapsed=False,
56
+ )
57
+ self.refresh()
58
+ return node
59
+
60
+ def append_message(self, text: str, *, style: str = "", parent: OutputNode | None = None) -> OutputNode | None:
61
+ clean = _clean(text)
62
+ if not clean.strip():
63
+ return None
64
+ target = parent or self._tree.root
65
+ lines = [_strip_ansi_trailing_space(line) for line in (clean.splitlines() or [clean])]
66
+ header = escape(lines[0])
67
+ if style:
68
+ header = f"[{style}]{header}[/]"
69
+ body_lines = [escape(line) for line in lines[1:]]
70
+ if style:
71
+ body_lines = [f"[{style}]{line}[/]" for line in body_lines]
72
+ node = self._tree.new_node(
73
+ parent=target,
74
+ node_type="message",
75
+ header=header,
76
+ body_lines=body_lines,
77
+ collapsed=False,
78
+ )
79
+ self.refresh()
80
+ return node
81
+
82
+ def append_error(self, message: str, *, parent: OutputNode | None = None) -> OutputNode | None:
83
+ clean = _clean(message)
84
+ if not clean.strip():
85
+ return None
86
+ lines = [_strip_ansi_trailing_space(line) for line in (clean.splitlines() or [clean])]
87
+ node = self._tree.new_node(
88
+ parent=parent or self._tree.root,
89
+ node_type="error",
90
+ header=f"[red]✗ {escape(lines[0])}[/red]",
91
+ body_lines=[f"[red] {escape(line)}[/red]" for line in lines[1:]],
92
+ collapsed=False,
93
+ status="error",
94
+ )
95
+ self.refresh()
96
+ return node
97
+
98
+ def append_ansi(self, text: str, *, parent: OutputNode | None = None) -> OutputNode | None:
99
+ clean = text.rstrip("\n")
100
+ if not clean.strip():
101
+ return None
102
+ lines = [_strip_ansi_trailing_space(line) for line in (clean.splitlines() or [clean])]
103
+ node = self._tree.new_node(
104
+ parent=parent or self._tree.root,
105
+ node_type="message",
106
+ header=_ansi_line(lines[0]),
107
+ body_lines=[_ansi_line(line) for line in lines[1:]],
108
+ collapsed=False,
109
+ )
110
+ self.refresh()
111
+ return node
112
+
113
+ def append_thought(self, text: str, elapsed: float | None = None) -> OutputNode | None:
114
+ clean = _clean(text).strip()
115
+ if not clean:
116
+ return None
117
+ lines = clean.splitlines()
118
+ summary = f"Thinking for {elapsed:.0f}s" if elapsed is not None else "Thinking"
119
+ if lines:
120
+ summary += f", {len(lines)} line{'s' if len(lines) != 1 else ''}"
121
+ node = self._tree.new_node(
122
+ parent=self.ensure_agent(),
123
+ node_type="thought",
124
+ header=f"[dim]●[/dim] [dim]{escape(summary)}[/dim]",
125
+ body_lines=[f"[dim]{escape(line)}[/dim]" for line in lines[-5:]],
126
+ collapsed=True,
127
+ meta=summary,
128
+ )
129
+ self.refresh()
130
+ return node
131
+
132
+ def start_tool(
133
+ self,
134
+ label: str,
135
+ args: str = "",
136
+ *,
137
+ parent: OutputNode | None = None,
138
+ tool_call_id: str | None = None,
139
+ tool_name: str = "",
140
+ raw_args: dict[str, Any] | None = None,
141
+ ) -> OutputNode:
142
+ if parent is None:
143
+ self._settle_stream_for_tool()
144
+ raw_args = raw_args or {}
145
+ body_lines: list[str] = []
146
+ header = f"[bold]{escape(label)}[/]"
147
+ if tool_name == "bash":
148
+ command = str(raw_args.get("command") or "")
149
+ if command:
150
+ body_lines = _bash_markdown_lines(command, self._markdown_width())
151
+ elif args:
152
+ header += f"({args})"
153
+ parent = parent or self.ensure_agent()
154
+ tool_body = header
155
+ self._current_tool = self._tree.new_node(
156
+ parent=parent,
157
+ node_type="tool_call",
158
+ header=f"[#A3BE8C]●[/#A3BE8C] {tool_body}",
159
+ body_lines=body_lines,
160
+ status="running",
161
+ collapsed=True,
162
+ meta=tool_body,
163
+ tool_call_id=tool_call_id,
164
+ payload={"tool_name": tool_name, "args": args, "raw_args": raw_args},
165
+ )
166
+ self.refresh()
167
+ return self._current_tool
168
+
169
+ def finish_tool(self, label: str, elapsed: float, ok: bool = True, detail: str = "") -> None:
170
+ if not self._current_tool:
171
+ return
172
+ self.finish_tool_node(self._current_tool, label, elapsed, ok, detail)
173
+
174
+ def finish_tool_node(
175
+ self,
176
+ node: OutputNode,
177
+ label: str,
178
+ elapsed: float,
179
+ ok: bool = True,
180
+ detail: str = "",
181
+ ) -> None:
182
+ color = "dim" if ok else "red"
183
+ icon = "●" if ok else "✗"
184
+ tool_body = node.meta or node.header
185
+ suffix = f" [dim]({elapsed:.1f}s)[/dim]"
186
+ if detail:
187
+ suffix += f" [dim]{detail}[/dim]"
188
+ node.header = f"[{color}]{icon}[/{color}] {tool_body}{suffix}"
189
+ node.elapsed = elapsed
190
+ node.status = "done" if ok else "error"
191
+ self._tree.mark_dirty()
192
+ self.refresh()
193
+
194
+ def append_tool_result(
195
+ self,
196
+ text: str,
197
+ *,
198
+ parent: OutputNode | None = None,
199
+ collapsed: bool = False,
200
+ tool_call_id: str | None = None,
201
+ ) -> OutputNode | None:
202
+ clean = _clean(text)
203
+ if not clean.strip():
204
+ return None
205
+ lines = [_strip_ansi_trailing_space(line) for line in (clean.splitlines() or [clean])]
206
+ while lines and not _clean(lines[0]).strip():
207
+ lines.pop(0)
208
+ while lines and not _clean(lines[-1]).strip():
209
+ lines.pop()
210
+ if not lines:
211
+ return None
212
+ node = self._tree.new_node(
213
+ parent=parent or self._current_tool or self._current_agent or self._tree.root,
214
+ node_type="tool_result",
215
+ header=escape(lines[0]) if lines else "",
216
+ body_lines=[escape(line) for line in lines[1:]],
217
+ collapsed=collapsed,
218
+ tool_call_id=tool_call_id,
219
+ )
220
+ self.refresh()
221
+ return node
222
+
223
+ def append_file_change(
224
+ self,
225
+ diff_text: str,
226
+ *,
227
+ parent: OutputNode | None = None,
228
+ tool_call_id: str | None = None,
229
+ preview_hunks: int = 1,
230
+ preview_lines: int = 8,
231
+ ) -> OutputNode | None:
232
+ from voidx.ui.diff import (
233
+ parse_unified_diff,
234
+ render_file_change_lines,
235
+ render_full_file_diff_lines,
236
+ )
237
+
238
+ parsed = parse_unified_diff(diff_text)
239
+ if not parsed.files:
240
+ return self.append_tool_result(
241
+ diff_text,
242
+ parent=parent,
243
+ collapsed=True,
244
+ tool_call_id=tool_call_id,
245
+ )
246
+
247
+ target = parent or self._current_tool or self._current_agent or self._tree.root
248
+ first_node: OutputNode | None = None
249
+ for index, file_diff in enumerate(parsed.files):
250
+ body_lines, omitted = render_file_change_lines(file_diff, preview_hunks, preview_lines)
251
+ header = (
252
+ f"[#A3BE8C]●[/#A3BE8C] "
253
+ f"[bold]{file_diff.operation}[/bold]({escape(_short_path(file_diff.path))})"
254
+ )
255
+ if index == 0 and target.node_type == "tool_call":
256
+ node = target
257
+ node.header = header
258
+ node.body_lines = body_lines
259
+ node.collapsed = True
260
+ node.status = "done"
261
+ node.meta = header
262
+ if tool_call_id:
263
+ node.tool_call_id = tool_call_id
264
+ node.payload["diff_text"] = diff_text
265
+ else:
266
+ node = self._tree.new_node(
267
+ parent=target,
268
+ node_type="tool_call",
269
+ header=header,
270
+ body_lines=body_lines,
271
+ collapsed=True,
272
+ status="done",
273
+ meta=header,
274
+ tool_call_id=tool_call_id,
275
+ payload={"diff_text": diff_text},
276
+ )
277
+ full_lines = render_full_file_diff_lines(file_diff)
278
+ if omitted and full_lines:
279
+ self._tree.new_node(
280
+ parent=node,
281
+ node_type="tool_result",
282
+ header="[dim]Full diff[/dim]",
283
+ body_lines=full_lines,
284
+ collapsed=True,
285
+ )
286
+ if first_node is None:
287
+ first_node = node
288
+ self._tree.mark_dirty()
289
+ self.refresh()
290
+ return first_node
291
+
292
+ def set_status(
293
+ self,
294
+ status_id: str,
295
+ label: str,
296
+ detail: str = "",
297
+ *,
298
+ parent: OutputNode | None = None,
299
+ stage: str = "working",
300
+ ) -> OutputNode:
301
+ node = self._status_nodes.get(status_id)
302
+ if node is None:
303
+ node = self._tree.new_node(
304
+ parent=parent or self._tree.root,
305
+ node_type="status",
306
+ header="",
307
+ collapsed=False,
308
+ )
309
+ self._status_nodes[status_id] = node
310
+ tick = self._status_ticks.get(status_id, 0)
311
+ self._status_ticks[status_id] = tick + 1
312
+ color = "#EBCB8B" if tick % 2 == 0 else "#F6D365"
313
+ node.header = f"[{color}]●[/{color}] {escape(label)}"
314
+ clean_detail = _clean(detail).strip()
315
+ node.body_lines = [f"[dim]{escape(line)}[/dim]" for line in _tail_lines(clean_detail, 5)]
316
+ node.collapsed = False
317
+ node.status = "running"
318
+ node.meta = label
319
+ self._tree.mark_dirty()
320
+ self.refresh()
321
+ return node
322
+
323
+ def finish_status(
324
+ self,
325
+ status_id: str,
326
+ *,
327
+ label: str = "",
328
+ detail: str = "",
329
+ ok: bool = True,
330
+ remove: bool = True,
331
+ ) -> None:
332
+ node = self._status_nodes.pop(status_id, None)
333
+ if node is None:
334
+ return
335
+ self._status_ticks.pop(status_id, None)
336
+ if remove:
337
+ self._remove_node(node)
338
+ self.refresh()
339
+ return
340
+ color = "dim" if ok else "red"
341
+ icon = "●" if ok else "✗"
342
+ text = label or _clean(node.header).strip() or "Done"
343
+ node.header = f"[{color}]{icon}[/{color}] [dim]{escape(text)}[/dim]"
344
+ clean_detail = _clean(detail).strip()
345
+ if clean_detail:
346
+ node.body_lines = [f"[dim]{escape(line)}[/dim]" for line in _tail_lines(clean_detail, 5)]
347
+ node.status = "done" if ok else "error"
348
+ node.collapsed = True
349
+ node.meta = text
350
+ self._tree.mark_dirty()
351
+ self.refresh()
352
+
353
+ def show_permission(
354
+ self,
355
+ prompt: str,
356
+ tools: list[dict[str, Any]],
357
+ *,
358
+ parent: OutputNode | None = None,
359
+ ) -> OutputNode:
360
+ self.clear_permission()
361
+ body: list[str] = []
362
+ for index, tool in enumerate(tools, 1):
363
+ name = str(tool.get("name") or "tool")
364
+ pattern = str(tool.get("pattern") or "")
365
+ body.append(escape(f"{index}. {name}"))
366
+ if pattern and pattern != "*":
367
+ body.append(escape(f" target: {pattern}"))
368
+ args = tool.get("args")
369
+ if isinstance(args, dict):
370
+ for key, value in args.items():
371
+ body.append(escape(f" {key}: {_short_value(value)}"))
372
+ self._permission_node = self._tree.new_node(
373
+ parent=parent or self._tree.root,
374
+ node_type="permission",
375
+ header=f"[yellow]Permission required[/yellow] {escape(prompt)}",
376
+ body_lines=body,
377
+ collapsed=False,
378
+ )
379
+ self.refresh()
380
+ return self._permission_node
381
+
382
+ def clear_permission(self) -> None:
383
+ if self._permission_node is None:
384
+ return
385
+ self._remove_node(self._permission_node)
386
+ self._permission_node = None
387
+ self.refresh()
388
+
389
+
390
+ def _bash_markdown_lines(command: str, width: int) -> list[str]:
391
+ command = command.rstrip("\n")
392
+ if not command:
393
+ return []
394
+ fence = _markdown_fence(command)
395
+ markdown = f"{fence}bash\n{command}\n{fence}"
396
+ return [_ansi_line(line) for line in _markdown_lines(markdown, width)]
397
+
398
+
399
+ def _markdown_fence(text: str) -> str:
400
+ runs = [len(match.group(0)) for match in re.finditer(r"`+", text)]
401
+ return "`" * max(3, max(runs, default=0) + 1)
@@ -0,0 +1,51 @@
1
+ """Context-local dock proxy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar
6
+ from typing import Any
7
+
8
+ _current_dock: ContextVar[Any | None] = ContextVar("current_dock", default=None)
9
+
10
+
11
+ def get_dock() -> Any | None:
12
+ return _current_dock.get()
13
+
14
+
15
+ def set_dock(dock: Any | None) -> None:
16
+ _current_dock.set(dock)
17
+
18
+
19
+ class _DockProxy:
20
+ @property
21
+ def active(self) -> bool:
22
+ d = get_dock()
23
+ return d.active if d is not None else False
24
+
25
+ def __getattr__(self, name):
26
+ d = get_dock()
27
+ if d is None:
28
+ if name in ("active",):
29
+ return False
30
+ if name in (
31
+ "begin_capture",
32
+ "deactivate",
33
+ "print",
34
+ "capture",
35
+ "start_turn",
36
+ "set_stream",
37
+ "start_tool",
38
+ "tool_output",
39
+ "append_tool_result",
40
+ "refresh",
41
+ "set_mode",
42
+ ):
43
+ return lambda *args, **kwargs: None
44
+ raise RuntimeError(f"No active dock in this context. Cannot access '{name}'.")
45
+ return getattr(d, name)
46
+
47
+ def __bool__(self):
48
+ return get_dock() is not None
49
+
50
+
51
+ dock = _DockProxy()
@@ -0,0 +1 @@
1
+ """Implementation parts for UI events."""
@@ -0,0 +1,249 @@
1
+ """Typed UI event schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, TypeAlias
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class UiEventBase(BaseModel):
11
+ model_config = ConfigDict(frozen=True)
12
+ agent_id: int = -1
13
+
14
+
15
+ class CaptureStarted(UiEventBase):
16
+ kind: Literal["capture.started"] = "capture.started"
17
+
18
+
19
+ class CaptureStopped(UiEventBase):
20
+ kind: Literal["capture.stopped"] = "capture.stopped"
21
+
22
+
23
+ class RefreshRequested(UiEventBase):
24
+ kind: Literal["refresh.requested"] = "refresh.requested"
25
+
26
+
27
+ class ResetRequested(UiEventBase):
28
+ kind: Literal["reset.requested"] = "reset.requested"
29
+
30
+
31
+ class TurnStarted(UiEventBase):
32
+ kind: Literal["turn.started"] = "turn.started"
33
+ text: str
34
+
35
+
36
+ class StartupShown(UiEventBase):
37
+ kind: Literal["startup.shown"] = "startup.shown"
38
+ model: str
39
+ provider: str
40
+ workspace: str
41
+ session_title: str
42
+ is_new: bool
43
+ profile_configured: bool = True
44
+
45
+
46
+ class MessageAppended(UiEventBase):
47
+ kind: Literal["message.appended"] = "message.appended"
48
+ text: str
49
+ style: str = ""
50
+
51
+
52
+ class AnsiAppended(UiEventBase):
53
+ kind: Literal["ansi.appended"] = "ansi.appended"
54
+ text: str
55
+
56
+
57
+ class MarkdownAppended(UiEventBase):
58
+ kind: Literal["markdown.appended"] = "markdown.appended"
59
+ content: str
60
+
61
+
62
+ class ThoughtAppended(UiEventBase):
63
+ kind: Literal["thought.appended"] = "thought.appended"
64
+ text: str
65
+ elapsed: float | None = None
66
+
67
+
68
+ class WarningAppended(UiEventBase):
69
+ kind: Literal["warning.appended"] = "warning.appended"
70
+ message: str
71
+
72
+
73
+ class ErrorAppended(UiEventBase):
74
+ kind: Literal["error.appended"] = "error.appended"
75
+ message: str
76
+
77
+
78
+ class DiffAppended(UiEventBase):
79
+ kind: Literal["diff.appended"] = "diff.appended"
80
+ diff_text: str
81
+ title: str = ""
82
+
83
+
84
+ class StatusUpdated(UiEventBase):
85
+ kind: Literal["status.updated"] = "status.updated"
86
+ status_id: str
87
+ label: str
88
+ detail: str = ""
89
+ stage: Literal[
90
+ "analyzing",
91
+ "thinking",
92
+ "streaming",
93
+ "agent_step",
94
+ "compacting",
95
+ "waiting_permission",
96
+ "working",
97
+ ] = "working"
98
+ parent_tool_call_id: str = ""
99
+
100
+
101
+ class StatusFinished(UiEventBase):
102
+ kind: Literal["status.finished"] = "status.finished"
103
+ status_id: str
104
+ label: str = ""
105
+ detail: str = ""
106
+ ok: bool = True
107
+ remove: bool = True
108
+
109
+
110
+ class AssistantStreamStarted(UiEventBase):
111
+ kind: Literal["assistant_stream.started"] = "assistant_stream.started"
112
+ stream_id: str = "default"
113
+
114
+
115
+ class AssistantStreamUpdated(UiEventBase):
116
+ kind: Literal["assistant_stream.updated"] = "assistant_stream.updated"
117
+ text: str
118
+ stream_id: str = "default"
119
+
120
+
121
+ class AssistantStreamCommitted(UiEventBase):
122
+ kind: Literal["assistant_stream.committed"] = "assistant_stream.committed"
123
+ stream_id: str = "default"
124
+
125
+
126
+ class AssistantStreamDiscarded(UiEventBase):
127
+ kind: Literal["assistant_stream.discarded"] = "assistant_stream.discarded"
128
+ stream_id: str = "default"
129
+
130
+
131
+ class ToolStarted(UiEventBase):
132
+ kind: Literal["tool.started"] = "tool.started"
133
+ tool_call_id: str
134
+ label: str
135
+ args: str = ""
136
+ tool_name: str = ""
137
+ raw_args: dict[str, Any] = Field(default_factory=dict)
138
+
139
+
140
+ class ToolFinished(UiEventBase):
141
+ kind: Literal["tool.finished"] = "tool.finished"
142
+ tool_call_id: str
143
+ label: str
144
+ elapsed: float
145
+ ok: bool = True
146
+ detail: str = ""
147
+
148
+
149
+ class ToolResultAppended(UiEventBase):
150
+ kind: Literal["tool_result.appended"] = "tool_result.appended"
151
+ tool_call_id: str = ""
152
+ text: str
153
+ collapsed: bool = False
154
+
155
+
156
+ class FileChangeAppended(UiEventBase):
157
+ kind: Literal["file_change.appended"] = "file_change.appended"
158
+ tool_call_id: str = ""
159
+ diff_text: str
160
+
161
+
162
+ class SubagentStarted(UiEventBase):
163
+ kind: Literal["subagent.started"] = "subagent.started"
164
+ agent_id: int
165
+ subagent_id: str
166
+ name: str
167
+ description: str = ""
168
+ parent_agent_id: int = -1
169
+ parent_tool_call_id: str = ""
170
+
171
+
172
+ class SubagentStepStarted(UiEventBase):
173
+ kind: Literal["subagent_step.started"] = "subagent_step.started"
174
+ agent_id: int
175
+ subagent_id: str
176
+ name: str
177
+ step: int
178
+ max_steps: int
179
+
180
+
181
+ class SubagentFinished(UiEventBase):
182
+ kind: Literal["subagent.finished"] = "subagent.finished"
183
+ agent_id: int
184
+ subagent_id: str
185
+ ok: bool = True
186
+ elapsed: float | None = None
187
+
188
+
189
+ class PermissionToolDetail(BaseModel):
190
+ name: str
191
+ pattern: str = ""
192
+ args: dict[str, Any] = Field(default_factory=dict)
193
+
194
+
195
+ class PermissionPromptShown(UiEventBase):
196
+ kind: Literal["permission_prompt.shown"] = "permission_prompt.shown"
197
+ prompt: str
198
+ choices: list[tuple[str, str, str]]
199
+ tools: list[PermissionToolDetail] = Field(default_factory=list)
200
+
201
+
202
+ class PermissionPromptCleared(UiEventBase):
203
+ kind: Literal["permission_prompt.cleared"] = "permission_prompt.cleared"
204
+
205
+
206
+ class InputSet(UiEventBase):
207
+ kind: Literal["input.set"] = "input.set"
208
+ text: str
209
+ hints: list[tuple[str, str, bool]] = Field(default_factory=list)
210
+ cursor_pos: int | None = None
211
+
212
+
213
+ class NoticeSet(UiEventBase):
214
+ kind: Literal["notice.set"] = "notice.set"
215
+ text: str
216
+
217
+
218
+ UiEvent: TypeAlias = (
219
+ CaptureStarted
220
+ | CaptureStopped
221
+ | RefreshRequested
222
+ | ResetRequested
223
+ | TurnStarted
224
+ | StartupShown
225
+ | MessageAppended
226
+ | AnsiAppended
227
+ | MarkdownAppended
228
+ | ThoughtAppended
229
+ | WarningAppended
230
+ | ErrorAppended
231
+ | DiffAppended
232
+ | StatusUpdated
233
+ | StatusFinished
234
+ | AssistantStreamStarted
235
+ | AssistantStreamUpdated
236
+ | AssistantStreamCommitted
237
+ | AssistantStreamDiscarded
238
+ | ToolStarted
239
+ | ToolFinished
240
+ | ToolResultAppended
241
+ | FileChangeAppended
242
+ | SubagentStarted
243
+ | SubagentStepStarted
244
+ | SubagentFinished
245
+ | PermissionPromptShown
246
+ | PermissionPromptCleared
247
+ | InputSet
248
+ | NoticeSet
249
+ )