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/ui/rendering.py
ADDED
|
@@ -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
|
+
)
|