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
synapse/commands/mcp.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""MCP slash-command handler and presentation helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from synapse.app.agent import build_coding_agent
|
|
10
|
+
from synapse.commands.helpers import markdown_escape
|
|
11
|
+
from synapse.commands.result import SlashResult
|
|
12
|
+
from synapse.integrations.mcp_client import (
|
|
13
|
+
get_active_mcp_pool,
|
|
14
|
+
load_mcp_server_configs,
|
|
15
|
+
load_mcp_tools,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _mcp_list_lines(settings: Any) -> list[str]:
|
|
20
|
+
servers = load_mcp_server_configs(
|
|
21
|
+
path=getattr(settings, "mcp_config_path", None),
|
|
22
|
+
json_blob=getattr(settings, "mcp_servers_json", None),
|
|
23
|
+
)
|
|
24
|
+
enable = bool(getattr(settings, "enable_mcp", True))
|
|
25
|
+
lines = [f"mcp enabled={enable}"]
|
|
26
|
+
pool = get_active_mcp_pool()
|
|
27
|
+
if pool is not None:
|
|
28
|
+
lines.append(f"live pool servers: {', '.join(pool.server_names) or '(none)'}")
|
|
29
|
+
if not servers:
|
|
30
|
+
lines.append("no MCP servers configured")
|
|
31
|
+
path = getattr(settings, "mcp_config_path", None)
|
|
32
|
+
if path:
|
|
33
|
+
lines.append(f"config path: {path}")
|
|
34
|
+
return lines
|
|
35
|
+
for s in servers:
|
|
36
|
+
if s.transport == "stdio":
|
|
37
|
+
dest = f"cmd={s.command!r} args={s.args!r}"
|
|
38
|
+
else:
|
|
39
|
+
dest = f"url={s.url!r}"
|
|
40
|
+
lines.append(f"- {s.name}: transport={s.transport} enabled={s.enabled} {dest}")
|
|
41
|
+
loaded = getattr(build_coding_agent, "last_mcp_servers", []) or []
|
|
42
|
+
warnings = getattr(build_coding_agent, "last_mcp_warnings", []) or []
|
|
43
|
+
tool_names = getattr(build_coding_agent, "last_mcp_tool_names", []) or []
|
|
44
|
+
lines.append(f"loaded at agent build: {', '.join(loaded) or '(none)'}")
|
|
45
|
+
lines.append(f"tools bound: {len(tool_names)}")
|
|
46
|
+
for w in warnings:
|
|
47
|
+
lines.append(f"warn: {w}")
|
|
48
|
+
return lines
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _md_mcp_list(settings: Any) -> str:
|
|
52
|
+
"""Format MCP server config as a Markdown table."""
|
|
53
|
+
servers = load_mcp_server_configs(
|
|
54
|
+
path=getattr(settings, "mcp_config_path", None),
|
|
55
|
+
json_blob=getattr(settings, "mcp_servers_json", None),
|
|
56
|
+
)
|
|
57
|
+
enable = bool(getattr(settings, "enable_mcp", True))
|
|
58
|
+
lines = [f"## MCP Servers (`mcp enabled={enable}`)", ""]
|
|
59
|
+
pool = get_active_mcp_pool()
|
|
60
|
+
if pool is not None:
|
|
61
|
+
lines.append(f"**live pool**: {', '.join(pool.server_names) or '(none)'}")
|
|
62
|
+
lines.append("")
|
|
63
|
+
if not servers:
|
|
64
|
+
lines.append("*No MCP servers configured*")
|
|
65
|
+
path = getattr(settings, "mcp_config_path", None)
|
|
66
|
+
if path:
|
|
67
|
+
lines.append(f"\nconfig path: `{path}`")
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
lines.append("| Name | Transport | Enabled | Endpoint |")
|
|
70
|
+
lines.append("|---|---|---|---|")
|
|
71
|
+
for s in servers:
|
|
72
|
+
if s.transport == "stdio":
|
|
73
|
+
dest = f"`{s.command or '-'}`"
|
|
74
|
+
if s.args:
|
|
75
|
+
dest += " " + " ".join(str(a) for a in s.args)
|
|
76
|
+
else:
|
|
77
|
+
dest = s.url or "-"
|
|
78
|
+
lines.append(
|
|
79
|
+
f"| {markdown_escape(s.name)} | {markdown_escape(s.transport)} "
|
|
80
|
+
f"| {'yes' if s.enabled else 'no'} | {markdown_escape(dest)} |"
|
|
81
|
+
)
|
|
82
|
+
loaded = getattr(build_coding_agent, "last_mcp_servers", []) or []
|
|
83
|
+
tool_names = getattr(build_coding_agent, "last_mcp_tool_names", []) or []
|
|
84
|
+
warnings_list = getattr(build_coding_agent, "last_mcp_warnings", []) or []
|
|
85
|
+
lines.append("")
|
|
86
|
+
lines.append(f"**loaded at agent build**: {', '.join(loaded) or '(none)'}")
|
|
87
|
+
lines.append(f"**tools bound**: {len(tool_names)}")
|
|
88
|
+
for w in warnings_list:
|
|
89
|
+
lines.append(f"- warn: {w}")
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _md_tool_list(names: list[str], warnings: list[str] | None = None) -> str:
|
|
94
|
+
"""Format MCP tool names as a Markdown list."""
|
|
95
|
+
lines = [f"## MCP Tools ({len(names)})", ""]
|
|
96
|
+
if not names:
|
|
97
|
+
lines.append("*(no tools discovered)*")
|
|
98
|
+
else:
|
|
99
|
+
for n in sorted(names):
|
|
100
|
+
lines.append(f"- `{markdown_escape(n)}`")
|
|
101
|
+
for w in warnings or []:
|
|
102
|
+
lines.append(f"\nwarn: {w}")
|
|
103
|
+
return "\n".join(lines)
|
|
104
|
+
|
|
105
|
+
def handle_mcp(
|
|
106
|
+
args: list[str],
|
|
107
|
+
*,
|
|
108
|
+
settings: Any,
|
|
109
|
+
agent: Any,
|
|
110
|
+
project_root: Path,
|
|
111
|
+
model_name: str | None,
|
|
112
|
+
rebuild_agent: Callable[..., Any],
|
|
113
|
+
) -> SlashResult:
|
|
114
|
+
sub = (args[0].lower() if args else "list").strip()
|
|
115
|
+
|
|
116
|
+
if sub in {"list", "ls", "status"}:
|
|
117
|
+
lines = _mcp_list_lines(settings)
|
|
118
|
+
return SlashResult(handled=True, lines=lines, markdown=_md_mcp_list(settings))
|
|
119
|
+
|
|
120
|
+
if sub == "config":
|
|
121
|
+
path = getattr(settings, "mcp_config_path", None)
|
|
122
|
+
blob = getattr(settings, "mcp_servers_json", None)
|
|
123
|
+
lines = [
|
|
124
|
+
f"enable_mcp={getattr(settings, 'enable_mcp', True)}",
|
|
125
|
+
f"mcp_config_path={path!s}",
|
|
126
|
+
f"mcp_servers_json set={bool(blob and str(blob).strip())}",
|
|
127
|
+
"transports: stdio | sse | streamable_http(http)",
|
|
128
|
+
]
|
|
129
|
+
if path and Path(path).is_file():
|
|
130
|
+
lines.append(f"config file exists: {path}")
|
|
131
|
+
elif path:
|
|
132
|
+
lines.append(f"config file missing: {path}")
|
|
133
|
+
file_status = ""
|
|
134
|
+
if path:
|
|
135
|
+
file_status = "exists" if Path(path).is_file() else "missing"
|
|
136
|
+
return SlashResult(
|
|
137
|
+
handled=True,
|
|
138
|
+
lines=lines,
|
|
139
|
+
markdown=(
|
|
140
|
+
"## MCP Config\n\n"
|
|
141
|
+
"| Setting | Value |\n|---|---|\n"
|
|
142
|
+
f"| enable_mcp | `{getattr(settings, 'enable_mcp', True)}` |\n"
|
|
143
|
+
f"| config_path | `{path}` |\n"
|
|
144
|
+
f"| config_json_set | `{bool(blob and str(blob).strip())}` |\n"
|
|
145
|
+
f"| transports | stdio, sse, streamable_http |\n"
|
|
146
|
+
+ (f"| file_status | {file_status} |\n" if path else "")
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if sub == "tools":
|
|
151
|
+
names = getattr(build_coding_agent, "last_mcp_tool_names", []) or []
|
|
152
|
+
pool = get_active_mcp_pool()
|
|
153
|
+
if pool is not None and pool.tool_names:
|
|
154
|
+
names = list(pool.tool_names)
|
|
155
|
+
if not names:
|
|
156
|
+
# Fall back to a probe without replacing active pool permanently if empty.
|
|
157
|
+
servers = load_mcp_server_configs(
|
|
158
|
+
path=getattr(settings, "mcp_config_path", None),
|
|
159
|
+
json_blob=getattr(settings, "mcp_servers_json", None),
|
|
160
|
+
)
|
|
161
|
+
if not servers:
|
|
162
|
+
return SlashResult(handled=True, lines=["no MCP servers configured"])
|
|
163
|
+
if not getattr(settings, "enable_mcp", True):
|
|
164
|
+
return SlashResult(
|
|
165
|
+
handled=True,
|
|
166
|
+
lines=["mcp disabled; use /mcp enable then /mcp reload"],
|
|
167
|
+
error=True,
|
|
168
|
+
)
|
|
169
|
+
result = load_mcp_tools(servers, enabled=True)
|
|
170
|
+
names = list(result.tool_names or [getattr(t, "name", str(t)) for t in result.tools])
|
|
171
|
+
lines = [f"mcp tools ({len(names)}):"]
|
|
172
|
+
if not names:
|
|
173
|
+
lines.append("(no tools discovered)")
|
|
174
|
+
for n in names:
|
|
175
|
+
lines.append(f"- {n}")
|
|
176
|
+
for w in result.warnings:
|
|
177
|
+
lines.append(f"warn: {w}")
|
|
178
|
+
md = _md_tool_list(names, result.warnings)
|
|
179
|
+
return SlashResult(handled=True, lines=lines, markdown=md)
|
|
180
|
+
lines = [f"mcp tools ({len(names)}):"]
|
|
181
|
+
for n in names:
|
|
182
|
+
lines.append(f"- {n}")
|
|
183
|
+
return SlashResult(handled=True, lines=lines, markdown=_md_tool_list(names, []))
|
|
184
|
+
|
|
185
|
+
if sub == "test":
|
|
186
|
+
servers = load_mcp_server_configs(
|
|
187
|
+
path=getattr(settings, "mcp_config_path", None),
|
|
188
|
+
json_blob=getattr(settings, "mcp_servers_json", None),
|
|
189
|
+
)
|
|
190
|
+
if not servers:
|
|
191
|
+
return SlashResult(handled=True, lines=["no MCP servers configured"])
|
|
192
|
+
# Group by transport for clearer diagnostics.
|
|
193
|
+
by_transport: dict[str, list[str]] = {}
|
|
194
|
+
for s in servers:
|
|
195
|
+
by_transport.setdefault(s.transport, []).append(s.name)
|
|
196
|
+
result = load_mcp_tools(servers, enabled=True)
|
|
197
|
+
lines = [
|
|
198
|
+
f"servers ok: {', '.join(result.servers) or '-'}",
|
|
199
|
+
f"tools: {len(result.tools)}",
|
|
200
|
+
"configured transports:",
|
|
201
|
+
]
|
|
202
|
+
for transport, names in sorted(by_transport.items()):
|
|
203
|
+
lines.append(f" {transport}: {', '.join(names)}")
|
|
204
|
+
for tool in result.tools[:30]:
|
|
205
|
+
lines.append(f"- {getattr(tool, 'name', tool)}")
|
|
206
|
+
if len(result.tools) > 30:
|
|
207
|
+
lines.append(f"... and {len(result.tools) - 30} more")
|
|
208
|
+
for w in result.warnings:
|
|
209
|
+
lines.append(f"warn: {w}")
|
|
210
|
+
# Build markdown
|
|
211
|
+
md_lines = ["## MCP Test Results", ""]
|
|
212
|
+
md_lines.append(f"- **servers ok**: {', '.join(result.servers) or '-'}")
|
|
213
|
+
md_lines.append(f"- **tools discovered**: {len(result.tools)}")
|
|
214
|
+
md_lines.append("")
|
|
215
|
+
md_lines.append("| Transport | Servers |")
|
|
216
|
+
md_lines.append("|---|---|")
|
|
217
|
+
for transport, names in sorted(by_transport.items()):
|
|
218
|
+
md_lines.append(
|
|
219
|
+
f"| {markdown_escape(transport)} | "
|
|
220
|
+
f"{markdown_escape(', '.join(names))} |"
|
|
221
|
+
)
|
|
222
|
+
if result.tools:
|
|
223
|
+
md_lines.append("")
|
|
224
|
+
md_lines.append(f"### Tools ({len(result.tools)})")
|
|
225
|
+
for tool in result.tools[:30]:
|
|
226
|
+
md_lines.append(f"- `{markdown_escape(getattr(tool, 'name', str(tool)))}`")
|
|
227
|
+
if len(result.tools) > 30:
|
|
228
|
+
md_lines.append(f"- *鈥?and {len(result.tools) - 30} more*")
|
|
229
|
+
for w in result.warnings:
|
|
230
|
+
md_lines.append(f"\nwarn: {w}")
|
|
231
|
+
return SlashResult(handled=True, lines=lines, markdown="\n".join(md_lines))
|
|
232
|
+
|
|
233
|
+
if sub == "toggle":
|
|
234
|
+
if len(args) < 2:
|
|
235
|
+
return SlashResult(
|
|
236
|
+
handled=True,
|
|
237
|
+
lines=["usage: /mcp toggle <server_name>"],
|
|
238
|
+
error=True,
|
|
239
|
+
)
|
|
240
|
+
target = args[1].strip()
|
|
241
|
+
servers = load_mcp_server_configs(
|
|
242
|
+
path=getattr(settings, "mcp_config_path", None),
|
|
243
|
+
json_blob=getattr(settings, "mcp_servers_json", None),
|
|
244
|
+
)
|
|
245
|
+
changed = None
|
|
246
|
+
for s in servers:
|
|
247
|
+
if s.name == target:
|
|
248
|
+
s.enabled = not s.enabled
|
|
249
|
+
changed = s
|
|
250
|
+
break
|
|
251
|
+
if changed is None:
|
|
252
|
+
return SlashResult(
|
|
253
|
+
handled=True,
|
|
254
|
+
lines=[f"mcp server not found: {target}"],
|
|
255
|
+
error=True,
|
|
256
|
+
)
|
|
257
|
+
# Serialize modified configs back so reload picks up the toggled state.
|
|
258
|
+
raw = {
|
|
259
|
+
"servers": [
|
|
260
|
+
{
|
|
261
|
+
"name": s.name,
|
|
262
|
+
"transport": s.transport,
|
|
263
|
+
"command": s.command,
|
|
264
|
+
"args": s.args,
|
|
265
|
+
"env": s.env,
|
|
266
|
+
"url": s.url,
|
|
267
|
+
"headers": s.headers,
|
|
268
|
+
"enabled": s.enabled,
|
|
269
|
+
"tool_prefix": s.tool_prefix,
|
|
270
|
+
"include_tools": s.include_tools,
|
|
271
|
+
"exclude_tools": s.exclude_tools,
|
|
272
|
+
}
|
|
273
|
+
for s in servers
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
settings.mcp_servers_json = json.dumps(raw)
|
|
277
|
+
try:
|
|
278
|
+
new_agent = rebuild_agent(
|
|
279
|
+
settings,
|
|
280
|
+
project_root=project_root,
|
|
281
|
+
model_name=model_name,
|
|
282
|
+
agent=agent,
|
|
283
|
+
load_mcp=True,
|
|
284
|
+
)
|
|
285
|
+
except Exception as exc: # noqa: BLE001
|
|
286
|
+
return SlashResult(
|
|
287
|
+
handled=True,
|
|
288
|
+
lines=[f"mcp toggle reload failed: {exc}"],
|
|
289
|
+
error=True,
|
|
290
|
+
)
|
|
291
|
+
loaded = getattr(build_coding_agent, "last_mcp_servers", []) or []
|
|
292
|
+
warnings_list = getattr(build_coding_agent, "last_mcp_warnings", []) or []
|
|
293
|
+
tools = getattr(build_coding_agent, "last_mcp_tool_names", []) or []
|
|
294
|
+
notice = (
|
|
295
|
+
f"mcp '{target}' {'enabled' if changed.enabled else 'disabled'} 路 tools={len(tools)}"
|
|
296
|
+
)
|
|
297
|
+
lines = [
|
|
298
|
+
f"mcp server '{target}' {'enabled' if changed.enabled else 'disabled'}; agent rebuilt",
|
|
299
|
+
f"loaded servers: {', '.join(loaded) or '(none)'}",
|
|
300
|
+
f"tools bound: {len(tools)}",
|
|
301
|
+
]
|
|
302
|
+
for w in warnings_list:
|
|
303
|
+
lines.append(f"warn: {w}")
|
|
304
|
+
md_lines = [
|
|
305
|
+
"## MCP Toggle",
|
|
306
|
+
"",
|
|
307
|
+
f"**{target}**: {'enabled' if changed.enabled else 'disabled'}",
|
|
308
|
+
"",
|
|
309
|
+
f"- loaded servers: {', '.join(loaded) or '(none)'}",
|
|
310
|
+
f"- tools bound: {len(tools)}",
|
|
311
|
+
]
|
|
312
|
+
for w in warnings_list:
|
|
313
|
+
md_lines.append(f"- warn: {w}")
|
|
314
|
+
return SlashResult(
|
|
315
|
+
handled=True,
|
|
316
|
+
lines=lines,
|
|
317
|
+
notice=notice,
|
|
318
|
+
markdown="\n".join(md_lines),
|
|
319
|
+
agent=new_agent,
|
|
320
|
+
settings_changed=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if sub in {"enable", "on"}:
|
|
324
|
+
settings.enable_mcp = True
|
|
325
|
+
return SlashResult(
|
|
326
|
+
handled=True,
|
|
327
|
+
lines=["mcp enabled (run /mcp reload to rebuild agent)"],
|
|
328
|
+
notice="mcp enabled 路 reload pending",
|
|
329
|
+
settings_changed=True,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if sub in {"disable", "off"}:
|
|
333
|
+
settings.enable_mcp = False
|
|
334
|
+
try:
|
|
335
|
+
new_agent = rebuild_agent(
|
|
336
|
+
settings,
|
|
337
|
+
project_root=project_root,
|
|
338
|
+
model_name=model_name,
|
|
339
|
+
agent=agent,
|
|
340
|
+
load_mcp=False,
|
|
341
|
+
)
|
|
342
|
+
except Exception as exc: # noqa: BLE001
|
|
343
|
+
return SlashResult(
|
|
344
|
+
handled=True,
|
|
345
|
+
lines=[f"mcp disable rebuild failed: {exc}"],
|
|
346
|
+
error=True,
|
|
347
|
+
)
|
|
348
|
+
return SlashResult(
|
|
349
|
+
handled=True,
|
|
350
|
+
lines=["mcp disabled; agent rebuilt without MCP tools"],
|
|
351
|
+
notice="mcp disabled 路 tools=0",
|
|
352
|
+
agent=new_agent,
|
|
353
|
+
settings_changed=True,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if sub == "reload":
|
|
357
|
+
try:
|
|
358
|
+
new_agent = rebuild_agent(
|
|
359
|
+
settings,
|
|
360
|
+
project_root=project_root,
|
|
361
|
+
model_name=model_name,
|
|
362
|
+
agent=agent,
|
|
363
|
+
load_mcp=True,
|
|
364
|
+
)
|
|
365
|
+
except Exception as exc: # noqa: BLE001
|
|
366
|
+
return SlashResult(
|
|
367
|
+
handled=True,
|
|
368
|
+
lines=[f"mcp reload failed: {exc}"],
|
|
369
|
+
error=True,
|
|
370
|
+
)
|
|
371
|
+
loaded = getattr(build_coding_agent, "last_mcp_servers", []) or []
|
|
372
|
+
warnings = getattr(build_coding_agent, "last_mcp_warnings", []) or []
|
|
373
|
+
tools = getattr(build_coding_agent, "last_mcp_tool_names", []) or []
|
|
374
|
+
enabled_flag = bool(getattr(settings, "enable_mcp", True))
|
|
375
|
+
notice = f"mcp reloaded 路 servers={len(loaded)} tools={len(tools)}"
|
|
376
|
+
if not enabled_flag:
|
|
377
|
+
notice = "mcp reloaded 路 disabled"
|
|
378
|
+
lines = [
|
|
379
|
+
f"agent rebuilt; mcp enabled={getattr(settings, 'enable_mcp', True)}",
|
|
380
|
+
f"loaded servers: {', '.join(loaded) or '(none)'}",
|
|
381
|
+
f"tools bound: {len(tools)}",
|
|
382
|
+
]
|
|
383
|
+
for w in warnings:
|
|
384
|
+
lines.append(f"warn: {w}")
|
|
385
|
+
md_lines = [
|
|
386
|
+
"## MCP Reloaded",
|
|
387
|
+
"",
|
|
388
|
+
f"- **enabled**: `{enabled_flag}`",
|
|
389
|
+
f"- **servers**: {', '.join(loaded) or '(none)'}",
|
|
390
|
+
f"- **tools**: {len(tools)}",
|
|
391
|
+
]
|
|
392
|
+
for w in warnings:
|
|
393
|
+
md_lines.append(f"- warn: {w}")
|
|
394
|
+
return SlashResult(
|
|
395
|
+
handled=True,
|
|
396
|
+
lines=lines,
|
|
397
|
+
notice=notice,
|
|
398
|
+
markdown="\n".join(md_lines),
|
|
399
|
+
agent=new_agent,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return SlashResult(
|
|
403
|
+
handled=True,
|
|
404
|
+
lines=["usage: /mcp [list|tools|test|reload|enable|disable|config]"],
|
|
405
|
+
error=True,
|
|
406
|
+
)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Model selection and thinking slash-command handler."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from synapse.commands.result import SlashResult
|
|
9
|
+
from synapse.models.registry import registry_from_settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_model(
|
|
13
|
+
args: list[str],
|
|
14
|
+
*,
|
|
15
|
+
settings: Any,
|
|
16
|
+
agent: Any,
|
|
17
|
+
project_root: Path,
|
|
18
|
+
thread_id: str | None = None,
|
|
19
|
+
apply_thinking_inplace: Callable[[Any, Any, str], bool],
|
|
20
|
+
rebuild_agent: Callable[..., Any],
|
|
21
|
+
persist_model_binding: Callable[[Any, str | None], None],
|
|
22
|
+
mcp_attach_pending: Callable[[Any], bool],
|
|
23
|
+
) -> SlashResult:
|
|
24
|
+
from synapse.models.registry import (
|
|
25
|
+
apply_thinking_to_settings,
|
|
26
|
+
format_model_status,
|
|
27
|
+
is_thinking_token,
|
|
28
|
+
settings_thinking_label,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
reg = registry_from_settings(settings)
|
|
32
|
+
cfg_path = getattr(settings, "models_config_path", None)
|
|
33
|
+
active = getattr(agent, "_coding_model_profile", None) or settings.active_model or reg.default
|
|
34
|
+
allowed = reg.allowed_thinking_levels(active)
|
|
35
|
+
allowed_help = "|".join(allowed) if allowed else "off|low|medium|high|max"
|
|
36
|
+
|
|
37
|
+
if not args:
|
|
38
|
+
lines = [
|
|
39
|
+
f"active={active}",
|
|
40
|
+
f"display={format_model_status(settings)}",
|
|
41
|
+
f"thinking={settings_thinking_label(settings)}",
|
|
42
|
+
f"thinking_levels={', '.join(allowed)}",
|
|
43
|
+
]
|
|
44
|
+
if cfg_path:
|
|
45
|
+
lines.append(f"config={cfg_path}")
|
|
46
|
+
for name in reg.list_names():
|
|
47
|
+
p = reg.get(name)
|
|
48
|
+
mark = "*" if name == (settings.active_model or reg.default) else " "
|
|
49
|
+
base = f" base={p.base_url}" if p.base_url else ""
|
|
50
|
+
levels = reg.allowed_thinking_levels(name)
|
|
51
|
+
lines.append(
|
|
52
|
+
f"{mark} {name} -> {p.model} default_thinking={p.thinking_label()}"
|
|
53
|
+
f" levels=[{', '.join(levels)}]{base}"
|
|
54
|
+
)
|
|
55
|
+
lines.append("usage: /model <alias|provider:model> [thinking]")
|
|
56
|
+
lines.append(f" /model thinking <{allowed_help}>")
|
|
57
|
+
lines.append(" /model <alias> thinking <level>")
|
|
58
|
+
lines.append(
|
|
59
|
+
"note: thinking_levels are independent of model identity; "
|
|
60
|
+
"session thinking overrides profile default"
|
|
61
|
+
)
|
|
62
|
+
return SlashResult(handled=True, lines=lines)
|
|
63
|
+
|
|
64
|
+
# /model thinking <level>
|
|
65
|
+
if args[0].strip().casefold() in {"thinking", "effort", "reasoning"}:
|
|
66
|
+
if len(args) < 2:
|
|
67
|
+
return SlashResult(
|
|
68
|
+
handled=True,
|
|
69
|
+
lines=[f"usage: /model thinking <{allowed_help}>"],
|
|
70
|
+
error=True,
|
|
71
|
+
)
|
|
72
|
+
try:
|
|
73
|
+
label = apply_thinking_to_settings(settings, args[1], allowed=allowed)
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
return SlashResult(handled=True, lines=[str(exc)], error=True)
|
|
76
|
+
model_name = settings.active_model or reg.default
|
|
77
|
+
new_agent = None
|
|
78
|
+
note = ""
|
|
79
|
+
if apply_thinking_inplace(settings, agent, model_name):
|
|
80
|
+
note = " (live, no rebuild)"
|
|
81
|
+
else:
|
|
82
|
+
try:
|
|
83
|
+
new_agent = rebuild_agent(
|
|
84
|
+
settings,
|
|
85
|
+
project_root=project_root,
|
|
86
|
+
model_name=model_name,
|
|
87
|
+
agent=agent,
|
|
88
|
+
defer_mcp_reconnect=True,
|
|
89
|
+
)
|
|
90
|
+
except Exception as exc: # noqa: BLE001
|
|
91
|
+
return SlashResult(
|
|
92
|
+
handled=True,
|
|
93
|
+
lines=[f"thinking update failed: {exc}"],
|
|
94
|
+
error=True,
|
|
95
|
+
)
|
|
96
|
+
persist_model_binding(settings, thread_id)
|
|
97
|
+
return SlashResult(
|
|
98
|
+
handled=True,
|
|
99
|
+
lines=[f"thinking set to {label}{note} ({format_model_status(settings)})"],
|
|
100
|
+
agent=new_agent,
|
|
101
|
+
settings_changed=True,
|
|
102
|
+
mcp_attach_pending=bool(new_agent is not None and mcp_attach_pending(settings)),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
target = args[0].strip()
|
|
106
|
+
try:
|
|
107
|
+
profile = reg.get(target)
|
|
108
|
+
except KeyError as exc:
|
|
109
|
+
return SlashResult(handled=True, lines=[str(exc)], error=True)
|
|
110
|
+
|
|
111
|
+
from synapse.models.registry import apply_profile_to_settings
|
|
112
|
+
|
|
113
|
+
apply_profile_to_settings(settings, profile, seed_thinking=True)
|
|
114
|
+
|
|
115
|
+
# /model <alias> high
|
|
116
|
+
# /model <alias> thinking high
|
|
117
|
+
think_raw: str | None = None
|
|
118
|
+
if len(args) >= 3 and args[1].strip().casefold() in {
|
|
119
|
+
"thinking",
|
|
120
|
+
"effort",
|
|
121
|
+
"reasoning",
|
|
122
|
+
}:
|
|
123
|
+
think_raw = args[2]
|
|
124
|
+
elif len(args) >= 2 and is_thinking_token(args[1]):
|
|
125
|
+
think_raw = args[1]
|
|
126
|
+
elif len(args) >= 2 and args[1].strip().casefold() not in {
|
|
127
|
+
"thinking",
|
|
128
|
+
"effort",
|
|
129
|
+
"reasoning",
|
|
130
|
+
}:
|
|
131
|
+
# Second arg present but not a known thinking token -> error for clarity
|
|
132
|
+
return SlashResult(
|
|
133
|
+
handled=True,
|
|
134
|
+
lines=[
|
|
135
|
+
f"unknown thinking level: {args[1]}",
|
|
136
|
+
f"usage: /model <alias> [{allowed_help}]",
|
|
137
|
+
],
|
|
138
|
+
error=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if think_raw is not None:
|
|
142
|
+
try:
|
|
143
|
+
apply_thinking_to_settings(
|
|
144
|
+
settings,
|
|
145
|
+
think_raw,
|
|
146
|
+
allowed=reg.allowed_thinking_levels(profile.name),
|
|
147
|
+
)
|
|
148
|
+
except ValueError as exc:
|
|
149
|
+
return SlashResult(handled=True, lines=[str(exc)], error=True)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
new_agent = rebuild_agent(
|
|
153
|
+
settings,
|
|
154
|
+
project_root=project_root,
|
|
155
|
+
model_name=profile.name,
|
|
156
|
+
agent=agent,
|
|
157
|
+
defer_mcp_reconnect=True,
|
|
158
|
+
)
|
|
159
|
+
except Exception as exc: # noqa: BLE001
|
|
160
|
+
return SlashResult(
|
|
161
|
+
handled=True,
|
|
162
|
+
lines=[f"model switch failed: {exc}"],
|
|
163
|
+
error=True,
|
|
164
|
+
)
|
|
165
|
+
persist_model_binding(settings, thread_id)
|
|
166
|
+
mcp_attach_pending = mcp_attach_pending(settings)
|
|
167
|
+
return SlashResult(
|
|
168
|
+
handled=True,
|
|
169
|
+
lines=[f"model switched to {profile.name} ({format_model_status(settings)})"],
|
|
170
|
+
agent=new_agent,
|
|
171
|
+
settings_changed=True,
|
|
172
|
+
mcp_attach_pending=mcp_attach_pending,
|
|
173
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared command result contract."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SlashResult:
|
|
10
|
+
"""Outcome of a slash command."""
|
|
11
|
+
|
|
12
|
+
handled: bool = False
|
|
13
|
+
lines: list[str] = field(default_factory=list)
|
|
14
|
+
error: bool = False
|
|
15
|
+
# Short one-line confirmation for the bottom status bar (never transcript).
|
|
16
|
+
notice: str | None = None
|
|
17
|
+
exit_requested: bool = False
|
|
18
|
+
clear_log: bool = False
|
|
19
|
+
reload_transcript: bool = False
|
|
20
|
+
thread_id: str | None = None
|
|
21
|
+
agent: Any | None = None
|
|
22
|
+
settings_changed: bool = False
|
|
23
|
+
# TUI should attach MCP after applying the rebuilt agent, without blocking
|
|
24
|
+
# the slash command/model switch worker.
|
|
25
|
+
# Rich Markdown text rendered directly into the transcript (#log). When set,
|
|
26
|
+
# the TUI renders it as a Markdown block instead of plain lines.
|
|
27
|
+
markdown: str | None = None
|
|
28
|
+
mcp_attach_pending: bool = False
|
|
29
|
+
# UI theme switch (TUI should re-apply CSS / palette).
|
|
30
|
+
theme_name: str | None = None
|
|
31
|
+
# HITL: UI should resume the paused graph with this decision.
|
|
32
|
+
resume_action: str | None = None # "approve" | "reject"
|
|
33
|
+
resume_message: str | None = None
|
|
34
|
+
|