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,204 @@
1
+ """跨会话引用工具 —— 让 Agent 能查阅其他会话的对话历史。
2
+
3
+ 通过工厂函数 ``build_session_tools`` 创建工具,注入 SessionStore 和
4
+ checkpoint 路径依赖。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from langchain.tools import ToolRuntime
14
+ from langchain_core.tools import tool
15
+
16
+ from synapse.tool_output.repository import ToolOutputRepository
17
+
18
+
19
+ def build_tool_result_reader_tool(tool_output_db_path: Path | str) -> Any:
20
+ """Create the guarded reader for reversible transformed output."""
21
+ results = ToolOutputRepository(tool_output_db_path)
22
+
23
+ @tool
24
+ def read_tool_result(
25
+ runtime: ToolRuntime,
26
+ ref: str,
27
+ offset: int = 0,
28
+ limit: int = 200,
29
+ query: str = "",
30
+ max_results: int = 20,
31
+ context_lines: int = 2,
32
+ ) -> str:
33
+ """读取当前会话被压缩工具输出的原文。
34
+
35
+ 支持精确分页,或通过 query 在原文中进行本地关键词召回。仅接受工具
36
+ 输出提供的 ``tool-output://...`` 引用,不能读取任意文件。
37
+
38
+ Args:
39
+ ref: 工具输出中的 ``tool-output://`` 引用。
40
+ offset: 精确分页的起始行号(0-indexed)。
41
+ limit: 精确分页行数,默认 200,最大 500。
42
+ query: 可选关键词;提供后返回相关片段。
43
+ max_results: 查询模式最多返回的命中行数,最大 50。
44
+ context_lines: 每个命中的相邻上下文行数,最大 10。
45
+ """
46
+ config = dict(getattr(runtime, "config", None) or {})
47
+ configurable = dict(config.get("configurable") or {})
48
+ thread_id = str(configurable.get("thread_id") or "")
49
+ record = results.get(ref, expected_thread_id=thread_id or None)
50
+ if record is None:
51
+ return "工具结果引用未找到、已损坏或无权读取。"
52
+ started = time.perf_counter()
53
+ if query.strip():
54
+ matches = results.search(
55
+ ref,
56
+ query,
57
+ expected_thread_id=thread_id or None,
58
+ max_results=min(50, max(1, int(max_results))),
59
+ context_lines=min(10, max(0, int(context_lines))),
60
+ )
61
+ if not matches:
62
+ return f"工具: {record.tool_name}\n引用: {record.ref}\n(没有匹配 {query!r} 的行)"
63
+ body = "\n".join(f"{line_no}: {line}" for line_no, line in matches)
64
+ result = (
65
+ f"工具: {record.tool_name}\n引用: {record.ref}\n查询: {query}\n{'─' * 40}\n{body}"
66
+ )
67
+ results.record_retrieval(
68
+ thread_id=thread_id or record.thread_id,
69
+ ref=ref,
70
+ mode="query",
71
+ returned_bytes=len(result.encode("utf-8")),
72
+ duration_ms=(time.perf_counter() - started) * 1000,
73
+ )
74
+ return result
75
+ start = max(0, int(offset))
76
+ count = min(500, max(1, int(limit)))
77
+ lines = record.content.splitlines()
78
+ selected = lines[start : start + count]
79
+ body = "\n".join(selected) or "(empty result)"
80
+ end = start + len(selected)
81
+ suffix = (
82
+ f"\n\n[还有 {len(lines) - end} 行,使用 offset={end} 继续读取]"
83
+ if end < len(lines)
84
+ else ""
85
+ )
86
+ result = (
87
+ f"工具: {record.tool_name}\n状态: {record.status}\n引用: {record.ref}\n"
88
+ f"行: {start}-{max(start, end - 1)} / {max(0, len(lines) - 1)}\n"
89
+ f"{'─' * 40}\n{body}{suffix}"
90
+ )
91
+ results.record_retrieval(
92
+ thread_id=thread_id or record.thread_id,
93
+ ref=ref,
94
+ mode="pagination",
95
+ returned_bytes=len(result.encode("utf-8")),
96
+ duration_ms=(time.perf_counter() - started) * 1000,
97
+ )
98
+ return result
99
+
100
+ return read_tool_result
101
+
102
+
103
+ def build_session_tools(
104
+ sessions_path: Path | str,
105
+ checkpoint_path: Path | str,
106
+ tool_output_db_path: Path | str | None = None,
107
+ ) -> list[Any]:
108
+ """创建会话查阅工具列表。
109
+
110
+ Args:
111
+ sessions_path: sessions.sqlite 路径
112
+ checkpoint_path: checkpoints.sqlite 路径
113
+ Returns:
114
+ [list_sessions, read_session]
115
+ """
116
+ from synapse.sessions.store import SessionStore, format_session_table
117
+
118
+ store = SessionStore(sessions_path)
119
+ ckpt = Path(checkpoint_path)
120
+
121
+ @tool
122
+ def list_sessions(query: str = "", limit: int = 20) -> str:
123
+ """列出本地会话记录,支持按标题/ID 模糊搜索。
124
+
125
+ **默认禁止调用**。仅当用户明确要求查阅、搜索、对比其他会话时使用
126
+ (例如“列出最近会话”“找之前那个 bug 讨论”“看看某某会话”)。
127
+ 闲聊、问候、普通编码/排障、意图不明、仅为“多了解上下文”时一律不要调用。
128
+
129
+ 返回会话基本信息,不包含对话内容;获取对话内容请用 read_session。
130
+
131
+ Args:
132
+ query: 可选,按标题或 thread_id 搜索关键词。为空时返回最近会话。
133
+ limit: 最大返回数,默认 20。
134
+ """
135
+ if query.strip():
136
+ items = store.search(query, limit=limit)
137
+ else:
138
+ items = store.list_nonempty(limit=limit)
139
+ if not items:
140
+ return "(没有找到匹配的会话记录)"
141
+ return format_session_table(items)
142
+
143
+ @tool
144
+ def read_session(
145
+ thread_id: str,
146
+ max_turns: int = 0,
147
+ include_summary: bool = True,
148
+ ) -> str:
149
+ """读取指定会话的对话历史内容。
150
+
151
+ **默认禁止调用**。仅当用户明确要求读取某个会话内容时使用
152
+ (通常先由 list_sessions 得到 thread_id,或用户直接给出会话 ID)。
153
+ 闲聊、问候、普通任务、未指明需要跨会话上下文时不要主动调用。
154
+
155
+ 按轮次切分对话,每轮 = 一条用户消息 + 后续 AI/工具消息。
156
+ 可指定只取最后 N 轮,避免上下文过长。
157
+
158
+ Args:
159
+ thread_id: 会话 ID(通过 list_sessions 获取)。
160
+ max_turns: 返回最近 N 轮。0 表示返回全部轮次。
161
+ include_summary: 是否在开头附带会话元信息。
162
+ """
163
+ from synapse.sessions.transcript import (
164
+ format_turns_as_text,
165
+ load_messages_from_sqlite_file,
166
+ split_messages_by_turns,
167
+ )
168
+
169
+ info = store.get(thread_id)
170
+ if info is None:
171
+ return (
172
+ f"会话未找到: {thread_id}\n"
173
+ f"提示:使用 list_sessions 查看可用会话列表,"
174
+ f"确保 thread_id 完全匹配。"
175
+ )
176
+
177
+ messages = load_messages_from_sqlite_file(ckpt, thread_id)
178
+ if not messages:
179
+ return (
180
+ f"会话 {thread_id} 没有对话记录。\n"
181
+ f"标题: {info.title}\n"
182
+ f"创建: {info.created_at} 更新: {info.updated_at}\n"
183
+ f"模型: {info.binding().display()}"
184
+ )
185
+
186
+ turns = split_messages_by_turns(messages)
187
+ body = format_turns_as_text(turns, max_turns=max_turns)
188
+
189
+ if include_summary:
190
+ bind = info.binding()
191
+ turns_display = min(max_turns, len(turns)) if max_turns else len(turns)
192
+ header = (
193
+ f"会话: {info.thread_id}\n"
194
+ f"标题: {info.title}\n"
195
+ f"模型: {bind.display()}\n"
196
+ f"轮次: {len(turns)}(显示 {turns_display} 轮)\n"
197
+ f"创建: {info.created_at} 更新: {info.updated_at}\n"
198
+ f"{'─' * 40}\n\n"
199
+ )
200
+ return header + body
201
+ return body
202
+
203
+ output_db = tool_output_db_path or (Path(sessions_path).parent / "tool-outputs.sqlite")
204
+ return [list_sessions, read_session, build_tool_result_reader_tool(output_db)]
synapse/ui/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """UI package.
2
+
3
+ - CLI rendering: Rich (``stream.RichStreamSink``)
4
+ - TUI rendering: Textual (``tui.TextualStreamSink``)
5
+ - Shared port: ``sink.StreamSink`` consumed by ``stream.stream_agent``
6
+ """
7
+
8
+ from synapse.ui.sink import StreamSink
9
+
10
+ __all__ = ["StreamSink"]
@@ -0,0 +1,73 @@
1
+ """Extensible bottombar package.
2
+
3
+ Public layout/registry API lives in ``core``; built-in components live under
4
+ ``components/`` and are registered via ``DEFAULT_COMPONENT_INSTALLERS``.
5
+
6
+ Add a component
7
+ ---------------
8
+ 1. Create ``components/foo.py`` with ``install(registry, ctx)``.
9
+ 2. Append ``foo.install`` to ``DEFAULT_COMPONENT_INSTALLERS`` in
10
+ ``components/__init__.py``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from synapse.ui.bottombar.components import (
16
+ DEFAULT_COMPONENT_INSTALLERS,
17
+ install_default_components,
18
+ install_default_regions,
19
+ )
20
+ from synapse.ui.bottombar.context import BottomBarContext
21
+ from synapse.ui.bottombar.core import (
22
+ DEFAULT_COL_GAP,
23
+ DEFAULT_REGION_GAP,
24
+ BottomBarAlign,
25
+ BottomBarComponent,
26
+ BottomBarLayout,
27
+ BottomBarRegion,
28
+ BottomBarRegionSpec,
29
+ BottomBarRegistry,
30
+ PackedRegion,
31
+ align_in_width,
32
+ center_in_width,
33
+ display_width,
34
+ join_region_parts,
35
+ layout_from_registry,
36
+ locate_component_span,
37
+ normalize_region_id,
38
+ pack_bottombar_regions,
39
+ pack_layout_from_registry,
40
+ pack_region_list,
41
+ render_packed_line,
42
+ render_region_text,
43
+ truncate_to_width,
44
+ )
45
+
46
+ __all__ = [
47
+ "DEFAULT_COL_GAP",
48
+ "DEFAULT_COMPONENT_INSTALLERS",
49
+ "DEFAULT_REGION_GAP",
50
+ "BottomBarAlign",
51
+ "BottomBarComponent",
52
+ "BottomBarContext",
53
+ "BottomBarLayout",
54
+ "BottomBarRegion",
55
+ "BottomBarRegionSpec",
56
+ "BottomBarRegistry",
57
+ "PackedRegion",
58
+ "align_in_width",
59
+ "center_in_width",
60
+ "display_width",
61
+ "install_default_components",
62
+ "install_default_regions",
63
+ "join_region_parts",
64
+ "layout_from_registry",
65
+ "locate_component_span",
66
+ "normalize_region_id",
67
+ "pack_bottombar_regions",
68
+ "pack_layout_from_registry",
69
+ "pack_region_list",
70
+ "render_packed_line",
71
+ "render_region_text",
72
+ "truncate_to_width",
73
+ ]
@@ -0,0 +1,143 @@
1
+ """Built-in bottombar components.
2
+
3
+ Extension pattern
4
+ -----------------
5
+ 1. Add ``components/my_widget.py`` with::
6
+
7
+ ID = "my_widget"
8
+
9
+ def install(registry, ctx) -> None:
10
+ registry.register_fn(ID, render, region=..., order=..., priority=...)
11
+
12
+ 2. Append ``my_widget.install`` to :data:`DEFAULT_COMPONENT_INSTALLERS` below.
13
+
14
+ No other bottombar core files need to change.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Callable
20
+ from typing import TYPE_CHECKING
21
+
22
+ from synapse.ui.bottombar.components import key_hints, mcp, mode, model
23
+ from synapse.ui.bottombar.core import (
24
+ DEFAULT_COL_GAP,
25
+ BottomBarAlign,
26
+ BottomBarRegion,
27
+ BottomBarRegistry,
28
+ )
29
+
30
+ if TYPE_CHECKING:
31
+ from synapse.ui.bottombar.context import BottomBarContext
32
+
33
+ ComponentInstaller = Callable[[BottomBarRegistry, "BottomBarContext"], None]
34
+
35
+ # Order here is install order only (layout uses each component's region/order).
36
+ # thread.install is available but not default (kept out of the chrome bar).
37
+ DEFAULT_COMPONENT_INSTALLERS: list[ComponentInstaller] = [
38
+ key_hints.install,
39
+ mode.install,
40
+ model.install,
41
+ mcp.install,
42
+ ]
43
+
44
+
45
+ def install_default_regions(registry: BottomBarRegistry) -> None:
46
+ """Ensure classic left / center / right region slots exist.
47
+
48
+ Layout: left = model/mcp (hug), right = key hints (flex fill).
49
+ Region ``fg`` is a fallback; host ``layout_from_registry`` left/right styles
50
+ override at paint time for theme-aware colors.
51
+ """
52
+ registry.register_region(
53
+ BottomBarRegion.LEFT.value,
54
+ order=10,
55
+ flex=0,
56
+ align=BottomBarAlign.LEFT,
57
+ priority=50,
58
+ gap_after=DEFAULT_COL_GAP,
59
+ fg="#8ab4f8", # accent (model/mcp) — distinct from muted hints
60
+ )
61
+ registry.register_region(
62
+ BottomBarRegion.CENTER.value,
63
+ order=20,
64
+ flex=0,
65
+ align=BottomBarAlign.CENTER,
66
+ priority=10,
67
+ min_width=0,
68
+ gap_after=DEFAULT_COL_GAP,
69
+ fg="#f4b183",
70
+ )
71
+ registry.register_region(
72
+ BottomBarRegion.RIGHT.value,
73
+ order=30,
74
+ flex=1,
75
+ align=BottomBarAlign.RIGHT,
76
+ priority=30,
77
+ min_width=8,
78
+ gap_after=0,
79
+ fg="#5f6368", # muted key hints
80
+ )
81
+
82
+
83
+ def install_default_components(
84
+ registry: BottomBarRegistry,
85
+ ctx: BottomBarContext | None = None,
86
+ /,
87
+ *,
88
+ busy: Callable[[], bool] | None = None,
89
+ thread: Callable[[], str] | None = None,
90
+ mode: Callable[[], str] | None = None,
91
+ idle_hints: Callable[[], str] | None = None,
92
+ busy_hints: Callable[[], str] | None = None,
93
+ model: Callable[[], str] | None = None,
94
+ mcp: Callable[[], str] | None = None,
95
+ installers: list[ComponentInstaller] | None = None,
96
+ ) -> None:
97
+ """Install default regions + component modules.
98
+
99
+ Prefer passing :class:`BottomBarContext`. Keyword providers remain for tests.
100
+ """
101
+ from synapse.ui.bottombar.context import BottomBarContext as _Ctx
102
+
103
+ def _empty() -> str:
104
+ return ""
105
+
106
+ if ctx is None:
107
+ if busy is None:
108
+ raise TypeError(
109
+ "install_default_components requires ctx= or at least busy="
110
+ )
111
+ ctx = _Ctx(
112
+ busy=busy,
113
+ thread=thread or _empty,
114
+ mode=mode or _empty,
115
+ idle_hints=idle_hints
116
+ or (lambda: "Tab complete · / commands · Esc cancel · F2 model · F4 sessions · F9 delete"),
117
+ busy_hints=busy_hints
118
+ or (lambda: "Esc cancel · Enter queue guidance"),
119
+ model=model or _empty,
120
+ mcp=mcp or _empty,
121
+ )
122
+ else:
123
+ ctx = _Ctx(
124
+ busy=ctx.busy,
125
+ thread=thread or ctx.thread,
126
+ mode=mode or ctx.mode,
127
+ idle_hints=idle_hints or ctx.idle_hints,
128
+ busy_hints=busy_hints or ctx.busy_hints,
129
+ model=model or ctx.model,
130
+ mcp=mcp or ctx.mcp,
131
+ )
132
+
133
+ install_default_regions(registry)
134
+ for install in installers if installers is not None else DEFAULT_COMPONENT_INSTALLERS:
135
+ install(registry, ctx)
136
+
137
+
138
+ __all__ = [
139
+ "DEFAULT_COMPONENT_INSTALLERS",
140
+ "ComponentInstaller",
141
+ "install_default_components",
142
+ "install_default_regions",
143
+ ]
@@ -0,0 +1,30 @@
1
+ """Bottombar component: contextual key hints (right)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from synapse.ui.bottombar.context import BottomBarContext
6
+ from synapse.ui.bottombar.core import BottomBarRegion, BottomBarRegistry
7
+
8
+ ID = "key_hints"
9
+ REGION = BottomBarRegion.RIGHT
10
+ ORDER = 10
11
+ PRIORITY = 30 # shrink before model/mcp when narrow
12
+ MIN_WIDTH = 12
13
+
14
+
15
+ def install(registry: BottomBarRegistry, ctx: BottomBarContext) -> None:
16
+ """Register the contextual key-hint line."""
17
+
18
+ def render() -> str:
19
+ if ctx.busy():
20
+ return (ctx.busy_hints() or "").strip()
21
+ return (ctx.idle_hints() or "").strip()
22
+
23
+ registry.register_fn(
24
+ ID,
25
+ render,
26
+ region=REGION,
27
+ order=ORDER,
28
+ priority=PRIORITY,
29
+ min_width=MIN_WIDTH,
30
+ )
@@ -0,0 +1,64 @@
1
+ """Bottombar component: MCP status (left), colored by state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.text import Text
6
+
7
+ from synapse.ui.bottombar.context import BottomBarContext
8
+ from synapse.ui.bottombar.core import BottomBarRegion, BottomBarRegistry
9
+
10
+ ID = "mcp"
11
+ REGION = BottomBarRegion.LEFT
12
+ ORDER = 20
13
+ PRIORITY = 55
14
+ MIN_WIDTH = 6
15
+
16
+ # Fallback palette (overridden by active theme when available).
17
+ _C_GREEN = "#81c995"
18
+ _C_ERROR = "#f28b82"
19
+ _C_MUTED = "#5f6368"
20
+
21
+
22
+ def _palette() -> tuple[str, str, str]:
23
+ """Return (green, error, muted) from the active theme when possible."""
24
+ try:
25
+ from synapse.ui.theme import get_theme
26
+
27
+ t = get_theme()
28
+ return (
29
+ str(getattr(t, "green", _C_GREEN) or _C_GREEN),
30
+ str(getattr(t, "error", _C_ERROR) or _C_ERROR),
31
+ str(getattr(t, "muted", _C_MUTED) or _C_MUTED),
32
+ )
33
+ except Exception: # noqa: BLE001
34
+ return _C_GREEN, _C_ERROR, _C_MUTED
35
+
36
+
37
+ def style_for_mcp_label(label: str) -> str:
38
+ """Map ``mcp on`` / ``mcp err`` / ``mcp off`` to a paint color."""
39
+ key = (label or "").strip().lower()
40
+ green, error, muted = _palette()
41
+ if key == "mcp on" or key.endswith(" on"):
42
+ return green
43
+ if key == "mcp err" or "err" in key or "error" in key:
44
+ return error
45
+ return muted
46
+
47
+
48
+ def install(registry: BottomBarRegistry, ctx: BottomBarContext) -> None:
49
+ """Register mcp on/off/err label with state colors."""
50
+
51
+ def render() -> str | Text:
52
+ label = (ctx.mcp() or "").strip()
53
+ if not label:
54
+ return ""
55
+ return Text(label, style=style_for_mcp_label(label))
56
+
57
+ registry.register_fn(
58
+ ID,
59
+ render,
60
+ region=REGION,
61
+ order=ORDER,
62
+ priority=PRIORITY,
63
+ min_width=MIN_WIDTH,
64
+ )
@@ -0,0 +1,24 @@
1
+ """Bottombar component: optional mode tag (center)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from synapse.ui.bottombar.context import BottomBarContext
6
+ from synapse.ui.bottombar.core import BottomBarRegion, BottomBarRegistry
7
+
8
+ ID = "mode"
9
+ REGION = BottomBarRegion.CENTER
10
+ ORDER = 10
11
+ PRIORITY = 10 # shrink first when narrow
12
+ MIN_WIDTH = 0
13
+
14
+
15
+ def install(registry: BottomBarRegistry, ctx: BottomBarContext) -> None:
16
+ """Register the optional mode label (empty string hides it)."""
17
+ registry.register_fn(
18
+ ID,
19
+ lambda: (ctx.mode() or "").strip(),
20
+ region=REGION,
21
+ order=ORDER,
22
+ priority=PRIORITY,
23
+ min_width=MIN_WIDTH,
24
+ )
@@ -0,0 +1,28 @@
1
+ """Bottombar component: model id + thinking level (left)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from synapse.ui.bottombar.context import BottomBarContext
6
+ from synapse.ui.bottombar.core import BottomBarRegion, BottomBarRegistry
7
+
8
+ ID = "model"
9
+ REGION = BottomBarRegion.LEFT
10
+ ORDER = 10
11
+ PRIORITY = 60 # keep when narrow
12
+ MIN_WIDTH = 8
13
+
14
+
15
+ def install(registry: BottomBarRegistry, ctx: BottomBarContext) -> None:
16
+ """Register model · thinking label."""
17
+
18
+ def render() -> str:
19
+ return (ctx.model() or "").strip()
20
+
21
+ registry.register_fn(
22
+ ID,
23
+ render,
24
+ region=REGION,
25
+ order=ORDER,
26
+ priority=PRIORITY,
27
+ min_width=MIN_WIDTH,
28
+ )
@@ -0,0 +1,29 @@
1
+ """Bottombar component: short thread / session id (left)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from synapse.ui.bottombar.context import BottomBarContext
6
+ from synapse.ui.bottombar.core import BottomBarRegion, BottomBarRegistry
7
+
8
+ ID = "thread"
9
+ REGION = BottomBarRegion.LEFT
10
+ ORDER = 30
11
+ PRIORITY = 20 # drop before model/mcp when narrow
12
+ MIN_WIDTH = 4
13
+
14
+
15
+ def install(registry: BottomBarRegistry, ctx: BottomBarContext) -> None:
16
+ """Register the short thread label on the left."""
17
+
18
+ def render() -> str:
19
+ label = (ctx.thread() or "").strip()
20
+ return label
21
+
22
+ registry.register_fn(
23
+ ID,
24
+ render,
25
+ region=REGION,
26
+ order=ORDER,
27
+ priority=PRIORITY,
28
+ min_width=MIN_WIDTH,
29
+ )
@@ -0,0 +1,36 @@
1
+ """Host context passed into bottombar component installers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+
8
+ from rich.text import Text
9
+
10
+ # Providers may return plain text or pre-styled Rich Text.
11
+ LabelFn = Callable[[], str]
12
+ RichLabelFn = Callable[[], str | Text]
13
+ BoolFn = Callable[[], bool]
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class BottomBarContext:
18
+ """Data sources for built-in (and custom) bottombar components.
19
+
20
+ App/host fills these callables; each component module only reads what it needs.
21
+ """
22
+
23
+ # True while the agent run is active (steer mode / cancelable).
24
+ busy: BoolFn
25
+ # Optional short session / thread label (left, with model/mcp).
26
+ thread: LabelFn
27
+ # Optional extra mode tag (e.g. "safe", "steer×2"); empty hides the piece.
28
+ mode: LabelFn
29
+ # Idle key-hint line (right).
30
+ idle_hints: LabelFn
31
+ # Busy key-hint line (right).
32
+ busy_hints: LabelFn
33
+ # Model id + thinking level (left; e.g. ``haha-grok-4.5 · max``).
34
+ model: LabelFn
35
+ # MCP chrome (left; ``mcp on`` / ``mcp off`` / ``mcp err``).
36
+ mcp: LabelFn