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.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +311 -0
- scc_cli/cli_common.py +190 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/commands/__init__.py +20 -0
- scc_cli/commands/admin.py +708 -0
- scc_cli/commands/audit.py +246 -0
- scc_cli/commands/config.py +528 -0
- scc_cli/commands/exceptions.py +696 -0
- scc_cli/commands/init.py +272 -0
- scc_cli/commands/launch/__init__.py +73 -0
- scc_cli/commands/launch/app.py +1247 -0
- scc_cli/commands/launch/render.py +309 -0
- scc_cli/commands/launch/sandbox.py +135 -0
- scc_cli/commands/launch/workspace.py +339 -0
- scc_cli/commands/org/__init__.py +49 -0
- scc_cli/commands/org/_builders.py +264 -0
- scc_cli/commands/org/app.py +41 -0
- scc_cli/commands/org/import_cmd.py +267 -0
- scc_cli/commands/org/init_cmd.py +269 -0
- scc_cli/commands/org/schema_cmd.py +76 -0
- scc_cli/commands/org/status_cmd.py +157 -0
- scc_cli/commands/org/update_cmd.py +330 -0
- scc_cli/commands/org/validate_cmd.py +138 -0
- scc_cli/commands/support.py +323 -0
- scc_cli/commands/team.py +910 -0
- scc_cli/commands/worktree/__init__.py +72 -0
- scc_cli/commands/worktree/_helpers.py +57 -0
- scc_cli/commands/worktree/app.py +170 -0
- scc_cli/commands/worktree/container_commands.py +385 -0
- scc_cli/commands/worktree/context_commands.py +61 -0
- scc_cli/commands/worktree/session_commands.py +128 -0
- scc_cli/commands/worktree/worktree_commands.py +734 -0
- scc_cli/config.py +647 -0
- scc_cli/confirm.py +20 -0
- scc_cli/console.py +562 -0
- scc_cli/contexts.py +394 -0
- scc_cli/core/__init__.py +68 -0
- scc_cli/core/constants.py +101 -0
- scc_cli/core/errors.py +297 -0
- scc_cli/core/exit_codes.py +91 -0
- scc_cli/core/workspace.py +57 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +467 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +595 -0
- scc_cli/doctor/__init__.py +105 -0
- scc_cli/doctor/checks/__init__.py +166 -0
- scc_cli/doctor/checks/cache.py +314 -0
- scc_cli/doctor/checks/config.py +107 -0
- scc_cli/doctor/checks/environment.py +182 -0
- scc_cli/doctor/checks/json_helpers.py +157 -0
- scc_cli/doctor/checks/organization.py +264 -0
- scc_cli/doctor/checks/worktree.py +278 -0
- scc_cli/doctor/render.py +365 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/git.py +84 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +159 -0
- scc_cli/kinds.py +65 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +846 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +281 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +279 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +689 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/services/__init__.py +1 -0
- scc_cli/services/git/__init__.py +79 -0
- scc_cli/services/git/branch.py +151 -0
- scc_cli/services/git/core.py +216 -0
- scc_cli/services/git/hooks.py +108 -0
- scc_cli/services/git/worktree.py +444 -0
- scc_cli/services/workspace/__init__.py +36 -0
- scc_cli/services/workspace/resolver.py +223 -0
- scc_cli/services/workspace/suspicious.py +200 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +589 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +383 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +154 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +401 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +794 -0
- scc_cli/ui/dashboard/loaders.py +452 -0
- scc_cli/ui/dashboard/models.py +185 -0
- scc_cli/ui/dashboard/orchestrator.py +735 -0
- scc_cli/ui/formatters.py +444 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/git_interactive.py +869 -0
- scc_cli/ui/git_render.py +176 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +615 -0
- scc_cli/ui/list_screen.py +437 -0
- scc_cli/ui/picker.py +763 -0
- scc_cli/ui/prompts.py +201 -0
- scc_cli/ui/quick_resume.py +116 -0
- scc_cli/ui/wizard.py +576 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +114 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.5.3.dist-info/METADATA +401 -0
- scc_cli-1.5.3.dist-info/RECORD +153 -0
- scc_cli-1.5.3.dist-info/WHEEL +4 -0
- scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
- scc_cli-1.5.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Data loading functions for dashboard tabs.
|
|
2
|
+
|
|
3
|
+
This module contains functions to load data for each dashboard tab:
|
|
4
|
+
- Status: System overview (team, organization, counts)
|
|
5
|
+
- Containers: Docker containers managed by SCC
|
|
6
|
+
- Sessions: Recent Claude sessions
|
|
7
|
+
- Worktrees: Git worktrees in current repository
|
|
8
|
+
|
|
9
|
+
Each loader function returns a TabData instance ready for display.
|
|
10
|
+
Loaders handle errors gracefully, returning placeholder items on failure.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from ..list_screen import ListItem
|
|
18
|
+
from .models import DashboardTab, TabData
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_status_tab_data() -> TabData:
|
|
22
|
+
"""Load Status tab data showing system overview.
|
|
23
|
+
|
|
24
|
+
The Status tab displays:
|
|
25
|
+
- Current team and organization info
|
|
26
|
+
- Sync status with remote config
|
|
27
|
+
- Resource counts for quick overview
|
|
28
|
+
- Optional enhancements (statusline)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
TabData with status summary items.
|
|
32
|
+
"""
|
|
33
|
+
# Import here to avoid circular imports
|
|
34
|
+
from ... import config, docker, sessions
|
|
35
|
+
from ...docker import core as docker_core
|
|
36
|
+
|
|
37
|
+
items: list[ListItem[str]] = []
|
|
38
|
+
|
|
39
|
+
# Start new session (primary action)
|
|
40
|
+
items.append(
|
|
41
|
+
ListItem(
|
|
42
|
+
value="start_session",
|
|
43
|
+
label="Start new session",
|
|
44
|
+
description="Launch Claude in a workspace",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Load current team info
|
|
49
|
+
try:
|
|
50
|
+
user_config = config.load_user_config()
|
|
51
|
+
team = user_config.get("selected_profile")
|
|
52
|
+
org_source = user_config.get("organization_source")
|
|
53
|
+
|
|
54
|
+
if team:
|
|
55
|
+
items.append(
|
|
56
|
+
ListItem(
|
|
57
|
+
value="team",
|
|
58
|
+
label="Team",
|
|
59
|
+
description=f"{team} (Enter to switch)",
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
items.append(
|
|
64
|
+
ListItem(
|
|
65
|
+
value="team",
|
|
66
|
+
label="Team",
|
|
67
|
+
description="No team selected (Enter to choose)",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Organization/sync status
|
|
72
|
+
if org_source and isinstance(org_source, dict):
|
|
73
|
+
org_url = org_source.get("url", "")
|
|
74
|
+
if org_url:
|
|
75
|
+
# Extract domain for display
|
|
76
|
+
domain = org_url.replace("https://", "").replace("http://", "").split("/")[0]
|
|
77
|
+
org_name = None
|
|
78
|
+
try:
|
|
79
|
+
org_config = config.load_cached_org_config()
|
|
80
|
+
if org_config:
|
|
81
|
+
org_name = org_config.get("organization", {}).get("name")
|
|
82
|
+
except Exception:
|
|
83
|
+
org_name = None
|
|
84
|
+
|
|
85
|
+
desc = domain
|
|
86
|
+
if org_name:
|
|
87
|
+
desc = f"{org_name} ({domain})"
|
|
88
|
+
|
|
89
|
+
items.append(
|
|
90
|
+
ListItem(
|
|
91
|
+
value="organization",
|
|
92
|
+
label="Organization",
|
|
93
|
+
description=desc,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
elif user_config.get("standalone"):
|
|
97
|
+
items.append(
|
|
98
|
+
ListItem(
|
|
99
|
+
value="organization",
|
|
100
|
+
label="Mode",
|
|
101
|
+
description="Standalone (no remote config)",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
except Exception:
|
|
106
|
+
items.append(
|
|
107
|
+
ListItem(
|
|
108
|
+
value="config_error",
|
|
109
|
+
label="Configuration",
|
|
110
|
+
description="Error loading config",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Load container count
|
|
115
|
+
try:
|
|
116
|
+
containers = docker_core.list_scc_containers()
|
|
117
|
+
running = sum(1 for c in containers if "Up" in c.status)
|
|
118
|
+
total = len(containers)
|
|
119
|
+
items.append(
|
|
120
|
+
ListItem(
|
|
121
|
+
value="containers",
|
|
122
|
+
label="Containers",
|
|
123
|
+
description=f"{running} running, {total} total",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
except Exception:
|
|
127
|
+
items.append(
|
|
128
|
+
ListItem(
|
|
129
|
+
value="containers",
|
|
130
|
+
label="Containers",
|
|
131
|
+
description="Unable to query Docker",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Load session count
|
|
136
|
+
try:
|
|
137
|
+
recent_sessions = sessions.list_recent(limit=100)
|
|
138
|
+
session_count = len(recent_sessions)
|
|
139
|
+
items.append(
|
|
140
|
+
ListItem(
|
|
141
|
+
value="sessions",
|
|
142
|
+
label="Sessions",
|
|
143
|
+
description=f"{session_count} recorded",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
except Exception:
|
|
147
|
+
items.append(
|
|
148
|
+
ListItem(
|
|
149
|
+
value="sessions",
|
|
150
|
+
label="Sessions",
|
|
151
|
+
description="Error loading sessions",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Check statusline status (optional enhancement)
|
|
156
|
+
try:
|
|
157
|
+
settings = docker.get_sandbox_settings()
|
|
158
|
+
has_statusline = settings is not None and "statusLine" in settings
|
|
159
|
+
if has_statusline:
|
|
160
|
+
items.append(
|
|
161
|
+
ListItem(
|
|
162
|
+
value="statusline_installed",
|
|
163
|
+
label="Statusline",
|
|
164
|
+
description="✓ Installed",
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
items.append(
|
|
169
|
+
ListItem(
|
|
170
|
+
value="statusline_not_installed",
|
|
171
|
+
label="Statusline",
|
|
172
|
+
description="Not installed - Enter to install",
|
|
173
|
+
governance_status="warning",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
except Exception:
|
|
177
|
+
# Docker not available - don't show statusline option
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return TabData(
|
|
181
|
+
tab=DashboardTab.STATUS,
|
|
182
|
+
title="Status",
|
|
183
|
+
items=items,
|
|
184
|
+
count_active=len(items),
|
|
185
|
+
count_total=len(items),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _load_containers_tab_data() -> TabData:
|
|
190
|
+
"""Load Containers tab data showing SCC-managed containers.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
TabData with container list items.
|
|
194
|
+
"""
|
|
195
|
+
from ...docker import core as docker_core
|
|
196
|
+
|
|
197
|
+
items: list[ListItem[str]] = []
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
containers = docker_core.list_scc_containers()
|
|
201
|
+
running_count = 0
|
|
202
|
+
|
|
203
|
+
for container in containers:
|
|
204
|
+
is_running = "Up" in container.status
|
|
205
|
+
if is_running:
|
|
206
|
+
running_count += 1
|
|
207
|
+
|
|
208
|
+
# Build description from available info
|
|
209
|
+
desc_parts = []
|
|
210
|
+
if container.profile:
|
|
211
|
+
desc_parts.append(container.profile)
|
|
212
|
+
if container.workspace:
|
|
213
|
+
# Show just the workspace name
|
|
214
|
+
workspace_name = container.workspace.split("/")[-1]
|
|
215
|
+
desc_parts.append(workspace_name)
|
|
216
|
+
if container.status:
|
|
217
|
+
# Simplify status (e.g., "Up 2 hours" → "Up 2h")
|
|
218
|
+
status_short = container.status.replace(" hours", "h").replace(" hour", "h")
|
|
219
|
+
status_short = status_short.replace(" minutes", "m").replace(" minute", "m")
|
|
220
|
+
status_short = status_short.replace(" days", "d").replace(" day", "d")
|
|
221
|
+
desc_parts.append(status_short)
|
|
222
|
+
|
|
223
|
+
items.append(
|
|
224
|
+
ListItem(
|
|
225
|
+
value=container.id,
|
|
226
|
+
label=container.name,
|
|
227
|
+
description=" ".join(desc_parts),
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if not items:
|
|
232
|
+
items.append(
|
|
233
|
+
ListItem(
|
|
234
|
+
value="no_containers",
|
|
235
|
+
label="No containers",
|
|
236
|
+
description="Run 'scc start' to create one",
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return TabData(
|
|
241
|
+
tab=DashboardTab.CONTAINERS,
|
|
242
|
+
title="Containers",
|
|
243
|
+
items=items,
|
|
244
|
+
count_active=running_count,
|
|
245
|
+
count_total=len(containers),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
except Exception:
|
|
249
|
+
return TabData(
|
|
250
|
+
tab=DashboardTab.CONTAINERS,
|
|
251
|
+
title="Containers",
|
|
252
|
+
items=[
|
|
253
|
+
ListItem(
|
|
254
|
+
value="error",
|
|
255
|
+
label="Error",
|
|
256
|
+
description="Unable to query Docker",
|
|
257
|
+
)
|
|
258
|
+
],
|
|
259
|
+
count_active=0,
|
|
260
|
+
count_total=0,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _load_sessions_tab_data() -> TabData:
|
|
265
|
+
"""Load Sessions tab data showing recent Claude sessions.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
TabData with session list items. Each ListItem.value contains
|
|
269
|
+
the raw session dict for access in the details pane.
|
|
270
|
+
"""
|
|
271
|
+
from ... import sessions
|
|
272
|
+
|
|
273
|
+
items: list[ListItem[dict[str, Any]]] = []
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
recent = sessions.list_recent(limit=20)
|
|
277
|
+
|
|
278
|
+
for session in recent:
|
|
279
|
+
name = session.get("name", "Unnamed")
|
|
280
|
+
desc_parts = []
|
|
281
|
+
|
|
282
|
+
if session.get("team"):
|
|
283
|
+
desc_parts.append(str(session["team"]))
|
|
284
|
+
if session.get("branch"):
|
|
285
|
+
desc_parts.append(str(session["branch"]))
|
|
286
|
+
if session.get("last_used"):
|
|
287
|
+
desc_parts.append(str(session["last_used"]))
|
|
288
|
+
|
|
289
|
+
# Store full session dict for details pane access
|
|
290
|
+
items.append(
|
|
291
|
+
ListItem(
|
|
292
|
+
value=session,
|
|
293
|
+
label=name,
|
|
294
|
+
description=" ".join(desc_parts),
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not items:
|
|
299
|
+
# Placeholder with sentinel dict (startable: True enables Enter action)
|
|
300
|
+
items.append(
|
|
301
|
+
ListItem(
|
|
302
|
+
value={"_placeholder": "no_sessions", "_startable": True},
|
|
303
|
+
label="No sessions",
|
|
304
|
+
description="Press Enter to start",
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return TabData(
|
|
309
|
+
tab=DashboardTab.SESSIONS,
|
|
310
|
+
title="Sessions",
|
|
311
|
+
items=items,
|
|
312
|
+
count_active=len(recent),
|
|
313
|
+
count_total=len(recent),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except Exception:
|
|
317
|
+
return TabData(
|
|
318
|
+
tab=DashboardTab.SESSIONS,
|
|
319
|
+
title="Sessions",
|
|
320
|
+
items=[
|
|
321
|
+
ListItem(
|
|
322
|
+
value="error",
|
|
323
|
+
label="Error",
|
|
324
|
+
description="Unable to load sessions",
|
|
325
|
+
)
|
|
326
|
+
],
|
|
327
|
+
count_active=0,
|
|
328
|
+
count_total=0,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _load_worktrees_tab_data(verbose: bool = False) -> TabData:
|
|
333
|
+
"""Load Worktrees tab data showing git worktrees.
|
|
334
|
+
|
|
335
|
+
Worktrees are loaded from the current working directory if it's a git repo.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
verbose: If True, fetch git status for each worktree (slower but shows
|
|
339
|
+
staged/modified/untracked counts with +N/!N/?N indicators).
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
TabData with worktree list items.
|
|
343
|
+
"""
|
|
344
|
+
import os
|
|
345
|
+
from pathlib import Path
|
|
346
|
+
|
|
347
|
+
from ... import git
|
|
348
|
+
|
|
349
|
+
items: list[ListItem[str]] = []
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
cwd = Path(os.getcwd())
|
|
353
|
+
worktrees = git.list_worktrees(cwd)
|
|
354
|
+
current_count = 0
|
|
355
|
+
|
|
356
|
+
# If verbose, fetch status for each worktree
|
|
357
|
+
if verbose:
|
|
358
|
+
for wt in worktrees:
|
|
359
|
+
staged, modified, untracked, timed_out = git.get_worktree_status(wt.path)
|
|
360
|
+
wt.staged_count = staged
|
|
361
|
+
wt.modified_count = modified
|
|
362
|
+
wt.untracked_count = untracked
|
|
363
|
+
wt.status_timed_out = timed_out
|
|
364
|
+
wt.has_changes = (staged + modified + untracked) > 0
|
|
365
|
+
|
|
366
|
+
for wt in worktrees:
|
|
367
|
+
if wt.is_current:
|
|
368
|
+
current_count += 1
|
|
369
|
+
|
|
370
|
+
desc_parts = []
|
|
371
|
+
if wt.branch:
|
|
372
|
+
desc_parts.append(wt.branch)
|
|
373
|
+
|
|
374
|
+
# Show status markers when verbose
|
|
375
|
+
if verbose:
|
|
376
|
+
if wt.status_timed_out:
|
|
377
|
+
desc_parts.append("…") # Timeout indicator
|
|
378
|
+
else:
|
|
379
|
+
status_parts = []
|
|
380
|
+
if wt.staged_count > 0:
|
|
381
|
+
status_parts.append(f"+{wt.staged_count}")
|
|
382
|
+
if wt.modified_count > 0:
|
|
383
|
+
status_parts.append(f"!{wt.modified_count}")
|
|
384
|
+
if wt.untracked_count > 0:
|
|
385
|
+
status_parts.append(f"?{wt.untracked_count}")
|
|
386
|
+
if status_parts:
|
|
387
|
+
desc_parts.append(" ".join(status_parts))
|
|
388
|
+
elif not wt.has_changes:
|
|
389
|
+
desc_parts.append(".") # Clean indicator
|
|
390
|
+
elif wt.has_changes:
|
|
391
|
+
desc_parts.append("*modified")
|
|
392
|
+
|
|
393
|
+
if wt.is_current:
|
|
394
|
+
desc_parts.append("(current)")
|
|
395
|
+
|
|
396
|
+
items.append(
|
|
397
|
+
ListItem(
|
|
398
|
+
value=wt.path,
|
|
399
|
+
label=Path(wt.path).name,
|
|
400
|
+
description=" ".join(desc_parts),
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if not items:
|
|
405
|
+
items.append(
|
|
406
|
+
ListItem(
|
|
407
|
+
value="no_worktrees",
|
|
408
|
+
label="No worktrees",
|
|
409
|
+
description="Press 'w' recent | 'i' init | 'c' clone",
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return TabData(
|
|
414
|
+
tab=DashboardTab.WORKTREES,
|
|
415
|
+
title="Worktrees",
|
|
416
|
+
items=items,
|
|
417
|
+
count_active=current_count,
|
|
418
|
+
count_total=len(worktrees),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
except Exception:
|
|
422
|
+
return TabData(
|
|
423
|
+
tab=DashboardTab.WORKTREES,
|
|
424
|
+
title="Worktrees",
|
|
425
|
+
items=[
|
|
426
|
+
ListItem(
|
|
427
|
+
value="no_git",
|
|
428
|
+
label="Not available",
|
|
429
|
+
description="Press 'w' recent | 'i' init | 'c' clone",
|
|
430
|
+
)
|
|
431
|
+
],
|
|
432
|
+
count_active=0,
|
|
433
|
+
count_total=0,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _load_all_tab_data(verbose_worktrees: bool = False) -> dict[DashboardTab, TabData]:
|
|
438
|
+
"""Load data for all dashboard tabs.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
verbose_worktrees: If True, fetch git status for each worktree
|
|
442
|
+
(shows +N/!N/?N indicators but takes longer).
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Dictionary mapping each tab to its data.
|
|
446
|
+
"""
|
|
447
|
+
return {
|
|
448
|
+
DashboardTab.STATUS: _load_status_tab_data(),
|
|
449
|
+
DashboardTab.CONTAINERS: _load_containers_tab_data(),
|
|
450
|
+
DashboardTab.SESSIONS: _load_sessions_tab_data(),
|
|
451
|
+
DashboardTab.WORKTREES: _load_worktrees_tab_data(verbose=verbose_worktrees),
|
|
452
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Data models for the dashboard module.
|
|
2
|
+
|
|
3
|
+
This module contains the core data structures used by the dashboard:
|
|
4
|
+
- DashboardTab: Enum for available tabs
|
|
5
|
+
- TabData: Content for a single tab
|
|
6
|
+
- DashboardState: State management for the dashboard
|
|
7
|
+
|
|
8
|
+
These models are intentionally simple dataclasses with no external dependencies
|
|
9
|
+
beyond the UI layer, enabling clean separation and testability.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum, auto
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from ..list_screen import ListItem, ListState
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DashboardTab(Enum):
|
|
23
|
+
"""Available dashboard tabs.
|
|
24
|
+
|
|
25
|
+
Each tab represents a major resource category in SCC.
|
|
26
|
+
Tabs are displayed in definition order (Status first, Worktrees last).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
STATUS = auto()
|
|
30
|
+
CONTAINERS = auto()
|
|
31
|
+
SESSIONS = auto()
|
|
32
|
+
WORKTREES = auto()
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def display_name(self) -> str:
|
|
36
|
+
"""Human-readable name for display in chrome."""
|
|
37
|
+
names = {
|
|
38
|
+
DashboardTab.STATUS: "Status",
|
|
39
|
+
DashboardTab.CONTAINERS: "Containers",
|
|
40
|
+
DashboardTab.SESSIONS: "Sessions",
|
|
41
|
+
DashboardTab.WORKTREES: "Worktrees",
|
|
42
|
+
}
|
|
43
|
+
return names[self]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Ordered list for tab cycling
|
|
47
|
+
TAB_ORDER: tuple[DashboardTab, ...] = (
|
|
48
|
+
DashboardTab.STATUS,
|
|
49
|
+
DashboardTab.CONTAINERS,
|
|
50
|
+
DashboardTab.SESSIONS,
|
|
51
|
+
DashboardTab.WORKTREES,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class TabData:
|
|
57
|
+
"""Data for a single dashboard tab.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
tab: The tab identifier.
|
|
61
|
+
title: Display title for the tab content area.
|
|
62
|
+
items: List items to display in this tab. Value type varies by tab:
|
|
63
|
+
- Containers/Worktrees: str (container ID or worktree name)
|
|
64
|
+
- Sessions: dict[str, Any] (full session data for details pane)
|
|
65
|
+
count_active: Number of active items (e.g., running containers).
|
|
66
|
+
count_total: Total number of items.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
tab: DashboardTab
|
|
70
|
+
title: str
|
|
71
|
+
items: Sequence[ListItem[Any]]
|
|
72
|
+
count_active: int
|
|
73
|
+
count_total: int
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def subtitle(self) -> str:
|
|
77
|
+
"""Generate subtitle from counts."""
|
|
78
|
+
if self.count_active == self.count_total:
|
|
79
|
+
return f"{self.count_total} total"
|
|
80
|
+
return f"{self.count_active} active, {self.count_total} total"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class DashboardState:
|
|
85
|
+
"""State for the tabbed dashboard view.
|
|
86
|
+
|
|
87
|
+
Manages which tab is active and provides methods for tab navigation.
|
|
88
|
+
Each tab switch resets the list state for the new tab.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
active_tab: Currently active tab.
|
|
92
|
+
tabs: Mapping from tab to its data.
|
|
93
|
+
list_state: Navigation state for the current tab's list.
|
|
94
|
+
status_message: Transient message to display (cleared on next action).
|
|
95
|
+
details_open: Whether the details pane is visible.
|
|
96
|
+
help_visible: Whether the help overlay is shown (rendered inside Live).
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
active_tab: DashboardTab
|
|
100
|
+
tabs: dict[DashboardTab, TabData]
|
|
101
|
+
list_state: ListState[str]
|
|
102
|
+
status_message: str | None = None
|
|
103
|
+
details_open: bool = False
|
|
104
|
+
help_visible: bool = False
|
|
105
|
+
verbose_worktrees: bool = False # Toggle for worktree status display
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def current_tab_data(self) -> TabData:
|
|
109
|
+
"""Get data for the currently active tab."""
|
|
110
|
+
return self.tabs[self.active_tab]
|
|
111
|
+
|
|
112
|
+
def is_placeholder_selected(self) -> bool:
|
|
113
|
+
"""Check if the current selection is a placeholder row.
|
|
114
|
+
|
|
115
|
+
Placeholder rows represent empty states or errors (e.g., "No containers",
|
|
116
|
+
"Error loading sessions") and shouldn't show details.
|
|
117
|
+
|
|
118
|
+
Placeholders can be identified by:
|
|
119
|
+
- String value matching known placeholder names (containers, worktrees)
|
|
120
|
+
- Dict value with "_placeholder" key (sessions)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if current item is a placeholder, False otherwise.
|
|
124
|
+
"""
|
|
125
|
+
current = self.list_state.current_item
|
|
126
|
+
if not current:
|
|
127
|
+
return True # No item = treat as placeholder
|
|
128
|
+
|
|
129
|
+
# Known placeholder string values from tab data loaders
|
|
130
|
+
placeholder_values = {
|
|
131
|
+
"no_containers",
|
|
132
|
+
"no_sessions",
|
|
133
|
+
"no_worktrees",
|
|
134
|
+
"no_git",
|
|
135
|
+
"error",
|
|
136
|
+
"config_error",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Check string placeholders (must be string type first - dicts are unhashable)
|
|
140
|
+
if isinstance(current.value, str) and current.value in placeholder_values:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
# Check dict placeholders (sessions tab uses dicts)
|
|
144
|
+
if isinstance(current.value, dict) and "_placeholder" in current.value:
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
def switch_tab(self, tab: DashboardTab) -> DashboardState:
|
|
150
|
+
"""Create new state with different active tab.
|
|
151
|
+
|
|
152
|
+
Resets list state (cursor, filter) for the new tab.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
tab: Tab to switch to.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
New DashboardState with the specified tab active.
|
|
159
|
+
"""
|
|
160
|
+
new_list_state = ListState(items=self.tabs[tab].items)
|
|
161
|
+
return DashboardState(
|
|
162
|
+
active_tab=tab,
|
|
163
|
+
tabs=self.tabs,
|
|
164
|
+
list_state=new_list_state,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def next_tab(self) -> DashboardState:
|
|
168
|
+
"""Switch to the next tab (wraps around).
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
New DashboardState with next tab active.
|
|
172
|
+
"""
|
|
173
|
+
current_index = TAB_ORDER.index(self.active_tab)
|
|
174
|
+
next_index = (current_index + 1) % len(TAB_ORDER)
|
|
175
|
+
return self.switch_tab(TAB_ORDER[next_index])
|
|
176
|
+
|
|
177
|
+
def prev_tab(self) -> DashboardState:
|
|
178
|
+
"""Switch to the previous tab (wraps around).
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
New DashboardState with previous tab active.
|
|
182
|
+
"""
|
|
183
|
+
current_index = TAB_ORDER.index(self.active_tab)
|
|
184
|
+
prev_index = (current_index - 1) % len(TAB_ORDER)
|
|
185
|
+
return self.switch_tab(TAB_ORDER[prev_index])
|