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
voidx/ui/dock.py ADDED
@@ -0,0 +1,372 @@
1
+ """Rich Live/Layout backed bottom input dock."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from io import StringIO
6
+ from typing import Callable
7
+
8
+ from rich.console import Console, Group
9
+ from rich.live import Live
10
+ from rich.markup import escape
11
+ from rich.text import Text
12
+
13
+ from voidx.ui.dock_components.formatting import (
14
+ ANSI_LINE_PREFIX,
15
+ _ansi_line,
16
+ _ansi_rgb,
17
+ _clean,
18
+ _markdown_lines,
19
+ _text_from_line,
20
+ )
21
+ from voidx.ui.dock_components.nodes import DockNodeMixin
22
+ from voidx.ui.dock_components.state import dock, get_dock, set_dock
23
+ from voidx.ui.tree import OutputNode, OutputTree
24
+
25
+
26
+ class BottomInputDock(DockNodeMixin):
27
+ """Render agent output above a fixed input box with Rich Live/Layout."""
28
+
29
+ def __init__(self) -> None:
30
+ self._console = Console()
31
+ self._active = False
32
+ self._live: Live | None = None
33
+ self._stopping = False
34
+ self._tree = OutputTree()
35
+ self._current_turn: OutputNode | None = None
36
+ self._current_agent: OutputNode | None = None
37
+ self._current_tool: OutputNode | None = None
38
+ self._stream_node: OutputNode | None = None
39
+ self._stream_text = ""
40
+ self._status_nodes: dict[str, OutputNode] = {}
41
+ self._status_ticks: dict[str, int] = {}
42
+ self._permission_node: OutputNode | None = None
43
+ self._input_text = ""
44
+ self._cursor_pos = 0
45
+ self._hints: list[tuple[str, str, bool]] = []
46
+ self._refresh_callback: Callable[[], None] | None = None
47
+ self._width_provider: Callable[[], int] | None = None
48
+
49
+ @property
50
+ def active(self) -> bool:
51
+ return self._active
52
+
53
+ @property
54
+ def tree(self) -> OutputTree:
55
+ return self._tree
56
+
57
+ @property
58
+ def current_turn(self) -> OutputNode | None:
59
+ return self._current_turn
60
+
61
+ @property
62
+ def current_agent(self) -> OutputNode | None:
63
+ return self._current_agent
64
+
65
+ def set_refresh_callback(self, callback: Callable[[], None] | None) -> None:
66
+ self._refresh_callback = callback
67
+
68
+ def set_width_provider(self, callback: Callable[[], int] | None) -> None:
69
+ self._width_provider = callback
70
+
71
+ def activate(self) -> None:
72
+ if self._live:
73
+ self.refresh()
74
+ return
75
+ self._active = True
76
+ self._stopping = False
77
+ self._stream_text = ""
78
+ self._live = Live(
79
+ console=self._console,
80
+ auto_refresh=False,
81
+ refresh_per_second=20,
82
+ transient=True,
83
+ redirect_stdout=False,
84
+ redirect_stderr=False,
85
+ get_renderable=self._render,
86
+ )
87
+ self._live.start(refresh=True)
88
+
89
+ def begin_capture(self) -> None:
90
+ if self._active:
91
+ return
92
+ self._active = True
93
+ self._stream_text = ""
94
+
95
+ def deactivate(self) -> None:
96
+ if not self._active:
97
+ return
98
+ self._stream_node = None
99
+ self._stream_text = ""
100
+ if self._live:
101
+ self._stopping = True
102
+ self._live.stop()
103
+ self._live = None
104
+ self._stopping = False
105
+ self._active = False
106
+
107
+ def reset(self) -> None:
108
+ self._tree = OutputTree()
109
+ self._reset_runtime_nodes()
110
+ self._input_text = ""
111
+ self._cursor_pos = 0
112
+ self._hints = []
113
+ self.refresh()
114
+
115
+ def restore_tree(self, tree: OutputTree, *, append: bool = False) -> None:
116
+ if append:
117
+ self._tree.extend_from(tree)
118
+ else:
119
+ self._tree = tree
120
+ self._reset_runtime_nodes()
121
+ self.refresh()
122
+
123
+ def _reset_runtime_nodes(self) -> None:
124
+ self._current_turn = None
125
+ self._current_agent = None
126
+ self._current_tool = None
127
+ self._stream_node = None
128
+ self._stream_text = ""
129
+ self._status_nodes = {}
130
+ self._status_ticks = {}
131
+ self._permission_node = None
132
+
133
+ def start_turn(self, text: str) -> OutputNode:
134
+ self.commit_stream()
135
+ self._current_tool = None
136
+ self._current_agent = None
137
+ if self._tree.root.children:
138
+ self._tree.new_node(
139
+ parent=self._tree.root,
140
+ node_type="message",
141
+ header="",
142
+ collapsed=False,
143
+ )
144
+ self._current_turn = self._tree.new_node(
145
+ parent=self._tree.root,
146
+ node_type="turn",
147
+ header=f"[bold white]❯[/] {escape(text[:160])}",
148
+ collapsed=False,
149
+ )
150
+ self.refresh()
151
+ return self._current_turn
152
+
153
+ def ensure_agent(self) -> OutputNode:
154
+ if self._current_agent is None:
155
+ self._current_agent = self._tree.new_node(
156
+ parent=self._tree.root,
157
+ node_type="assistant",
158
+ header="[#EBCB8B]●[/#EBCB8B] Working",
159
+ collapsed=False,
160
+ )
161
+ self.refresh()
162
+ return self._current_agent
163
+
164
+ def print(self, *args, **kwargs) -> bool:
165
+ if not self._active:
166
+ return False
167
+ self.capture(lambda console: console.print(*args, **kwargs))
168
+ return True
169
+
170
+ def capture(self, render: Callable[[Console], None]) -> bool:
171
+ if not self._active:
172
+ return False
173
+ buffer = StringIO()
174
+ console = Console(
175
+ file=buffer,
176
+ force_terminal=True,
177
+ color_system="truecolor",
178
+ width=self._width(),
179
+ )
180
+ render(console)
181
+ text = buffer.getvalue().rstrip("\n")
182
+ if text:
183
+ self.append_ansi(text)
184
+ return True
185
+
186
+ def set_stream(self, text: str, *, parent: OutputNode | None = None) -> bool:
187
+ if not self._active:
188
+ return False
189
+ self._stream_text = text
190
+ self._update_stream_node(parent=parent)
191
+ self.refresh()
192
+ return True
193
+
194
+ def commit_stream(self) -> bool:
195
+ if not self._active:
196
+ return False
197
+ self._stream_node = None
198
+ self._stream_text = ""
199
+ self.refresh()
200
+ return True
201
+
202
+ def discard_stream(self) -> bool:
203
+ if not self._active:
204
+ return False
205
+ if self._stream_node and not self._stream_text.strip():
206
+ self._remove_node(self._stream_node)
207
+ if self._current_agent is self._stream_node:
208
+ self._current_agent = None
209
+ elif self._stream_node:
210
+ self._remove_node(self._stream_node)
211
+ if self._current_agent is self._stream_node:
212
+ self._current_agent = None
213
+ self._stream_node = None
214
+ self._stream_text = ""
215
+ self.refresh()
216
+ return True
217
+
218
+ def set_input(
219
+ self,
220
+ text: str,
221
+ hints: list[tuple[str, str, bool]] | None = None,
222
+ cursor_pos: int | None = None,
223
+ ) -> None:
224
+ self._input_text = text
225
+ self._cursor_pos = max(0, min(len(text), len(text) if cursor_pos is None else cursor_pos))
226
+ self._hints = hints or []
227
+ self.refresh()
228
+
229
+ def after_output(self) -> None:
230
+ self.refresh()
231
+
232
+ def render(self) -> None:
233
+ self.refresh()
234
+
235
+ def refresh(self) -> None:
236
+ if self._refresh_callback:
237
+ self._refresh_callback()
238
+ if self._live:
239
+ self._live.update(self._render(), refresh=True)
240
+
241
+ def _render(self) -> Group:
242
+ if self._stopping:
243
+ return Group(Text(""))
244
+
245
+ width = max(self._width() - 1, 3)
246
+ hint_lines = []
247
+ for name, desc, selected in self._hints[:6]:
248
+ style = "bold blue" if selected else "dim"
249
+ hint_lines.append(Text.assemble((" " + name, style), (" " + desc, style)))
250
+
251
+ input_height = 3 + len(hint_lines)
252
+ body_limit = max((self._console.height or 24) - input_height - 1, 1)
253
+ lines = self._tree.render(self._width())
254
+ body = Group(*[_text_from_line(line) for line in lines[-body_limit:]]) if lines else Text("")
255
+
256
+ border = "─" * width
257
+ input_box = Text.assemble(
258
+ (border + "\n", "white"),
259
+ ("❯ ", "bold white"),
260
+ *self._render_input_text(),
261
+ ("\n" + border, "white"),
262
+ )
263
+ input_renderable = Group(input_box, *hint_lines)
264
+ return Group(body, input_renderable)
265
+
266
+ def _render_input_text(self) -> list[tuple[str, str]]:
267
+ text = self._input_text
268
+ cursor = self._cursor_pos
269
+ before = text[:cursor]
270
+ at = text[cursor:cursor + 1]
271
+ after = text[cursor + 1:]
272
+ parts: list[tuple[str, str]] = []
273
+ if before:
274
+ parts.append((before, "white"))
275
+ if at and at != "\n":
276
+ parts.append((at, "reverse white"))
277
+ else:
278
+ parts.append((" ", "reverse white"))
279
+ if at == "\n":
280
+ parts.append(("\n", "white"))
281
+ if after:
282
+ parts.append((after, "white"))
283
+ return parts
284
+
285
+ def _update_stream_node(self, *, parent: OutputNode | None = None) -> None:
286
+ clean = _clean(self._stream_text).strip("\n")
287
+ if not clean:
288
+ return
289
+ if self._stream_node is None or (
290
+ parent is not None and self._stream_node.parent is not parent
291
+ ):
292
+ self._stream_node = self._new_stream_node(parent=parent)
293
+ if clean.startswith("● "):
294
+ clean = clean[2:]
295
+ lines = _markdown_lines(clean, self._markdown_width())
296
+ if not lines:
297
+ return
298
+
299
+ # Render the bullet and first line as one ANSI run so prompt_toolkit
300
+ # keeps them on the same visual baseline.
301
+ bullet = _ansi_rgb("●", (163, 190, 140))
302
+ self._stream_node.header = _ansi_line(f"{bullet} {lines[0]}")
303
+ self._stream_node.body_lines = [_ansi_line(f" {line}") for line in lines[1:]]
304
+ self._tree.mark_dirty()
305
+
306
+ def _new_stream_node(self, *, parent: OutputNode | None = None) -> OutputNode:
307
+ if parent is not None:
308
+ return self._tree.new_node(
309
+ parent=parent,
310
+ node_type="assistant",
311
+ header="",
312
+ collapsed=False,
313
+ )
314
+
315
+ if (
316
+ self._current_agent is not None
317
+ and self._current_agent.node_type == "assistant"
318
+ and self._current_agent.header == "[#EBCB8B]●[/#EBCB8B] Working"
319
+ and not self._current_agent.children
320
+ ):
321
+ return self._current_agent
322
+
323
+ if self._current_agent is not None:
324
+ if self._current_agent.header == "[#EBCB8B]●[/#EBCB8B] Working":
325
+ self._current_agent.header = "[dim]●[/dim] voidx"
326
+ self._tree.mark_dirty()
327
+ self._current_agent = self._tree.new_node(
328
+ parent=self._tree.root,
329
+ node_type="assistant",
330
+ header="",
331
+ collapsed=False,
332
+ )
333
+ return self._current_agent
334
+
335
+ self._current_agent = self._tree.new_node(
336
+ parent=self._tree.root,
337
+ node_type="assistant",
338
+ header="",
339
+ collapsed=False,
340
+ )
341
+ return self._current_agent
342
+
343
+ def _settle_stream_for_tool(self) -> None:
344
+ if (
345
+ self._stream_node is not None
346
+ and self._stream_node is self._current_agent
347
+ and not self._stream_node.children
348
+ and self._stream_text.strip()
349
+ ):
350
+ self._stream_node.header = "[#EBCB8B]●[/#EBCB8B] Working"
351
+ self._stream_node.body_lines = []
352
+ self._tree.mark_dirty()
353
+ self.commit_stream()
354
+
355
+ def _remove_node(self, node: OutputNode) -> None:
356
+ parent = node.parent
357
+ if parent and node in parent.children:
358
+ parent.children.remove(node)
359
+ for index, child in enumerate(parent.children):
360
+ child._is_last_sibling = index == len(parent.children) - 1
361
+ for child in list(node.children):
362
+ self._remove_node(child)
363
+ self._tree._all.pop(node.id, None)
364
+ self._tree.mark_dirty()
365
+
366
+ def _width(self) -> int:
367
+ if self._width_provider is not None:
368
+ return max(self._width_provider(), 20)
369
+ return max(self._console.width or 80, 20)
370
+
371
+ def _markdown_width(self) -> int:
372
+ return max(self._width() - 4, 20)
@@ -0,0 +1 @@
1
+ """Implementation parts for BottomInputDock."""
@@ -0,0 +1,123 @@
1
+ """Formatting helpers for dock rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from io import StringIO
7
+
8
+ from rich.console import Console
9
+ from rich.markdown import Markdown
10
+ from rich.text import Text
11
+
12
+ _ANSI_RE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]')
13
+ _ANSI_SGR_RE = re.compile(r"\x1b\[([0-9;]*)m")
14
+ ANSI_LINE_PREFIX = "\x00voidx-ansi\x00"
15
+
16
+
17
+ def _clean(text: str) -> str:
18
+ return _ANSI_RE.sub("", text).rstrip("\n")
19
+
20
+
21
+ def _ansi_line(text: str) -> str:
22
+ return ANSI_LINE_PREFIX + text
23
+
24
+
25
+ def _ansi_rgb(text: str, rgb: tuple[int, int, int]) -> str:
26
+ r, g, b = rgb
27
+ return f"\x1b[38;2;{r};{g};{b}m{text}\x1b[0m"
28
+
29
+
30
+ def _text_from_line(line: str) -> Text:
31
+ marker = line.find(ANSI_LINE_PREFIX)
32
+ if marker == -1:
33
+ return Text.from_markup(line)
34
+ text = Text.from_markup(line[:marker])
35
+ text.append_text(Text.from_ansi(line[marker + len(ANSI_LINE_PREFIX):]))
36
+ return text
37
+
38
+
39
+ def _markdown_lines(text: str, width: int) -> list[str]:
40
+ buffer = StringIO()
41
+ console = Console(
42
+ file=buffer,
43
+ force_terminal=True,
44
+ color_system="truecolor",
45
+ width=width or 80,
46
+ )
47
+ console.print(Markdown(text), end="")
48
+
49
+ lines: list[str] = []
50
+ for raw_line in buffer.getvalue().rstrip("\n").splitlines():
51
+ stripped = _strip_ansi_trailing_space(raw_line)
52
+ parts = stripped.splitlines() or [stripped]
53
+ lines.extend(parts)
54
+
55
+ return [line for line in lines if _clean(line).strip()]
56
+
57
+
58
+ def _strip_ansi_trailing_space(line: str) -> str:
59
+ text = Text.from_ansi(_strip_ansi_backgrounds(line))
60
+ text.rstrip()
61
+ buffer = StringIO()
62
+ console = Console(
63
+ file=buffer,
64
+ force_terminal=True,
65
+ color_system="truecolor",
66
+ width=10_000,
67
+ )
68
+ console.print(text, end="")
69
+ return buffer.getvalue()
70
+
71
+
72
+ def _short_value(value: object) -> str:
73
+ text = str(value).replace("\n", "\\n")
74
+ return text[:157] + "..." if len(text) > 160 else text
75
+
76
+
77
+ def _short_path(path: str, limit: int = 96) -> str:
78
+ if len(path) <= limit:
79
+ return path
80
+ keep = max((limit - 1) // 2, 1)
81
+ return f"{path[:keep]}…{path[-keep:]}"
82
+
83
+
84
+ def _tail_lines(text: str, limit: int) -> list[str]:
85
+ if not text.strip():
86
+ return []
87
+ lines = [line for line in text.splitlines() if line.strip()]
88
+ return lines[-limit:]
89
+
90
+
91
+ def _strip_ansi_backgrounds(text: str) -> str:
92
+ def replace(match: re.Match[str]) -> str:
93
+ raw = match.group(1)
94
+ if raw == "":
95
+ return match.group(0)
96
+ parts = raw.split(";")
97
+ kept: list[str] = []
98
+ i = 0
99
+ while i < len(parts):
100
+ part = parts[i]
101
+ if part == "48":
102
+ mode = parts[i + 1] if i + 1 < len(parts) else ""
103
+ if mode == "5":
104
+ i += 3
105
+ elif mode == "2":
106
+ i += 5
107
+ else:
108
+ i += 1
109
+ continue
110
+ try:
111
+ value = int(part)
112
+ except ValueError:
113
+ kept.append(part)
114
+ i += 1
115
+ continue
116
+ if 40 <= value <= 49 or 100 <= value <= 107:
117
+ i += 1
118
+ continue
119
+ kept.append(part)
120
+ i += 1
121
+ return f"\x1b[{';'.join(kept)}m" if kept else ""
122
+
123
+ return _ANSI_SGR_RE.sub(replace, text)