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,350 @@
1
+ """Rich Markdown, LaTeX, and Mermaid rendering helpers."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import threading
6
+ from typing import ClassVar
7
+
8
+ from rich import box
9
+ from rich.console import Console, ConsoleOptions, RenderResult
10
+ from rich.markdown import CodeBlock as _CodeBlock
11
+ from rich.markdown import Markdown as _Markdown
12
+ from rich.markdown import MarkdownElement
13
+ from rich.markdown import TableElement as _TableElement
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+
18
+ console = Console(highlight=False, soft_wrap=True, emoji=False)
19
+
20
+ def _theme():
21
+ try:
22
+ from synapse.ui.theme import get_theme
23
+
24
+ return get_theme()
25
+ except Exception: # noqa: BLE001
26
+ return None
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # LaTeX math → Unicode art (powered by TeXicode)
31
+ # ---------------------------------------------------------------------------
32
+
33
+ _LATEX_MATH_RE = re.compile(
34
+ r"\$\$.*?\$\$|\$.*?\$|\\\[.*?\\\]|\\\(.*?\\\)|\\begin\{.*?\}.*?\\end\{.*?\}",
35
+ re.DOTALL,
36
+ )
37
+
38
+
39
+ _TEXICODE_ERROR_PREFIX = "texicode:"
40
+
41
+
42
+ def _replace_latex(match: re.Match) -> str:
43
+ """Render one LaTeX block, preserving source when TeXicode cannot parse it."""
44
+ tex_block = match.group(0)
45
+ if tex_block.startswith("$$"):
46
+ clean = tex_block[2:-2]
47
+ ctx = "md_block"
48
+ elif tex_block.startswith("\\["):
49
+ clean = tex_block[2:-2]
50
+ ctx = "md_block"
51
+ elif tex_block.startswith("\\("):
52
+ clean = tex_block[2:-2]
53
+ ctx = "md_inline"
54
+ elif tex_block.startswith("\\begin"):
55
+ clean = tex_block
56
+ ctx = "md_block"
57
+ else:
58
+ clean = tex_block[1:-1]
59
+ ctx = "md_inline"
60
+ try:
61
+ from texicode.pipeline import render_tex
62
+
63
+ # TeXicode returns lexer/parser/renderer failures as ordinary strings
64
+ # instead of raising, so validate the rendered value below.
65
+ rendered = render_tex(clean, False, False, ctx, {"fonts": "normal"})
66
+ except Exception: # noqa: BLE001
67
+ return tex_block
68
+
69
+ if not isinstance(rendered, str) or not rendered.strip():
70
+ return tex_block
71
+ if _TEXICODE_ERROR_PREFIX in rendered.casefold():
72
+ return tex_block
73
+ return rendered
74
+
75
+
76
+ def render_math_in_text(text: str) -> str:
77
+ """Replace $$...$$ / $...$ / \\[...\\] / \\(...\\) with Unicode math art."""
78
+ return _LATEX_MATH_RE.sub(_replace_latex, text)
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Rich Markdown rendering
83
+ # ---------------------------------------------------------------------------
84
+
85
+ _MERMAID_LANGS = frozenset({"mermaid", "mmd"})
86
+
87
+ # Hard caps so dense graphs cannot pin the Textual event loop.
88
+ # termaid pathfinding is CPU-bound; Textual layout/measure re-enters Rich
89
+ # render many times for one answer seal.
90
+ _MERMAID_MAX_SOURCE_CHARS = 6_000
91
+ _MERMAID_MAX_EDGES = 48
92
+ _MERMAID_MAX_NODES = 28
93
+ _MERMAID_RENDER_TIMEOUT_S = 0.35
94
+ _MERMAID_EDGE_RE = re.compile(
95
+ r"(?:-->|---|-\.-|==>|==|~~>|~~|-.->|-->>|<-+>|<-+|-+\.|o--+|x--+)"
96
+ )
97
+ _MERMAID_NODE_RE = re.compile(r"\b([A-Za-z][\w-]*)\b")
98
+ _MERMAID_SKIP_TOKENS = frozenset(
99
+ {
100
+ "graph",
101
+ "flowchart",
102
+ "subgraph",
103
+ "end",
104
+ "classdef",
105
+ "class",
106
+ "style",
107
+ "linkstyle",
108
+ "click",
109
+ "direction",
110
+ "tb",
111
+ "td",
112
+ "bt",
113
+ "rl",
114
+ "lr",
115
+ "statediagram",
116
+ "statediagram-v2",
117
+ "sequencediagram",
118
+ "participant",
119
+ "actor",
120
+ "note",
121
+ "loop",
122
+ "alt",
123
+ "else",
124
+ "opt",
125
+ "par",
126
+ "and",
127
+ "rect",
128
+ "activate",
129
+ "deactivate",
130
+ "autonumber",
131
+ }
132
+ )
133
+ # source -> rendered Text, or None meaning "known bad / too heavy / timed out"
134
+ _mermaid_render_cache: dict[str, Text | None] = {}
135
+ _mermaid_executor = None
136
+ _mermaid_executor_lock = threading.Lock()
137
+
138
+
139
+ def _get_mermaid_executor():
140
+ """Single-worker pool so a timed-out render cannot pile up CPU workers."""
141
+ global _mermaid_executor
142
+ with _mermaid_executor_lock:
143
+ if _mermaid_executor is None:
144
+ import concurrent.futures
145
+
146
+ _mermaid_executor = concurrent.futures.ThreadPoolExecutor(
147
+ max_workers=1,
148
+ thread_name_prefix="synapse-mermaid",
149
+ )
150
+ return _mermaid_executor
151
+
152
+
153
+ def _replace_mermaid_executor() -> None:
154
+ """Drop a timed-out worker without waiting for termaid to finish."""
155
+ global _mermaid_executor
156
+ with _mermaid_executor_lock:
157
+ old = _mermaid_executor
158
+ _mermaid_executor = None
159
+ if old is not None:
160
+ try:
161
+ old.shutdown(wait=False, cancel_futures=True)
162
+ except Exception: # noqa: BLE001
163
+ pass
164
+
165
+
166
+ def _mermaid_complexity(source: str) -> tuple[int, int]:
167
+ """Rough (edge_count, node_count) estimate for cheap preflight limits."""
168
+ edges = len(_MERMAID_EDGE_RE.findall(source))
169
+ nodes: set[str] = set()
170
+ for token in _MERMAID_NODE_RE.findall(source):
171
+ low = token.lower()
172
+ if low in _MERMAID_SKIP_TOKENS:
173
+ continue
174
+ nodes.add(token)
175
+ return edges, len(nodes)
176
+
177
+
178
+ def _render_mermaid_text(source: str) -> Text:
179
+ from termaid import render_rich
180
+
181
+ rendered = render_rich(source)
182
+ if isinstance(rendered, Text):
183
+ return rendered
184
+ return Text(str(rendered))
185
+
186
+
187
+ def render_mermaid_diagram(source: str) -> Text | None:
188
+ """Render mermaid to Rich Text with cache, complexity caps, and a hard timeout.
189
+
190
+ Returns ``None`` when the diagram should fall back to the source fence.
191
+ """
192
+ text = (source or "").strip()
193
+ if not text:
194
+ return None
195
+ if text in _mermaid_render_cache:
196
+ return _mermaid_render_cache[text]
197
+
198
+ if len(text) > _MERMAID_MAX_SOURCE_CHARS:
199
+ _mermaid_render_cache[text] = None
200
+ return None
201
+
202
+ edges, nodes = _mermaid_complexity(text)
203
+ if edges > _MERMAID_MAX_EDGES or nodes > _MERMAID_MAX_NODES:
204
+ _mermaid_render_cache[text] = None
205
+ return None
206
+
207
+ # Mark in-flight as failed first so concurrent re-entries do not stampede.
208
+ _mermaid_render_cache[text] = None
209
+ try:
210
+ future = _get_mermaid_executor().submit(_render_mermaid_text, text)
211
+ rendered = future.result(timeout=_MERMAID_RENDER_TIMEOUT_S)
212
+ except TimeoutError:
213
+ # Abandon the busy worker so the next diagram is not queued behind it.
214
+ _replace_mermaid_executor()
215
+ return None
216
+ except Exception: # noqa: BLE001 — parse / layout failures all fall back
217
+ return None
218
+
219
+ _mermaid_render_cache[text] = rendered
220
+ return rendered
221
+
222
+
223
+ class _MermaidCodeBlock(_CodeBlock):
224
+ """Code fence that draws mermaid via termaid ``render_rich`` (Rich Text).
225
+
226
+ Non-mermaid fences keep Rich's default Syntax highlighting.
227
+ On termaid failure / timeout / oversize input, falls back to the source fence.
228
+ """
229
+
230
+ def __rich_console__(
231
+ self, console: Console, options: ConsoleOptions
232
+ ) -> RenderResult:
233
+ lexer = (self.lexer_name or "").strip().lower()
234
+ if lexer in _MERMAID_LANGS:
235
+ source = str(self.text).strip()
236
+ if source:
237
+ rendered = render_mermaid_diagram(source)
238
+ if rendered is not None:
239
+ yield rendered
240
+ return
241
+ yield from super().__rich_console__(console, options)
242
+
243
+
244
+ class _FullTableElement(_TableElement):
245
+ """Rich table element with full rounded borders instead of just a header line."""
246
+
247
+ def __rich_console__(
248
+ self, console: Console, options: ConsoleOptions
249
+ ) -> RenderResult:
250
+ table = Table(
251
+ box=box.ROUNDED,
252
+ pad_edge=False,
253
+ style="markdown.table.border",
254
+ show_edge=True,
255
+ show_lines=True, # draw grid lines between body rows/cells
256
+ collapse_padding=True,
257
+ )
258
+
259
+ if self.header is not None and self.header.row is not None:
260
+ for column in self.header.row.cells:
261
+ heading = column.content.copy()
262
+ heading.stylize("markdown.table.header")
263
+ table.add_column(heading)
264
+
265
+ if self.body is not None:
266
+ for row in self.body.rows:
267
+ row_content = [element.content for element in row.cells]
268
+ table.add_row(*row_content)
269
+
270
+ yield table
271
+
272
+
273
+ class _FullBorderMarkdown(_Markdown):
274
+ """Rich Markdown with full table borders and mermaid diagram fences."""
275
+
276
+ elements: ClassVar[dict[str, type[MarkdownElement]]] = {
277
+ **_Markdown.elements,
278
+ "table_open": _FullTableElement,
279
+ "fence": _MermaidCodeBlock,
280
+ "code_block": _MermaidCodeBlock,
281
+ }
282
+
283
+
284
+ def render_markdown(text: str) -> _Markdown:
285
+ """Build a Rich Markdown renderable for assistant answers."""
286
+ theme = _theme()
287
+ code_theme = getattr(theme, "code_theme", None) or "monokai"
288
+ # LaTeX math preprocessor; mermaid is drawn inside Markdown fence elements.
289
+ text = render_math_in_text(text)
290
+ return _FullBorderMarkdown(
291
+ text or "(empty response)",
292
+ code_theme=code_theme,
293
+ hyperlinks=True,
294
+ )
295
+
296
+
297
+ def print_markdown(text: str) -> None:
298
+ """Print markdown body without a panel."""
299
+ console.print(render_markdown(text))
300
+
301
+
302
+ def print_banner(workspace: str, model: str, require_approval: bool) -> None:
303
+ approval = "ON" if require_approval else "OFF (auto-pass)"
304
+ theme = _theme()
305
+ border = getattr(theme, "rich_info_border", None) or "blue"
306
+ console.print(
307
+ Panel.fit(
308
+ f"[bold]Coding Agent[/bold]\n"
309
+ f"workspace: [cyan]{workspace}[/cyan]\n"
310
+ f"model: [green]{model}[/green]\n"
311
+ f"approval: [yellow]{approval}[/yellow]\n"
312
+ f"backend: LocalShell · parallel tools · token/reasoning stream",
313
+ border_style=border,
314
+ )
315
+ )
316
+
317
+
318
+ def print_user(text: str) -> None:
319
+ theme = _theme()
320
+ style = getattr(theme, "rich_user", None) or "bold cyan"
321
+ console.print(Text(f"You: {text}", style=style))
322
+
323
+
324
+ def print_error(message: str) -> None:
325
+ theme = _theme()
326
+ style = getattr(theme, "rich_error", None) or "bold red"
327
+ # Keep the ERROR: prefix visible even if theme uses hex colors.
328
+ if " " in style and not style.startswith("bold "):
329
+ console.print(f"[{style}]ERROR:[/{style}] {message}")
330
+ else:
331
+ console.print(f"[{style}]ERROR:[/{style}] {message}")
332
+
333
+
334
+ def print_info(message: str) -> None:
335
+ console.print(f"[dim]{message}[/dim]")
336
+
337
+
338
+ def print_final(text: str) -> None:
339
+ """Print the final assistant answer with markdown rendering."""
340
+ theme = _theme()
341
+ border = getattr(theme, "rich_ok_border", None) or "green"
342
+ console.print()
343
+ console.print(
344
+ Panel(
345
+ render_markdown(text),
346
+ title="Assistant",
347
+ border_style=border,
348
+ padding=(0, 1),
349
+ )
350
+ )
synapse/ui/sink.py ADDED
@@ -0,0 +1,70 @@
1
+ """UI-agnostic stream sink protocol.
2
+
3
+ CLI (Rich) and TUI (Textual) both consume the same stream_agent loop via this port.
4
+
5
+ Optional tool-item methods (duck-typed by stream_agent):
6
+ - tool_item_started / tool_item_finished / tool_group_closed
7
+ - tool_item_updated (optional; refresh label after streaming args)
8
+ Falls back to tool_calls_started / tool_result when absent.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Protocol, runtime_checkable
14
+
15
+
16
+ @runtime_checkable
17
+ class StreamSink(Protocol):
18
+ """Consumer of one agent turn's streaming events."""
19
+
20
+ streamed_answer: bool
21
+ answer_buf: list[str]
22
+ reasoning_buf: list[str]
23
+ streamed_reasoning: bool
24
+
25
+ def activity_start(self, phase: str = "thinking", detail: str = "waiting for model") -> None:
26
+ """Start/replace the activity indicator."""
27
+
28
+ def activity_update(
29
+ self,
30
+ phase: str,
31
+ detail: str = "",
32
+ *,
33
+ reset_timer: bool = False,
34
+ ) -> None:
35
+ """Update activity phase/detail."""
36
+
37
+ def activity_stop(self) -> None:
38
+ """Stop activity indicator."""
39
+
40
+ def write_reasoning(self, text: str) -> None:
41
+ """Buffer reasoning/thinking tokens."""
42
+
43
+ def close_reasoning(self) -> None:
44
+ """Commit buffered reasoning (if any)."""
45
+
46
+ def write_answer_token(self, text: str, *, msg_id: str | None = None) -> None:
47
+ """Buffer answer tokens (no permanent render yet)."""
48
+
49
+ def write_answer_complete(self, text: str, *, msg_id: str | None = None) -> None:
50
+ """Commit a complete assistant message."""
51
+
52
+ def finalize_line(self) -> None:
53
+ """Flush open reasoning/answer buffers before tool calls or end."""
54
+
55
+ def tool_calls_started(self, calls: list[Any], *, parallel: bool) -> None:
56
+ """Model requested one or more tool calls (legacy bulk API)."""
57
+
58
+ def tool_result(self, name: str, status: str, *, sub: bool = False) -> None:
59
+ """Tool returned a result summary (legacy bulk API)."""
60
+
61
+ def info(self, message: str) -> None:
62
+ """Non-error status line."""
63
+
64
+
65
+ def sink_supports_tool_items(sink: Any) -> bool:
66
+ """True if sink implements Cursor-style per-item tool events."""
67
+ return all(
68
+ callable(getattr(sink, name, None))
69
+ for name in ("tool_item_started", "tool_item_finished", "tool_group_closed")
70
+ )