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,71 @@
1
+ """Pure helpers for the TUI turn-rail minimap."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+ _WS_RE = re.compile(r"\s+")
7
+ _RAIL_PREVIEW_MAX = 28
8
+ _RAIL_BAR = "───"
9
+ _RAIL_BAR_DENSE = "━━━"
10
+ _RAIL_BAR_HEAVY = "▓▓▓"
11
+
12
+ def format_turn_rail_preview(
13
+ text: str,
14
+ *,
15
+ max_len: int = _RAIL_PREVIEW_MAX,
16
+ ) -> str:
17
+ """Single-line user-turn preview for the right rail (ellipsis when long)."""
18
+ one = _WS_RE.sub(" ", (text or "").strip())
19
+ if not one:
20
+ return "(empty)"
21
+ limit = max(8, int(max_len or _RAIL_PREVIEW_MAX))
22
+ if len(one) > limit:
23
+ return one[: limit - 1].rstrip() + "…"
24
+ return one
25
+
26
+
27
+ def turn_rail_tick_slots(n: int, height: int) -> list[list[int]]:
28
+ """Map ``n`` turns onto ``height`` minimap rows.
29
+
30
+ When ``n <= height`` the turns are packed tightly and centered vertically
31
+ so the mouse need not travel far. When ``n > height`` the rows are filled
32
+ proportionally with bucket merging (same as before).
33
+ """
34
+ h = max(1, int(height or 1))
35
+ n = max(0, int(n or 0))
36
+ slots: list[list[int]] = [[] for _ in range(h)]
37
+ if n <= 0:
38
+ return slots
39
+ if n <= h:
40
+ # Compact, centered placement.
41
+ start = (h - n) // 2
42
+ for i in range(n):
43
+ slots[start + i].append(i)
44
+ return slots
45
+ # n > h — proportional bucket merging.
46
+ for i in range(n):
47
+ y = i * h // n
48
+ y = min(h - 1, max(0, y))
49
+ slots[y].append(i)
50
+ return slots
51
+
52
+
53
+ def format_turn_rail_bucket_label(
54
+ indices: list[int],
55
+ previews: list[str],
56
+ *,
57
+ max_len: int = _RAIL_PREVIEW_MAX,
58
+ ) -> str:
59
+ """Hover label for a minimap slot (single turn or merged bucket)."""
60
+ if not indices:
61
+ return ""
62
+ if len(indices) == 1:
63
+ return previews[0] if previews else f"#{indices[0] + 1}"
64
+ first = indices[0] + 1
65
+ last = indices[-1] + 1
66
+ head = previews[0] if previews else ""
67
+ prefix = f"#{first}-{last} "
68
+ room = max(6, int(max_len or _RAIL_PREVIEW_MAX) - len(prefix))
69
+ if len(head) > room:
70
+ head = head[: max(0, room - 1)].rstrip() + "…"
71
+ return f"{prefix}{head}" if head else f"#{first}-{last}"
@@ -0,0 +1,83 @@
1
+ """Pure formatting helpers for user transcript turns."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+ from synapse.ui.topbar import display_width, truncate_to_width
7
+
8
+ _WS_RE = re.compile(r"\s+")
9
+ _USER_PREVIEW_MAX_LINES = 3
10
+
11
+ def wrap_user_turn_text(
12
+ text: str,
13
+ *,
14
+ width: int,
15
+ max_lines: int | None = _USER_PREVIEW_MAX_LINES,
16
+ ) -> tuple[list[str], bool]:
17
+ """Word-wrap user prompt for the transcript bar.
18
+
19
+ Returns ``(lines, truncated)``. When ``max_lines`` is None, never truncates.
20
+ Prefers breaks at spaces; falls back to display-width chunks (CJK-safe).
21
+ """
22
+ width = max(8, int(width or 8))
23
+ raw = _WS_RE.sub(" ", (text or "").strip())
24
+ if not raw:
25
+ return [""], False
26
+
27
+ lines: list[str] = []
28
+ i = 0
29
+ n = len(raw)
30
+ while i < n:
31
+ acc = ""
32
+ last_space_acc_len = -1
33
+ j = i
34
+ while j < n:
35
+ ch = raw[j]
36
+ trial = acc + ch
37
+ if display_width(trial) > width:
38
+ break
39
+ acc = trial
40
+ if ch == " ":
41
+ last_space_acc_len = len(acc)
42
+ j += 1
43
+ if not acc:
44
+ # Single character wider than width (rare); force one cell.
45
+ acc = raw[i]
46
+ j = i + 1
47
+ elif j < n and last_space_acc_len > 0:
48
+ # Break at last space inside this line.
49
+ acc = acc[:last_space_acc_len].rstrip()
50
+ j = i + last_space_acc_len
51
+ # skip the space
52
+ if j < n and raw[j] == " ":
53
+ j += 1
54
+ lines.append(acc)
55
+ i = j
56
+
57
+ if max_lines is None or len(lines) <= max_lines:
58
+ return lines, False
59
+ kept = list(lines[: max(1, int(max_lines))])
60
+ last = kept[-1]
61
+ kept[-1] = truncate_to_width(last, max(4, width))
62
+ if not kept[-1].endswith("…"):
63
+ kept[-1] = truncate_to_width(kept[-1], max(4, width - 1)).rstrip("…") + "…"
64
+ return kept, True
65
+
66
+
67
+ def format_user_turn_meta(
68
+ *,
69
+ stamp: str,
70
+ turn_index: int | None = None,
71
+ image_count: int = 0,
72
+ expanded: bool = False,
73
+ truncated: bool = False,
74
+ ) -> str:
75
+ """Right-side meta: optional #n, img count, time; expand hint is separate."""
76
+ bits: list[str] = []
77
+ if turn_index is not None and int(turn_index) > 0:
78
+ bits.append(f"#{int(turn_index)}")
79
+ if image_count and int(image_count) > 0:
80
+ bits.append(f"img×{int(image_count)}")
81
+ if stamp:
82
+ bits.append(stamp)
83
+ return " · ".join(bits)
synapse/ui/welcome.py ADDED
@@ -0,0 +1,261 @@
1
+ """Animated welcome screen for the Synapse TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from rich.cells import cell_len
9
+ from rich.text import Text
10
+ from textual.widgets import Static
11
+
12
+ _BRAILLE_BLANK = "\u2800"
13
+ _SHIMMER_FPS = 12.0
14
+ _REVEAL_DURATION = 3.0
15
+ _VISIBLE_DURATION = 3.0
16
+ _ERASE_DURATION = 3.0
17
+ _HIDDEN_DURATION = 0.8
18
+ _CYCLE_SECONDS = (
19
+ _REVEAL_DURATION + _VISIBLE_DURATION + _ERASE_DURATION + _HIDDEN_DURATION
20
+ )
21
+ _LEFT_COL_MASK = 0x4D # braille left-column bits: 1+2+4+64
22
+ _BRAILLE_DOTS = (
23
+ ((0, 0, 1), (1, 0, 2), (2, 0, 4), (3, 0, 64)),
24
+ ((0, 1, 8), (1, 1, 16), (2, 1, 32), (3, 1, 128)),
25
+ )
26
+ _WORD_BITMAPS = {
27
+ "S": ("11110", "10000", "10000", "11110", "00001", "00001", "11110"),
28
+ "Y": ("10001", "10001", "01010", "00100", "00100", "00100", "00100"),
29
+ "N": ("10001", "11001", "11001", "10101", "10011", "10011", "10001"),
30
+ "A": ("01110", "10001", "10001", "11111", "10001", "10001", "10001"),
31
+ "P": ("11110", "10001", "10001", "11110", "10000", "10000", "10000"),
32
+ "E": ("11111", "10000", "10000", "11110", "10000", "10000", "11111"),
33
+ }
34
+
35
+
36
+ def _braille_cell(bitmap: list[str], row: int, column: int) -> str:
37
+ value = 0
38
+ for row_offset, column_offset, bit in (
39
+ dot for column_dots in _BRAILLE_DOTS for dot in column_dots
40
+ ):
41
+ source_row = row * 4 + row_offset
42
+ source_column = column * 2 + column_offset
43
+ if (
44
+ source_row < len(bitmap)
45
+ and source_column < len(bitmap[source_row])
46
+ and bitmap[source_row][source_column] == "1"
47
+ ):
48
+ value |= bit
49
+ return chr(0x2800 + value)
50
+
51
+
52
+ def _braille_glyph(pattern: tuple[str, ...], x_scale: int, y_scale: int) -> tuple[str, ...]:
53
+ bitmap = [
54
+ "".join(pixel * x_scale for pixel in source_row)
55
+ for source_row in pattern
56
+ for _ in range(y_scale)
57
+ ]
58
+ cell_rows = (len(bitmap) + 3) // 4
59
+ cell_columns = (len(bitmap[0]) + 1) // 2
60
+ return tuple(
61
+ "".join(_braille_cell(bitmap, row, column) for column in range(cell_columns))
62
+ for row in range(cell_rows)
63
+ )
64
+
65
+
66
+ def _build_braille_logo(x_scale: int, y_scale: int) -> tuple[str, ...]:
67
+ letters = "SYNAPSE"
68
+ glyphs = {
69
+ letter: _braille_glyph(pattern, x_scale, y_scale)
70
+ for letter, pattern in _WORD_BITMAPS.items()
71
+ }
72
+ return tuple(
73
+ _BRAILLE_BLANK.join(glyphs[letter][letter_row] for letter in letters)
74
+ for letter_row in range(len(next(iter(glyphs.values()))))
75
+ )
76
+
77
+
78
+ _LOGO = _build_braille_logo(x_scale=3, y_scale=4)
79
+ _COMPACT_LOGO = _build_braille_logo(x_scale=2, y_scale=2)
80
+
81
+ def _workspace_label(workspace: str | Path) -> str:
82
+ value = str(workspace or "workspace").rstrip("/\\")
83
+ return Path(value).name or value or "workspace"
84
+
85
+
86
+ def _logo_phase(elapsed: float) -> tuple[str, float]:
87
+ """Return the cycle phase and its elapsed time in seconds."""
88
+ cycle_elapsed = elapsed % _CYCLE_SECONDS
89
+ reveal_end = _REVEAL_DURATION
90
+ visible_end = reveal_end + _VISIBLE_DURATION
91
+ erase_end = visible_end + _ERASE_DURATION
92
+ if cycle_elapsed < reveal_end:
93
+ return "revealing", cycle_elapsed
94
+ if cycle_elapsed < visible_end:
95
+ return "visible", cycle_elapsed - reveal_end
96
+ if cycle_elapsed < erase_end:
97
+ return "erasing", cycle_elapsed - visible_end
98
+ return "hidden", cycle_elapsed - erase_end
99
+
100
+
101
+ def _dot_char(full_value: int, fraction: float) -> str:
102
+ """Build a Braille glyph with only a subset of dots active.
103
+
104
+ ``fraction`` 0→1 reveals left-column first, then right-column.
105
+ ``fraction >= 1`` returns the full glyph.
106
+ """
107
+ if full_value == 0:
108
+ return _BRAILLE_BLANK
109
+ if fraction >= 1.0:
110
+ return chr(0x2800 + full_value)
111
+ if fraction < 0.5:
112
+ partial = full_value & _LEFT_COL_MASK
113
+ else:
114
+ partial = full_value
115
+ return chr(0x2800 + partial) if partial else _BRAILLE_BLANK
116
+
117
+
118
+ def _dot_char_erase(full_value: int, fraction: float) -> str:
119
+ """Build a Braille glyph erasing right-column then left-column.
120
+
121
+ ``fraction`` 0→1 removes right-column first, then everything.
122
+ """
123
+ if full_value == 0:
124
+ return _BRAILLE_BLANK
125
+ if fraction >= 1.0:
126
+ return _BRAILLE_BLANK
127
+ if fraction < 0.5:
128
+ partial = full_value & _LEFT_COL_MASK
129
+ else:
130
+ partial = 0
131
+ return chr(0x2800 + partial) if partial else _BRAILLE_BLANK
132
+
133
+
134
+ def _logo_style(
135
+ frame: int,
136
+ column: int,
137
+ row: int,
138
+ width: int,
139
+ full_value: int,
140
+ *,
141
+ muted: str,
142
+ fg: str,
143
+ accent: str,
144
+ total_rows: int = 7,
145
+ ) -> tuple[str, str]:
146
+ """Return ``(braille_char, style)`` for one animation frame."""
147
+ elapsed = frame / _SHIMMER_FPS
148
+ phase, phase_elapsed = _logo_phase(elapsed)
149
+
150
+ if phase == "hidden":
151
+ return _dot_char(full_value, 0), muted
152
+ if phase == "revealing":
153
+ reveal_column = (phase_elapsed / _REVEAL_DURATION) * width
154
+ if column > reveal_column:
155
+ return _dot_char(full_value, 0), muted
156
+ # typing cursor: recently-revealed chars flash accent then settle to fg
157
+ distance = reveal_column - column
158
+ if distance < 1.2:
159
+ return _dot_char(full_value, 1.0), f"bold {accent}"
160
+ if distance < 3.5:
161
+ return _dot_char(full_value, 1.0), fg
162
+ return _dot_char(full_value, 1.0), fg
163
+ if phase == "erasing":
164
+ column_norm = column / max(1, width - 1)
165
+ position = row + column_norm * 0.4
166
+ progress = (phase_elapsed / _ERASE_DURATION) * (total_rows + 0.4)
167
+ local = max(0.0, min(1.0, (progress - position) / 0.6))
168
+ return _dot_char_erase(full_value, local), fg
169
+ return _dot_char(full_value, 1.0), fg
170
+
171
+
172
+ def render_welcome_frame(
173
+ frame: int,
174
+ *,
175
+ workspace: str | Path = "workspace",
176
+ compact: bool = False,
177
+ theme: Any | None = None,
178
+ ) -> Text:
179
+ """Build one animation frame as theme-aware Rich text."""
180
+ if theme is None:
181
+ from synapse.ui.theme import get_theme
182
+
183
+ theme = get_theme()
184
+
185
+ fg = str(getattr(theme, "fg", "#e8eaed"))
186
+ dim = str(getattr(theme, "dim", "#9aa0a6"))
187
+ muted = str(getattr(theme, "muted", "#5f6368"))
188
+ accent = str(getattr(theme, "user", "#8ab4f8"))
189
+ green = str(getattr(theme, "green", "#81c995"))
190
+
191
+ out = Text(justify="center")
192
+ logo = _COMPACT_LOGO if compact else _LOGO
193
+ logo_width = max(cell_len(line) for line in logo)
194
+ logo_rows = len(logo)
195
+ for row, line in enumerate(logo):
196
+ left = max(0, (logo_width - cell_len(line)) // 2)
197
+ for column, char in enumerate(line):
198
+ if char in {" ", _BRAILLE_BLANK}:
199
+ out.append(char, style=muted)
200
+ else:
201
+ anim_char, style = _logo_style(
202
+ frame,
203
+ left + column,
204
+ row,
205
+ logo_width,
206
+ ord(char) - 0x2800,
207
+ muted=muted,
208
+ fg=fg,
209
+ accent=accent,
210
+ total_rows=logo_rows,
211
+ )
212
+ out.append(anim_char, style=style)
213
+ out.append("\n")
214
+
215
+ out.append("\n", style=muted)
216
+ out.append("\nLOCAL CODING INTELLIGENCE\n", style=f"bold {green}")
217
+ out.append("Inspect. Plan. Build. Verify.\n", style=dim)
218
+ out.append(f"\n{_workspace_label(workspace)}\n", style=f"bold {fg}")
219
+ out.append("Describe the outcome you want to create.\n", style=dim)
220
+ out.append("@ files / commands F2 model F3 theme", style=muted)
221
+ return out
222
+
223
+
224
+ class WelcomeView(Static):
225
+ """A restrained animated Braille welcome screen for an empty timeline."""
226
+
227
+ def __init__(self, workspace: str | Path, **kwargs: Any) -> None:
228
+ super().__init__(**kwargs)
229
+ self.workspace = workspace
230
+ self._frame = 0
231
+ self._animate = True
232
+
233
+ def on_mount(self) -> None:
234
+ self.refresh_logo()
235
+ self.set_interval(1 / _SHIMMER_FPS, self._advance_frame)
236
+
237
+ def on_resize(self) -> None:
238
+ self.refresh_logo()
239
+
240
+ def start_animation(self) -> None:
241
+ self._animate = True
242
+ self.refresh_logo()
243
+
244
+ def stop_animation(self) -> None:
245
+ self._animate = False
246
+
247
+ def _advance_frame(self) -> None:
248
+ if not self._animate:
249
+ return
250
+ self._frame += 1
251
+ self.refresh_logo()
252
+
253
+ def refresh_logo(self) -> None:
254
+ compact = bool(self.size.width and self.size.width < 66)
255
+ self.update(
256
+ render_welcome_frame(
257
+ self._frame,
258
+ workspace=self.workspace,
259
+ compact=compact,
260
+ )
261
+ )