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/agent/slash.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Slash command handler — extracted from graph.py to keep it focused."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from voidx.agent.slash_components.code_ide import SlashCodeIdeMixin
|
|
8
|
+
from voidx.agent.slash_components.lsp import SlashLspMixin
|
|
9
|
+
from voidx.agent.slash_components.mcp import SlashMcpMixin
|
|
10
|
+
from voidx.agent.slash_components.model import SlashModelMixin
|
|
11
|
+
from voidx.agent.slash_components.skills import SlashSkillsMixin
|
|
12
|
+
from voidx.agent.slash_components.runtime import PROVIDERS, _select_from_list, _w, ui
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from voidx.agent.graph import VoidXGraph
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SlashHandler(SlashCodeIdeMixin, SlashLspMixin, SlashSkillsMixin, SlashMcpMixin, SlashModelMixin):
|
|
19
|
+
"""Handles all slash commands (/help, /model, /plan, etc.).
|
|
20
|
+
|
|
21
|
+
Takes a reference to the parent VoidXGraph since commands need access
|
|
22
|
+
to session, config, permission, and model state.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, graph: VoidXGraph) -> None:
|
|
26
|
+
self._g = graph
|
|
27
|
+
|
|
28
|
+
async def dispatch(self, inp: str) -> bool:
|
|
29
|
+
from voidx.ui.commands import COMMANDS
|
|
30
|
+
|
|
31
|
+
parts = inp.split(None, 1)
|
|
32
|
+
cmd = parts[0]
|
|
33
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
34
|
+
|
|
35
|
+
known = [n for n, _ in COMMANDS if n == cmd]
|
|
36
|
+
if not known:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
ui.print()
|
|
40
|
+
|
|
41
|
+
if cmd in ("/exit", "/quit"):
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
if cmd == "/clear":
|
|
45
|
+
await self._clear()
|
|
46
|
+
elif cmd == "/code-ide":
|
|
47
|
+
await self._code_ide(args)
|
|
48
|
+
elif cmd == "/list":
|
|
49
|
+
await self._list_sessions()
|
|
50
|
+
elif cmd.startswith("/resume"):
|
|
51
|
+
await self._resume(inp)
|
|
52
|
+
elif cmd.startswith("/title"):
|
|
53
|
+
await self._set_title(inp)
|
|
54
|
+
elif cmd == "/mode":
|
|
55
|
+
await self._mode(args)
|
|
56
|
+
elif cmd == "/goal":
|
|
57
|
+
await self._goal(args)
|
|
58
|
+
elif cmd == "/plan":
|
|
59
|
+
self._set_interaction_mode("plan")
|
|
60
|
+
if hasattr(self._g, "_persist_runtime_state"):
|
|
61
|
+
await self._g._persist_runtime_state()
|
|
62
|
+
elif cmd == "/unplan":
|
|
63
|
+
self._set_interaction_mode("auto")
|
|
64
|
+
if hasattr(self._g, "_persist_runtime_state"):
|
|
65
|
+
await self._g._persist_runtime_state()
|
|
66
|
+
elif cmd.startswith("/allow"):
|
|
67
|
+
tool = args or cmd.removeprefix("/allow").strip()
|
|
68
|
+
if tool:
|
|
69
|
+
self._g._permission.allow(tool)
|
|
70
|
+
elif cmd.startswith("/deny"):
|
|
71
|
+
tool = args or cmd.removeprefix("/deny").strip()
|
|
72
|
+
if tool:
|
|
73
|
+
self._g._permission.deny(tool)
|
|
74
|
+
elif cmd == "/permissions":
|
|
75
|
+
ui.print(self._g._permission.show_rules())
|
|
76
|
+
elif cmd == "/permission-mode":
|
|
77
|
+
await self._permission_mode(args)
|
|
78
|
+
elif cmd == "/sandbox":
|
|
79
|
+
self._sandbox(args)
|
|
80
|
+
elif cmd == "/approval":
|
|
81
|
+
self._approval(args)
|
|
82
|
+
elif cmd == "/usage":
|
|
83
|
+
self._usage()
|
|
84
|
+
elif cmd == "/mcp":
|
|
85
|
+
await self._mcp(args)
|
|
86
|
+
elif cmd == "/lsp":
|
|
87
|
+
await self._lsp(args)
|
|
88
|
+
elif cmd == "/skills":
|
|
89
|
+
await self._skills(args)
|
|
90
|
+
elif cmd == "/paste":
|
|
91
|
+
self._paste_clipboard_image()
|
|
92
|
+
elif cmd.startswith("/debug"):
|
|
93
|
+
self._debug(args)
|
|
94
|
+
elif cmd == "/compact":
|
|
95
|
+
compacted = await self._g._compact_session_history(force=True)
|
|
96
|
+
if compacted:
|
|
97
|
+
ui.print("[dim]Compacted context.[/dim]")
|
|
98
|
+
else:
|
|
99
|
+
ui.print("[dim]Nothing to compact.[/dim]")
|
|
100
|
+
elif cmd == "/diff":
|
|
101
|
+
await self._show_diff()
|
|
102
|
+
elif cmd == "/tavily":
|
|
103
|
+
await self._tavily(args)
|
|
104
|
+
elif cmd == "/model":
|
|
105
|
+
if args == "new":
|
|
106
|
+
await self._model_new()
|
|
107
|
+
elif args == "list":
|
|
108
|
+
await self._model_list()
|
|
109
|
+
elif args == "test" or args.startswith("test "):
|
|
110
|
+
target = args.removeprefix("test").strip()
|
|
111
|
+
await self._model_test(target)
|
|
112
|
+
elif args == "del" or args.startswith("del "):
|
|
113
|
+
target = args.removeprefix("del").strip()
|
|
114
|
+
await self._model_del(target)
|
|
115
|
+
elif args == "switch" or args.startswith("switch "):
|
|
116
|
+
target = args.removeprefix("switch").strip()
|
|
117
|
+
await self._model_switch(target)
|
|
118
|
+
elif args == "reasoning" or args.startswith("reasoning "):
|
|
119
|
+
target = args.removeprefix("reasoning").strip()
|
|
120
|
+
await self._model_reasoning(target)
|
|
121
|
+
elif args:
|
|
122
|
+
await self._switch_model(args)
|
|
123
|
+
else:
|
|
124
|
+
await self._model_switch("")
|
|
125
|
+
elif cmd == "/help":
|
|
126
|
+
ui.print("[bold]Commands:[/bold]")
|
|
127
|
+
for name, desc in COMMANDS:
|
|
128
|
+
ui.print(f" [cyan]{name}[/cyan] — {desc}")
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
def _set_interaction_mode(self, mode: str) -> None:
|
|
132
|
+
from voidx.agent.runtime_context import InteractionMode
|
|
133
|
+
|
|
134
|
+
parsed = InteractionMode.parse(mode)
|
|
135
|
+
setter = getattr(self._g, "set_interaction_mode", None)
|
|
136
|
+
if callable(setter):
|
|
137
|
+
setter(parsed)
|
|
138
|
+
else:
|
|
139
|
+
self._g._plan_mode = parsed == InteractionMode.PLAN
|
|
140
|
+
self._g._interaction_mode = parsed
|
|
141
|
+
labels = {
|
|
142
|
+
InteractionMode.AUTO: "Auto",
|
|
143
|
+
InteractionMode.PLAN: "Plan",
|
|
144
|
+
InteractionMode.GOAL: "Goal",
|
|
145
|
+
}
|
|
146
|
+
notes = {
|
|
147
|
+
InteractionMode.PLAN: "write/edit/bash/lsp_format blocked",
|
|
148
|
+
InteractionMode.GOAL: "keep work scoped to the current goal",
|
|
149
|
+
}
|
|
150
|
+
suffix = f" — {notes[parsed]}" if parsed in notes else ""
|
|
151
|
+
ui.print(f"[dim]Mode set to [cyan]{labels[parsed]}[/cyan]{suffix}[/dim]")
|
|
152
|
+
|
|
153
|
+
async def _mode(self, arg: str) -> None:
|
|
154
|
+
from voidx.agent.runtime_context import InteractionMode
|
|
155
|
+
|
|
156
|
+
mode = arg.strip().lower()
|
|
157
|
+
choices = [
|
|
158
|
+
("Auto", InteractionMode.AUTO.value, "Infer the task intent from each turn."),
|
|
159
|
+
("Plan", InteractionMode.PLAN.value, "Read-only exploration and implementation planning."),
|
|
160
|
+
("Goal", InteractionMode.GOAL.value, "Keep multi-step work scoped to the current goal."),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
if not mode and getattr(self._g, "_app", None):
|
|
164
|
+
mode = await self._g._app.ask_choice("Interaction mode", choices) or ""
|
|
165
|
+
|
|
166
|
+
if not mode:
|
|
167
|
+
current = getattr(getattr(self._g, "_interaction_mode", None), "value", None)
|
|
168
|
+
if current is None:
|
|
169
|
+
current = "plan" if getattr(self._g, "_plan_mode", False) else "auto"
|
|
170
|
+
ui.print(f"Mode: [cyan]{current}[/cyan]")
|
|
171
|
+
ui.print("Usage: /mode [auto|plan|goal]")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
parsed = InteractionMode.parse(mode)
|
|
176
|
+
except ValueError:
|
|
177
|
+
ui.error(f"Invalid mode: {mode}. Use: auto, plan, goal")
|
|
178
|
+
return
|
|
179
|
+
self._set_interaction_mode(parsed.value)
|
|
180
|
+
if hasattr(self._g, "_persist_runtime_state"):
|
|
181
|
+
await self._g._persist_runtime_state()
|
|
182
|
+
|
|
183
|
+
async def _goal(self, arg: str) -> None:
|
|
184
|
+
from voidx.agent.runtime_context import InteractionMode
|
|
185
|
+
from voidx.agent.task_state import TaskRun
|
|
186
|
+
|
|
187
|
+
task_run = getattr(self._g, "_task_run", None)
|
|
188
|
+
if task_run is None:
|
|
189
|
+
task_run = TaskRun()
|
|
190
|
+
self._g._task_run = task_run
|
|
191
|
+
|
|
192
|
+
goal = arg.strip()
|
|
193
|
+
if goal.lower() in {"clear", "reset"}:
|
|
194
|
+
task_run.clear()
|
|
195
|
+
task_state = getattr(self._g, "_task_state", None)
|
|
196
|
+
if task_state is not None:
|
|
197
|
+
task_state.awaiting_implementation_approval = False
|
|
198
|
+
task_state.approved_scope = ""
|
|
199
|
+
self._set_interaction_mode(InteractionMode.AUTO.value)
|
|
200
|
+
if hasattr(self._g, "_persist_runtime_state"):
|
|
201
|
+
await self._g._persist_runtime_state()
|
|
202
|
+
ui.print("[dim]Goal cleared.[/dim]")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if not goal:
|
|
206
|
+
if task_run.goal:
|
|
207
|
+
ui.print(
|
|
208
|
+
f"Goal: [cyan]{task_run.goal}[/cyan] "
|
|
209
|
+
f"[dim]({task_run.phase.value}, {task_run.status.value}, turns {task_run.turn_count})[/dim]"
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
ui.print("Usage: /goal <goal>|clear")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
task_run.set_goal(goal)
|
|
216
|
+
self._set_interaction_mode(InteractionMode.GOAL.value)
|
|
217
|
+
if hasattr(self._g, "_persist_runtime_state"):
|
|
218
|
+
await self._g._persist_runtime_state()
|
|
219
|
+
ui.print(f"[dim]Goal set to [cyan]{task_run.goal}[/cyan][/dim]")
|
|
220
|
+
|
|
221
|
+
def _usage(self) -> None:
|
|
222
|
+
from voidx.llm.usage import format_cache_hit_rate, format_token_count
|
|
223
|
+
|
|
224
|
+
stats = getattr(self._g, "_usage_stats", None)
|
|
225
|
+
if stats is None:
|
|
226
|
+
ui.print("[dim]No usage data available.[/dim]")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
ui.print("[bold]Token Usage[/bold]")
|
|
230
|
+
ui.print(
|
|
231
|
+
f" Context: [cyan]{format_token_count(stats.context_tokens)}[/cyan]"
|
|
232
|
+
f" / {format_token_count(stats.context_limit)}"
|
|
233
|
+
)
|
|
234
|
+
ui.print(
|
|
235
|
+
f" Last call: in [cyan]{format_token_count(stats.last_input_tokens)}[/cyan]"
|
|
236
|
+
f" · out [cyan]{format_token_count(stats.last_output_tokens)}[/cyan]"
|
|
237
|
+
" · cache read "
|
|
238
|
+
f"[cyan]{format_token_count(stats.last_cache_read_tokens or stats.last_estimated_cache_read_tokens)}[/cyan]"
|
|
239
|
+
f" · write [cyan]{format_token_count(stats.last_cache_write_tokens)}[/cyan]"
|
|
240
|
+
)
|
|
241
|
+
ui.print(
|
|
242
|
+
f" Session: in [cyan]{format_token_count(stats.total_input_tokens)}[/cyan]"
|
|
243
|
+
f" · out [cyan]{format_token_count(stats.total_output_tokens)}[/cyan]"
|
|
244
|
+
f" · total [cyan]{format_token_count(stats.total_tokens)}[/cyan]"
|
|
245
|
+
f" · cache {format_cache_hit_rate(stats)}"
|
|
246
|
+
f" · calls {stats.total_calls}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _sandbox(self, arg: str) -> None:
|
|
250
|
+
mode = arg.strip().lower()
|
|
251
|
+
valid = {"read-only", "workspace-write", "danger-full-access"}
|
|
252
|
+
if not mode:
|
|
253
|
+
ui.print(f"Sandbox mode: [cyan]{self._g._permission.sandbox_mode}[/cyan]")
|
|
254
|
+
ui.print("Usage: /sandbox [read-only|workspace-write|danger-full-access]")
|
|
255
|
+
return
|
|
256
|
+
if mode not in valid:
|
|
257
|
+
ui.error(f"Invalid sandbox mode: {mode}. Use: {', '.join(valid)}")
|
|
258
|
+
return
|
|
259
|
+
self._g._permission.sandbox_mode = mode
|
|
260
|
+
self._g._permission.mark_custom_mode()
|
|
261
|
+
if getattr(self._g, "_settings", None):
|
|
262
|
+
from voidx.config import SandboxMode
|
|
263
|
+
self._g._settings.set_sandbox_mode(SandboxMode(mode))
|
|
264
|
+
ui.print(f"[dim]Sandbox mode set to [cyan]{mode}[/cyan][/dim]")
|
|
265
|
+
|
|
266
|
+
def _approval(self, arg: str) -> None:
|
|
267
|
+
policy = arg.strip().lower()
|
|
268
|
+
valid = {"untrusted", "on-failure", "on-request", "never"}
|
|
269
|
+
if not policy:
|
|
270
|
+
ui.print(f"Approval policy: [cyan]{self._g._permission.approval_policy}[/cyan]")
|
|
271
|
+
ui.print("Usage: /approval [untrusted|on-failure|on-request|never]")
|
|
272
|
+
return
|
|
273
|
+
if policy not in valid:
|
|
274
|
+
ui.error(f"Invalid approval policy: {policy}. Use: {', '.join(valid)}")
|
|
275
|
+
return
|
|
276
|
+
self._g._permission.approval_policy = policy
|
|
277
|
+
self._g._permission.mark_custom_mode()
|
|
278
|
+
if getattr(self._g, "_settings", None):
|
|
279
|
+
from voidx.config import ApprovalPolicy
|
|
280
|
+
self._g._settings.set_approval_policy(ApprovalPolicy(policy))
|
|
281
|
+
ui.print(f"[dim]Approval policy set to [cyan]{policy}[/cyan][/dim]")
|
|
282
|
+
|
|
283
|
+
async def _permission_mode(self, arg: str) -> None:
|
|
284
|
+
from voidx.config import PermissionMode
|
|
285
|
+
|
|
286
|
+
mode = arg.strip().lower()
|
|
287
|
+
labels = {
|
|
288
|
+
PermissionMode.DEFAULT.value: "Default",
|
|
289
|
+
PermissionMode.READ_ONLY.value: "Read only",
|
|
290
|
+
PermissionMode.ACCEPT_EDITS.value: "Accept edits",
|
|
291
|
+
PermissionMode.AUTO_REVIEW.value: "Auto review",
|
|
292
|
+
PermissionMode.FULL_ACCESS.value: "Full access",
|
|
293
|
+
PermissionMode.CUSTOM.value: "Custom (voidx.json)",
|
|
294
|
+
}
|
|
295
|
+
valid = set(labels)
|
|
296
|
+
|
|
297
|
+
if not mode and getattr(self._g, "_app", None):
|
|
298
|
+
choices = [
|
|
299
|
+
(labels[PermissionMode.DEFAULT.value], PermissionMode.DEFAULT.value, "Ask before write/edit/bash."),
|
|
300
|
+
(labels[PermissionMode.READ_ONLY.value], PermissionMode.READ_ONLY.value, "Block all writes and implement delegation."),
|
|
301
|
+
(labels[PermissionMode.ACCEPT_EDITS.value], PermissionMode.ACCEPT_EDITS.value, "Allow workspace file edits; still ask for bash."),
|
|
302
|
+
(labels[PermissionMode.AUTO_REVIEW.value], PermissionMode.AUTO_REVIEW.value, "Use reviewer-assisted approvals where possible."),
|
|
303
|
+
(labels[PermissionMode.FULL_ACCESS.value], PermissionMode.FULL_ACCESS.value, "No sandbox or approval prompts."),
|
|
304
|
+
(labels[PermissionMode.CUSTOM.value], PermissionMode.CUSTOM.value, "Use explicit sandbox/approval config."),
|
|
305
|
+
]
|
|
306
|
+
mode = await self._g._app.ask_choice("Permission mode", choices) or ""
|
|
307
|
+
|
|
308
|
+
if not mode:
|
|
309
|
+
current = self._g._permission.permission_mode
|
|
310
|
+
ui.print(f"Permission mode: [cyan]{labels.get(current, 'Custom')}[/cyan]")
|
|
311
|
+
ui.print("Usage: /permission-mode [default|read-only|accept-edits|auto-review|full-access|custom]")
|
|
312
|
+
return
|
|
313
|
+
if mode not in valid:
|
|
314
|
+
ui.error(f"Invalid permission mode: {mode}. Use: {', '.join(sorted(valid))}")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
self._g._permission.set_permission_mode(mode)
|
|
318
|
+
if getattr(self._g, "_settings", None):
|
|
319
|
+
self._g._settings.set_permission_mode(PermissionMode(mode))
|
|
320
|
+
ui.print(f"[dim]Permission mode set to [cyan]{labels[mode]}[/cyan][/dim]")
|
|
321
|
+
|
|
322
|
+
def _debug(self, arg: str) -> None:
|
|
323
|
+
value = arg.strip().lower()
|
|
324
|
+
if value in ("on", "true", "1", "yes"):
|
|
325
|
+
self._g.set_debug(True)
|
|
326
|
+
elif value in ("off", "false", "0", "no"):
|
|
327
|
+
self._g.set_debug(False)
|
|
328
|
+
elif value:
|
|
329
|
+
ui.error("Usage: /debug [on|off]")
|
|
330
|
+
return
|
|
331
|
+
else:
|
|
332
|
+
self._g.set_debug(not self._g._debug)
|
|
333
|
+
|
|
334
|
+
state = "on" if self._g._debug else "off"
|
|
335
|
+
ui.print(f"[dim]debug {state}[/dim]")
|
|
336
|
+
|
|
337
|
+
def _paste_clipboard_image(self) -> None:
|
|
338
|
+
app = getattr(self._g, "_app", None)
|
|
339
|
+
if app is None or not hasattr(app, "paste_clipboard_image"):
|
|
340
|
+
ui.error("/paste requires the interactive UI.")
|
|
341
|
+
return
|
|
342
|
+
result = app.paste_clipboard_image()
|
|
343
|
+
if result.ok:
|
|
344
|
+
ui.print(f"[dim]{result.message}[/dim]")
|
|
345
|
+
return
|
|
346
|
+
ui.error(result.message)
|
|
347
|
+
|
|
348
|
+
async def _show_diff(self) -> None:
|
|
349
|
+
from voidx.ui.diff import git_diff, git_diff_stat
|
|
350
|
+
stat = git_diff_stat(self._g._workspace)
|
|
351
|
+
if stat:
|
|
352
|
+
ui.print(f"[bold]Changes:[/bold]\n{stat}\n")
|
|
353
|
+
diff_text = git_diff(self._g._workspace)
|
|
354
|
+
if diff_text:
|
|
355
|
+
ui.diff(diff_text)
|
|
356
|
+
else:
|
|
357
|
+
ui.print("[dim]No diff content.[/dim]")
|
|
358
|
+
else:
|
|
359
|
+
ui.print("[dim]No changes in working tree.[/dim]")
|
|
360
|
+
|
|
361
|
+
async def _clear(self) -> None:
|
|
362
|
+
if self._g._session:
|
|
363
|
+
from voidx.memory.session import clear_messages, update_title
|
|
364
|
+
await clear_messages(self._g._session.id)
|
|
365
|
+
await update_title(self._g._session.id, "New session")
|
|
366
|
+
if hasattr(self._g, "_clear_runtime_state"):
|
|
367
|
+
await self._g._clear_runtime_state()
|
|
368
|
+
self._g._session = self._g._session.model_copy(update={
|
|
369
|
+
"title": "New session",
|
|
370
|
+
"message_count": 0,
|
|
371
|
+
})
|
|
372
|
+
self._g._tracker._todos = []
|
|
373
|
+
self._g._permission.clear_session_permissions()
|
|
374
|
+
stats = getattr(self._g, "_usage_stats", None)
|
|
375
|
+
if stats is not None:
|
|
376
|
+
stats.reset()
|
|
377
|
+
from voidx.ui.session_changes import session_tracker
|
|
378
|
+
session_tracker.clear()
|
|
379
|
+
from voidx.ui.dock import get_dock
|
|
380
|
+
active_dock = get_dock()
|
|
381
|
+
if active_dock is not None:
|
|
382
|
+
active_dock.reset()
|
|
383
|
+
await self._g._show_startup()
|
|
384
|
+
ui.print("[dim]✓ Session cleared — ready for a new conversation[/dim]")
|
|
385
|
+
|
|
386
|
+
async def _list_sessions(self) -> None:
|
|
387
|
+
from voidx.memory.session import list_sessions
|
|
388
|
+
sessions = await list_sessions()
|
|
389
|
+
if not sessions:
|
|
390
|
+
ui.print("No saved sessions.")
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
ui.print("[bold]Sessions:[/bold]")
|
|
394
|
+
items = []
|
|
395
|
+
for s in sessions:
|
|
396
|
+
title = s.title[:50] + ("..." if len(s.title) > 50 else "")
|
|
397
|
+
items.append(f"{s.id[:8]} | {title} | {getattr(s, 'updated_at', '')[:16]}")
|
|
398
|
+
|
|
399
|
+
idx = None
|
|
400
|
+
if getattr(self._g, "_app", None):
|
|
401
|
+
idx = await _select_from_list(self._g._app, "Resume session?", items)
|
|
402
|
+
|
|
403
|
+
if idx is not None:
|
|
404
|
+
await self._resume(f"/resume {sessions[idx].id}")
|
|
405
|
+
|
|
406
|
+
async def _resume(self, cmd: str) -> None:
|
|
407
|
+
from voidx.memory.session import get_session
|
|
408
|
+
sid = cmd.removeprefix("/resume").strip()
|
|
409
|
+
if not sid:
|
|
410
|
+
ui.error("Usage: /resume <session_id>")
|
|
411
|
+
return
|
|
412
|
+
session = await get_session(sid)
|
|
413
|
+
if not session:
|
|
414
|
+
ui.error(f"Session not found: {sid}")
|
|
415
|
+
return
|
|
416
|
+
self._g._session = session
|
|
417
|
+
self._g._workspace = session.workspace
|
|
418
|
+
self._g.config.workspace = session.workspace
|
|
419
|
+
if hasattr(self._g, "_restore_runtime_state"):
|
|
420
|
+
await self._g._restore_runtime_state()
|
|
421
|
+
from voidx.ui.dock import get_dock
|
|
422
|
+
active_dock = get_dock()
|
|
423
|
+
if active_dock is not None:
|
|
424
|
+
active_dock.reset()
|
|
425
|
+
await self._g._show_startup(append_transcript=True)
|
|
426
|
+
ui.print(f"[dim]Resumed: {session.id} — {session.title} ({session.message_count} msgs)[/dim]")
|
|
427
|
+
|
|
428
|
+
async def _set_title(self, cmd: str) -> None:
|
|
429
|
+
from voidx.memory.session import update_title
|
|
430
|
+
if not self._g._session:
|
|
431
|
+
return
|
|
432
|
+
title = cmd.removeprefix("/title").strip()
|
|
433
|
+
if title:
|
|
434
|
+
await update_title(self._g._session.id, title)
|
|
435
|
+
ui.print(f"[dim]Title set: {title}[/dim]")
|
|
436
|
+
|
|
437
|
+
async def _tavily(self, args: str) -> None:
|
|
438
|
+
"""Configure Tavily API key for web search."""
|
|
439
|
+
settings = self._g._settings
|
|
440
|
+
if not settings:
|
|
441
|
+
ui.error("No settings available.")
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
if not args or args.strip() == "show":
|
|
445
|
+
key = settings.get_tavily_api_key()
|
|
446
|
+
if key:
|
|
447
|
+
masked = key[:4] + "****" + key[-4:] if len(key) > 8 else key
|
|
448
|
+
ui.print(f"Tavily API key: [cyan]{masked}[/cyan]")
|
|
449
|
+
else:
|
|
450
|
+
ui.print("[dim]Tavily API key not configured. Using DuckDuckGo fallback.[/dim]")
|
|
451
|
+
ui.print("[dim]Usage: /tavily set <api_key> | /tavily delete[/dim]")
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
if args.startswith("set "):
|
|
455
|
+
api_key = args[4:].strip()
|
|
456
|
+
if api_key:
|
|
457
|
+
settings.set_tavily_api_key(api_key)
|
|
458
|
+
masked = api_key[:4] + "****" + api_key[-4:] if len(api_key) > 8 else api_key
|
|
459
|
+
ui.print(f"Tavily API key saved: [cyan]{masked}[/cyan]")
|
|
460
|
+
else:
|
|
461
|
+
ui.error("Usage: /tavily set <api_key>")
|
|
462
|
+
elif args.strip() == "delete":
|
|
463
|
+
settings.delete_tavily_api_key()
|
|
464
|
+
ui.print("[dim]Tavily API key deleted. Using DuckDuckGo fallback.[/dim]")
|
|
465
|
+
else:
|
|
466
|
+
ui.print("[dim]Usage: /tavily [set <api_key>|delete|show][/dim]")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Implementation parts for slash commands."""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Slash command support for /code-ide."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from voidx.agent.slash_components.runtime import ui
|
|
6
|
+
from voidx.config import CodeIde
|
|
7
|
+
from voidx.ui.code_ide import code_ide_status, detect_code_ides, normalize_ide
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SlashCodeIdeMixin:
|
|
11
|
+
async def _code_ide(self, args: str) -> None:
|
|
12
|
+
settings = getattr(self._g, "_settings", None)
|
|
13
|
+
if settings is None:
|
|
14
|
+
ui.error("No settings file available.")
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
value = args.strip().lower()
|
|
18
|
+
if value == "status":
|
|
19
|
+
ui.print(code_ide_status(settings))
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
valid = {item.value for item in CodeIde}
|
|
23
|
+
if not value:
|
|
24
|
+
app = getattr(self._g, "_app", None)
|
|
25
|
+
if app is not None:
|
|
26
|
+
detected = detect_code_ides()
|
|
27
|
+
detected_ids = {item.id for item in detected}
|
|
28
|
+
choices = []
|
|
29
|
+
for ide in CodeIde:
|
|
30
|
+
label = _ide_label(ide.value)
|
|
31
|
+
desc = "configured default" if ide.value == settings.get_code_ide().value else ""
|
|
32
|
+
if ide.value in detected_ids:
|
|
33
|
+
desc = (desc + " · " if desc else "") + "detected"
|
|
34
|
+
elif ide.value not in {CodeIde.AUTO.value, CodeIde.SYSTEM.value}:
|
|
35
|
+
desc = (desc + " · " if desc else "") + "not detected"
|
|
36
|
+
choices.append((label, ide.value, desc))
|
|
37
|
+
selected = await app.ask_choice("Code IDE", choices)
|
|
38
|
+
if selected:
|
|
39
|
+
value = selected
|
|
40
|
+
if not value:
|
|
41
|
+
ui.print(code_ide_status(settings))
|
|
42
|
+
ui.print("Usage: /code-ide [auto|trae|cursor|code|windsurf|zed|sublime|jetbrains|ghostty|system|status]")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
value = normalize_ide(value)
|
|
46
|
+
if value not in valid:
|
|
47
|
+
ui.error(f"Invalid code IDE: {value}. Use: {', '.join(sorted(valid))}")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
path = settings.set_code_ide(CodeIde(value))
|
|
51
|
+
ui.print(f"[dim]Code IDE set to [cyan]{value}[/cyan]. Saved to {path}[/dim]")
|
|
52
|
+
ui.print(code_ide_status(settings))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _ide_label(value: str) -> str:
|
|
56
|
+
labels = {
|
|
57
|
+
CodeIde.AUTO.value: "Auto",
|
|
58
|
+
CodeIde.TRAE.value: "Trae",
|
|
59
|
+
CodeIde.CURSOR.value: "Cursor",
|
|
60
|
+
CodeIde.CODE.value: "VS Code",
|
|
61
|
+
CodeIde.WINDSURF.value: "Windsurf",
|
|
62
|
+
CodeIde.ZED.value: "Zed",
|
|
63
|
+
CodeIde.SUBLIME.value: "Sublime Text",
|
|
64
|
+
CodeIde.JETBRAINS.value: "JetBrains",
|
|
65
|
+
CodeIde.GHOSTTY.value: "Ghostty",
|
|
66
|
+
CodeIde.SYSTEM.value: "System default",
|
|
67
|
+
}
|
|
68
|
+
return labels.get(value, value)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Slash command support for /lsp operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from voidx.agent.slash_components.runtime import ui
|
|
6
|
+
from voidx.lsp.config import lsp_config_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SlashLspMixin:
|
|
10
|
+
async def _lsp(self, args: str) -> None:
|
|
11
|
+
parts = args.split(None, 1)
|
|
12
|
+
action = parts[0] if parts else "status"
|
|
13
|
+
target = parts[1].strip() if len(parts) > 1 else ""
|
|
14
|
+
|
|
15
|
+
if action in ("", "status"):
|
|
16
|
+
self._lsp_status()
|
|
17
|
+
elif action == "doctor":
|
|
18
|
+
self._lsp_doctor()
|
|
19
|
+
elif action == "restart":
|
|
20
|
+
await self._lsp_restart(target or None)
|
|
21
|
+
elif action == "servers":
|
|
22
|
+
self._lsp_servers()
|
|
23
|
+
else:
|
|
24
|
+
ui.error("Usage: /lsp [status|doctor|restart|servers]")
|
|
25
|
+
|
|
26
|
+
def _lsp_status(self) -> None:
|
|
27
|
+
manager = getattr(self._g, "_lsp_manager", None)
|
|
28
|
+
if manager is None:
|
|
29
|
+
ui.error("No LSP manager available.")
|
|
30
|
+
return
|
|
31
|
+
ui.print("[bold]LSP status:[/bold]")
|
|
32
|
+
for status in manager.statuses():
|
|
33
|
+
label = {
|
|
34
|
+
"connected": "[green]connected[/green]",
|
|
35
|
+
"disconnected": "[dim]disconnected[/dim]",
|
|
36
|
+
"disabled": "[dim]disabled[/dim]",
|
|
37
|
+
"error": "[red]error[/red]",
|
|
38
|
+
}.get(status.status, status.status)
|
|
39
|
+
detail = f" · pid {status.pid}" if status.pid else ""
|
|
40
|
+
docs = f" · {status.open_documents} doc{'s' if status.open_documents != 1 else ''}"
|
|
41
|
+
ui.print(f" [cyan]{status.language}[/cyan] · {label}{detail}{docs}")
|
|
42
|
+
if status.error_message:
|
|
43
|
+
ui.print(f" [red]{status.error_message}[/red]")
|
|
44
|
+
ui.print("[dim]Usage: /lsp status|doctor|restart|servers[/dim]")
|
|
45
|
+
|
|
46
|
+
def _lsp_doctor(self) -> None:
|
|
47
|
+
manager = getattr(self._g, "_lsp_manager", None)
|
|
48
|
+
if manager is None:
|
|
49
|
+
ui.error("No LSP manager available.")
|
|
50
|
+
return
|
|
51
|
+
ui.print("[bold]LSP doctor:[/bold]")
|
|
52
|
+
missing = 0
|
|
53
|
+
disabled = 0
|
|
54
|
+
auto_detected = 0
|
|
55
|
+
for check in manager.doctor():
|
|
56
|
+
if not check.enabled:
|
|
57
|
+
disabled += 1
|
|
58
|
+
ui.print(f" [cyan]{check.language}[/cyan] · [dim]disabled[/dim] · {check.command}")
|
|
59
|
+
continue
|
|
60
|
+
source = f" [dim]({check.detected_source})[/dim]" if check.detected_source else ""
|
|
61
|
+
if check.available:
|
|
62
|
+
if check.detected_source:
|
|
63
|
+
auto_detected += 1
|
|
64
|
+
ui.print(
|
|
65
|
+
f" [cyan]{check.language}[/cyan] · [green]ok[/green] · "
|
|
66
|
+
f"{check.command} [dim]({check.resolved_path})[/dim]{source}"
|
|
67
|
+
)
|
|
68
|
+
continue
|
|
69
|
+
missing += 1
|
|
70
|
+
ui.print(f" [cyan]{check.language}[/cyan] · [red]missing[/red] · {check.command}")
|
|
71
|
+
if check.install_hint:
|
|
72
|
+
ui.print(f" [dim]{check.install_hint}[/dim]")
|
|
73
|
+
if missing:
|
|
74
|
+
ui.print(f"[yellow]{missing} LSP server{'s' if missing != 1 else ''} missing.[/yellow]")
|
|
75
|
+
elif disabled:
|
|
76
|
+
ui.print("[dim]No missing enabled LSP servers.[/dim]")
|
|
77
|
+
else:
|
|
78
|
+
msg = "All enabled LSP servers are available."
|
|
79
|
+
if auto_detected:
|
|
80
|
+
msg += f" ({auto_detected} auto-detected)"
|
|
81
|
+
ui.print(f"[green]{msg}[/green]")
|
|
82
|
+
|
|
83
|
+
async def _lsp_restart(self, language: str | None) -> None:
|
|
84
|
+
manager = getattr(self._g, "_lsp_manager", None)
|
|
85
|
+
if manager is None:
|
|
86
|
+
ui.error("No LSP manager available.")
|
|
87
|
+
return
|
|
88
|
+
await manager.restart(language)
|
|
89
|
+
target = language or "all servers"
|
|
90
|
+
ui.print(f"[green]✓ restarted {target}[/green]")
|
|
91
|
+
|
|
92
|
+
def _lsp_servers(self) -> None:
|
|
93
|
+
manager = getattr(self._g, "_lsp_manager", None)
|
|
94
|
+
workspace = getattr(self._g, "_workspace", ".")
|
|
95
|
+
ui.print("[bold]LSP servers:[/bold]")
|
|
96
|
+
ui.print(f"[dim]{lsp_config_path(workspace)}[/dim]")
|
|
97
|
+
if manager is None:
|
|
98
|
+
ui.error("No LSP manager available.")
|
|
99
|
+
return
|
|
100
|
+
for config in manager.servers.values():
|
|
101
|
+
state = "[green]enabled[/green]" if config.enabled else "[dim]disabled[/dim]"
|
|
102
|
+
exts = ", ".join(config.extensions) or "no extensions"
|
|
103
|
+
command = " ".join([config.command, *config.args]).strip()
|
|
104
|
+
ui.print(f" [cyan]{config.language}[/cyan] · {state} · [dim]{exts}[/dim]")
|
|
105
|
+
ui.print(f" {command}")
|