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,74 @@
1
+ """Extensible bottombar: freeform regions + components.
2
+
3
+ Reuses the topbar packing engine so layout/priority/flex semantics stay identical.
4
+ Regions default to classic left / center / right; custom region ids are allowed.
5
+
6
+ Built-in chrome paints into a single-line Textual ``Static`` under the prompt.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # Re-export topbar layout primitives under bottombar names.
12
+ from synapse.ui.topbar.core import (
13
+ DEFAULT_COL_GAP,
14
+ DEFAULT_REGION_GAP,
15
+ PackedRegion,
16
+ align_in_width,
17
+ center_in_width,
18
+ display_width,
19
+ join_region_parts,
20
+ layout_from_registry,
21
+ locate_component_span,
22
+ normalize_region_id,
23
+ pack_layout_from_registry,
24
+ pack_region_list,
25
+ render_packed_line,
26
+ render_region_text,
27
+ truncate_to_width,
28
+ )
29
+ from synapse.ui.topbar.core import (
30
+ TopBarAlign as BottomBarAlign,
31
+ )
32
+ from synapse.ui.topbar.core import (
33
+ TopBarComponent as BottomBarComponent,
34
+ )
35
+ from synapse.ui.topbar.core import (
36
+ TopBarLayout as BottomBarLayout,
37
+ )
38
+ from synapse.ui.topbar.core import (
39
+ TopBarRegion as BottomBarRegion,
40
+ )
41
+ from synapse.ui.topbar.core import (
42
+ TopBarRegionSpec as BottomBarRegionSpec,
43
+ )
44
+ from synapse.ui.topbar.core import (
45
+ TopBarRegistry as BottomBarRegistry,
46
+ )
47
+ from synapse.ui.topbar.core import (
48
+ pack_topbar_regions as pack_bottombar_regions,
49
+ )
50
+
51
+ __all__ = [
52
+ "DEFAULT_COL_GAP",
53
+ "DEFAULT_REGION_GAP",
54
+ "BottomBarAlign",
55
+ "BottomBarComponent",
56
+ "BottomBarLayout",
57
+ "BottomBarRegion",
58
+ "BottomBarRegionSpec",
59
+ "BottomBarRegistry",
60
+ "PackedRegion",
61
+ "align_in_width",
62
+ "center_in_width",
63
+ "display_width",
64
+ "join_region_parts",
65
+ "layout_from_registry",
66
+ "locate_component_span",
67
+ "normalize_region_id",
68
+ "pack_bottombar_regions",
69
+ "pack_layout_from_registry",
70
+ "pack_region_list",
71
+ "render_packed_line",
72
+ "render_region_text",
73
+ "truncate_to_width",
74
+ ]
@@ -0,0 +1,25 @@
1
+ """Interactive dialog screens for slash commands."""
2
+
3
+ from synapse.ui.dialogs.base import DialogBase, OptionItem, dialog_css
4
+ from synapse.ui.dialogs.codex_session_list import CodexSessionListDialog
5
+ from synapse.ui.dialogs.compression_diagnostics import CompressionDiagnosticsDialog
6
+ from synapse.ui.dialogs.git_explore import GitExploreScreen
7
+ from synapse.ui.dialogs.mcp_panel import McpPanelDialog
8
+ from synapse.ui.dialogs.model_picker import ModelPickerDialog
9
+ from synapse.ui.dialogs.safety_panel import SafetyPanelDialog
10
+ from synapse.ui.dialogs.session_list import SessionListDialog
11
+ from synapse.ui.dialogs.theme_picker import ThemePickerDialog
12
+
13
+ __all__ = [
14
+ "CodexSessionListDialog",
15
+ "CompressionDiagnosticsDialog",
16
+ "DialogBase",
17
+ "GitExploreScreen",
18
+ "McpPanelDialog",
19
+ "ModelPickerDialog",
20
+ "OptionItem",
21
+ "SafetyPanelDialog",
22
+ "SessionListDialog",
23
+ "ThemePickerDialog",
24
+ "dialog_css",
25
+ ]
@@ -0,0 +1,362 @@
1
+ """ModalScreen base with shared styling, keyboard, and option-list helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from rich.text import Text
9
+ from textual.app import ComposeResult
10
+ from textual.binding import Binding
11
+ from textual.containers import Vertical, VerticalScroll
12
+ from textual.events import Click
13
+ from textual.screen import ModalScreen
14
+ from textual.widgets import Static
15
+
16
+
17
+ @dataclass
18
+ class OptionItem:
19
+ """One selectable row in a dialog list."""
20
+
21
+ key: str
22
+ label: str
23
+ detail: str = ""
24
+ selected: bool = False
25
+ meta: str = "" # right-aligned hint (e.g. "enabled" / "3 turns")
26
+ checked: bool = False # multi-select toggle state
27
+ checkable: bool = True # whether this row shows checkbox
28
+ show_bullet: bool = True # whether to show ○/● (non-checkable) or ☑/☐ prefix
29
+ indent: str = "" # left-padding before bullet, e.g. " " for tree indent
30
+
31
+
32
+ # Window-like modal: title bar + list + keyboard-only footer.
33
+ # Uses $theme-* variables from the app's get_css_variables().
34
+ dialog_css = """
35
+ DialogBase {
36
+ align: center middle;
37
+ background: $theme-bg 60%;
38
+ }
39
+ DialogBase > #dialog-window {
40
+ width: 66;
41
+ height: auto;
42
+ max-height: 28;
43
+ background: $theme-bg;
44
+ border: round $theme-user;
45
+ border-title-color: $theme-fg;
46
+ border-title-background: $theme-top;
47
+ border-title-style: bold;
48
+ border-title-align: left;
49
+ border-subtitle-color: $theme-muted;
50
+ border-subtitle-align: right;
51
+ padding: 0;
52
+ layout: vertical;
53
+ }
54
+ #dialog-body {
55
+ height: auto;
56
+ max-height: 22;
57
+ min-height: 3;
58
+ width: 1fr;
59
+ padding: 0 1;
60
+ background: $theme-bg;
61
+ overflow-y: auto;
62
+ overflow-x: hidden;
63
+ scrollbar-size: 0 0;
64
+ scrollbar-background: $theme-bg;
65
+ scrollbar-color: $theme-bg;
66
+ scrollbar-background-hover: $theme-bg;
67
+ scrollbar-color-hover: $theme-bg;
68
+ scrollbar-background-active: $theme-bg;
69
+ scrollbar-color-active: $theme-bg;
70
+ }
71
+ DialogBody OptionRow {
72
+ height: 1;
73
+ width: 1fr;
74
+ color: $theme-dim;
75
+ padding: 0 1;
76
+ background: $theme-bg;
77
+ overflow: hidden;
78
+ text-overflow: ellipsis;
79
+ }
80
+ DialogBody OptionRow.-selected {
81
+ color: $theme-user;
82
+ background: $theme-bar;
83
+ text-style: bold;
84
+ }
85
+ DialogBody SectionHeader {
86
+ height: 1;
87
+ width: 1fr;
88
+ color: $theme-orange;
89
+ padding: 0 1;
90
+ text-style: bold;
91
+ }
92
+ """
93
+
94
+
95
+ class OptionRow(Static):
96
+ """One row in a dialog option list. Rendered as Rich Text.
97
+
98
+ Class hooks:
99
+ -selected -> active / hovered
100
+ -detail -> second-line detail
101
+ -meta -> right-aligned hint
102
+ -checked -> multi-select toggled on
103
+ """
104
+
105
+ def __init__(
106
+ self,
107
+ item: OptionItem,
108
+ *,
109
+ mark: str = " ",
110
+ checkable: bool = False,
111
+ ) -> None:
112
+ super().__init__(Text(""))
113
+ self.item = item
114
+ self._mark = mark
115
+ self._checkable = checkable # effective checkable (global & per-item combined)
116
+ self._update_content()
117
+
118
+ def _update_content(self) -> None:
119
+ item = self.item
120
+ prefix = self._mark + item.indent
121
+ if not item.show_bullet:
122
+ row = Text(f"{prefix}{item.label}")
123
+ elif self._checkable:
124
+ bullet = "\u2612" if item.checked else "\u2610" # ☑ / ☐
125
+ row = Text(f"{prefix}{bullet} {item.label}")
126
+ else:
127
+ bullet = "\u25cf" if item.selected else "\u25cb" # ● / ○
128
+ row = Text(f"{prefix}{bullet} {item.label}")
129
+ if item.detail:
130
+ row.append(f" {item.detail}", style="dim")
131
+ if item.meta:
132
+ row.append(f" {item.meta}", style="dim")
133
+ self.update(row)
134
+
135
+ def set_hover(self, on: bool) -> None:
136
+ if on:
137
+ self.add_class("-selected")
138
+ else:
139
+ self.remove_class("-selected")
140
+ if self._checkable:
141
+ if on:
142
+ self.add_class("-checked")
143
+ else:
144
+ self.remove_class("-checked")
145
+
146
+
147
+ class DetailRow(Static):
148
+ """Second line: muted detail text."""
149
+
150
+ def __init__(self, text: str, *, indent: int = 4) -> None:
151
+ super().__init__(Text(" " * indent + text, style=""))
152
+ self.add_class("-detail")
153
+
154
+
155
+ class SectionHeader(Static):
156
+ """Section divider inside dialog body."""
157
+
158
+ def __init__(self, text: str) -> None:
159
+ super().__init__(Text(text, style=""))
160
+
161
+
162
+ class DialogBody(VerticalScroll):
163
+ """Scrollable option-list container (scrollbar chrome hidden)."""
164
+
165
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
166
+ super().__init__(*args, **kwargs)
167
+ self._rows: list[OptionRow] = []
168
+ self._selected_idx: int = 0
169
+ self._option_keys: list[str] = []
170
+ self._checkable: bool = False
171
+ self.on_row_click: Any | None = None # callable(key) on click
172
+
173
+ def set_options(
174
+ self, items: list[OptionItem], *, mark: str = " ", checkable: bool = False
175
+ ) -> None:
176
+ self._rows.clear()
177
+ self._option_keys.clear()
178
+ self.remove_children()
179
+ self._selected_idx = 0
180
+ self._checkable = checkable
181
+ for i, item in enumerate(items):
182
+ if item.selected:
183
+ self._selected_idx = i
184
+ break
185
+ self.append_options(items, mark=mark)
186
+
187
+ def append_section(self, text: str) -> None:
188
+ """Append a non-selectable section header to the option list."""
189
+ self.mount(SectionHeader(text))
190
+
191
+ def append_options(self, items: list[OptionItem], *, mark: str = " ") -> None:
192
+ """Append selectable options while preserving existing navigation state."""
193
+ for item in items:
194
+ effective = self._checkable and item.checkable
195
+ row = OptionRow(item, mark=mark, checkable=effective)
196
+ self._rows.append(row)
197
+ self._option_keys.append(item.key)
198
+ self.mount(row)
199
+ # detail/meta render on the same OptionRow line (no wrap).
200
+ self._sync_hover()
201
+
202
+ def _sync_hover(self) -> None:
203
+ for i, row in enumerate(self._rows):
204
+ row.set_hover(i == self._selected_idx)
205
+
206
+ @property
207
+ def selected_key(self) -> str | None:
208
+ if 0 <= self._selected_idx < len(self._option_keys):
209
+ return self._option_keys[self._selected_idx]
210
+ return None
211
+
212
+ @property
213
+ def checked_keys(self) -> list[str]:
214
+ """Return keys of all checked items (multi-select mode)."""
215
+ return [
216
+ r.item.key
217
+ for r in self._rows
218
+ if self._checkable and r.item.checkable and r.item.checked
219
+ ]
220
+
221
+ @property
222
+ def _all_checked(self) -> bool:
223
+ """True if every checkable row is checked (and there's at least one)."""
224
+ if not self._checkable:
225
+ return False
226
+ checkable_rows = [r for r in self._rows if r.item.checkable]
227
+ if not checkable_rows:
228
+ return False
229
+ return all(r.item.checked for r in checkable_rows)
230
+
231
+ def toggle_check(self) -> None:
232
+ """Toggle checked state of the currently hovered row."""
233
+ if not self._checkable or not self._rows:
234
+ return
235
+ idx = self._selected_idx
236
+ if 0 <= idx < len(self._rows):
237
+ row = self._rows[idx]
238
+ if not row.item.checkable:
239
+ return
240
+ row.item.checked = not row.item.checked
241
+ row._update_content()
242
+
243
+ def select_all(self) -> None:
244
+ """Toggle: check all or uncheck all checkable rows."""
245
+ if not self._checkable:
246
+ return
247
+ target = not self._all_checked
248
+ for row in self._rows:
249
+ if row.item.checkable:
250
+ row.item.checked = target
251
+ row._update_content()
252
+
253
+ def move_up(self) -> None:
254
+ if self._selected_idx > 0:
255
+ self._selected_idx -= 1
256
+ self._sync_hover()
257
+ self.scroll_to_widget(self._rows[self._selected_idx], animate=False)
258
+
259
+ def move_down(self) -> None:
260
+ if self._selected_idx < len(self._rows) - 1:
261
+ self._selected_idx += 1
262
+ self._sync_hover()
263
+ self.scroll_to_widget(self._rows[self._selected_idx], animate=False)
264
+
265
+ def on_click(self, event: Click) -> None:
266
+ """Select an option row when it is clicked."""
267
+ if isinstance(event.widget, OptionRow):
268
+ self._selected_idx = self._rows.index(event.widget)
269
+ self._sync_hover()
270
+ if self.on_row_click is not None:
271
+ self.on_row_click(event.widget.item.key)
272
+ event.stop()
273
+
274
+
275
+ class DialogBase(ModalScreen[Any]):
276
+ """Shared ModalScreen with title bar, scrollable body, and keyboard footer.
277
+
278
+ Subclasses override:
279
+ - ``title_text`` property
280
+ - ``compose_body()`` -> yields widgets into #dialog-body
281
+ - ``_title_keys`` for border subtitle (keyboard hint)
282
+ - ``_on_selected`` / ``_on_apply`` for action handlers
283
+
284
+ Default keymap:
285
+ Esc -> ``dismiss(None)``
286
+ Up/Down -> body option navigation
287
+ Enter -> confirm / select
288
+ Space -> toggle check (multi-select mode)
289
+ Ctrl+A -> select all (multi-select mode)
290
+ Ctrl+D -> deselect all (multi-select mode)
291
+ """
292
+
293
+ DEFAULT_CSS = dialog_css
294
+ BINDINGS = [
295
+ Binding("escape", "cancel", "Close", show=False, priority=True),
296
+ Binding("up", "select_previous", "Previous", show=False, priority=True),
297
+ Binding("down", "select_next", "Next", show=False, priority=True),
298
+ Binding("enter", "confirm", "Apply", show=False, priority=True),
299
+ Binding("space", "toggle_check", "Toggle", show=False),
300
+ Binding("ctrl+a", "select_all", "All", show=False),
301
+ ]
302
+
303
+ # Left title-bar glyph (ASCII/Unicode mark, not emoji).
304
+ _title_icon: str = "\u25c6" # ◆
305
+ # Border subtitle (bottom-right), compact keyboard hint.
306
+ _title_keys: str = "\u2191\u2193 enter \u00b7 esc"
307
+
308
+ @property
309
+ def title_text(self) -> str:
310
+ return "Dialog"
311
+
312
+ def compose(self) -> ComposeResult:
313
+ with Vertical(id="dialog-window"):
314
+ with DialogBody(id="dialog-body"):
315
+ yield from self.compose_body()
316
+
317
+ def compose_body(self) -> ComposeResult:
318
+ yield Static("")
319
+ return
320
+
321
+ def on_mount(self) -> None:
322
+ # Title lives on the window border (always visible, high contrast).
323
+ win = self.query_one("#dialog-window")
324
+ icon = (self._title_icon or "").strip()
325
+ title = (self.title_text or "Dialog").strip()
326
+ win.border_title = f"{icon} {title}".strip() if icon else title
327
+ win.border_subtitle = self._title_keys
328
+ # Keep keyboard input inside the modal list.
329
+ body = self.query_one("#dialog-body", DialogBody)
330
+ body.on_row_click = self._on_selected
331
+ self.set_focus(body)
332
+
333
+ def _on_apply(self) -> None:
334
+ """Override in subclass."""
335
+ self.dismiss(None)
336
+
337
+ def action_cancel(self) -> None:
338
+ self.dismiss(None)
339
+
340
+ def action_select_previous(self) -> None:
341
+ body = self.query_one("#dialog-body", DialogBody)
342
+ body.move_up()
343
+
344
+ def action_select_next(self) -> None:
345
+ body = self.query_one("#dialog-body", DialogBody)
346
+ body.move_down()
347
+
348
+ def action_confirm(self) -> None:
349
+ body = self.query_one("#dialog-body", DialogBody)
350
+ self._on_selected(body.selected_key)
351
+
352
+ def action_toggle_check(self) -> None:
353
+ body = self.query_one("#dialog-body", DialogBody)
354
+ body.toggle_check()
355
+
356
+ def action_select_all(self) -> None:
357
+ body = self.query_one("#dialog-body", DialogBody)
358
+ body.select_all()
359
+
360
+ def _on_selected(self, key: str | None) -> None:
361
+ """Override in subclass. Called for the selected option on Enter."""
362
+ self.dismiss(None)
@@ -0,0 +1,84 @@
1
+ """Read-only Codex session picker for the Synapse TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from textual.app import ComposeResult
9
+
10
+ from synapse.ui.dialogs.base import DialogBase, OptionItem, SectionHeader
11
+
12
+
13
+ class CodexSessionListDialog(DialogBase):
14
+ """List readable Codex sessions and return ``("codex-import", native_id)``."""
15
+
16
+ _title_icon = "C"
17
+ _title_keys = "up/down enter import · esc"
18
+
19
+ def __init__(self, settings: Any, *, codex_home: Path | None = None) -> None:
20
+ super().__init__()
21
+ self._settings = settings
22
+ self._codex_home = codex_home
23
+ self._sessions: tuple[Any, ...] = ()
24
+ self._warnings: tuple[str, ...] = ()
25
+ try:
26
+ from synapse.integrations.codex_history import CodexHistoryProjector
27
+ from synapse.integrations.codex_sessions import CodexSessionScanner
28
+
29
+ workspace = Path(getattr(settings, "workspace", Path.cwd()))
30
+ result = CodexSessionScanner(codex_home).scan(
31
+ workspace,
32
+ include_rollout_fallback=True,
33
+ )
34
+ projector = CodexHistoryProjector()
35
+ self._sessions = tuple(
36
+ session
37
+ for session in result.sessions
38
+ if (
39
+ (snapshot := projector.project_path(session.rollout_path)).importable
40
+ and snapshot.messages
41
+ )
42
+ )
43
+ skipped = len(result.sessions) - len(self._sessions)
44
+ self._warnings = (*result.warnings, *(
45
+ (f"{skipped} Codex session(s) have no importable visible text",)
46
+ if skipped
47
+ else ()
48
+ ))
49
+ except Exception: # noqa: BLE001
50
+ self._sessions = ()
51
+ self._warnings = ("Codex session discovery failed",)
52
+
53
+ @property
54
+ def title_text(self) -> str:
55
+ return "Import Codex Session"
56
+
57
+ def compose_body(self) -> ComposeResult:
58
+ if not self._sessions:
59
+ yield SectionHeader("No Codex sessions with importable visible text")
60
+ if self._warnings:
61
+ yield SectionHeader(self._warnings[-1])
62
+ return
63
+ yield SectionHeader("Select a safe text snapshot to import")
64
+ self._items = [
65
+ OptionItem(
66
+ key=session.native_id,
67
+ label=session.title or "(untitled Codex session)",
68
+ detail=f"{session.updated_at:%Y-%m-%d %H:%M} {session.source}",
69
+ )
70
+ for session in self._sessions
71
+ ]
72
+
73
+ def on_mount(self) -> None:
74
+ super().on_mount()
75
+ if self._sessions:
76
+ self.query_one("#dialog-body").set_options(self._items, mark=" ")
77
+
78
+ def _on_apply(self) -> None:
79
+ body = self.query_one("#dialog-body")
80
+ key = body.selected_key
81
+ self.dismiss(("codex-import", key) if key else None)
82
+
83
+ def _on_selected(self, key: str | None) -> None:
84
+ self.dismiss(("codex-import", key) if key else None)