scc-cli 1.5.3__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.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

Files changed (153) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +311 -0
  8. scc_cli/cli_common.py +190 -0
  9. scc_cli/cli_helpers.py +244 -0
  10. scc_cli/commands/__init__.py +20 -0
  11. scc_cli/commands/admin.py +708 -0
  12. scc_cli/commands/audit.py +246 -0
  13. scc_cli/commands/config.py +528 -0
  14. scc_cli/commands/exceptions.py +696 -0
  15. scc_cli/commands/init.py +272 -0
  16. scc_cli/commands/launch/__init__.py +73 -0
  17. scc_cli/commands/launch/app.py +1247 -0
  18. scc_cli/commands/launch/render.py +309 -0
  19. scc_cli/commands/launch/sandbox.py +135 -0
  20. scc_cli/commands/launch/workspace.py +339 -0
  21. scc_cli/commands/org/__init__.py +49 -0
  22. scc_cli/commands/org/_builders.py +264 -0
  23. scc_cli/commands/org/app.py +41 -0
  24. scc_cli/commands/org/import_cmd.py +267 -0
  25. scc_cli/commands/org/init_cmd.py +269 -0
  26. scc_cli/commands/org/schema_cmd.py +76 -0
  27. scc_cli/commands/org/status_cmd.py +157 -0
  28. scc_cli/commands/org/update_cmd.py +330 -0
  29. scc_cli/commands/org/validate_cmd.py +138 -0
  30. scc_cli/commands/support.py +323 -0
  31. scc_cli/commands/team.py +910 -0
  32. scc_cli/commands/worktree/__init__.py +72 -0
  33. scc_cli/commands/worktree/_helpers.py +57 -0
  34. scc_cli/commands/worktree/app.py +170 -0
  35. scc_cli/commands/worktree/container_commands.py +385 -0
  36. scc_cli/commands/worktree/context_commands.py +61 -0
  37. scc_cli/commands/worktree/session_commands.py +128 -0
  38. scc_cli/commands/worktree/worktree_commands.py +734 -0
  39. scc_cli/config.py +647 -0
  40. scc_cli/confirm.py +20 -0
  41. scc_cli/console.py +562 -0
  42. scc_cli/contexts.py +394 -0
  43. scc_cli/core/__init__.py +68 -0
  44. scc_cli/core/constants.py +101 -0
  45. scc_cli/core/errors.py +297 -0
  46. scc_cli/core/exit_codes.py +91 -0
  47. scc_cli/core/workspace.py +57 -0
  48. scc_cli/deprecation.py +54 -0
  49. scc_cli/deps.py +189 -0
  50. scc_cli/docker/__init__.py +127 -0
  51. scc_cli/docker/core.py +467 -0
  52. scc_cli/docker/credentials.py +726 -0
  53. scc_cli/docker/launch.py +595 -0
  54. scc_cli/doctor/__init__.py +105 -0
  55. scc_cli/doctor/checks/__init__.py +166 -0
  56. scc_cli/doctor/checks/cache.py +314 -0
  57. scc_cli/doctor/checks/config.py +107 -0
  58. scc_cli/doctor/checks/environment.py +182 -0
  59. scc_cli/doctor/checks/json_helpers.py +157 -0
  60. scc_cli/doctor/checks/organization.py +264 -0
  61. scc_cli/doctor/checks/worktree.py +278 -0
  62. scc_cli/doctor/render.py +365 -0
  63. scc_cli/doctor/types.py +66 -0
  64. scc_cli/evaluation/__init__.py +27 -0
  65. scc_cli/evaluation/apply_exceptions.py +207 -0
  66. scc_cli/evaluation/evaluate.py +97 -0
  67. scc_cli/evaluation/models.py +80 -0
  68. scc_cli/git.py +84 -0
  69. scc_cli/json_command.py +166 -0
  70. scc_cli/json_output.py +159 -0
  71. scc_cli/kinds.py +65 -0
  72. scc_cli/marketplace/__init__.py +123 -0
  73. scc_cli/marketplace/adapter.py +74 -0
  74. scc_cli/marketplace/compute.py +377 -0
  75. scc_cli/marketplace/constants.py +87 -0
  76. scc_cli/marketplace/managed.py +135 -0
  77. scc_cli/marketplace/materialize.py +846 -0
  78. scc_cli/marketplace/normalize.py +548 -0
  79. scc_cli/marketplace/render.py +281 -0
  80. scc_cli/marketplace/resolve.py +459 -0
  81. scc_cli/marketplace/schema.py +506 -0
  82. scc_cli/marketplace/sync.py +279 -0
  83. scc_cli/marketplace/team_cache.py +195 -0
  84. scc_cli/marketplace/team_fetch.py +689 -0
  85. scc_cli/marketplace/trust.py +244 -0
  86. scc_cli/models/__init__.py +41 -0
  87. scc_cli/models/exceptions.py +273 -0
  88. scc_cli/models/plugin_audit.py +434 -0
  89. scc_cli/org_templates.py +269 -0
  90. scc_cli/output_mode.py +167 -0
  91. scc_cli/panels.py +113 -0
  92. scc_cli/platform.py +350 -0
  93. scc_cli/profiles.py +960 -0
  94. scc_cli/remote.py +443 -0
  95. scc_cli/schemas/__init__.py +1 -0
  96. scc_cli/schemas/org-v1.schema.json +456 -0
  97. scc_cli/schemas/team-config.v1.schema.json +163 -0
  98. scc_cli/services/__init__.py +1 -0
  99. scc_cli/services/git/__init__.py +79 -0
  100. scc_cli/services/git/branch.py +151 -0
  101. scc_cli/services/git/core.py +216 -0
  102. scc_cli/services/git/hooks.py +108 -0
  103. scc_cli/services/git/worktree.py +444 -0
  104. scc_cli/services/workspace/__init__.py +36 -0
  105. scc_cli/services/workspace/resolver.py +223 -0
  106. scc_cli/services/workspace/suspicious.py +200 -0
  107. scc_cli/sessions.py +425 -0
  108. scc_cli/setup.py +589 -0
  109. scc_cli/source_resolver.py +470 -0
  110. scc_cli/stats.py +378 -0
  111. scc_cli/stores/__init__.py +13 -0
  112. scc_cli/stores/exception_store.py +251 -0
  113. scc_cli/subprocess_utils.py +88 -0
  114. scc_cli/teams.py +383 -0
  115. scc_cli/templates/__init__.py +2 -0
  116. scc_cli/templates/org/__init__.py +0 -0
  117. scc_cli/templates/org/minimal.json +19 -0
  118. scc_cli/templates/org/reference.json +74 -0
  119. scc_cli/templates/org/strict.json +38 -0
  120. scc_cli/templates/org/teams.json +42 -0
  121. scc_cli/templates/statusline.sh +75 -0
  122. scc_cli/theme.py +348 -0
  123. scc_cli/ui/__init__.py +154 -0
  124. scc_cli/ui/branding.py +68 -0
  125. scc_cli/ui/chrome.py +401 -0
  126. scc_cli/ui/dashboard/__init__.py +62 -0
  127. scc_cli/ui/dashboard/_dashboard.py +794 -0
  128. scc_cli/ui/dashboard/loaders.py +452 -0
  129. scc_cli/ui/dashboard/models.py +185 -0
  130. scc_cli/ui/dashboard/orchestrator.py +735 -0
  131. scc_cli/ui/formatters.py +444 -0
  132. scc_cli/ui/gate.py +350 -0
  133. scc_cli/ui/git_interactive.py +869 -0
  134. scc_cli/ui/git_render.py +176 -0
  135. scc_cli/ui/help.py +157 -0
  136. scc_cli/ui/keys.py +615 -0
  137. scc_cli/ui/list_screen.py +437 -0
  138. scc_cli/ui/picker.py +763 -0
  139. scc_cli/ui/prompts.py +201 -0
  140. scc_cli/ui/quick_resume.py +116 -0
  141. scc_cli/ui/wizard.py +576 -0
  142. scc_cli/update.py +680 -0
  143. scc_cli/utils/__init__.py +39 -0
  144. scc_cli/utils/fixit.py +264 -0
  145. scc_cli/utils/fuzzy.py +124 -0
  146. scc_cli/utils/locks.py +114 -0
  147. scc_cli/utils/ttl.py +376 -0
  148. scc_cli/validate.py +455 -0
  149. scc_cli-1.5.3.dist-info/METADATA +401 -0
  150. scc_cli-1.5.3.dist-info/RECORD +153 -0
  151. scc_cli-1.5.3.dist-info/WHEEL +4 -0
  152. scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
  153. scc_cli-1.5.3.dist-info/licenses/LICENSE +21 -0
scc_cli/ui/chrome.py ADDED
@@ -0,0 +1,401 @@
1
+ """Shared chrome layout rendering for interactive UI components.
2
+
3
+ This module provides the consistent visual wrapper (chrome) around all
4
+ list-based UI components. It handles:
5
+ - Title and subtitle rendering
6
+ - Tab row for dashboard views
7
+ - Search/filter query display
8
+ - Footer hints with keybindings
9
+ - Consistent spacing and styling
10
+
11
+ The chrome pattern ensures visual consistency across pickers, multi-select
12
+ lists, and the dashboard while keeping content rendering separate.
13
+
14
+ Example:
15
+ >>> config = ChromeConfig.for_picker("Select Team", 5)
16
+ >>> chrome = Chrome(config)
17
+ >>> rendered = chrome.render(body_content, search_query="dev")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+ from typing import TYPE_CHECKING
24
+
25
+ from rich.console import Group
26
+ from rich.panel import Panel
27
+ from rich.text import Text
28
+
29
+ from ..theme import Borders, Indicators
30
+
31
+ if TYPE_CHECKING:
32
+ from rich.console import RenderableType
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class FooterHint:
37
+ """Single hint displayed in the footer.
38
+
39
+ Attributes:
40
+ key: The key or key combination (e.g., "↑↓", "Enter", "q").
41
+ action: Description of what the key does (e.g., "navigate", "select").
42
+ dimmed: Whether to show this hint as dimmed/disabled (e.g., standalone mode).
43
+ """
44
+
45
+ key: str
46
+ action: str
47
+ dimmed: bool = False
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ChromeConfig:
52
+ """Configuration for the shared chrome layout.
53
+
54
+ Use the factory methods for common configurations:
55
+ - for_picker(): Standard single-select picker
56
+ - for_multi_select(): Multi-select list
57
+ - for_dashboard(): Tabbed dashboard view
58
+
59
+ Attributes:
60
+ title: Main title displayed at top.
61
+ subtitle: Secondary text (e.g., item count).
62
+ context_label: Current work context (e.g., "team · repo · worktree").
63
+ show_tabs: Whether to display tab row.
64
+ tabs: List of tab names when show_tabs=True.
65
+ active_tab_index: Index of currently active tab.
66
+ show_search: Whether to show search/filter row.
67
+ footer_hints: List of keybinding hints for footer.
68
+ """
69
+
70
+ title: str
71
+ subtitle: str = ""
72
+ context_label: str = ""
73
+ show_tabs: bool = False
74
+ tabs: tuple[str, ...] = ()
75
+ active_tab_index: int = 0
76
+ show_search: bool = True
77
+ footer_hints: tuple[FooterHint, ...] = ()
78
+
79
+ @classmethod
80
+ def for_picker(
81
+ cls,
82
+ title: str,
83
+ subtitle: str | None = None,
84
+ *,
85
+ item_count: int | None = None,
86
+ standalone: bool = False,
87
+ ) -> ChromeConfig:
88
+ """Create standard config for single-select pickers.
89
+
90
+ Args:
91
+ title: Picker title (e.g., "Select Team").
92
+ subtitle: Optional subtitle text. If not provided and item_count is,
93
+ generates "{item_count} available".
94
+ item_count: Deprecated, use subtitle instead. Number of available items.
95
+ standalone: If True, dim the "t teams" hint (not available without org).
96
+
97
+ Returns:
98
+ ChromeConfig with standard picker hints.
99
+ """
100
+ if subtitle is None and item_count is not None:
101
+ subtitle = f"{item_count} available"
102
+ return cls(
103
+ title=title,
104
+ subtitle=subtitle or "",
105
+ show_tabs=False,
106
+ show_search=True,
107
+ footer_hints=(
108
+ FooterHint("↑↓", "navigate"),
109
+ FooterHint("Enter", "select"),
110
+ FooterHint("Esc", "back"),
111
+ FooterHint("q", "quit"),
112
+ FooterHint("t", "teams", dimmed=standalone),
113
+ ),
114
+ )
115
+
116
+ @classmethod
117
+ def for_multi_select(cls, title: str, selected: int, total: int) -> ChromeConfig:
118
+ """Create standard config for multi-select lists.
119
+
120
+ Args:
121
+ title: List title (e.g., "Stop Containers").
122
+ selected: Number of currently selected items.
123
+ total: Total number of items.
124
+
125
+ Returns:
126
+ ChromeConfig with multi-select hints.
127
+ """
128
+ return cls(
129
+ title=title,
130
+ subtitle=f"{selected} of {total} selected",
131
+ show_tabs=False,
132
+ show_search=True,
133
+ footer_hints=(
134
+ FooterHint("↑↓", "navigate"),
135
+ FooterHint("Space", "toggle"),
136
+ FooterHint("a", "toggle all"),
137
+ FooterHint("Enter", "confirm"),
138
+ FooterHint("Esc", "back"),
139
+ FooterHint("q", "quit"),
140
+ FooterHint("t", "teams"),
141
+ ),
142
+ )
143
+
144
+ @classmethod
145
+ def for_quick_resume(
146
+ cls, title: str, subtitle: str | None = None, *, standalone: bool = False
147
+ ) -> ChromeConfig:
148
+ """Create config for Quick Resume picker with consistent key hints.
149
+
150
+ The Quick Resume picker follows the standard TUI key contract:
151
+ - Enter: Select highlighted item (New Session or resume context)
152
+ - n: Explicitly start a new session (skip resume)
153
+ - a: Toggle all teams view (show contexts from all teams)
154
+ - Esc: Back/dismiss (cancel wizard from this screen)
155
+ - q: Quit app
156
+
157
+ Args:
158
+ title: Picker title (typically "Quick Resume").
159
+ subtitle: Optional subtitle (defaults to hint about n/Esc).
160
+ standalone: If True, dim the "t teams" and "a all teams" hints.
161
+
162
+ Returns:
163
+ ChromeConfig with Quick Resume-specific hints.
164
+ """
165
+ default_subtitle = "n for new session · a all teams · Esc to go back"
166
+ if standalone:
167
+ default_subtitle = "n for new session · Esc to go back"
168
+
169
+ return cls(
170
+ title=title,
171
+ subtitle=subtitle or default_subtitle,
172
+ show_tabs=False,
173
+ show_search=True,
174
+ footer_hints=(
175
+ FooterHint("↑↓", "navigate"),
176
+ FooterHint("Enter", "select"),
177
+ FooterHint("n", "new session"),
178
+ FooterHint("a", "all teams", dimmed=standalone),
179
+ FooterHint("Esc", "back"),
180
+ FooterHint("q", "quit"),
181
+ FooterHint("t", "teams", dimmed=standalone),
182
+ ),
183
+ )
184
+
185
+ @classmethod
186
+ def for_dashboard(
187
+ cls,
188
+ tabs: list[str],
189
+ active: int,
190
+ *,
191
+ standalone: bool = False,
192
+ details_open: bool = False,
193
+ custom_hints: tuple[FooterHint, ...] | None = None,
194
+ ) -> ChromeConfig:
195
+ """Create standard config for dashboard view.
196
+
197
+ Args:
198
+ tabs: List of tab names.
199
+ active: Index of active tab (0-based).
200
+ standalone: If True, dim the "t teams" hint (not available without org).
201
+ details_open: If True, show "Esc close" instead of "Enter details".
202
+ custom_hints: Optional custom footer hints to override defaults.
203
+ When provided, these hints are used instead of the standard set.
204
+
205
+ Returns:
206
+ ChromeConfig with dashboard hints.
207
+ """
208
+ # Use custom hints if provided
209
+ if custom_hints is not None:
210
+ footer_hints = custom_hints
211
+ # Otherwise fall back to standard hints based on details state
212
+ elif details_open:
213
+ footer_hints = (
214
+ FooterHint("↑↓", "navigate"),
215
+ FooterHint("Esc", "close"),
216
+ FooterHint("Tab", "switch tab"),
217
+ FooterHint("t", "teams", dimmed=standalone),
218
+ FooterHint("q", "quit"),
219
+ FooterHint("?", "help"),
220
+ )
221
+ else:
222
+ footer_hints = (
223
+ FooterHint("↑↓", "navigate"),
224
+ FooterHint("Tab", "switch tab"),
225
+ FooterHint("Enter", "details"),
226
+ FooterHint("t", "teams", dimmed=standalone),
227
+ FooterHint("q", "quit"),
228
+ FooterHint("?", "help"),
229
+ )
230
+
231
+ return cls(
232
+ title="SCC Dashboard",
233
+ show_tabs=True,
234
+ tabs=tuple(tabs),
235
+ active_tab_index=active,
236
+ show_search=True,
237
+ footer_hints=footer_hints,
238
+ )
239
+
240
+ def with_context(self, context_label: str) -> ChromeConfig:
241
+ """Create a new config with context label added.
242
+
243
+ This is useful for adding current work context (team/repo/worktree)
244
+ to any existing chrome configuration.
245
+
246
+ Args:
247
+ context_label: The context label (e.g., "platform · api · main").
248
+
249
+ Returns:
250
+ New ChromeConfig with context_label set.
251
+ """
252
+ return ChromeConfig(
253
+ title=self.title,
254
+ subtitle=self.subtitle,
255
+ context_label=context_label,
256
+ show_tabs=self.show_tabs,
257
+ tabs=self.tabs,
258
+ active_tab_index=self.active_tab_index,
259
+ show_search=self.show_search,
260
+ footer_hints=self.footer_hints,
261
+ )
262
+
263
+
264
+ class Chrome:
265
+ """Renderer for the shared chrome layout.
266
+
267
+ Chrome wraps content in a consistent visual frame with title,
268
+ tabs, search, and footer hints.
269
+
270
+ Attributes:
271
+ config: The ChromeConfig defining layout options.
272
+ """
273
+
274
+ def __init__(self, config: ChromeConfig) -> None:
275
+ """Initialize chrome renderer.
276
+
277
+ Args:
278
+ config: Layout configuration.
279
+ """
280
+ self.config = config
281
+
282
+ def render(
283
+ self,
284
+ body: RenderableType,
285
+ *,
286
+ search_query: str = "",
287
+ ) -> RenderableType:
288
+ """Render complete chrome with body content.
289
+
290
+ Args:
291
+ body: The main content to display inside chrome.
292
+ search_query: Current filter/search query.
293
+
294
+ Returns:
295
+ A Rich renderable combining all chrome elements.
296
+ """
297
+ elements: list[RenderableType] = []
298
+
299
+ # Tabs row (if enabled)
300
+ if self.config.show_tabs:
301
+ elements.append(self._render_tabs())
302
+
303
+ # Search row (if enabled and has query)
304
+ if self.config.show_search:
305
+ elements.append(self._render_search(search_query))
306
+
307
+ # Body content
308
+ elements.append(body)
309
+
310
+ # Footer hints
311
+ if self.config.footer_hints:
312
+ elements.append(self._render_footer())
313
+
314
+ # Combine into panel with title
315
+ title = self._build_title()
316
+ return Panel(
317
+ Group(*elements),
318
+ title=title,
319
+ title_align="left",
320
+ border_style="blue",
321
+ padding=(0, 1),
322
+ )
323
+
324
+ def _build_title(self) -> str:
325
+ """Build the panel title string.
326
+
327
+ Format: "Title │ context_label │ subtitle"
328
+ - If only subtitle: "Title │ subtitle"
329
+ - If only context: "Title │ context_label"
330
+ - If neither: "Title"
331
+ """
332
+ parts = [self.config.title]
333
+ if self.config.context_label:
334
+ parts.append(self.config.context_label)
335
+ if self.config.subtitle:
336
+ parts.append(self.config.subtitle)
337
+ sep = Indicators.get("VERTICAL_LINE")
338
+ return f" {sep} ".join(parts)
339
+
340
+ def _render_tabs(self) -> Text:
341
+ """Render the tab row."""
342
+ text = Text()
343
+ for i, tab in enumerate(self.config.tabs):
344
+ if i > 0:
345
+ text.append(" ")
346
+ if i == self.config.active_tab_index:
347
+ text.append(f"[{tab}]", style="bold cyan")
348
+ else:
349
+ text.append(f" {tab} ", style="dim")
350
+ text.append("\n")
351
+ return text
352
+
353
+ def _render_search(self, query: str) -> Text:
354
+ """Render the search/filter row."""
355
+ text = Text()
356
+ if query:
357
+ text.append(f"{Indicators.get('SEARCH_ICON')} ", style="dim")
358
+ text.append(query, style="yellow")
359
+ text.append(Indicators.get("TEXT_CURSOR"), style="yellow bold")
360
+ else:
361
+ text.append("Type to filter...", style="dim italic")
362
+ text.append("\n")
363
+ return text
364
+
365
+ def _render_footer(self) -> Text:
366
+ """Render the footer hints row."""
367
+ text = Text()
368
+ text.append(Borders.FOOTER_SEPARATOR * 40 + "\n", style="dim")
369
+ for i, hint in enumerate(self.config.footer_hints):
370
+ if i > 0:
371
+ text.append(f" {Indicators.get('VERTICAL_LINE')} ", style="dim")
372
+ # Dimmed hints (e.g., teams in standalone mode) show in strike-through dim
373
+ if hint.dimmed:
374
+ text.append(hint.key, style="dim strike")
375
+ text.append(" ", style="dim")
376
+ text.append(hint.action, style="dim strike")
377
+ else:
378
+ text.append(hint.key, style="cyan bold")
379
+ text.append(" ", style="dim")
380
+ text.append(hint.action, style="dim")
381
+ return text
382
+
383
+
384
+ def render_chrome(
385
+ config: ChromeConfig,
386
+ body: RenderableType,
387
+ *,
388
+ search_query: str = "",
389
+ ) -> RenderableType:
390
+ """Convenience function to render chrome without instantiating Chrome class.
391
+
392
+ Args:
393
+ config: Chrome configuration.
394
+ body: Body content to wrap.
395
+ search_query: Current search/filter query.
396
+
397
+ Returns:
398
+ Complete rendered chrome with body.
399
+ """
400
+ chrome = Chrome(config)
401
+ return chrome.render(body, search_query=search_query)
@@ -0,0 +1,62 @@
1
+ """Dashboard package for SCC CLI interactive interface.
2
+
3
+ This package provides the tabbed dashboard interface shown when running
4
+ `scc` with no arguments in an interactive terminal.
5
+
6
+ Public API:
7
+ run_dashboard: Entry point for the dashboard UI
8
+ Dashboard: Main dashboard class for direct instantiation
9
+ DashboardTab: Enum of available tabs
10
+ DashboardState: State management for the dashboard
11
+ TabData: Data container for individual tabs
12
+ TAB_ORDER: Canonical ordering of tabs
13
+
14
+ Module structure:
15
+ models.py: Data models (DashboardTab, TabData, DashboardState)
16
+ loaders.py: Tab data loading functions
17
+ _dashboard.py: Dashboard class implementation
18
+ orchestrator.py: Entry point and flow handlers
19
+
20
+ Example:
21
+ >>> from scc_cli.ui.dashboard import run_dashboard
22
+ >>> run_dashboard() # Interactive dashboard
23
+ """
24
+
25
+ from .loaders import (
26
+ _load_all_tab_data,
27
+ _load_containers_tab_data,
28
+ _load_sessions_tab_data,
29
+ _load_status_tab_data,
30
+ _load_worktrees_tab_data,
31
+ )
32
+ from .models import TAB_ORDER, DashboardState, DashboardTab, TabData
33
+ from .orchestrator import _prepare_for_nested_ui, run_dashboard
34
+
35
+ # Lazy import for Dashboard to avoid circular imports
36
+ # (Dashboard depends on models, but models doesn't depend on Dashboard)
37
+
38
+
39
+ def __getattr__(name: str) -> object:
40
+ """Lazy import for Dashboard class."""
41
+ if name == "Dashboard":
42
+ from ._dashboard import Dashboard
43
+
44
+ return Dashboard
45
+ msg = f"module {__name__!r} has no attribute {name!r}"
46
+ raise AttributeError(msg)
47
+
48
+
49
+ __all__ = [
50
+ "Dashboard",
51
+ "DashboardState",
52
+ "DashboardTab",
53
+ "TAB_ORDER",
54
+ "TabData",
55
+ "_load_all_tab_data",
56
+ "_load_containers_tab_data",
57
+ "_load_sessions_tab_data",
58
+ "_load_status_tab_data",
59
+ "_load_worktrees_tab_data",
60
+ "_prepare_for_nested_ui",
61
+ "run_dashboard",
62
+ ]