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,443 @@
1
+ """Session lifecycle and transcript export slash-command handler."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from synapse.commands.helpers import markdown_escape
9
+ from synapse.commands.result import SlashResult
10
+ from synapse.sessions.store import (
11
+ SessionStore,
12
+ allocate_thread_id,
13
+ binding_from_settings,
14
+ format_session_table,
15
+ )
16
+ from synapse.sessions.transcript import (
17
+ export_transcript_json,
18
+ export_transcript_markdown,
19
+ load_thread_messages,
20
+ )
21
+
22
+
23
+ def _store(settings: Any) -> SessionStore:
24
+ return SessionStore(settings.resolved_sessions_path())
25
+
26
+
27
+ def _model_name(settings: Any) -> str:
28
+ return str(getattr(settings, "model", "") or "")
29
+
30
+
31
+ def _session_show(store: SessionStore, thread_id: str, settings: Any) -> list[str]:
32
+ info = store.get(thread_id) or store.ensure(
33
+ thread_id,
34
+ model=_model_name(settings),
35
+ active_model=getattr(settings, "active_model", None),
36
+ thinking=binding_from_settings(settings).thinking,
37
+ )
38
+ bind = info.binding()
39
+ return [
40
+ f"current session: {info.thread_id}",
41
+ f" title: {info.title}",
42
+ f" model: {bind.display()}",
43
+ f" active_model: {info.active_model or '-'}",
44
+ f" thinking: {info.thinking or '-'}",
45
+ f" created: {info.created_at}",
46
+ f" updated: {info.updated_at}",
47
+ f" tags: {', '.join(info.tags) if info.tags else '-'}",
48
+ ]
49
+
50
+
51
+ def _md_session_show(store: SessionStore, thread_id: str, settings: Any) -> str:
52
+ """Format current session info as a Markdown table."""
53
+ info = store.get(thread_id) or store.ensure(
54
+ thread_id,
55
+ model=_model_name(settings),
56
+ active_model=getattr(settings, "active_model", None),
57
+ thinking=binding_from_settings(settings).thinking,
58
+ )
59
+ bind = info.binding()
60
+ rows = [
61
+ ("thread_id", f"`{info.thread_id}`"),
62
+ ("title", info.title),
63
+ ("model", bind.display()),
64
+ ("active_model", info.active_model or "-"),
65
+ ("thinking", info.thinking or "-"),
66
+ ("created", info.created_at),
67
+ ("updated", info.updated_at),
68
+ ("tags", ", ".join(info.tags) if info.tags else "-"),
69
+ ]
70
+ lines = ["## Current Session", "", "| Property | Value |", "|---|---|"]
71
+ for k, v in rows:
72
+ lines.append(f"| {markdown_escape(k)} | {markdown_escape(v)} |")
73
+ return "\n".join(lines)
74
+
75
+
76
+ def _md_session_table(items: list[Any]) -> str:
77
+ """Format session list as a Markdown table."""
78
+ if not items:
79
+ return "*No sessions found*"
80
+ lines = [f"## Sessions ({len(items)})", ""]
81
+ lines.append("| ID | Title | Model | Updated |")
82
+ lines.append("|---|---|---|---|")
83
+ for s in items:
84
+ tid = s.thread_id[:12]
85
+ title = (s.title or "-")[:48]
86
+ model = (s.binding().display() or "-")[:20]
87
+ updated = s.updated_at or "-"
88
+ lines.append(
89
+ f"| `{markdown_escape(tid)}` | {markdown_escape(title)} "
90
+ f"| {markdown_escape(model)} | {markdown_escape(updated)} |"
91
+ )
92
+ return "\n".join(lines)
93
+ def _load_messages(agent: Any, settings: Any, thread_id: str) -> list[Any]:
94
+ return load_thread_messages(agent=agent, settings=settings, thread_id=thread_id)
95
+
96
+
97
+ def _normalize_export_fmt(fmt: str | None) -> str:
98
+ raw = (fmt or "md").strip().lower()
99
+ if raw in {"json", "j"}:
100
+ return "json"
101
+ return "md"
102
+
103
+
104
+ def _default_export_path(settings: Any, thread_id: str, fmt: str) -> Path:
105
+ """Default export location next to session/checkpoint state."""
106
+ ext = "json" if fmt == "json" else "md"
107
+ safe = "".join(c if c.isalnum() or c in "-_" else "_" for c in (thread_id or "session"))
108
+ safe = (safe or "session")[:80]
109
+ parent = Path(settings.checkpoint_path).expanduser().resolve().parent
110
+ return parent / "exports" / f"{safe}.{ext}"
111
+
112
+
113
+ def _export_lines(
114
+ *,
115
+ settings: Any,
116
+ agent: Any,
117
+ thread_id: str,
118
+ fmt: str,
119
+ out_path: Path | None,
120
+ ) -> SlashResult:
121
+ """Export transcript to a file only (never dump body into TUI/chat log)."""
122
+ store = _store(settings)
123
+ model = _model_name(settings)
124
+ info = store.get(thread_id) or store.ensure(thread_id, model=model)
125
+ messages = _load_messages(agent, settings, thread_id)
126
+ fmt_n = _normalize_export_fmt(fmt)
127
+
128
+ if fmt_n == "json":
129
+ payload = export_transcript_json(
130
+ thread_id=thread_id,
131
+ title=info.title,
132
+ model=info.model or model,
133
+ messages=messages,
134
+ meta=info.to_dict(),
135
+ )
136
+ text = json.dumps(payload, ensure_ascii=False, indent=2)
137
+ else:
138
+ text = export_transcript_markdown(
139
+ thread_id=thread_id,
140
+ title=info.title,
141
+ model=info.model or model,
142
+ messages=messages,
143
+ )
144
+ # Keep metadata section readable when transcript empty.
145
+ if not messages:
146
+ meta = store.export_markdown(thread_id) or ""
147
+ text = meta + "\n## Transcript\n\n(no checkpoint messages found)\n"
148
+
149
+ target = out_path if out_path is not None else _default_export_path(settings, thread_id, fmt_n)
150
+ try:
151
+ target = Path(target).expanduser()
152
+ if not target.is_absolute():
153
+ target = (Path.cwd() / target).resolve()
154
+ else:
155
+ target = target.resolve()
156
+ target.parent.mkdir(parents=True, exist_ok=True)
157
+ target.write_text(text, encoding="utf-8")
158
+ except OSError as exc:
159
+ return SlashResult(
160
+ handled=True,
161
+ lines=[f"export failed: {exc}"],
162
+ error=True,
163
+ )
164
+
165
+ confirm = f"exported {fmt_n} -> {target}"
166
+ return SlashResult(
167
+ handled=True,
168
+ lines=[confirm, f"messages: {len(messages)}"],
169
+ notice=confirm,
170
+ markdown=(
171
+ "## Export\n\n"
172
+ f"- **format**: {fmt_n}\n"
173
+ f"- **path**: `{target}`\n"
174
+ f"- **messages**: {len(messages)}\n"
175
+ ),
176
+ )
177
+
178
+
179
+ def handle_session(
180
+ cmd: str,
181
+ args: list[str],
182
+ *,
183
+ settings: Any,
184
+ agent: Any,
185
+ thread_id: str,
186
+ ) -> SlashResult:
187
+ store = _store(settings)
188
+ model = _model_name(settings)
189
+
190
+ if cmd in {"/sessions", "/session"} and not args:
191
+ if cmd == "/session":
192
+ return SlashResult(
193
+ handled=True,
194
+ lines=_session_show(store, thread_id, settings),
195
+ markdown=_md_session_show(store, thread_id, settings),
196
+ )
197
+ return SlashResult(
198
+ handled=True,
199
+ lines=[format_session_table(store.list_nonempty())],
200
+ markdown=_md_session_table(store.list_nonempty()),
201
+ )
202
+
203
+ if cmd == "/sessions" and args:
204
+ sub = args[0].lower()
205
+ rest = args[1:]
206
+ return handle_session(
207
+ "/session",
208
+ [sub, *rest],
209
+ settings=settings,
210
+ agent=agent,
211
+ thread_id=thread_id,
212
+ )
213
+
214
+ if cmd == "/new":
215
+ tid = allocate_thread_id()
216
+ bind = binding_from_settings(settings)
217
+ # Do not persist until the first user message (avoids empty session junk).
218
+ store.set_last_model_binding(bind)
219
+ return SlashResult(
220
+ handled=True,
221
+ lines=[
222
+ f"new session thread_id={tid} model={bind.display()}",
223
+ "session metadata is saved on the first message",
224
+ ],
225
+ markdown=(
226
+ "## New Session\n\n"
227
+ f"- **thread_id**: `{tid}`\n"
228
+ f"- **model**: {bind.display()}\n\n"
229
+ "*Session metadata is saved on the first message.*"
230
+ ),
231
+ thread_id=tid,
232
+ clear_log=True,
233
+ reload_transcript=False,
234
+ )
235
+
236
+ if cmd == "/switch":
237
+ if not args:
238
+ return SlashResult(
239
+ handled=True,
240
+ lines=["usage: /switch <thread_id|title>"],
241
+ error=True,
242
+ )
243
+ query = " ".join(args).strip()
244
+ info = store.resolve_session_ref(query)
245
+ if info is None:
246
+ tid = args[0].strip()
247
+ if len(args) > 1 or " " in query:
248
+ return SlashResult(
249
+ handled=True,
250
+ lines=[
251
+ f"session not found: {query}",
252
+ "tip: /sessions 鈥?list titles; match must be unique",
253
+ ],
254
+ error=True,
255
+ )
256
+ store.ensure(tid, model=model)
257
+ return SlashResult(
258
+ handled=True,
259
+ lines=[f"switched thread_id={tid}"],
260
+ markdown=f"## Switched\n\n- **thread_id**: `{tid}`",
261
+ thread_id=tid,
262
+ settings_changed=True,
263
+ clear_log=True,
264
+ reload_transcript=True,
265
+ )
266
+ return SlashResult(
267
+ handled=True,
268
+ lines=[f"switched thread_id={info.thread_id} title={info.title}"],
269
+ markdown=(
270
+ "## Switched\n\n"
271
+ f"- **thread_id**: `{info.thread_id}`\n"
272
+ f"- **title**: {markdown_escape(info.title)}"
273
+ ),
274
+ thread_id=info.thread_id,
275
+ settings_changed=True,
276
+ clear_log=True,
277
+ reload_transcript=True,
278
+ )
279
+
280
+ if cmd == "/rename":
281
+ if not args:
282
+ return SlashResult(handled=True, lines=["usage: /rename <title>"], error=True)
283
+ title = " ".join(args).strip()
284
+ store.ensure(thread_id, model=model)
285
+ info = store.rename(thread_id, title)
286
+ new_title = info.title if info else title
287
+ return SlashResult(
288
+ handled=True,
289
+ lines=[f"renamed to: {new_title}"],
290
+ markdown=f"## Renamed\n\n- **title**: {markdown_escape(new_title)}",
291
+ )
292
+
293
+ if cmd == "/export":
294
+ fmt = "md"
295
+ out_path: Path | None = None
296
+ if args:
297
+ first = args[0].lower()
298
+ if first in {"md", "markdown", "m", "json", "j"}:
299
+ fmt = "json" if first in {"json", "j"} else "md"
300
+ if len(args) >= 2:
301
+ out_path = Path(" ".join(args[1:])).expanduser()
302
+ else:
303
+ # /export path/to/file.md (format inferred from suffix)
304
+ out_path = Path(" ".join(args)).expanduser()
305
+ suffix = out_path.suffix.lower()
306
+ if suffix == ".json":
307
+ fmt = "json"
308
+ else:
309
+ fmt = "md"
310
+ return _export_lines(
311
+ settings=settings,
312
+ agent=agent,
313
+ thread_id=thread_id,
314
+ fmt=fmt,
315
+ out_path=out_path,
316
+ )
317
+
318
+ if cmd != "/session":
319
+ return SlashResult(handled=False)
320
+
321
+ if not args:
322
+ return SlashResult(
323
+ handled=True,
324
+ lines=_session_show(store, thread_id, settings),
325
+ markdown=_md_session_show(store, thread_id, settings),
326
+ )
327
+
328
+ sub = args[0].lower()
329
+ rest = args[1:]
330
+
331
+ if sub in {"list", "ls"}:
332
+ limit = 50
333
+ if rest:
334
+ try:
335
+ limit = max(1, int(rest[0]))
336
+ except ValueError:
337
+ return SlashResult(
338
+ handled=True,
339
+ lines=["usage: /session list [n]"],
340
+ error=True,
341
+ )
342
+ sessions = store.list_nonempty(limit=limit)
343
+ return SlashResult(
344
+ handled=True,
345
+ lines=[format_session_table(sessions)],
346
+ markdown=_md_session_table(sessions),
347
+ )
348
+
349
+ if sub == "prune":
350
+ deleted = store.prune_empty(except_ids={thread_id} if thread_id else set())
351
+ lines = [f"pruned {len(deleted)} empty session(s)"]
352
+ lines.extend(f" - {tid}" for tid in deleted[:20])
353
+ if len(deleted) > 20:
354
+ lines.append(f" 鈥?and {len(deleted) - 20} more")
355
+ md = f"## Pruned\n\n**{len(deleted)}** empty session(s) removed.\n"
356
+ if deleted:
357
+ md += "\n" + "\n".join(f"- `{tid}`" for tid in deleted[:20])
358
+ if len(deleted) > 20:
359
+ md += f"\n- *鈥?and {len(deleted) - 20} more*"
360
+ return SlashResult(handled=True, lines=lines, markdown=md)
361
+
362
+ if sub == "show":
363
+ if rest:
364
+ info = store.resolve_session_ref(" ".join(rest))
365
+ if info is None:
366
+ return SlashResult(
367
+ handled=True,
368
+ lines=[f"session not found: {' '.join(rest)}"],
369
+ error=True,
370
+ )
371
+ tid = info.thread_id
372
+ else:
373
+ tid = thread_id
374
+ store.ensure(tid, model=model)
375
+ return SlashResult(
376
+ handled=True,
377
+ lines=_session_show(store, tid, settings),
378
+ markdown=_md_session_show(store, tid, settings),
379
+ )
380
+
381
+ if sub == "new":
382
+ return handle_session("/new", [], settings=settings, agent=agent, thread_id=thread_id)
383
+
384
+ if sub == "switch":
385
+ return handle_session("/switch", rest, settings=settings, agent=agent, thread_id=thread_id)
386
+
387
+ if sub == "rename":
388
+ return handle_session("/rename", rest, settings=settings, agent=agent, thread_id=thread_id)
389
+
390
+ if sub == "delete":
391
+ if not rest:
392
+ return SlashResult(
393
+ handled=True,
394
+ lines=["usage: /session delete <thread_id|title>"],
395
+ error=True,
396
+ )
397
+ query = " ".join(rest).strip()
398
+ info = store.resolve_session_ref(query)
399
+ tid = info.thread_id if info is not None else rest[0]
400
+ if tid == thread_id:
401
+ return SlashResult(
402
+ handled=True,
403
+ lines=["cannot delete the active session; /switch first"],
404
+ error=True,
405
+ )
406
+ ok = store.delete(tid)
407
+ if ok:
408
+ label = info.title if info is not None else tid
409
+ return SlashResult(
410
+ handled=True,
411
+ lines=[f"deleted session metadata: {tid} ({label})"],
412
+ markdown=(
413
+ f"## Deleted\n\n- **thread_id**: `{tid}`\n- **title**: {markdown_escape(label)}"
414
+ ),
415
+ )
416
+ return SlashResult(handled=True, lines=[f"session not found: {query}"], error=True)
417
+
418
+ if sub == "search":
419
+ if not rest:
420
+ return SlashResult(
421
+ handled=True,
422
+ lines=["usage: /session search <query>"],
423
+ error=True,
424
+ )
425
+ q = " ".join(rest)
426
+ results = store.search(q)
427
+ return SlashResult(
428
+ handled=True,
429
+ lines=[format_session_table(results)],
430
+ markdown=_md_session_table(results),
431
+ )
432
+
433
+ if sub == "export":
434
+ return handle_session("/export", rest, settings=settings, agent=agent, thread_id=thread_id)
435
+
436
+ return SlashResult(
437
+ handled=True,
438
+ lines=[
439
+ "usage: /session [list|show|new|switch|rename|delete|search|export]",
440
+ "also: /sessions /new /switch /rename /export",
441
+ ],
442
+ error=True,
443
+ )