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,794 @@
|
|
|
1
|
+
"""Dashboard component for interactive tabbed view.
|
|
2
|
+
|
|
3
|
+
This module contains the Dashboard class that provides the interactive
|
|
4
|
+
tabbed interface for SCC resources. It handles:
|
|
5
|
+
- Tab state management and navigation
|
|
6
|
+
- List rendering within each tab
|
|
7
|
+
- Details pane with responsive layout
|
|
8
|
+
- Action handling and state updates
|
|
9
|
+
|
|
10
|
+
The underscore prefix signals this is an internal implementation module.
|
|
11
|
+
Public API is exported via __init__.py.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from rich.console import Group, RenderableType
|
|
19
|
+
from rich.live import Live
|
|
20
|
+
from rich.padding import Padding
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
|
|
24
|
+
# Import config for standalone mode detection
|
|
25
|
+
from ... import config as scc_config
|
|
26
|
+
from ...theme import Indicators
|
|
27
|
+
from ..chrome import Chrome, ChromeConfig, FooterHint
|
|
28
|
+
from ..keys import (
|
|
29
|
+
Action,
|
|
30
|
+
ActionType,
|
|
31
|
+
CreateWorktreeRequested,
|
|
32
|
+
GitInitRequested,
|
|
33
|
+
KeyReader,
|
|
34
|
+
RecentWorkspacesRequested,
|
|
35
|
+
RefreshRequested,
|
|
36
|
+
SessionResumeRequested,
|
|
37
|
+
StartRequested,
|
|
38
|
+
StatuslineInstallRequested,
|
|
39
|
+
TeamSwitchRequested,
|
|
40
|
+
VerboseToggleRequested,
|
|
41
|
+
)
|
|
42
|
+
from ..list_screen import ListItem
|
|
43
|
+
from .models import TAB_ORDER, DashboardState, DashboardTab
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Dashboard:
|
|
47
|
+
"""Interactive tabbed dashboard for SCC resources.
|
|
48
|
+
|
|
49
|
+
The Dashboard provides a unified view of SCC resources organized by tabs.
|
|
50
|
+
It handles tab switching, navigation within tabs, and rendering.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
state: Current dashboard state (tabs, active tab, list state).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, state: DashboardState) -> None:
|
|
57
|
+
"""Initialize dashboard.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
state: Initial dashboard state with tab data.
|
|
61
|
+
"""
|
|
62
|
+
self.state = state
|
|
63
|
+
from ...console import get_err_console
|
|
64
|
+
|
|
65
|
+
self._console = get_err_console()
|
|
66
|
+
# Track last layout mode for hysteresis (prevents flip-flop at resize boundary)
|
|
67
|
+
self._last_side_by_side: bool | None = None
|
|
68
|
+
|
|
69
|
+
def run(self) -> None:
|
|
70
|
+
"""Run the interactive dashboard.
|
|
71
|
+
|
|
72
|
+
Blocks until the user quits (q or Esc).
|
|
73
|
+
"""
|
|
74
|
+
# Use custom_keys for dashboard-specific actions that aren't in DEFAULT_KEY_MAP
|
|
75
|
+
# This allows 'r' to be a filter char in pickers but REFRESH in dashboard
|
|
76
|
+
# 'n' (new session) is also screen-specific to avoid global key conflicts
|
|
77
|
+
# 'w', 'i', 'c' are for Worktrees tab: recent workspaces, git init, create/clone
|
|
78
|
+
# 'v' toggles verbose status display in Worktrees tab
|
|
79
|
+
reader = KeyReader(
|
|
80
|
+
custom_keys={
|
|
81
|
+
"r": "refresh",
|
|
82
|
+
"n": "new_session",
|
|
83
|
+
"w": "recent_workspaces",
|
|
84
|
+
"i": "git_init",
|
|
85
|
+
"c": "create_worktree",
|
|
86
|
+
"v": "verbose_toggle",
|
|
87
|
+
},
|
|
88
|
+
enable_filter=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
with Live(
|
|
92
|
+
self._render(),
|
|
93
|
+
console=self._console,
|
|
94
|
+
auto_refresh=False, # Manual refresh for instant response
|
|
95
|
+
transient=True,
|
|
96
|
+
) as live:
|
|
97
|
+
while True:
|
|
98
|
+
# Pass filter_active based on actual filter state, not always True
|
|
99
|
+
# When filter is empty, j/k navigate; when typing, j/k become filter chars
|
|
100
|
+
action = reader.read(filter_active=bool(self.state.list_state.filter_query))
|
|
101
|
+
|
|
102
|
+
# Help overlay dismissal: any key while help is visible just closes help
|
|
103
|
+
# This is the standard pattern for modal overlays in Rich Live applications
|
|
104
|
+
if self.state.help_visible:
|
|
105
|
+
self.state.help_visible = False
|
|
106
|
+
live.update(self._render(), refresh=True)
|
|
107
|
+
continue # Consume the keypress (don't process it further)
|
|
108
|
+
|
|
109
|
+
result = self._handle_action(action)
|
|
110
|
+
if result is False:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Refresh if action changed state OR handler requests refresh
|
|
114
|
+
needs_refresh = result is True or action.state_changed
|
|
115
|
+
if needs_refresh:
|
|
116
|
+
live.update(self._render(), refresh=True)
|
|
117
|
+
|
|
118
|
+
def _render(self) -> RenderableType:
|
|
119
|
+
"""Render the current dashboard state.
|
|
120
|
+
|
|
121
|
+
Uses responsive layout when details pane is open:
|
|
122
|
+
- ≥110 columns: side-by-side (list | details)
|
|
123
|
+
- <110 columns: stacked (list above details)
|
|
124
|
+
- Status tab: details auto-hidden via render rule
|
|
125
|
+
|
|
126
|
+
Help overlay is rendered INSIDE the Live context to avoid scroll artifacts.
|
|
127
|
+
When help_visible is True, the help panel overlays the normal content.
|
|
128
|
+
"""
|
|
129
|
+
# If help overlay is visible, render it instead of normal content
|
|
130
|
+
# This renders INSIDE the Live context, avoiding scroll artifacts
|
|
131
|
+
if self.state.help_visible:
|
|
132
|
+
from ..help import HelpMode, render_help_content
|
|
133
|
+
|
|
134
|
+
return render_help_content(HelpMode.DASHBOARD)
|
|
135
|
+
|
|
136
|
+
list_body = self._render_list_body()
|
|
137
|
+
config = self._get_chrome_config()
|
|
138
|
+
chrome = Chrome(config)
|
|
139
|
+
|
|
140
|
+
# Check if details should be shown (render rule: not on Status tab)
|
|
141
|
+
show_details = self.state.details_open and self.state.active_tab != DashboardTab.STATUS
|
|
142
|
+
|
|
143
|
+
body: RenderableType = list_body
|
|
144
|
+
if show_details and not self.state.is_placeholder_selected():
|
|
145
|
+
# Render details pane content
|
|
146
|
+
details = self._render_details_pane()
|
|
147
|
+
|
|
148
|
+
# Responsive layout with hysteresis to prevent flip-flop at resize boundary
|
|
149
|
+
# Thresholds: ≥112 → side-by-side, ≤108 → stacked, 109-111 → maintain previous
|
|
150
|
+
terminal_width = self._console.size.width
|
|
151
|
+
if terminal_width >= 112:
|
|
152
|
+
side_by_side = True
|
|
153
|
+
elif terminal_width <= 108:
|
|
154
|
+
side_by_side = False
|
|
155
|
+
elif self._last_side_by_side is not None:
|
|
156
|
+
# In dead zone (109-111): maintain previous layout
|
|
157
|
+
side_by_side = self._last_side_by_side
|
|
158
|
+
else:
|
|
159
|
+
# First render in dead zone: default to stacked (conservative)
|
|
160
|
+
side_by_side = False
|
|
161
|
+
|
|
162
|
+
self._last_side_by_side = side_by_side
|
|
163
|
+
body = self._render_split_view(list_body, details, side_by_side=side_by_side)
|
|
164
|
+
|
|
165
|
+
return chrome.render(body, search_query=self.state.list_state.filter_query)
|
|
166
|
+
|
|
167
|
+
def _render_list_body(self) -> Text:
|
|
168
|
+
"""Render the list content for the active tab."""
|
|
169
|
+
text = Text()
|
|
170
|
+
filtered = self.state.list_state.filtered_items
|
|
171
|
+
visible = self.state.list_state.visible_items
|
|
172
|
+
|
|
173
|
+
if not filtered:
|
|
174
|
+
text.append("No items", style="dim italic")
|
|
175
|
+
else:
|
|
176
|
+
for i, item in enumerate(visible):
|
|
177
|
+
actual_index = self.state.list_state.scroll_offset + i
|
|
178
|
+
is_cursor = actual_index == self.state.list_state.cursor
|
|
179
|
+
|
|
180
|
+
if is_cursor:
|
|
181
|
+
text.append(f"{Indicators.get('CURSOR')} ", style="cyan bold")
|
|
182
|
+
else:
|
|
183
|
+
text.append(" ")
|
|
184
|
+
|
|
185
|
+
label_style = "bold" if is_cursor else ""
|
|
186
|
+
text.append(item.label, style=label_style)
|
|
187
|
+
|
|
188
|
+
if item.description:
|
|
189
|
+
text.append(f" {item.description}", style="dim")
|
|
190
|
+
|
|
191
|
+
text.append("\n")
|
|
192
|
+
|
|
193
|
+
# Render status message if present (transient toast)
|
|
194
|
+
if self.state.status_message:
|
|
195
|
+
text.append("\n")
|
|
196
|
+
text.append(f"{Indicators.get('INFO_ICON')} ", style="yellow")
|
|
197
|
+
text.append(self.state.status_message, style="yellow")
|
|
198
|
+
text.append("\n")
|
|
199
|
+
|
|
200
|
+
return text
|
|
201
|
+
|
|
202
|
+
def _render_split_view(
|
|
203
|
+
self,
|
|
204
|
+
list_body: RenderableType,
|
|
205
|
+
details: RenderableType,
|
|
206
|
+
*,
|
|
207
|
+
side_by_side: bool,
|
|
208
|
+
) -> RenderableType:
|
|
209
|
+
"""Render list and details in split view.
|
|
210
|
+
|
|
211
|
+
Uses consistent padding and separators for smooth transitions
|
|
212
|
+
between side-by-side and stacked layouts.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
list_body: The list content.
|
|
216
|
+
details: The details pane content.
|
|
217
|
+
side_by_side: If True, render columns; otherwise stack vertically.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Combined renderable.
|
|
221
|
+
"""
|
|
222
|
+
# Wrap details in consistent padding for visual balance
|
|
223
|
+
padded_details = Padding(details, (0, 0, 0, 1)) # Left padding
|
|
224
|
+
|
|
225
|
+
if side_by_side:
|
|
226
|
+
# Use Table.grid for side-by-side with vertical separator
|
|
227
|
+
# Table handles row height automatically (no fixed separator height)
|
|
228
|
+
table = Table.grid(expand=True, padding=(0, 1))
|
|
229
|
+
table.add_column("list", ratio=1, no_wrap=False)
|
|
230
|
+
table.add_column("sep", width=1, style="dim", justify="center")
|
|
231
|
+
table.add_column("details", ratio=1, no_wrap=False)
|
|
232
|
+
|
|
233
|
+
# Single vertical bar - Rich expands it to match row height
|
|
234
|
+
table.add_row(list_body, Indicators.get("VERTICAL_LINE"), padded_details)
|
|
235
|
+
return table
|
|
236
|
+
else:
|
|
237
|
+
# Stacked: list above details with thin separator
|
|
238
|
+
# Use same visual weight as side-by-side for smooth switching
|
|
239
|
+
separator = Text(Indicators.get("HORIZONTAL_LINE") * 30, style="dim")
|
|
240
|
+
return Group(
|
|
241
|
+
list_body,
|
|
242
|
+
Text(""), # Blank line for spacing
|
|
243
|
+
separator,
|
|
244
|
+
Text(""), # Blank line for spacing
|
|
245
|
+
padded_details,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def _render_details_pane(self) -> RenderableType:
|
|
249
|
+
"""Render details pane content for the current item.
|
|
250
|
+
|
|
251
|
+
Content varies by active tab:
|
|
252
|
+
- Containers: ID, status, profile, workspace, commands
|
|
253
|
+
- Sessions: name, path, branch, last_used, resume command
|
|
254
|
+
- Worktrees: path, branch, dirty status, start command
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Details pane as Rich renderable.
|
|
258
|
+
"""
|
|
259
|
+
current = self.state.list_state.current_item
|
|
260
|
+
if not current:
|
|
261
|
+
return Text("No item selected", style="dim italic")
|
|
262
|
+
|
|
263
|
+
tab = self.state.active_tab
|
|
264
|
+
|
|
265
|
+
if tab == DashboardTab.CONTAINERS:
|
|
266
|
+
return self._render_container_details(current)
|
|
267
|
+
elif tab == DashboardTab.SESSIONS:
|
|
268
|
+
return self._render_session_details(current)
|
|
269
|
+
elif tab == DashboardTab.WORKTREES:
|
|
270
|
+
return self._render_worktree_details(current)
|
|
271
|
+
else:
|
|
272
|
+
return Text("Details not available", style="dim")
|
|
273
|
+
|
|
274
|
+
def _render_container_details(self, item: ListItem[Any]) -> RenderableType:
|
|
275
|
+
"""Render details for a container item using structured key/value table."""
|
|
276
|
+
# Header
|
|
277
|
+
header = Text()
|
|
278
|
+
header.append("Container Details\n", style="bold cyan")
|
|
279
|
+
header.append(Indicators.get("HORIZONTAL_LINE") * 20, style="dim")
|
|
280
|
+
|
|
281
|
+
# Key/value table
|
|
282
|
+
table = Table.grid(padding=(0, 1))
|
|
283
|
+
table.add_column("key", style="dim", width=10)
|
|
284
|
+
table.add_column("value")
|
|
285
|
+
|
|
286
|
+
table.add_row("Name", Text(item.label, style="bold"))
|
|
287
|
+
|
|
288
|
+
# Short container ID
|
|
289
|
+
container_id = item.value[:12] if len(item.value) > 12 else item.value
|
|
290
|
+
table.add_row("ID", container_id)
|
|
291
|
+
|
|
292
|
+
# Parse description into fields if available
|
|
293
|
+
if item.description:
|
|
294
|
+
parts = item.description.split(" ")
|
|
295
|
+
if len(parts) >= 1 and parts[0]:
|
|
296
|
+
table.add_row("Profile", parts[0])
|
|
297
|
+
if len(parts) >= 2 and parts[1]:
|
|
298
|
+
table.add_row("Workspace", parts[1])
|
|
299
|
+
if len(parts) >= 3 and parts[2]:
|
|
300
|
+
table.add_row("Status", parts[2])
|
|
301
|
+
|
|
302
|
+
# Commands section
|
|
303
|
+
commands = Text()
|
|
304
|
+
commands.append("\nCommands\n", style="dim")
|
|
305
|
+
commands.append(f" docker exec -it {item.label} bash\n", style="cyan")
|
|
306
|
+
|
|
307
|
+
return Group(header, table, commands)
|
|
308
|
+
|
|
309
|
+
def _render_session_details(self, item: ListItem[Any]) -> RenderableType:
|
|
310
|
+
"""Render details for a session item using structured key/value table.
|
|
311
|
+
|
|
312
|
+
Uses the raw session dict stored in item.value for field access.
|
|
313
|
+
"""
|
|
314
|
+
session = item.value
|
|
315
|
+
|
|
316
|
+
# Header
|
|
317
|
+
header = Text()
|
|
318
|
+
header.append("Session Details\n", style="bold cyan")
|
|
319
|
+
header.append(Indicators.get("HORIZONTAL_LINE") * 20, style="dim")
|
|
320
|
+
|
|
321
|
+
# Key/value table
|
|
322
|
+
table = Table.grid(padding=(0, 1))
|
|
323
|
+
table.add_column("key", style="dim", width=10)
|
|
324
|
+
table.add_column("value")
|
|
325
|
+
|
|
326
|
+
table.add_row("Name", Text(item.label, style="bold"))
|
|
327
|
+
|
|
328
|
+
# Read fields directly from session dict (with None protection)
|
|
329
|
+
if session.get("team"):
|
|
330
|
+
table.add_row("Team", str(session["team"]))
|
|
331
|
+
if session.get("branch"):
|
|
332
|
+
table.add_row("Branch", str(session["branch"]))
|
|
333
|
+
if session.get("workspace"):
|
|
334
|
+
table.add_row("Workspace", str(session["workspace"]))
|
|
335
|
+
if session.get("last_used"):
|
|
336
|
+
table.add_row("Last Used", str(session["last_used"]))
|
|
337
|
+
|
|
338
|
+
# Commands section with None protection and helpful tips
|
|
339
|
+
commands = Text()
|
|
340
|
+
commands.append("\nCommands\n", style="dim")
|
|
341
|
+
|
|
342
|
+
container_name = session.get("container_name")
|
|
343
|
+
session_id = session.get("id")
|
|
344
|
+
|
|
345
|
+
if container_name:
|
|
346
|
+
# Container is available - show resume command
|
|
347
|
+
commands.append(f" scc resume {container_name}\n", style="cyan")
|
|
348
|
+
elif session_id:
|
|
349
|
+
# Session exists but container stopped - show restart tip
|
|
350
|
+
commands.append(" Container stopped. Start new session:\n", style="dim italic")
|
|
351
|
+
commands.append(
|
|
352
|
+
f" scc start --workspace {session.get('workspace', '.')}\n", style="cyan"
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
# Minimal session info - generic tip
|
|
356
|
+
commands.append(" Start session: scc start\n", style="cyan dim")
|
|
357
|
+
|
|
358
|
+
return Group(header, table, commands)
|
|
359
|
+
|
|
360
|
+
def _render_worktree_details(self, item: ListItem[Any]) -> RenderableType:
|
|
361
|
+
"""Render details for a worktree item using structured key/value table."""
|
|
362
|
+
# Header
|
|
363
|
+
header = Text()
|
|
364
|
+
header.append("Worktree Details\n", style="bold cyan")
|
|
365
|
+
header.append(Indicators.get("HORIZONTAL_LINE") * 20, style="dim")
|
|
366
|
+
|
|
367
|
+
# Key/value table
|
|
368
|
+
table = Table.grid(padding=(0, 1))
|
|
369
|
+
table.add_column("key", style="dim", width=10)
|
|
370
|
+
table.add_column("value")
|
|
371
|
+
|
|
372
|
+
table.add_row("Name", Text(item.label, style="bold"))
|
|
373
|
+
table.add_row("Path", item.value)
|
|
374
|
+
|
|
375
|
+
# Parse description into fields (branch *modified (current))
|
|
376
|
+
if item.description:
|
|
377
|
+
parts = item.description.split(" ")
|
|
378
|
+
for part in parts:
|
|
379
|
+
if part.startswith("(") and part.endswith(")"):
|
|
380
|
+
table.add_row("Status", Text(part, style="green"))
|
|
381
|
+
elif part == "*modified":
|
|
382
|
+
table.add_row("Changes", Text("Modified", style="yellow"))
|
|
383
|
+
elif part:
|
|
384
|
+
table.add_row("Branch", part)
|
|
385
|
+
|
|
386
|
+
# Commands section
|
|
387
|
+
commands = Text()
|
|
388
|
+
commands.append("\nCommands\n", style="dim")
|
|
389
|
+
commands.append(f" scc start {item.value}\n", style="cyan")
|
|
390
|
+
|
|
391
|
+
return Group(header, table, commands)
|
|
392
|
+
|
|
393
|
+
def _get_placeholder_tip(self, value: str | dict[str, Any]) -> str:
|
|
394
|
+
"""Get contextual help tip for placeholder items.
|
|
395
|
+
|
|
396
|
+
Returns actionable guidance for empty/error states.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
value: Either a string placeholder key or a dict with "_placeholder" key.
|
|
400
|
+
"""
|
|
401
|
+
tips: dict[str, str] = {
|
|
402
|
+
# Container placeholders (first-time user friendly)
|
|
403
|
+
"no_containers": (
|
|
404
|
+
"No containers running. Press 'n' to start a new session, "
|
|
405
|
+
"or run `scc start <path>` from the terminal."
|
|
406
|
+
),
|
|
407
|
+
# Session placeholders (first-time user friendly)
|
|
408
|
+
"no_sessions": ("No sessions recorded yet. Press 'n' to create your first session!"),
|
|
409
|
+
# Worktree placeholders - updated with actionable shortcuts
|
|
410
|
+
"no_worktrees": (
|
|
411
|
+
"Not in a git repository. Press 'w' for recent workspaces, "
|
|
412
|
+
"'i' to initialize git here, or 'c' to clone."
|
|
413
|
+
),
|
|
414
|
+
"no_git": (
|
|
415
|
+
"Not in a git repository. Press 'w' for recent workspaces, "
|
|
416
|
+
"'i' to initialize git here, or 'c' to clone."
|
|
417
|
+
),
|
|
418
|
+
# Error placeholders (actionable doctor suggestion)
|
|
419
|
+
"error": (
|
|
420
|
+
"Unable to load data. Run `scc doctor` to check Docker status and diagnose issues."
|
|
421
|
+
),
|
|
422
|
+
"config_error": ("Configuration issue detected. Run `scc doctor` to diagnose and fix."),
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# Extract placeholder key from dict if needed
|
|
426
|
+
placeholder_key = value
|
|
427
|
+
if isinstance(value, dict):
|
|
428
|
+
placeholder_key = value.get("_placeholder", "")
|
|
429
|
+
|
|
430
|
+
return tips.get(str(placeholder_key), "No details available for this item.")
|
|
431
|
+
|
|
432
|
+
def _compute_footer_hints(self, standalone: bool, show_details: bool) -> tuple[FooterHint, ...]:
|
|
433
|
+
"""Compute context-aware footer hints based on current state.
|
|
434
|
+
|
|
435
|
+
Hints reflect available actions for the current selection:
|
|
436
|
+
- Details open: "Esc close"
|
|
437
|
+
- Status tab: Enter on actionable rows (start/team/resources/statusline)
|
|
438
|
+
- Startable placeholder: "Enter start"
|
|
439
|
+
- Non-startable placeholder: No Enter hint
|
|
440
|
+
- Real item: "Enter details"
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
standalone: Whether running in standalone mode (dims team hint).
|
|
444
|
+
show_details: Whether the details pane is currently showing.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Tuple of FooterHint objects for the chrome footer.
|
|
448
|
+
"""
|
|
449
|
+
hints: list[FooterHint] = [FooterHint("↑↓", "navigate")]
|
|
450
|
+
|
|
451
|
+
# Determine primary action hint based on context
|
|
452
|
+
if show_details:
|
|
453
|
+
# Details pane is open
|
|
454
|
+
if self.state.active_tab == DashboardTab.SESSIONS:
|
|
455
|
+
# Sessions tab: Enter resumes, Esc closes
|
|
456
|
+
hints.append(FooterHint("Enter", "resume"))
|
|
457
|
+
elif self.state.active_tab == DashboardTab.WORKTREES:
|
|
458
|
+
# Worktrees tab: Enter starts session here, Esc closes
|
|
459
|
+
hints.append(FooterHint("Enter", "start here"))
|
|
460
|
+
hints.append(FooterHint("Esc", "close"))
|
|
461
|
+
elif self.state.active_tab == DashboardTab.STATUS:
|
|
462
|
+
current = self.state.list_state.current_item
|
|
463
|
+
if current:
|
|
464
|
+
if current.value == "start_session":
|
|
465
|
+
hints.append(FooterHint("Enter", "start"))
|
|
466
|
+
elif current.value == "team":
|
|
467
|
+
hints.append(FooterHint("Enter", "switch team"))
|
|
468
|
+
elif current.value in {"containers", "sessions", "worktrees"}:
|
|
469
|
+
hints.append(FooterHint("Enter", "open"))
|
|
470
|
+
elif current.value == "statusline_not_installed":
|
|
471
|
+
hints.append(FooterHint("Enter", "install"))
|
|
472
|
+
elif self.state.is_placeholder_selected():
|
|
473
|
+
# Check if placeholder is startable
|
|
474
|
+
current = self.state.list_state.current_item
|
|
475
|
+
is_startable = False
|
|
476
|
+
if current:
|
|
477
|
+
if isinstance(current.value, str):
|
|
478
|
+
is_startable = current.value in {"no_containers", "no_sessions"}
|
|
479
|
+
elif isinstance(current.value, dict):
|
|
480
|
+
is_startable = current.value.get("_startable", False)
|
|
481
|
+
|
|
482
|
+
if is_startable:
|
|
483
|
+
hints.append(FooterHint("Enter", "start"))
|
|
484
|
+
# Non-startable placeholders get no Enter hint
|
|
485
|
+
else:
|
|
486
|
+
# Real item selected - show details action
|
|
487
|
+
hints.append(FooterHint("Enter", "details"))
|
|
488
|
+
|
|
489
|
+
# Worktrees tab specific actions
|
|
490
|
+
if self.state.active_tab == DashboardTab.WORKTREES and not show_details:
|
|
491
|
+
current = self.state.list_state.current_item
|
|
492
|
+
# Detect if we're in a git repo based on placeholder type
|
|
493
|
+
is_git_repo = True
|
|
494
|
+
if current and isinstance(current.value, str):
|
|
495
|
+
is_git_repo = current.value not in {"no_git", "no_worktrees"}
|
|
496
|
+
|
|
497
|
+
if not is_git_repo:
|
|
498
|
+
# Not in git repo: show w (recent), i (init), c (clone)
|
|
499
|
+
hints.append(FooterHint("w", "recent"))
|
|
500
|
+
hints.append(FooterHint("i", "init"))
|
|
501
|
+
hints.append(FooterHint("c", "clone"))
|
|
502
|
+
else:
|
|
503
|
+
# In git repo: show c (create worktree), v (status toggle)
|
|
504
|
+
hints.append(FooterHint("c", "create"))
|
|
505
|
+
# Show v toggle with current state indicator
|
|
506
|
+
status_label = "status off" if self.state.verbose_worktrees else "status"
|
|
507
|
+
hints.append(FooterHint("v", status_label))
|
|
508
|
+
|
|
509
|
+
# Sessions tab quick hint
|
|
510
|
+
if self.state.active_tab == DashboardTab.SESSIONS and not show_details:
|
|
511
|
+
hints.append(FooterHint("Enter", "details/resume"))
|
|
512
|
+
|
|
513
|
+
# Tab navigation and refresh
|
|
514
|
+
hints.append(FooterHint("Tab", "switch tab"))
|
|
515
|
+
hints.append(FooterHint("r", "refresh"))
|
|
516
|
+
|
|
517
|
+
# Global actions
|
|
518
|
+
hints.append(FooterHint("t", "teams", dimmed=standalone))
|
|
519
|
+
hints.append(FooterHint("q", "quit"))
|
|
520
|
+
hints.append(FooterHint("?", "help"))
|
|
521
|
+
|
|
522
|
+
return tuple(hints)
|
|
523
|
+
|
|
524
|
+
def _get_chrome_config(self) -> ChromeConfig:
|
|
525
|
+
"""Get chrome configuration for current state."""
|
|
526
|
+
tab_names = [tab.display_name for tab in TAB_ORDER]
|
|
527
|
+
active_index = TAB_ORDER.index(self.state.active_tab)
|
|
528
|
+
standalone = scc_config.is_standalone_mode()
|
|
529
|
+
|
|
530
|
+
# Render rule: auto-hide details on Status tab (no state mutation)
|
|
531
|
+
show_details = self.state.details_open and self.state.active_tab != DashboardTab.STATUS
|
|
532
|
+
|
|
533
|
+
# Compute dynamic footer hints based on current context
|
|
534
|
+
footer_hints = self._compute_footer_hints(standalone, show_details)
|
|
535
|
+
|
|
536
|
+
return ChromeConfig.for_dashboard(
|
|
537
|
+
tab_names,
|
|
538
|
+
active_index,
|
|
539
|
+
standalone=standalone,
|
|
540
|
+
details_open=show_details,
|
|
541
|
+
custom_hints=footer_hints,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def _handle_action(self, action: Action[None]) -> bool | None:
|
|
545
|
+
"""Handle an action and update state.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
True to force refresh (state changed by us, not action).
|
|
549
|
+
False to exit dashboard.
|
|
550
|
+
None to continue (refresh only if action.state_changed).
|
|
551
|
+
"""
|
|
552
|
+
# Selective status clearing: only clear on navigation/filter/tab actions
|
|
553
|
+
# This preserves toast messages during non-state-changing actions (e.g., help)
|
|
554
|
+
status_clearing_actions = {
|
|
555
|
+
ActionType.NAVIGATE_UP,
|
|
556
|
+
ActionType.NAVIGATE_DOWN,
|
|
557
|
+
ActionType.TAB_NEXT,
|
|
558
|
+
ActionType.TAB_PREV,
|
|
559
|
+
ActionType.FILTER_CHAR,
|
|
560
|
+
ActionType.FILTER_DELETE,
|
|
561
|
+
}
|
|
562
|
+
# Also clear status on 'r' (refresh), which is a CUSTOM action in dashboard
|
|
563
|
+
is_refresh_action = action.action_type == ActionType.CUSTOM and action.custom_key == "r"
|
|
564
|
+
if self.state.status_message and (
|
|
565
|
+
action.action_type in status_clearing_actions or is_refresh_action
|
|
566
|
+
):
|
|
567
|
+
self.state.status_message = None
|
|
568
|
+
|
|
569
|
+
match action.action_type:
|
|
570
|
+
case ActionType.NAVIGATE_UP:
|
|
571
|
+
self.state.list_state.move_cursor(-1)
|
|
572
|
+
|
|
573
|
+
case ActionType.NAVIGATE_DOWN:
|
|
574
|
+
self.state.list_state.move_cursor(1)
|
|
575
|
+
|
|
576
|
+
case ActionType.TAB_NEXT:
|
|
577
|
+
self.state = self.state.next_tab()
|
|
578
|
+
|
|
579
|
+
case ActionType.TAB_PREV:
|
|
580
|
+
self.state = self.state.prev_tab()
|
|
581
|
+
|
|
582
|
+
case ActionType.FILTER_CHAR:
|
|
583
|
+
if action.filter_char:
|
|
584
|
+
self.state.list_state.add_filter_char(action.filter_char)
|
|
585
|
+
|
|
586
|
+
case ActionType.FILTER_DELETE:
|
|
587
|
+
self.state.list_state.delete_filter_char()
|
|
588
|
+
|
|
589
|
+
case ActionType.CANCEL:
|
|
590
|
+
# ESC precedence: details → filter → no-op
|
|
591
|
+
if self.state.details_open:
|
|
592
|
+
self.state.details_open = False
|
|
593
|
+
return True # Refresh to hide details
|
|
594
|
+
if self.state.list_state.filter_query:
|
|
595
|
+
self.state.list_state.clear_filter()
|
|
596
|
+
return True # Refresh to show unfiltered list
|
|
597
|
+
return None # No-op
|
|
598
|
+
|
|
599
|
+
case ActionType.QUIT:
|
|
600
|
+
return False
|
|
601
|
+
|
|
602
|
+
case ActionType.SELECT:
|
|
603
|
+
# On Status tab, Enter triggers different actions based on item
|
|
604
|
+
if self.state.active_tab == DashboardTab.STATUS:
|
|
605
|
+
current = self.state.list_state.current_item
|
|
606
|
+
if current:
|
|
607
|
+
# Start session row
|
|
608
|
+
if current.value == "start_session":
|
|
609
|
+
raise StartRequested(
|
|
610
|
+
return_to=self.state.active_tab.name,
|
|
611
|
+
reason="dashboard_start",
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Team row: same behavior as 't' key
|
|
615
|
+
if current.value == "team":
|
|
616
|
+
if scc_config.is_standalone_mode():
|
|
617
|
+
self.state.status_message = (
|
|
618
|
+
"Teams require org mode. Run `scc setup` to configure."
|
|
619
|
+
)
|
|
620
|
+
return True # Refresh to show message
|
|
621
|
+
raise TeamSwitchRequested()
|
|
622
|
+
|
|
623
|
+
# Resource rows: drill down to corresponding tab
|
|
624
|
+
tab_mapping: dict[str, DashboardTab] = {
|
|
625
|
+
"containers": DashboardTab.CONTAINERS,
|
|
626
|
+
"sessions": DashboardTab.SESSIONS,
|
|
627
|
+
"worktrees": DashboardTab.WORKTREES,
|
|
628
|
+
}
|
|
629
|
+
target_tab = tab_mapping.get(current.value)
|
|
630
|
+
if target_tab:
|
|
631
|
+
# Clear filter on drill-down (avoids confusion)
|
|
632
|
+
self.state.list_state.clear_filter()
|
|
633
|
+
self.state = self.state.switch_tab(target_tab)
|
|
634
|
+
return True # Refresh to show new tab
|
|
635
|
+
|
|
636
|
+
# Statusline row: Enter triggers installation
|
|
637
|
+
if current.value == "statusline_not_installed":
|
|
638
|
+
raise StatuslineInstallRequested(return_to=self.state.active_tab.name)
|
|
639
|
+
else:
|
|
640
|
+
# Resource tabs handling (Containers, Worktrees, Sessions)
|
|
641
|
+
current = self.state.list_state.current_item
|
|
642
|
+
|
|
643
|
+
# All resource tabs: toggle details pane on first Enter
|
|
644
|
+
if self.state.details_open:
|
|
645
|
+
# Sessions tab: Enter in details pane resumes the session
|
|
646
|
+
if (
|
|
647
|
+
self.state.active_tab == DashboardTab.SESSIONS
|
|
648
|
+
and current
|
|
649
|
+
and not self.state.is_placeholder_selected()
|
|
650
|
+
and isinstance(current.value, dict)
|
|
651
|
+
and not current.value.get("_placeholder")
|
|
652
|
+
):
|
|
653
|
+
raise SessionResumeRequested(
|
|
654
|
+
session=current.value,
|
|
655
|
+
return_to=self.state.active_tab.name,
|
|
656
|
+
)
|
|
657
|
+
# Worktrees tab: Enter in details pane starts session here
|
|
658
|
+
if (
|
|
659
|
+
self.state.active_tab == DashboardTab.WORKTREES
|
|
660
|
+
and current
|
|
661
|
+
and not self.state.is_placeholder_selected()
|
|
662
|
+
and isinstance(current.value, str)
|
|
663
|
+
):
|
|
664
|
+
# Start session in the selected worktree
|
|
665
|
+
raise StartRequested(
|
|
666
|
+
return_to=self.state.active_tab.name,
|
|
667
|
+
reason=f"worktree:{current.value}",
|
|
668
|
+
)
|
|
669
|
+
# All tabs (including Sessions without valid session):
|
|
670
|
+
# Close details
|
|
671
|
+
self.state.details_open = False
|
|
672
|
+
return True
|
|
673
|
+
elif not self.state.is_placeholder_selected():
|
|
674
|
+
# Open details (only for real items, not placeholders)
|
|
675
|
+
self.state.details_open = True
|
|
676
|
+
return True
|
|
677
|
+
else:
|
|
678
|
+
# Placeholder or empty state: handle appropriately
|
|
679
|
+
if current:
|
|
680
|
+
# Check if this is a startable placeholder
|
|
681
|
+
# (containers/sessions empty → user can start a new session)
|
|
682
|
+
is_startable = False
|
|
683
|
+
reason = ""
|
|
684
|
+
|
|
685
|
+
# String placeholders (containers, worktrees)
|
|
686
|
+
if isinstance(current.value, str):
|
|
687
|
+
startable_strings = {"no_containers", "no_sessions"}
|
|
688
|
+
if current.value in startable_strings:
|
|
689
|
+
is_startable = True
|
|
690
|
+
reason = current.value
|
|
691
|
+
|
|
692
|
+
# Dict placeholders (sessions tab uses dicts)
|
|
693
|
+
elif isinstance(current.value, dict):
|
|
694
|
+
if current.value.get("_startable"):
|
|
695
|
+
is_startable = True
|
|
696
|
+
reason = current.value.get("_placeholder", "unknown")
|
|
697
|
+
|
|
698
|
+
if is_startable:
|
|
699
|
+
# Uses .name (stable identifier) not .value (display string)
|
|
700
|
+
raise StartRequested(
|
|
701
|
+
return_to=self.state.active_tab.name,
|
|
702
|
+
reason=reason,
|
|
703
|
+
)
|
|
704
|
+
else:
|
|
705
|
+
# Non-startable placeholders show a tip
|
|
706
|
+
self.state.status_message = self._get_placeholder_tip(current.value)
|
|
707
|
+
elif self.state.list_state.filter_query:
|
|
708
|
+
# Filter has no matches
|
|
709
|
+
self.state.status_message = (
|
|
710
|
+
f"No matches for '{self.state.list_state.filter_query}'. "
|
|
711
|
+
"Press Esc to clear filter."
|
|
712
|
+
)
|
|
713
|
+
else:
|
|
714
|
+
# Truly empty list (shouldn't happen normally)
|
|
715
|
+
self.state.status_message = "No items available."
|
|
716
|
+
return True
|
|
717
|
+
|
|
718
|
+
case ActionType.TEAM_SWITCH:
|
|
719
|
+
# In standalone mode, show guidance instead of switching
|
|
720
|
+
if scc_config.is_standalone_mode():
|
|
721
|
+
self.state.status_message = (
|
|
722
|
+
"Teams require org mode. Run `scc setup` to configure."
|
|
723
|
+
)
|
|
724
|
+
return True # Refresh to show message
|
|
725
|
+
# Bubble up to orchestrator for consistent team switching
|
|
726
|
+
raise TeamSwitchRequested()
|
|
727
|
+
|
|
728
|
+
case ActionType.HELP:
|
|
729
|
+
# Show help overlay INSIDE the Live context (avoids scroll artifacts)
|
|
730
|
+
# The overlay is rendered in _render() and dismissed on next keypress
|
|
731
|
+
self.state.help_visible = True
|
|
732
|
+
return True # Refresh to show help overlay
|
|
733
|
+
|
|
734
|
+
case ActionType.CUSTOM:
|
|
735
|
+
# Handle dashboard-specific custom keys (not in DEFAULT_KEY_MAP)
|
|
736
|
+
if action.custom_key == "r":
|
|
737
|
+
# User pressed 'r' - signal orchestrator to reload tab data
|
|
738
|
+
# Uses .name (stable identifier) not .value (display string)
|
|
739
|
+
raise RefreshRequested(return_to=self.state.active_tab.name)
|
|
740
|
+
elif action.custom_key == "n":
|
|
741
|
+
# User pressed 'n' - start new session (skip any resume prompts)
|
|
742
|
+
raise StartRequested(
|
|
743
|
+
return_to=self.state.active_tab.name,
|
|
744
|
+
reason="dashboard_new_session",
|
|
745
|
+
)
|
|
746
|
+
elif action.custom_key == "w":
|
|
747
|
+
# User pressed 'w' - show recent workspaces picker
|
|
748
|
+
# Only active on Worktrees tab
|
|
749
|
+
if self.state.active_tab == DashboardTab.WORKTREES:
|
|
750
|
+
raise RecentWorkspacesRequested(return_to=self.state.active_tab.name)
|
|
751
|
+
elif action.custom_key == "i":
|
|
752
|
+
# User pressed 'i' - initialize git repo
|
|
753
|
+
# Only active on Worktrees tab when not in a git repo
|
|
754
|
+
if self.state.active_tab == DashboardTab.WORKTREES:
|
|
755
|
+
current = self.state.list_state.current_item
|
|
756
|
+
# Only show when not in git repo (placeholder is no_git or no_worktrees)
|
|
757
|
+
is_non_git = (
|
|
758
|
+
current
|
|
759
|
+
and isinstance(current.value, str)
|
|
760
|
+
and current.value
|
|
761
|
+
in {
|
|
762
|
+
"no_git",
|
|
763
|
+
"no_worktrees",
|
|
764
|
+
}
|
|
765
|
+
)
|
|
766
|
+
if is_non_git:
|
|
767
|
+
raise GitInitRequested(return_to=self.state.active_tab.name)
|
|
768
|
+
else:
|
|
769
|
+
self.state.status_message = "Already in a git repository"
|
|
770
|
+
return True
|
|
771
|
+
elif action.custom_key == "c":
|
|
772
|
+
# User pressed 'c' - create worktree (or clone if not git)
|
|
773
|
+
# Only active on Worktrees tab
|
|
774
|
+
if self.state.active_tab == DashboardTab.WORKTREES:
|
|
775
|
+
current = self.state.list_state.current_item
|
|
776
|
+
# Check if we're in a git repo
|
|
777
|
+
is_git_repo = True
|
|
778
|
+
if current and isinstance(current.value, str):
|
|
779
|
+
is_git_repo = current.value not in {"no_git", "no_worktrees"}
|
|
780
|
+
raise CreateWorktreeRequested(
|
|
781
|
+
return_to=self.state.active_tab.name,
|
|
782
|
+
is_git_repo=is_git_repo,
|
|
783
|
+
)
|
|
784
|
+
elif action.custom_key == "verbose_toggle":
|
|
785
|
+
# User pressed 'v' - toggle verbose status display
|
|
786
|
+
# Only active on Worktrees tab
|
|
787
|
+
if self.state.active_tab == DashboardTab.WORKTREES:
|
|
788
|
+
new_verbose = not self.state.verbose_worktrees
|
|
789
|
+
raise VerboseToggleRequested(
|
|
790
|
+
return_to=self.state.active_tab.name,
|
|
791
|
+
verbose=new_verbose,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
return None
|