synapse-cli-agent 0.1.13__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.
- synapse/__init__.py +13 -0
- synapse/__main__.py +6 -0
- synapse/app/__init__.py +1 -0
- synapse/app/agent.py +492 -0
- synapse/app/agent_md.py +107 -0
- synapse/cli.py +750 -0
- synapse/commands/__init__.py +1 -0
- synapse/commands/compression.py +573 -0
- synapse/commands/helpers.py +22 -0
- synapse/commands/mcp.py +406 -0
- synapse/commands/model.py +173 -0
- synapse/commands/result.py +34 -0
- synapse/commands/sessions.py +443 -0
- synapse/commands/slash_cmds.py +521 -0
- synapse/commands/slash_complete.py +816 -0
- synapse/commands/theme.py +99 -0
- synapse/config.py +27 -0
- synapse/content/__init__.py +1 -0
- synapse/content/input_history.py +122 -0
- synapse/content/multimodal.py +733 -0
- synapse/content/prompts.py +249 -0
- synapse/content/skills_catalog.py +128 -0
- synapse/integrations/__init__.py +1 -0
- synapse/integrations/checkpoint_seed.py +281 -0
- synapse/integrations/codex_history.py +375 -0
- synapse/integrations/codex_import.py +393 -0
- synapse/integrations/codex_sessions.py +629 -0
- synapse/integrations/describe_image.py +370 -0
- synapse/integrations/http_clients.py +199 -0
- synapse/integrations/llm_openai_compat.py +90 -0
- synapse/integrations/llm_openai_websocket.py +187 -0
- synapse/integrations/mcp_client.py +646 -0
- synapse/integrations/vision_middleware.py +62 -0
- synapse/models/__init__.py +5 -0
- synapse/models/config.py +240 -0
- synapse/models/helpers.py +206 -0
- synapse/models/profile.py +59 -0
- synapse/models/registry.py +722 -0
- synapse/models_registry.py +7 -0
- synapse/observability/__init__.py +1 -0
- synapse/observability/startup_trace.py +127 -0
- synapse/runtime/__init__.py +1 -0
- synapse/runtime/async_runtime.py +176 -0
- synapse/runtime/backends.py +458 -0
- synapse/runtime/context_compact.py +249 -0
- synapse/runtime/execute_capture.py +48 -0
- synapse/runtime/fs_permissions.py +79 -0
- synapse/runtime/harness.py +57 -0
- synapse/runtime/hitl.py +197 -0
- synapse/runtime/interaction_ledger.py +82 -0
- synapse/runtime/middleware.py +802 -0
- synapse/runtime/model_request_compression_middleware.py +745 -0
- synapse/runtime/pathing.py +146 -0
- synapse/runtime/safety.py +184 -0
- synapse/runtime/steer.py +240 -0
- synapse/runtime/subagents.py +207 -0
- synapse/runtime/tool_ignore.py +221 -0
- synapse/runtime/tool_output_eval.py +118 -0
- synapse/runtime/tool_output_middleware.py +585 -0
- synapse/runtime/tool_output_usage_middleware.py +60 -0
- synapse/sessions/__init__.py +31 -0
- synapse/sessions/cancel_repair.py +208 -0
- synapse/sessions/session_recap.py +174 -0
- synapse/sessions/store.py +695 -0
- synapse/sessions/transcript.py +754 -0
- synapse/settings/__init__.py +5 -0
- synapse/settings/config_paths.py +184 -0
- synapse/settings/schema.py +464 -0
- synapse/tool_output/__init__.py +59 -0
- synapse/tool_output/detection.py +170 -0
- synapse/tool_output/metrics.py +32 -0
- synapse/tool_output/models.py +173 -0
- synapse/tool_output/pipeline.py +330 -0
- synapse/tool_output/repository.py +721 -0
- synapse/tool_output/transformers.py +648 -0
- synapse/tools/__init__.py +5 -0
- synapse/tools/session_tools.py +204 -0
- synapse/ui/__init__.py +10 -0
- synapse/ui/bottombar/__init__.py +73 -0
- synapse/ui/bottombar/components/__init__.py +143 -0
- synapse/ui/bottombar/components/key_hints.py +30 -0
- synapse/ui/bottombar/components/mcp.py +64 -0
- synapse/ui/bottombar/components/mode.py +24 -0
- synapse/ui/bottombar/components/model.py +28 -0
- synapse/ui/bottombar/components/thread.py +29 -0
- synapse/ui/bottombar/context.py +36 -0
- synapse/ui/bottombar/core.py +74 -0
- synapse/ui/dialogs/__init__.py +25 -0
- synapse/ui/dialogs/base.py +362 -0
- synapse/ui/dialogs/codex_session_list.py +84 -0
- synapse/ui/dialogs/compression_diagnostics.py +210 -0
- synapse/ui/dialogs/git_explore.py +702 -0
- synapse/ui/dialogs/mcp_panel.py +407 -0
- synapse/ui/dialogs/model_picker.py +128 -0
- synapse/ui/dialogs/safety_panel.py +63 -0
- synapse/ui/dialogs/session_list.py +98 -0
- synapse/ui/dialogs/theme_designer.py +863 -0
- synapse/ui/dialogs/theme_picker.py +113 -0
- synapse/ui/git_explore/__init__.py +31 -0
- synapse/ui/git_explore/engine.py +82 -0
- synapse/ui/git_explore/provider.py +242 -0
- synapse/ui/git_explore/unified.py +85 -0
- synapse/ui/rendering.py +350 -0
- synapse/ui/sink.py +70 -0
- synapse/ui/steer_widget.py +367 -0
- synapse/ui/stream.py +1207 -0
- synapse/ui/stream_events.py +421 -0
- synapse/ui/stream_runtime.py +252 -0
- synapse/ui/theme.py +1154 -0
- synapse/ui/timeline.py +621 -0
- synapse/ui/topbar/__init__.py +97 -0
- synapse/ui/topbar/components/__init__.py +150 -0
- synapse/ui/topbar/components/branch.py +41 -0
- synapse/ui/topbar/components/title.py +24 -0
- synapse/ui/topbar/components/tool_output.py +24 -0
- synapse/ui/topbar/components/usage.py +24 -0
- synapse/ui/topbar/components/workspace.py +32 -0
- synapse/ui/topbar/context.py +32 -0
- synapse/ui/topbar/core.py +979 -0
- synapse/ui/topbar/git_changes_popover.py +178 -0
- synapse/ui/topbar/git_chrome.py +475 -0
- synapse/ui/topbar/tool_output_popover.py +84 -0
- synapse/ui/topbar/widget.py +474 -0
- synapse/ui/tui.py +5717 -0
- synapse/ui/turn_rail.py +71 -0
- synapse/ui/user_turn.py +83 -0
- synapse/ui/welcome.py +261 -0
- synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
- synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
- synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
- synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Shared interactive slash commands for chat CLI and TUI.
|
|
2
|
+
|
|
3
|
+
Focus: session management + MCP management first.
|
|
4
|
+
Returns structured results so UIs only need to render/apply side effects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from synapse.app.agent import build_coding_agent
|
|
13
|
+
from synapse.commands.compression import handle_compression
|
|
14
|
+
from synapse.commands.helpers import markdown_escape, parts
|
|
15
|
+
from synapse.commands.mcp import handle_mcp
|
|
16
|
+
from synapse.commands.model import handle_model
|
|
17
|
+
from synapse.commands.result import SlashResult
|
|
18
|
+
from synapse.commands.sessions import handle_session
|
|
19
|
+
from synapse.commands.theme import handle_theme
|
|
20
|
+
from synapse.integrations.mcp_client import (
|
|
21
|
+
get_active_mcp_pool,
|
|
22
|
+
)
|
|
23
|
+
from synapse.sessions.store import (
|
|
24
|
+
SessionStore,
|
|
25
|
+
apply_binding_to_settings,
|
|
26
|
+
binding_from_settings,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
HELP_TEXT = """## Slash Commands
|
|
30
|
+
|
|
31
|
+
### General
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `/help`, `/?` | Show this help |
|
|
35
|
+
| `/thread`, `/id` | Show current thread ID |
|
|
36
|
+
| `/clear` | Clear transcript (TUI only) |
|
|
37
|
+
| `/exit`, `/quit` | Exit |
|
|
38
|
+
|
|
39
|
+
### Completion
|
|
40
|
+
| Key | Action |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Tab / Right | Accept suggestion |
|
|
43
|
+
| Shift+Tab | Previous candidate (TUI) |
|
|
44
|
+
| Ctrl+Space | List candidates (TUI) |
|
|
45
|
+
|
|
46
|
+
### Session
|
|
47
|
+
| Command | Description |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `/sessions`, `/session list [n]` | List recent sessions |
|
|
50
|
+
| `/session`, `/session show` | Show current session |
|
|
51
|
+
| `/new` | Create new session |
|
|
52
|
+
| `/switch <id>` | Switch to session |
|
|
53
|
+
| `/rename <title>` | Rename current session |
|
|
54
|
+
| `/session delete <id>` | Delete session metadata |
|
|
55
|
+
| `/session search <query>` | Search sessions |
|
|
56
|
+
| `/session prune` | Remove empty sessions |
|
|
57
|
+
| `/export [md\\|json] [path]` | Export transcript to file |
|
|
58
|
+
| `/codex import [id]` | Import Codex session (TUI) |
|
|
59
|
+
|
|
60
|
+
### MCP
|
|
61
|
+
| Command | Description |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `/mcp`, `/mcp list` | List MCP servers |
|
|
64
|
+
| `/mcp tools` | List MCP tools |
|
|
65
|
+
| `/mcp test` | Test MCP connectivity |
|
|
66
|
+
| `/mcp reload` | Reload MCP servers |
|
|
67
|
+
| `/mcp enable`, `/mcp disable` | Toggle MCP |
|
|
68
|
+
| `/mcp config` | Show MCP config |
|
|
69
|
+
|
|
70
|
+
### Model
|
|
71
|
+
| Command | Description |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `/model` | Open model picker (TUI) |
|
|
74
|
+
| `/model <alias\\|provider:model>` | Switch model |
|
|
75
|
+
| `/model thinking <level>` | Set thinking level |
|
|
76
|
+
|
|
77
|
+
### Appearance
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `/theme` | Open theme picker (TUI) |
|
|
81
|
+
| `/theme list` | List themes |
|
|
82
|
+
| `/theme <name>` | Apply theme |
|
|
83
|
+
|
|
84
|
+
### Safety / HITL
|
|
85
|
+
| Command | Description |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `/safety` | Show safety profile |
|
|
88
|
+
| `/safety <profile>` | Switch profile |
|
|
89
|
+
| `/approve` | Approve pending tools |
|
|
90
|
+
| `/reject [reason]` | Reject pending tools |
|
|
91
|
+
|
|
92
|
+
### Diagnostics
|
|
93
|
+
| Command | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `/context` | Context usage stats |
|
|
96
|
+
| `/compact` | Force context compact |
|
|
97
|
+
| `/compression [session]` | Compression diagnostics summary |
|
|
98
|
+
| `/compression profile [session]` | Request content breakdown and opportunity ranking |
|
|
99
|
+
| `/compression export [session] [json|csv] [path]` | Export complete compression diagnostics |
|
|
100
|
+
| `/compression events [session] [limit]` | Recent compression decisions |
|
|
101
|
+
| `/compression requests [session] [limit]` | Model request before/after ledger |
|
|
102
|
+
| `/compression request <request_id> [session]` | One model request accounting event |
|
|
103
|
+
| `/compression skipped [session] [limit]` | Outputs skipped by policy or threshold |
|
|
104
|
+
| `/compression fallback [session] [limit]` | Compression attempts that reverted |
|
|
105
|
+
| `/compression tool <tool_call_id> [session] [limit]` | Decisions for one tool call |
|
|
106
|
+
| `/tool-output ...` | Alias for `/compression ...` |
|
|
107
|
+
| `/skills` | List skills |
|
|
108
|
+
| `/memory` | List memory files |
|
|
109
|
+
| `/subagents` | List sub-agents |
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _store(settings: Any) -> SessionStore:
|
|
115
|
+
return SessionStore(settings.resolved_sessions_path())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _persist_model_binding(settings: Any, thread_id: str | None) -> None:
|
|
119
|
+
try:
|
|
120
|
+
store = _store(settings)
|
|
121
|
+
store.save_model_binding(thread_id, binding_from_settings(settings), also_last=True)
|
|
122
|
+
except Exception: # noqa: BLE001
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _restore_thread_model(
|
|
127
|
+
*,
|
|
128
|
+
settings: Any,
|
|
129
|
+
agent: Any,
|
|
130
|
+
project_root: Path,
|
|
131
|
+
thread_id: str,
|
|
132
|
+
) -> tuple[Any | None, list[str]]:
|
|
133
|
+
"""Restore model binding for a thread. Returns (new_agent|None, notes)."""
|
|
134
|
+
store = _store(settings)
|
|
135
|
+
binding = store.get_model_binding(thread_id)
|
|
136
|
+
if not binding.has_data():
|
|
137
|
+
return None, []
|
|
138
|
+
changed = apply_binding_to_settings(settings, binding)
|
|
139
|
+
if not changed:
|
|
140
|
+
return None, [f"model binding: {binding.display()}"]
|
|
141
|
+
try:
|
|
142
|
+
new_agent = _rebuild_agent(
|
|
143
|
+
settings,
|
|
144
|
+
project_root=project_root,
|
|
145
|
+
model_name=settings.active_model or settings.model,
|
|
146
|
+
agent=agent,
|
|
147
|
+
)
|
|
148
|
+
except Exception as exc: # noqa: BLE001
|
|
149
|
+
return None, [f"restore model failed: {exc}"]
|
|
150
|
+
return new_agent, [f"restored model: {binding.display()}"]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _rebuild_agent(
|
|
154
|
+
settings: Any,
|
|
155
|
+
*,
|
|
156
|
+
project_root: Path,
|
|
157
|
+
model_name: str | None,
|
|
158
|
+
agent: Any,
|
|
159
|
+
load_mcp: bool | None = None,
|
|
160
|
+
defer_mcp_reconnect: bool = False,
|
|
161
|
+
) -> Any:
|
|
162
|
+
checkpointer = getattr(agent, "_coding_checkpointer", None)
|
|
163
|
+
steer_queue = getattr(agent, "_coding_steer_queue", None)
|
|
164
|
+
# Reuse live model only when not switching profiles.
|
|
165
|
+
reuse_model = model_name is None
|
|
166
|
+
model = getattr(agent, "_coding_model", None) if reuse_model else None
|
|
167
|
+
registry = getattr(agent, "_coding_model_registry", None) if reuse_model else None
|
|
168
|
+
model_cache = getattr(agent, "_coding_model_cache", None)
|
|
169
|
+
mcp_tools: list[Any] | None = None
|
|
170
|
+
if load_mcp is not None:
|
|
171
|
+
# Explicit caller intent (/mcp reload, /mcp disable, ...).
|
|
172
|
+
want_mcp = bool(load_mcp)
|
|
173
|
+
elif not bool(getattr(settings, "enable_mcp", True)):
|
|
174
|
+
want_mcp = False
|
|
175
|
+
else:
|
|
176
|
+
# Prefer the live pool when it already has tools, regardless of the
|
|
177
|
+
# old agent's attached flag. This covers:
|
|
178
|
+
# - normal attached agent (flag True + pool alive) 鈫?reuse
|
|
179
|
+
# - startup race: pool connected but agent not yet swapped 鈫?reuse
|
|
180
|
+
# - after /mcp reload that created a new pool 鈫?reuse
|
|
181
|
+
pool = get_active_mcp_pool()
|
|
182
|
+
pool_tools = list(getattr(pool, "tools", None) or []) if pool is not None else []
|
|
183
|
+
if pool is not None:
|
|
184
|
+
mcp_tools = pool_tools
|
|
185
|
+
want_mcp = False
|
|
186
|
+
elif bool(getattr(agent, "_coding_mcp_attached", False)):
|
|
187
|
+
# Model switching may defer this network I/O to a TUI worker. Other
|
|
188
|
+
# rebuild callers keep the historical synchronous reconnect behavior.
|
|
189
|
+
want_mcp = not defer_mcp_reconnect
|
|
190
|
+
else:
|
|
191
|
+
# MCP was deferred at startup and no pool yet: stay deferred.
|
|
192
|
+
want_mcp = False
|
|
193
|
+
return build_coding_agent(
|
|
194
|
+
settings,
|
|
195
|
+
project_root=project_root,
|
|
196
|
+
model_name=model_name,
|
|
197
|
+
checkpointer=checkpointer,
|
|
198
|
+
model=model,
|
|
199
|
+
model_registry=registry,
|
|
200
|
+
model_cache=model_cache,
|
|
201
|
+
load_mcp=want_mcp,
|
|
202
|
+
mcp_tools=mcp_tools,
|
|
203
|
+
steer_queue=steer_queue,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _mcp_attach_pending(settings: Any) -> bool:
|
|
208
|
+
return bool(getattr(settings, "enable_mcp", True) and get_active_mcp_pool() is None)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _apply_thinking_inplace(settings: Any, agent: Any, model_name: str) -> bool:
|
|
212
|
+
"""Update thinking params on the live model without rebuilding the graph.
|
|
213
|
+
|
|
214
|
+
Constructs a fresh (cheap, no network) chat model with the new settings and
|
|
215
|
+
copies thinking-related attributes onto the live instance. Returns False
|
|
216
|
+
when in-place update is not possible so callers can fall back to rebuild.
|
|
217
|
+
"""
|
|
218
|
+
from synapse.models.registry import build_model_from_settings
|
|
219
|
+
|
|
220
|
+
live = getattr(agent, "_coding_model", None)
|
|
221
|
+
if live is None:
|
|
222
|
+
return False
|
|
223
|
+
try:
|
|
224
|
+
_, fresh = build_model_from_settings(settings, model_name=model_name)
|
|
225
|
+
except Exception: # noqa: BLE001
|
|
226
|
+
return False
|
|
227
|
+
try:
|
|
228
|
+
if type(fresh) is not type(live):
|
|
229
|
+
return False
|
|
230
|
+
copied = False
|
|
231
|
+
for attr in ("reasoning_effort", "extra_body", "thinking", "model_kwargs"):
|
|
232
|
+
if not (hasattr(fresh, attr) and hasattr(live, attr)):
|
|
233
|
+
continue
|
|
234
|
+
try:
|
|
235
|
+
setattr(live, attr, getattr(fresh, attr))
|
|
236
|
+
copied = True
|
|
237
|
+
except Exception: # noqa: BLE001
|
|
238
|
+
return False
|
|
239
|
+
if copied:
|
|
240
|
+
try:
|
|
241
|
+
from synapse.models.registry import model_cache_key
|
|
242
|
+
|
|
243
|
+
cache = getattr(agent, "_coding_model_cache", None)
|
|
244
|
+
if isinstance(cache, dict):
|
|
245
|
+
stale = [key for key, value in cache.items() if value is live]
|
|
246
|
+
for key in stale:
|
|
247
|
+
cache.pop(key, None)
|
|
248
|
+
cache[model_cache_key(settings, model_name=model_name)] = live
|
|
249
|
+
except Exception: # noqa: BLE001
|
|
250
|
+
pass
|
|
251
|
+
return copied
|
|
252
|
+
finally:
|
|
253
|
+
try:
|
|
254
|
+
from synapse.integrations.http_clients import close_model_async_http_client
|
|
255
|
+
|
|
256
|
+
close_model_async_http_client(fresh)
|
|
257
|
+
except Exception: # noqa: BLE001
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def handle_slash(
|
|
262
|
+
text: str,
|
|
263
|
+
*,
|
|
264
|
+
settings: Any,
|
|
265
|
+
agent: Any,
|
|
266
|
+
thread_id: str,
|
|
267
|
+
project_root: Path | None = None,
|
|
268
|
+
) -> SlashResult:
|
|
269
|
+
"""Parse and handle a slash command. Non-commands return handled=False."""
|
|
270
|
+
raw = (text or "").strip()
|
|
271
|
+
if not raw.startswith("/") and raw not in {":q"}:
|
|
272
|
+
return SlashResult(handled=False)
|
|
273
|
+
|
|
274
|
+
root = Path(project_root or Path.cwd()).resolve()
|
|
275
|
+
model_name = getattr(settings, "active_model", None)
|
|
276
|
+
|
|
277
|
+
if raw in {"/exit", "/quit", ":q"}:
|
|
278
|
+
return SlashResult(
|
|
279
|
+
handled=True,
|
|
280
|
+
lines=[f"bye. thread_id={thread_id}"],
|
|
281
|
+
exit_requested=True,
|
|
282
|
+
)
|
|
283
|
+
if raw in {"/thread", "/id"}:
|
|
284
|
+
return SlashResult(handled=True, lines=[f"thread_id={thread_id}"])
|
|
285
|
+
if raw == "/clear":
|
|
286
|
+
return SlashResult(handled=True, clear_log=True, lines=["log cleared"])
|
|
287
|
+
if raw in {"/help", "/?"}:
|
|
288
|
+
return SlashResult(
|
|
289
|
+
handled=True,
|
|
290
|
+
lines=HELP_TEXT.splitlines(),
|
|
291
|
+
markdown=HELP_TEXT,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
command_parts = parts(raw)
|
|
295
|
+
cmd = command_parts[0].lower()
|
|
296
|
+
args = command_parts[1:]
|
|
297
|
+
|
|
298
|
+
if cmd in {
|
|
299
|
+
"/sessions",
|
|
300
|
+
"/session",
|
|
301
|
+
"/new",
|
|
302
|
+
"/switch",
|
|
303
|
+
"/rename",
|
|
304
|
+
"/export",
|
|
305
|
+
}:
|
|
306
|
+
result = handle_session(cmd, args, settings=settings, agent=agent, thread_id=thread_id)
|
|
307
|
+
# When switching sessions, restore that session's model binding.
|
|
308
|
+
if (
|
|
309
|
+
result.handled
|
|
310
|
+
and not result.error
|
|
311
|
+
and result.thread_id
|
|
312
|
+
and result.thread_id != thread_id
|
|
313
|
+
and cmd in {"/switch", "/session"}
|
|
314
|
+
):
|
|
315
|
+
new_agent, notes = _restore_thread_model(
|
|
316
|
+
settings=settings,
|
|
317
|
+
agent=agent,
|
|
318
|
+
project_root=root,
|
|
319
|
+
thread_id=result.thread_id,
|
|
320
|
+
)
|
|
321
|
+
if notes:
|
|
322
|
+
result.lines = [*result.lines, *notes]
|
|
323
|
+
if new_agent is not None:
|
|
324
|
+
result.agent = new_agent
|
|
325
|
+
result.settings_changed = True
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
if cmd == "/mcp":
|
|
329
|
+
return handle_mcp(
|
|
330
|
+
args,
|
|
331
|
+
settings=settings,
|
|
332
|
+
agent=agent,
|
|
333
|
+
project_root=root,
|
|
334
|
+
model_name=model_name,
|
|
335
|
+
rebuild_agent=_rebuild_agent,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if cmd == "/model":
|
|
339
|
+
return handle_model(
|
|
340
|
+
args,
|
|
341
|
+
settings=settings,
|
|
342
|
+
agent=agent,
|
|
343
|
+
project_root=root,
|
|
344
|
+
thread_id=thread_id,
|
|
345
|
+
apply_thinking_inplace=_apply_thinking_inplace,
|
|
346
|
+
rebuild_agent=_rebuild_agent,
|
|
347
|
+
persist_model_binding=_persist_model_binding,
|
|
348
|
+
mcp_attach_pending=_mcp_attach_pending,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if cmd == "/theme":
|
|
352
|
+
return handle_theme(args, settings=settings, project_root=root)
|
|
353
|
+
|
|
354
|
+
if cmd == "/compact":
|
|
355
|
+
from synapse.runtime.context_compact import force_compact_via_agent
|
|
356
|
+
|
|
357
|
+
ok, lines = force_compact_via_agent(agent, thread_id=thread_id)
|
|
358
|
+
md = "## Compact\n\n" + "\n".join(f"- {x}" for x in lines)
|
|
359
|
+
return SlashResult(handled=True, lines=lines, error=not ok, markdown=md)
|
|
360
|
+
|
|
361
|
+
if cmd in {"/compression", "/tool-output", "/tool-compress"}:
|
|
362
|
+
return handle_compression(settings, thread_id, args)
|
|
363
|
+
|
|
364
|
+
if cmd == "/context":
|
|
365
|
+
from synapse.runtime.context_compact import context_status_lines
|
|
366
|
+
|
|
367
|
+
plain = context_status_lines(agent, thread_id)
|
|
368
|
+
rows = []
|
|
369
|
+
for line in plain:
|
|
370
|
+
if "=" in line:
|
|
371
|
+
k, v = line.split("=", 1)
|
|
372
|
+
rows.append((k.strip(), v.strip()))
|
|
373
|
+
elif ": " in line:
|
|
374
|
+
k, v = line.split(": ", 1)
|
|
375
|
+
rows.append((k.strip(), v.strip()))
|
|
376
|
+
else:
|
|
377
|
+
rows.append(("", line.strip()))
|
|
378
|
+
md = "## Context\n\n| Key | Value |\n|---|---|\n"
|
|
379
|
+
for k, v in rows:
|
|
380
|
+
md += f"| {markdown_escape(k)} | {markdown_escape(v)} |\n"
|
|
381
|
+
return SlashResult(handled=True, lines=plain, markdown=md)
|
|
382
|
+
|
|
383
|
+
if cmd == "/safety":
|
|
384
|
+
from synapse.runtime.safety import (
|
|
385
|
+
apply_safety_to_settings,
|
|
386
|
+
format_safety_status,
|
|
387
|
+
get_safety_profile,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if not args:
|
|
391
|
+
plain = format_safety_status(settings)
|
|
392
|
+
md = "## Safety\n\n| Setting | Value |\n|---|---|\n"
|
|
393
|
+
for line in plain:
|
|
394
|
+
if ": " in line:
|
|
395
|
+
k, v = line.split(": ", 1)
|
|
396
|
+
md += f"| {markdown_escape(k.strip())} | {markdown_escape(v.strip())} |\n"
|
|
397
|
+
elif line.startswith("profiles:") or line.startswith("switch:"):
|
|
398
|
+
md += f"\n*{markdown_escape(line)}*\n"
|
|
399
|
+
return SlashResult(handled=True, lines=plain, markdown=md)
|
|
400
|
+
profile = get_safety_profile(args[0])
|
|
401
|
+
notes = apply_safety_to_settings(settings, profile)
|
|
402
|
+
try:
|
|
403
|
+
new_agent = _rebuild_agent(
|
|
404
|
+
settings,
|
|
405
|
+
project_root=root,
|
|
406
|
+
model_name=model_name,
|
|
407
|
+
agent=agent,
|
|
408
|
+
)
|
|
409
|
+
except Exception as exc: # noqa: BLE001
|
|
410
|
+
return SlashResult(
|
|
411
|
+
handled=True,
|
|
412
|
+
lines=[*notes, f"rebuild failed: {exc}"],
|
|
413
|
+
error=True,
|
|
414
|
+
settings_changed=True,
|
|
415
|
+
)
|
|
416
|
+
md = "## Safety\n\n" + "\n".join(f"- {n}" for n in notes) + "\n- agent rebuilt"
|
|
417
|
+
return SlashResult(
|
|
418
|
+
handled=True,
|
|
419
|
+
lines=[*notes, "agent rebuilt"],
|
|
420
|
+
markdown=md,
|
|
421
|
+
agent=new_agent,
|
|
422
|
+
settings_changed=True,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if cmd == "/approve":
|
|
426
|
+
return SlashResult(
|
|
427
|
+
handled=True,
|
|
428
|
+
lines=["resume: approve pending tool call(s)"],
|
|
429
|
+
resume_action="approve",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if cmd == "/reject":
|
|
433
|
+
reason = " ".join(args).strip() or None
|
|
434
|
+
return SlashResult(
|
|
435
|
+
handled=True,
|
|
436
|
+
lines=["resume: reject pending tool call(s)"],
|
|
437
|
+
resume_action="reject",
|
|
438
|
+
resume_message=reason,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if cmd == "/skills":
|
|
442
|
+
from synapse.content.skills_catalog import (
|
|
443
|
+
discover_skills,
|
|
444
|
+
format_skills_lines,
|
|
445
|
+
skills_paths_from_settings,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
paths = skills_paths_from_settings(settings, root)
|
|
449
|
+
skills = discover_skills(paths)
|
|
450
|
+
plain = format_skills_lines(skills)
|
|
451
|
+
if not skills:
|
|
452
|
+
md = "## Skills\n\n*(none found)*\n\ntip: put `SKILL.md` under `skills/<name>/`"
|
|
453
|
+
else:
|
|
454
|
+
md = f"## Skills ({len(skills)})\n\n| Name | Description | Path |\n|---|---|---|\n"
|
|
455
|
+
for s in skills:
|
|
456
|
+
desc = s.description or "-"
|
|
457
|
+
if len(desc) > 80:
|
|
458
|
+
desc = desc[:79] + "..."
|
|
459
|
+
md += (
|
|
460
|
+
f"| {markdown_escape(s.name)} | {markdown_escape(desc)} "
|
|
461
|
+
f"| `{markdown_escape(s.path)}` |\n"
|
|
462
|
+
)
|
|
463
|
+
return SlashResult(handled=True, lines=plain, markdown=md)
|
|
464
|
+
|
|
465
|
+
if cmd == "/memory":
|
|
466
|
+
from synapse.content.skills_catalog import (
|
|
467
|
+
format_memory_lines,
|
|
468
|
+
list_memory_files,
|
|
469
|
+
memory_paths_from_settings,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
paths = memory_paths_from_settings(settings, root)
|
|
473
|
+
entries = list_memory_files(paths)
|
|
474
|
+
plain = format_memory_lines(entries)
|
|
475
|
+
if not entries:
|
|
476
|
+
md = "## Memory\n\n*(no paths configured)*"
|
|
477
|
+
else:
|
|
478
|
+
md = f"## Memory Files ({len(entries)})\n\n| Path | Size | Status |\n|---|---|---|\n"
|
|
479
|
+
for path, exists, size in entries:
|
|
480
|
+
status = "ok" if exists else "missing"
|
|
481
|
+
md += f"| `{markdown_escape(path)}` | {size} | {status} |\n"
|
|
482
|
+
md += "\n*Existing files are injected via `create_deep_agent(memory=...)`*"
|
|
483
|
+
return SlashResult(handled=True, lines=plain, markdown=md)
|
|
484
|
+
|
|
485
|
+
if cmd in {"/subagents", "/subagent"}:
|
|
486
|
+
from synapse.runtime.subagents import build_default_subagents, format_subagents_lines
|
|
487
|
+
|
|
488
|
+
specs = getattr(agent, "_coding_subagents", None)
|
|
489
|
+
if specs is None:
|
|
490
|
+
specs = build_default_subagents(
|
|
491
|
+
enabled=getattr(settings, "enable_subagents", True),
|
|
492
|
+
isolate_tools=True,
|
|
493
|
+
)
|
|
494
|
+
plain = format_subagents_lines(specs)
|
|
495
|
+
if not specs:
|
|
496
|
+
md = "## Sub-agents\n\n*disabled*"
|
|
497
|
+
else:
|
|
498
|
+
md = f"## Sub-agents ({len(specs)})\n\n"
|
|
499
|
+
md += "| Name | Model | Isolation | Tools |\n|---|---|---|---|\n"
|
|
500
|
+
for spec in specs:
|
|
501
|
+
name = spec.get("name") or "?"
|
|
502
|
+
model = spec.get("model") or "(inherit)"
|
|
503
|
+
tools = spec.get("tools") or []
|
|
504
|
+
tool_names = [getattr(t, "name", getattr(t, "__name__", str(t))) for t in tools]
|
|
505
|
+
mw = spec.get("middleware") or []
|
|
506
|
+
isolation = "tool-exclude" if mw else ("tools+" if tools else "default")
|
|
507
|
+
tool_list = ", ".join(str(n) for n in tool_names) if tool_names else "-"
|
|
508
|
+
md += (
|
|
509
|
+
f"| {markdown_escape(name)} | {markdown_escape(model)} "
|
|
510
|
+
f"| {markdown_escape(isolation)} "
|
|
511
|
+
f"| {markdown_escape(tool_list)} |\n"
|
|
512
|
+
)
|
|
513
|
+
return SlashResult(handled=True, lines=plain, markdown=md)
|
|
514
|
+
|
|
515
|
+
if raw.startswith("/"):
|
|
516
|
+
return SlashResult(
|
|
517
|
+
handled=True,
|
|
518
|
+
lines=[f"unknown command: {cmd}", "type /help for commands"],
|
|
519
|
+
error=True,
|
|
520
|
+
)
|
|
521
|
+
return SlashResult(handled=False)
|