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.
Files changed (131) hide show
  1. synapse/__init__.py +13 -0
  2. synapse/__main__.py +6 -0
  3. synapse/app/__init__.py +1 -0
  4. synapse/app/agent.py +492 -0
  5. synapse/app/agent_md.py +107 -0
  6. synapse/cli.py +750 -0
  7. synapse/commands/__init__.py +1 -0
  8. synapse/commands/compression.py +573 -0
  9. synapse/commands/helpers.py +22 -0
  10. synapse/commands/mcp.py +406 -0
  11. synapse/commands/model.py +173 -0
  12. synapse/commands/result.py +34 -0
  13. synapse/commands/sessions.py +443 -0
  14. synapse/commands/slash_cmds.py +521 -0
  15. synapse/commands/slash_complete.py +816 -0
  16. synapse/commands/theme.py +99 -0
  17. synapse/config.py +27 -0
  18. synapse/content/__init__.py +1 -0
  19. synapse/content/input_history.py +122 -0
  20. synapse/content/multimodal.py +733 -0
  21. synapse/content/prompts.py +249 -0
  22. synapse/content/skills_catalog.py +128 -0
  23. synapse/integrations/__init__.py +1 -0
  24. synapse/integrations/checkpoint_seed.py +281 -0
  25. synapse/integrations/codex_history.py +375 -0
  26. synapse/integrations/codex_import.py +393 -0
  27. synapse/integrations/codex_sessions.py +629 -0
  28. synapse/integrations/describe_image.py +370 -0
  29. synapse/integrations/http_clients.py +199 -0
  30. synapse/integrations/llm_openai_compat.py +90 -0
  31. synapse/integrations/llm_openai_websocket.py +187 -0
  32. synapse/integrations/mcp_client.py +646 -0
  33. synapse/integrations/vision_middleware.py +62 -0
  34. synapse/models/__init__.py +5 -0
  35. synapse/models/config.py +240 -0
  36. synapse/models/helpers.py +206 -0
  37. synapse/models/profile.py +59 -0
  38. synapse/models/registry.py +722 -0
  39. synapse/models_registry.py +7 -0
  40. synapse/observability/__init__.py +1 -0
  41. synapse/observability/startup_trace.py +127 -0
  42. synapse/runtime/__init__.py +1 -0
  43. synapse/runtime/async_runtime.py +176 -0
  44. synapse/runtime/backends.py +458 -0
  45. synapse/runtime/context_compact.py +249 -0
  46. synapse/runtime/execute_capture.py +48 -0
  47. synapse/runtime/fs_permissions.py +79 -0
  48. synapse/runtime/harness.py +57 -0
  49. synapse/runtime/hitl.py +197 -0
  50. synapse/runtime/interaction_ledger.py +82 -0
  51. synapse/runtime/middleware.py +802 -0
  52. synapse/runtime/model_request_compression_middleware.py +745 -0
  53. synapse/runtime/pathing.py +146 -0
  54. synapse/runtime/safety.py +184 -0
  55. synapse/runtime/steer.py +240 -0
  56. synapse/runtime/subagents.py +207 -0
  57. synapse/runtime/tool_ignore.py +221 -0
  58. synapse/runtime/tool_output_eval.py +118 -0
  59. synapse/runtime/tool_output_middleware.py +585 -0
  60. synapse/runtime/tool_output_usage_middleware.py +60 -0
  61. synapse/sessions/__init__.py +31 -0
  62. synapse/sessions/cancel_repair.py +208 -0
  63. synapse/sessions/session_recap.py +174 -0
  64. synapse/sessions/store.py +695 -0
  65. synapse/sessions/transcript.py +754 -0
  66. synapse/settings/__init__.py +5 -0
  67. synapse/settings/config_paths.py +184 -0
  68. synapse/settings/schema.py +464 -0
  69. synapse/tool_output/__init__.py +59 -0
  70. synapse/tool_output/detection.py +170 -0
  71. synapse/tool_output/metrics.py +32 -0
  72. synapse/tool_output/models.py +173 -0
  73. synapse/tool_output/pipeline.py +330 -0
  74. synapse/tool_output/repository.py +721 -0
  75. synapse/tool_output/transformers.py +648 -0
  76. synapse/tools/__init__.py +5 -0
  77. synapse/tools/session_tools.py +204 -0
  78. synapse/ui/__init__.py +10 -0
  79. synapse/ui/bottombar/__init__.py +73 -0
  80. synapse/ui/bottombar/components/__init__.py +143 -0
  81. synapse/ui/bottombar/components/key_hints.py +30 -0
  82. synapse/ui/bottombar/components/mcp.py +64 -0
  83. synapse/ui/bottombar/components/mode.py +24 -0
  84. synapse/ui/bottombar/components/model.py +28 -0
  85. synapse/ui/bottombar/components/thread.py +29 -0
  86. synapse/ui/bottombar/context.py +36 -0
  87. synapse/ui/bottombar/core.py +74 -0
  88. synapse/ui/dialogs/__init__.py +25 -0
  89. synapse/ui/dialogs/base.py +362 -0
  90. synapse/ui/dialogs/codex_session_list.py +84 -0
  91. synapse/ui/dialogs/compression_diagnostics.py +210 -0
  92. synapse/ui/dialogs/git_explore.py +702 -0
  93. synapse/ui/dialogs/mcp_panel.py +407 -0
  94. synapse/ui/dialogs/model_picker.py +128 -0
  95. synapse/ui/dialogs/safety_panel.py +63 -0
  96. synapse/ui/dialogs/session_list.py +98 -0
  97. synapse/ui/dialogs/theme_designer.py +863 -0
  98. synapse/ui/dialogs/theme_picker.py +113 -0
  99. synapse/ui/git_explore/__init__.py +31 -0
  100. synapse/ui/git_explore/engine.py +82 -0
  101. synapse/ui/git_explore/provider.py +242 -0
  102. synapse/ui/git_explore/unified.py +85 -0
  103. synapse/ui/rendering.py +350 -0
  104. synapse/ui/sink.py +70 -0
  105. synapse/ui/steer_widget.py +367 -0
  106. synapse/ui/stream.py +1207 -0
  107. synapse/ui/stream_events.py +421 -0
  108. synapse/ui/stream_runtime.py +252 -0
  109. synapse/ui/theme.py +1154 -0
  110. synapse/ui/timeline.py +621 -0
  111. synapse/ui/topbar/__init__.py +97 -0
  112. synapse/ui/topbar/components/__init__.py +150 -0
  113. synapse/ui/topbar/components/branch.py +41 -0
  114. synapse/ui/topbar/components/title.py +24 -0
  115. synapse/ui/topbar/components/tool_output.py +24 -0
  116. synapse/ui/topbar/components/usage.py +24 -0
  117. synapse/ui/topbar/components/workspace.py +32 -0
  118. synapse/ui/topbar/context.py +32 -0
  119. synapse/ui/topbar/core.py +979 -0
  120. synapse/ui/topbar/git_changes_popover.py +178 -0
  121. synapse/ui/topbar/git_chrome.py +475 -0
  122. synapse/ui/topbar/tool_output_popover.py +84 -0
  123. synapse/ui/topbar/widget.py +474 -0
  124. synapse/ui/tui.py +5717 -0
  125. synapse/ui/turn_rail.py +71 -0
  126. synapse/ui/user_turn.py +83 -0
  127. synapse/ui/welcome.py +261 -0
  128. synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
  129. synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
  130. synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
  131. 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)