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,419 @@
1
+ """Slash command support for /model operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ from voidx.agent.slash_components.runtime import PROVIDERS, get_providers, _select_from_list, ui
8
+
9
+
10
+ class SlashModelMixin:
11
+ async def _model_new(self) -> None:
12
+ """Interactive model configuration — create or update a named profile."""
13
+ from voidx.config import Profile
14
+ from voidx.llm.provider import create_chat_model
15
+
16
+ ui.print("[bold]Configure LLM[/bold]")
17
+
18
+ # Step 1: choose provider via arrow keys
19
+ providers = get_providers(self._g._settings)
20
+ provider_choices = providers + ["Add custom provider..."]
21
+ idx = await _select_from_list(self._g._app, "Provider", provider_choices)
22
+ if idx is None:
23
+ ui.print("[dim]Cancelled.[/dim]")
24
+ return
25
+ if provider_choices[idx] == "Add custom provider...":
26
+ new_provider = await self._prompt("Provider name")
27
+ if not new_provider or not new_provider.strip():
28
+ ui.error("Provider name is required.")
29
+ return
30
+ new_provider = new_provider.strip()
31
+ protocol_choices = ["openai", "anthropic"]
32
+ proto_idx = await _select_from_list(self._g._app, "Protocol", protocol_choices)
33
+ if proto_idx is None:
34
+ ui.print("[dim]Cancelled.[/dim]")
35
+ return
36
+ protocol = protocol_choices[proto_idx]
37
+ custom_base_url = await self._prompt("Base URL (optional)", default="")
38
+ if custom_base_url is None:
39
+ ui.print("[dim]Cancelled.[/dim]")
40
+ return
41
+ custom_base_url = custom_base_url.strip()
42
+ ui.print(f"[dim] Custom provider: {new_provider} (protocol={protocol})[/dim]")
43
+ else:
44
+ new_provider = provider_choices[idx]
45
+ protocol = self._g._settings.resolve_protocol(new_provider) if self._g._settings else None
46
+ custom_base_url = ""
47
+ ui.print(f"[dim] Provider: {new_provider}[/dim]")
48
+
49
+ # Step 2: choose model from known list or enter manually
50
+ from voidx.llm.catalog import list_models as list_provider_models
51
+ known = await list_provider_models(new_provider)
52
+ model_choices = known + ["Other (enter manually)"]
53
+ ui.print()
54
+ model_idx = await _select_from_list(self._g._app, "Model", model_choices)
55
+ if model_idx is None:
56
+ ui.print("[dim]Cancelled.[/dim]")
57
+ return
58
+ if model_choices[model_idx] == "Other (enter manually)":
59
+ new_model = await self._prompt(
60
+ f"Model name",
61
+ default=self._g.config.model.model,
62
+ )
63
+ if new_model is None:
64
+ ui.print("[dim]Cancelled.[/dim]")
65
+ return
66
+ if not new_model.strip():
67
+ ui.error("Model name is required.")
68
+ return
69
+ new_model = new_model.strip()
70
+ else:
71
+ new_model = model_choices[model_idx]
72
+ ui.print(f"[dim] Model: {new_model}[/dim]")
73
+
74
+ # Step 3: API key
75
+ current_key = ""
76
+ if self._g._settings:
77
+ current_key = self._g._settings.resolve_api_key(new_provider) or ""
78
+ masked = self._mask_key(current_key) if current_key else "(not set)"
79
+ ui.print(f"[dim]Current: {masked}[/dim]")
80
+ new_key = await self._prompt("API key", default="", secret=True)
81
+ if new_key is None:
82
+ ui.print("[dim]Cancelled.[/dim]")
83
+ return
84
+ if new_key.strip():
85
+ api_key = new_key.strip()
86
+ else:
87
+ if self._g._settings:
88
+ key = self._g._settings.resolve_api_key(new_provider)
89
+ if not key:
90
+ ui.error(
91
+ f"No API key found for '{new_provider}'. Provide one now."
92
+ )
93
+ return
94
+ api_key = key
95
+ else:
96
+ return
97
+
98
+ # Step 4: build and validate
99
+ base_url = custom_base_url or (self._g._settings.resolve_base_url(new_provider) if self._g._settings else None)
100
+ test_cfg = self._g.config.model.model_copy()
101
+ test_cfg.provider = new_provider
102
+ test_cfg.model = new_model
103
+ test_cfg.base_url = base_url
104
+ test_cfg.protocol = protocol
105
+
106
+ test_model = create_chat_model(api_key, test_cfg)
107
+
108
+ ui.print()
109
+ ui.print(f"[dim] Testing connection to {new_provider}/{new_model}...[/dim]")
110
+
111
+ ok, err_msg = await self._test_connection(test_model)
112
+ if not ok:
113
+ ui.error(f"Connection failed: {err_msg}")
114
+ ui.print("[dim]Configuration not saved. Check your API key and try again.[/dim]")
115
+ return
116
+
117
+ # Step 5: save profile (key = provider/model) and activate
118
+ profile_key = f"{new_provider}/{new_model}"
119
+ profile = Profile(
120
+ name=profile_key,
121
+ api_key=api_key,
122
+ base_url=base_url,
123
+ protocol=protocol,
124
+ )
125
+ env_path = self._g._settings.save_profile(profile)
126
+
127
+ self._g.config.model.provider = new_provider
128
+ self._g.config.model.model = new_model
129
+ self._g.config.model.base_url = base_url
130
+ self._g.config.model.protocol = protocol
131
+ self._sync_context_limit()
132
+ self._g.api_key = api_key
133
+ self._g.model = test_model
134
+
135
+ ui.print(f" [cyan]{profile_key}[/cyan] [green]✓ configured[/green]")
136
+ ui.print(f"[dim]Saved to {env_path}[/dim]")
137
+
138
+ @staticmethod
139
+ async def _test_connection(model) -> tuple[bool, str]:
140
+ """Test an LLM connection with a minimal prompt. Returns (ok, error_msg)."""
141
+ from langchain_core.messages import HumanMessage
142
+ try:
143
+ resp = await model.ainvoke([HumanMessage(content="hi")])
144
+ if resp and getattr(resp, "content", None):
145
+ return True, ""
146
+ return False, "empty response"
147
+ except Exception as e:
148
+ msg = str(e)
149
+ # Extract the most useful part of the error
150
+ if len(msg) > 300:
151
+ msg = msg[:300] + "..."
152
+ return False, msg
153
+
154
+ @staticmethod
155
+ def _mask_key(key: str) -> str:
156
+ if len(key) <= 8:
157
+ return "*" * len(key)
158
+ return key[:4] + "****" + key[-4:]
159
+
160
+ async def _prompt(self, text: str, default: str = "", secret: bool = False) -> str | None:
161
+ app = getattr(self._g, "_app", None)
162
+ if app is not None and hasattr(app, "ask_text"):
163
+ return await app.ask_text(text, default=default, secret=secret)
164
+
165
+ loop = asyncio.get_event_loop()
166
+ result = await loop.run_in_executor(
167
+ None,
168
+ lambda: input(f" {text}: ").strip(),
169
+ )
170
+ return result if result else default
171
+
172
+ async def _list_models(self) -> None:
173
+ from voidx.llm.catalog import list_models
174
+
175
+ current = f"{self._g.config.model.provider}/{self._g.config.model.model}"
176
+ ui.print(f"[bold]Current:[/bold] [cyan]{current}[/cyan]\n")
177
+
178
+ for provider in get_providers(self._g._settings):
179
+ ui.print(f" [bold]{provider}[/bold] ", end="")
180
+ models = await list_models(provider)
181
+ if models:
182
+ shown = models[:8]
183
+ suffix = f" [dim](+{len(models) - 8} more)[/dim]" if len(models) > 8 else ""
184
+ ui.print(f"{' '.join(shown)}{suffix}")
185
+ else:
186
+ ui.print("[dim](none)[/dim]")
187
+ ui.print()
188
+ ui.print("[dim]Usage: /model list|new|reasoning|test|del|switch|<name>[/dim]")
189
+
190
+ async def _model_list(self) -> None:
191
+ cfg = self._g.config
192
+ if self._g._settings is None:
193
+ ui.error("No Settings reference.")
194
+ return
195
+
196
+ current = f"{cfg.model.provider}/{cfg.model.model}"
197
+ ui.print(f"[bold]Current:[/bold] [cyan]{current}[/cyan]")
198
+
199
+ profiles = self._g._settings.list_profiles()
200
+ if not profiles:
201
+ ui.print("[dim]No profiles configured. Use /model new.[/dim]")
202
+ return
203
+
204
+ ui.print()
205
+ for p in profiles:
206
+ is_active = p.name == current
207
+ marker = " *" if is_active else " "
208
+ masked = self._mask_key(p.api_key) if p.api_key else "(env)"
209
+ ui.print(f" {marker} [cyan]{p.name}[/cyan] {masked}")
210
+
211
+ # ── /model action helpers ─────────────────────────────────────────────
212
+
213
+ def _profile_names(self) -> list[str]:
214
+ """Return names of configured profiles."""
215
+ if self._g._settings is None:
216
+ return []
217
+ return [p.name for p in self._g._settings.list_profiles()]
218
+
219
+ async def _pick_or_act(self, action: str, target: str, callback) -> None:
220
+ """If *target* is a profile name, call callback(target).
221
+ Otherwise show profiles for arrow-key selection, then call callback."""
222
+ import sys as _sys
223
+
224
+ if target:
225
+ await callback(target)
226
+ _sys.stdout.flush()
227
+ return
228
+
229
+ names = self._profile_names()
230
+ if not names:
231
+ ui.print("[yellow]No profiles configured. Use /model new first.[/yellow]")
232
+ return
233
+
234
+ ui.print(f"[bold]{action}[/bold] — select profile (↑↓ Enter, ESC cancel):")
235
+ idx = await _select_from_list(self._g._app, action, names)
236
+ if idx is None:
237
+ ui.print("[dim]Cancelled.[/dim]")
238
+ return
239
+ await callback(names[idx])
240
+ _sys.stdout.flush()
241
+
242
+ async def _model_test(self, target: str) -> None:
243
+ async def _do_test(profile_name: str) -> None:
244
+ from voidx.llm.provider import create_chat_model
245
+ settings = self._g._settings
246
+ if settings is None:
247
+ ui.error("No Settings reference.")
248
+ return
249
+ profile = settings.resolve_profile(profile_name)
250
+ if not profile:
251
+ ui.error(f"Profile not found: {profile_name}")
252
+ return
253
+ cfg = self._g.config.model.model_copy()
254
+ cfg.provider = profile.provider
255
+ cfg.model = profile.model
256
+ cfg.base_url = profile.base_url or settings.resolve_base_url(profile.provider)
257
+ cfg.protocol = profile.protocol or settings.resolve_protocol(profile.provider)
258
+ model = create_chat_model(profile.api_key, cfg)
259
+ ui.print(f"[dim]Testing {profile.name} ({profile.provider}/{profile.model})...[/dim]")
260
+ ok, err_msg = await self._test_connection(model)
261
+ if ok:
262
+ ui.print(f"[green]✓ {profile.name} — connection successful[/green]")
263
+ else:
264
+ ui.print(f"[red]✗ {profile.name} — {err_msg}[/red]")
265
+
266
+ await self._pick_or_act("Test", target, _do_test)
267
+
268
+ async def _model_del(self, target: str) -> None:
269
+ async def _do_delete(profile_name: str) -> None:
270
+ if self._g._settings is None:
271
+ ui.error("No Settings reference.")
272
+ return
273
+ profile = self._g._settings.resolve_profile(profile_name)
274
+ if not profile:
275
+ ui.error(f"Profile not found: {profile_name}")
276
+ return
277
+ env_path = self._g._settings.delete_profile(profile_name)
278
+ was_active = (self._g.config.model.provider == profile.provider
279
+ and self._g.config.model.model == profile.model)
280
+ if was_active:
281
+ self._g.model = None
282
+ self._g.api_key = None
283
+ ui.print(f"[yellow]'{profile_name}' removed. Model disconnected.[/yellow]")
284
+ else:
285
+ ui.print(f"[dim]'{profile_name}' removed.[/dim]")
286
+ ui.print(f"[dim]Cleaned {env_path}[/dim]")
287
+
288
+ await self._pick_or_act("Delete", target, _do_delete)
289
+
290
+ async def _model_switch(self, target: str) -> None:
291
+ async def _do_switch(profile_name: str) -> None:
292
+ from voidx.llm.provider import create_chat_model
293
+ settings = self._g._settings
294
+ if settings is None:
295
+ ui.error("No Settings reference.")
296
+ return
297
+ profile = settings.resolve_profile(profile_name)
298
+ if not profile:
299
+ ui.error(f"Profile not found: {profile_name}")
300
+ return
301
+ self._g.config.model.provider = profile.provider
302
+ self._g.config.model.model = profile.model
303
+ self._g.config.model.base_url = profile.base_url or settings.resolve_base_url(profile.provider)
304
+ self._g.config.model.protocol = profile.protocol or settings.resolve_protocol(profile.provider)
305
+ self._sync_context_limit()
306
+ self._g.api_key = profile.api_key
307
+ self._g.model = create_chat_model(profile.api_key, self._g.config.model)
308
+ settings.save_profile(profile)
309
+ ui.print(f"[cyan]{profile.name}[/cyan] ({profile.provider}/{profile.model}) [green]✓ switched[/green]")
310
+
311
+ await self._pick_or_act("Switch", target, _do_switch)
312
+
313
+ async def _model_reasoning(self, effort: str) -> None:
314
+ valid = ("off", "low", "medium", "high", "xhigh")
315
+
316
+ if effort and effort in valid:
317
+ new_effort = effort
318
+ elif not effort:
319
+ current = self._g.config.model.reasoning_effort or "xhigh"
320
+ choices = list(valid)
321
+ idx = await _select_from_list(self._g._app, "Select effort", choices)
322
+ if idx is None:
323
+ ui.print("[dim]Cancelled.[/dim]")
324
+ return
325
+ new_effort = choices[idx]
326
+ else:
327
+ ui.error(f"Invalid effort: '{effort}'. Use: {', '.join(valid)}")
328
+ return
329
+
330
+ self._g.config.model.reasoning_effort = new_effort
331
+ self._sync_context_limit()
332
+
333
+ if self._g.api_key:
334
+ from voidx.llm.provider import create_chat_model
335
+ self._g.model = create_chat_model(self._g.api_key, self._g.config.model)
336
+
337
+ ui.print(f"Reasoning effort: [cyan]{new_effort}[/cyan] [green]✓[/green]")
338
+
339
+ async def _switch_model(self, model_spec: str) -> None:
340
+ from voidx.llm.provider import create_chat_model
341
+ from voidx.memory.session import update_session_model
342
+
343
+ if not model_spec:
344
+ await self._list_models()
345
+ return
346
+
347
+ spec = model_spec.strip()
348
+
349
+ if " " in spec:
350
+ parts = spec.split(None, 1)
351
+ new_provider = parts[0].lower()
352
+ new_model = parts[1]
353
+ elif "/" in spec:
354
+ new_provider, new_model = spec.split("/", 1)
355
+ new_provider = new_provider.lower()
356
+ else:
357
+ new_provider = self._g.config.model.provider
358
+ new_model = spec
359
+
360
+ # Resolve API key for the target provider
361
+ if self._g._settings is None:
362
+ ui.error("No Settings reference available.")
363
+ return
364
+ new_key = self._g._settings.resolve_api_key(new_provider)
365
+ if not new_key:
366
+ ui.error(
367
+ f"No API key found for '{new_provider}'. Use /model new."
368
+ )
369
+ return
370
+
371
+ self._g.api_key = new_key
372
+
373
+ old = f"{self._g.config.model.provider}/{self._g.config.model.model}"
374
+ self._g.config.model.provider = new_provider
375
+ self._g.config.model.model = new_model
376
+ self._g.config.model.base_url = (
377
+ self._g._settings.resolve_base_url(new_provider) if self._g._settings else None
378
+ )
379
+ self._g.config.model.protocol = (
380
+ self._g._settings.resolve_protocol(new_provider) if self._g._settings else None
381
+ )
382
+
383
+ from voidx.config import Profile
384
+ existing = self._g._settings.resolve_profile(f"{new_provider}/{new_model}")
385
+ if existing:
386
+ self._g.config.model.base_url = existing.base_url or self._g.config.model.base_url
387
+ self._g.config.model.protocol = existing.protocol or self._g.config.model.protocol
388
+
389
+ self._sync_context_limit()
390
+
391
+ self._g.model = create_chat_model(self._g.api_key, self._g.config.model)
392
+
393
+ new_profile = Profile(
394
+ name=f"{new_provider}/{new_model}",
395
+ api_key=new_key,
396
+ base_url=self._g.config.model.base_url,
397
+ protocol=self._g.config.model.protocol,
398
+ )
399
+ self._g._settings.save_profile(new_profile)
400
+
401
+ if self._g._session:
402
+ await update_session_model(self._g._session.id, new_provider, new_model)
403
+
404
+ ui.print(f"[dim] {old}[/dim]")
405
+ ui.print(f" [cyan]→ {new_provider}/{new_model}[/cyan] [green]✓[/green]")
406
+
407
+ def _sync_context_limit(self) -> None:
408
+ from voidx.llm.provider import get_context_limit
409
+
410
+ limit = get_context_limit(self._g.config.model.provider)
411
+ stats = getattr(self._g, "_usage_stats", None)
412
+ if stats is not None:
413
+ stats.context_limit = limit
414
+ app = getattr(self._g, "_app", None)
415
+ if app is not None and hasattr(app, "status"):
416
+ app.status.context_limit = limit
417
+ app.status.provider = self._g.config.model.provider
418
+ app.status.model = self._g.config.model.model
419
+ app.status.reasoning_effort = self._g.config.model.reasoning_effort or "xhigh"
@@ -0,0 +1,55 @@
1
+ """Shared runtime helpers for slash commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from voidx.ui.console import VoidConsole
8
+
9
+ ui = VoidConsole()
10
+
11
+ _STATIC_PROVIDERS = [
12
+ "anthropic",
13
+ "openai",
14
+ "deepseek",
15
+ "openrouter",
16
+ "mimo",
17
+ "mimo-token-plan",
18
+ "qwen",
19
+ "zhipu",
20
+ "kimi",
21
+ "doubao",
22
+ ]
23
+
24
+
25
+ def get_providers(settings=None) -> list[str]:
26
+ """Return providers list, merging static + custom providers from settings."""
27
+ base = list(_STATIC_PROVIDERS)
28
+ if settings:
29
+ for profile in settings.list_profiles():
30
+ if profile.provider not in base:
31
+ base.append(profile.provider)
32
+ for cp in settings.list_custom_providers():
33
+ name = cp["name"]
34
+ if name not in base:
35
+ base.append(name)
36
+ return base
37
+
38
+
39
+ # Backward-compatible alias (static list only).
40
+ PROVIDERS = list(_STATIC_PROVIDERS)
41
+
42
+
43
+ def _w(text: str) -> None:
44
+ sys.stdout.write(text)
45
+ sys.stdout.flush()
46
+
47
+
48
+ async def _select_from_list(app, prompt: str, items: list[str]) -> int | None:
49
+ if not app or not items:
50
+ return None
51
+ choices = [(item, str(i), "") for i, item in enumerate(items)]
52
+ res = await app.ask_choice(prompt, choices)
53
+ if res is not None:
54
+ return int(res)
55
+ return None
@@ -0,0 +1,94 @@
1
+ """Slash command support for /skills operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from voidx.agent.slash_components.runtime import ui
6
+ from voidx.skills.registry import SkillRegistry
7
+ from voidx.skills.service import SkillService
8
+
9
+
10
+ class SlashSkillsMixin:
11
+ async def _skills(self, args: str) -> None:
12
+ parts = args.split(None, 1)
13
+ action = parts[0] if parts else ""
14
+ target = parts[1].strip() if len(parts) > 1 else ""
15
+
16
+ if action in ("", "list"):
17
+ self._skills_list()
18
+ elif action == "show":
19
+ self._skills_show(target)
20
+ elif action == "enable":
21
+ self._skills_set_enabled(target, True)
22
+ elif action == "disable":
23
+ self._skills_set_enabled(target, False)
24
+ elif action == "paths":
25
+ self._skills_paths()
26
+ else:
27
+ ui.error("Usage: /skills [list|show|enable|disable|paths]")
28
+
29
+ def _skill_service(self) -> SkillService:
30
+ selection = (
31
+ self._g._settings.get_skill_selection()
32
+ if getattr(self._g, "_settings", None) is not None
33
+ else None
34
+ )
35
+ return SkillService(
36
+ SkillRegistry(getattr(self._g, "_workspace", ".")),
37
+ selection=selection,
38
+ )
39
+
40
+ def _skills_list(self) -> None:
41
+ service = self._skill_service()
42
+ skills = service.list_skills()
43
+ ui.print("[bold]Skills:[/bold]")
44
+ if not skills:
45
+ ui.print("[dim]No skills found. Add SKILL.md files under ~/.voidx/skills or .voidx/skills.[/dim]")
46
+ return
47
+ for skill in skills:
48
+ state = "[green]enabled[/green]" if service.is_enabled(skill) else "[dim]disabled[/dim]"
49
+ scope = skill.meta.scope
50
+ desc = f" — {skill.meta.description}" if skill.meta.description else ""
51
+ ui.print(f" [cyan]{skill.name}[/cyan] · {state} · [dim]{scope}[/dim]{desc}")
52
+ ui.print("[dim]Usage: /skills show|enable|disable|paths[/dim]")
53
+
54
+ def _skills_show(self, name: str) -> None:
55
+ if not name:
56
+ ui.error("Usage: /skills show <name>")
57
+ return
58
+ service = self._skill_service()
59
+ skill = service.get(name)
60
+ if skill is None:
61
+ ui.error(f"Skill not found: {name}")
62
+ return
63
+ state = "enabled" if service.is_enabled(skill) else "disabled"
64
+ ui.print(f"[bold]{skill.name}[/bold] [{state}]")
65
+ ui.print(f"[dim]{skill.path}[/dim]")
66
+ if skill.meta.description:
67
+ ui.print(skill.meta.description)
68
+ if skill.meta.triggers:
69
+ ui.print(f"[dim]Triggers: {', '.join(skill.meta.triggers)}[/dim]")
70
+ ui.print()
71
+ ui.print(skill.body or "[dim](empty skill body)[/dim]")
72
+
73
+ def _skills_set_enabled(self, name: str, enabled: bool) -> None:
74
+ if not name:
75
+ command = "enable" if enabled else "disable"
76
+ ui.error(f"Usage: /skills {command} <name>")
77
+ return
78
+ if getattr(self._g, "_settings", None) is None:
79
+ ui.error("No settings file available.")
80
+ return
81
+ service = self._skill_service()
82
+ if service.get(name) is None:
83
+ ui.error(f"Skill not found: {name}")
84
+ return
85
+ path = self._g._settings.set_skill_enabled(name, enabled)
86
+ state = "enabled" if enabled else "disabled"
87
+ ui.print(f"[dim]{name} {state}. Saved to {path}[/dim]")
88
+
89
+ def _skills_paths(self) -> None:
90
+ registry = SkillRegistry(getattr(self._g, "_workspace", "."))
91
+ ui.print("[bold]Skill paths:[/bold]")
92
+ ui.print(f" bundled [dim]{registry.bundled_dir}[/dim]")
93
+ ui.print(f" global [dim]{registry.global_dir}[/dim]")
94
+ ui.print(f" project [dim]{registry.project_dir}[/dim]")
voidx/agent/state.py ADDED
@@ -0,0 +1,32 @@
1
+ """Agent state — typed, explicit, every field has a known type."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Any
6
+
7
+ from langgraph.graph.message import add_messages
8
+ from langchain_core.messages import BaseMessage
9
+ from typing_extensions import NotRequired, TypedDict
10
+
11
+
12
+ class AgentState(TypedDict):
13
+ """Complete agent state. LangGraph manages this across the graph."""
14
+ messages: Annotated[list[BaseMessage], add_messages] # LangGraph auto-merge
15
+ workspace: str # absolute path to working directory
16
+ agent: str # current agent name (orchestrator/explore/plan/implement/review)
17
+ plan_mode: bool # when True, write/edit are denied — plan→implement→review enforced
18
+ interaction_mode: NotRequired[str] # auto/plan/goal
19
+ task_intent: NotRequired[str] # chat/inspect/design/review/implement/debug/ambiguous
20
+ implementation_allowed: NotRequired[bool] # intent hint for context, not a permission gate
21
+ intent_resolution_reason: NotRequired[str]
22
+ awaiting_implementation_approval: NotRequired[bool]
23
+ approved_scope: NotRequired[str]
24
+ goal: NotRequired[str]
25
+ goal_phase: NotRequired[str]
26
+ goal_status: NotRequired[str]
27
+ goal_turn_count: NotRequired[int]
28
+ user_message_id: NotRequired[int]
29
+ tool_results: dict[str, str] # tool_call_id → result text
30
+ step_count: int # current step number
31
+ max_steps: int # safety limit
32
+ should_continue: bool # router flag