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
|
@@ -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)
|