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
@@ -0,0 +1,437 @@
1
+ """Core navigation engine for interactive list-based UI.
2
+
3
+ This module provides the ListScreen component - the heart of the interactive
4
+ UI system. It handles:
5
+ - State management (cursor, scroll, filter, selection)
6
+ - Key handling (using the keys module)
7
+ - Rendering (using the chrome module)
8
+ - Event loop (blocking key reads → state updates → re-render)
9
+
10
+ The design is mode-agnostic: SINGLE_SELECT, MULTI_SELECT, and ACTIONABLE
11
+ modes use the same engine with different handlers for actions.
12
+
13
+ Example:
14
+ >>> items = [ListItem(value=t, label=t.name) for t in teams]
15
+ >>> screen = ListScreen(items, title="Select Team")
16
+ >>> selected = screen.run() # Blocking until selection or cancel
17
+ >>> if selected:
18
+ ... print(f"Selected: {selected.value}")
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Callable, Sequence
24
+ from dataclasses import dataclass, field
25
+ from enum import Enum, auto
26
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
27
+
28
+ from rich.console import RenderableType
29
+ from rich.live import Live
30
+ from rich.text import Text
31
+
32
+ from scc_cli.theme import Indicators
33
+
34
+ from .chrome import Chrome, ChromeConfig
35
+ from .keys import Action, ActionType, KeyReader, TeamSwitchRequested
36
+
37
+ if TYPE_CHECKING:
38
+ pass
39
+
40
+ T = TypeVar("T")
41
+
42
+
43
+ class ListMode(Enum):
44
+ """Operating mode for ListScreen.
45
+
46
+ Determines how the screen handles selection and confirmation.
47
+ """
48
+
49
+ SINGLE_SELECT = auto() # Enter returns single item
50
+ MULTI_SELECT = auto() # Space toggles, Enter returns list
51
+ ACTIONABLE = auto() # Action keys dispatch callbacks
52
+
53
+
54
+ @dataclass
55
+ class ListItem(Generic[T]):
56
+ """Wrapper for items in a list with display metadata.
57
+
58
+ Attributes:
59
+ value: The underlying value (domain object).
60
+ label: Primary display text.
61
+ description: Secondary text (optional).
62
+ metadata: Additional key-value pairs for display.
63
+ governance_status: "blocked", "warning", or None.
64
+ """
65
+
66
+ value: T
67
+ label: str
68
+ description: str = ""
69
+ metadata: dict[str, str] = field(default_factory=dict)
70
+ governance_status: str | None = None
71
+
72
+
73
+ @dataclass
74
+ class ListState(Generic[T]):
75
+ """Mutable state for ListScreen navigation and selection.
76
+
77
+ Attributes:
78
+ items: All items in the list.
79
+ cursor: Current cursor position (in filtered items).
80
+ scroll_offset: Scroll position for viewport.
81
+ filter_query: Current type-to-filter query.
82
+ selected: Set of selected indices (for multi-select).
83
+ viewport_height: Max items visible at once.
84
+ """
85
+
86
+ items: Sequence[ListItem[T]]
87
+ cursor: int = 0
88
+ scroll_offset: int = 0
89
+ filter_query: str = ""
90
+ selected: set[int] = field(default_factory=set)
91
+ viewport_height: int = 10
92
+
93
+ @property
94
+ def filtered_items(self) -> list[ListItem[T]]:
95
+ """Items matching the current filter query."""
96
+ if not self.filter_query:
97
+ return list(self.items)
98
+ query = self.filter_query.lower()
99
+ return [
100
+ item
101
+ for item in self.items
102
+ if query in item.label.lower() or query in item.description.lower()
103
+ ]
104
+
105
+ @property
106
+ def visible_items(self) -> list[ListItem[T]]:
107
+ """Items visible in the current viewport."""
108
+ filtered = self.filtered_items
109
+ end = min(self.scroll_offset + self.viewport_height, len(filtered))
110
+ return filtered[self.scroll_offset : end]
111
+
112
+ @property
113
+ def current_item(self) -> ListItem[T] | None:
114
+ """Item at cursor position, or None if list empty."""
115
+ filtered = self.filtered_items
116
+ if 0 <= self.cursor < len(filtered):
117
+ return filtered[self.cursor]
118
+ return None
119
+
120
+ def move_cursor(self, delta: int) -> bool:
121
+ """Move cursor by delta, clamping to valid range.
122
+
123
+ Returns True if cursor position changed.
124
+ """
125
+ filtered = self.filtered_items
126
+ if not filtered:
127
+ return False
128
+
129
+ old_cursor = self.cursor
130
+ self.cursor = max(0, min(len(filtered) - 1, self.cursor + delta))
131
+
132
+ # Adjust scroll to keep cursor visible
133
+ if self.cursor < self.scroll_offset:
134
+ self.scroll_offset = self.cursor
135
+ elif self.cursor >= self.scroll_offset + self.viewport_height:
136
+ self.scroll_offset = self.cursor - self.viewport_height + 1
137
+
138
+ return self.cursor != old_cursor
139
+
140
+ def toggle_selection(self) -> bool:
141
+ """Toggle selection state of current item."""
142
+ if self.current_item is None:
143
+ return False
144
+
145
+ if self.cursor in self.selected:
146
+ self.selected.discard(self.cursor)
147
+ else:
148
+ self.selected.add(self.cursor)
149
+ return True
150
+
151
+ def toggle_all(self) -> bool:
152
+ """Toggle all items selected/deselected."""
153
+ filtered = self.filtered_items
154
+ if not filtered:
155
+ return False
156
+
157
+ all_selected = len(self.selected) == len(filtered)
158
+ if all_selected:
159
+ self.selected.clear()
160
+ else:
161
+ self.selected = set(range(len(filtered)))
162
+ return True
163
+
164
+ def add_filter_char(self, char: str) -> bool:
165
+ """Add character to filter query."""
166
+ self.filter_query += char
167
+ # Reset cursor and selection when filter changes
168
+ # Selection indices become stale when filtered list shrinks
169
+ self.cursor = 0
170
+ self.scroll_offset = 0
171
+ self.selected.clear()
172
+ return True
173
+
174
+ def delete_filter_char(self) -> bool:
175
+ """Remove last character from filter query."""
176
+ if not self.filter_query:
177
+ return False
178
+ self.filter_query = self.filter_query[:-1]
179
+ # Reset cursor and selection when filter changes
180
+ # Selection indices become stale when filtered list changes
181
+ self.cursor = 0
182
+ self.scroll_offset = 0
183
+ self.selected.clear()
184
+ return True
185
+
186
+ def clear_filter(self) -> bool:
187
+ """Clear the entire filter query.
188
+
189
+ Returns True if filter was cleared, False if already empty.
190
+ """
191
+ if not self.filter_query:
192
+ return False
193
+ self.filter_query = ""
194
+ # Reset cursor and selection when filter changes
195
+ # Selection indices become stale when filtered list changes
196
+ self.cursor = 0
197
+ self.scroll_offset = 0
198
+ self.selected.clear()
199
+ return True
200
+
201
+
202
+ class ListScreen(Generic[T]):
203
+ """Core navigation engine for list-based UI.
204
+
205
+ ListScreen combines state management, key handling, and rendering
206
+ into a cohesive event loop that blocks for user input and returns
207
+ the selected value(s).
208
+
209
+ Attributes:
210
+ state: Current list state.
211
+ mode: Operating mode (SINGLE_SELECT, MULTI_SELECT, ACTIONABLE).
212
+ title: Display title for the chrome.
213
+ custom_actions: Custom key handlers for ACTIONABLE mode.
214
+ """
215
+
216
+ def __init__(
217
+ self,
218
+ items: Sequence[ListItem[T]],
219
+ *,
220
+ title: str = "Select",
221
+ mode: ListMode = ListMode.SINGLE_SELECT,
222
+ custom_actions: dict[str, Callable[[ListItem[T]], None]] | None = None,
223
+ viewport_height: int = 10,
224
+ initial_filter: str = "",
225
+ ) -> None:
226
+ """Initialize the list screen.
227
+
228
+ Args:
229
+ items: Items to display in the list.
230
+ title: Title for the chrome header.
231
+ mode: Operating mode for selection behavior.
232
+ custom_actions: Key → handler map for ACTIONABLE mode.
233
+ viewport_height: Max items visible at once.
234
+ initial_filter: Pre-populate the filter query (for prefilled pickers).
235
+ """
236
+ self.state = ListState(
237
+ items=items,
238
+ viewport_height=viewport_height,
239
+ filter_query=initial_filter,
240
+ )
241
+ self.mode = mode
242
+ self.title = title
243
+ self.custom_actions = custom_actions or {}
244
+ from ..console import get_err_console
245
+
246
+ self._console = get_err_console()
247
+
248
+ def run(self) -> T | list[T] | None:
249
+ """Run the interactive list screen.
250
+
251
+ Blocks until the user makes a selection or cancels.
252
+
253
+ Returns:
254
+ - SINGLE_SELECT: The selected item's value, or None if cancelled.
255
+ - MULTI_SELECT: List of selected values, or None if cancelled.
256
+ - ACTIONABLE: None (actions handled via callbacks).
257
+ """
258
+ # Set up key reader with custom keys if in actionable mode
259
+ custom_keys = {k: k for k in self.custom_actions} if self.custom_actions else None
260
+ reader = KeyReader(custom_keys=custom_keys, enable_filter=True)
261
+
262
+ # Use Rich Live for efficient updates
263
+ with Live(
264
+ self._render(),
265
+ console=self._console,
266
+ auto_refresh=False, # Manual refresh for instant response
267
+ transient=True, # Clear on exit
268
+ ) as live:
269
+ while True:
270
+ action = reader.read(filter_active=bool(self.state.filter_query))
271
+
272
+ # Handle action based on type
273
+ result = self._handle_action(action)
274
+
275
+ if result is not None:
276
+ return result
277
+
278
+ if action.should_exit:
279
+ return None
280
+
281
+ # Re-render if state changed
282
+ if action.state_changed:
283
+ live.update(self._render(), refresh=True)
284
+
285
+ def _render(self) -> RenderableType:
286
+ """Render the current state to a Rich renderable."""
287
+ # Build the list body
288
+ body = self._render_list_body()
289
+
290
+ # Get appropriate chrome config
291
+ config = self._get_chrome_config()
292
+
293
+ # Render with chrome
294
+ chrome = Chrome(config)
295
+ return chrome.render(body, search_query=self.state.filter_query)
296
+
297
+ def _render_list_body(self) -> Text:
298
+ """Render the list items."""
299
+ text = Text()
300
+ filtered = self.state.filtered_items
301
+ visible = self.state.visible_items
302
+
303
+ if not filtered:
304
+ text.append("No matches found", style="dim italic")
305
+ if self.state.filter_query:
306
+ text.append(" — ", style="dim")
307
+ text.append("Backspace", style="cyan")
308
+ text.append(" to edit filter", style="dim")
309
+ return text
310
+
311
+ for i, item in enumerate(visible):
312
+ # Calculate actual index in filtered list
313
+ actual_index = self.state.scroll_offset + i
314
+ is_cursor = actual_index == self.state.cursor
315
+ is_selected = actual_index in self.state.selected
316
+
317
+ # Build line with cursor indicator
318
+ if is_cursor:
319
+ text.append(f"{Indicators.get('CURSOR')} ", style="cyan bold")
320
+ else:
321
+ text.append(" ")
322
+
323
+ # Selection checkbox for multi-select
324
+ if self.mode == ListMode.MULTI_SELECT:
325
+ if is_selected:
326
+ text.append(f"[{Indicators.get('PASS')}] ", style="green")
327
+ else:
328
+ text.append("[ ] ", style="dim")
329
+
330
+ # Governance indicator
331
+ if item.governance_status == "blocked":
332
+ text.append("⛔ ", style="red")
333
+ elif item.governance_status == "warning":
334
+ text.append("⚠️ ", style="yellow")
335
+
336
+ # Label and description
337
+ label_style = "bold" if is_cursor else ""
338
+ text.append(item.label, style=label_style)
339
+
340
+ if item.description:
341
+ text.append(f" {item.description}", style="dim")
342
+
343
+ # Metadata badges
344
+ if item.metadata:
345
+ for key, value in item.metadata.items():
346
+ if value: # Only show non-empty values
347
+ text.append(f" [{value}]", style="cyan dim")
348
+
349
+ text.append("\n")
350
+
351
+ # Scroll indicators
352
+ if self.state.scroll_offset > 0:
353
+ text.append(f"{Indicators.get('SCROLL_UP')} more above\n", style="dim")
354
+ if self.state.scroll_offset + self.state.viewport_height < len(filtered):
355
+ text.append(f"{Indicators.get('SCROLL_DOWN')} more below\n", style="dim")
356
+
357
+ return text
358
+
359
+ def _get_chrome_config(self) -> ChromeConfig:
360
+ """Get the appropriate chrome config for current mode."""
361
+ filtered_count = len(self.state.filtered_items)
362
+
363
+ if self.mode == ListMode.MULTI_SELECT:
364
+ return ChromeConfig.for_multi_select(
365
+ self.title,
366
+ len(self.state.selected),
367
+ filtered_count,
368
+ )
369
+ # SINGLE_SELECT and ACTIONABLE use picker style
370
+ return ChromeConfig.for_picker(self.title, item_count=filtered_count)
371
+
372
+ def _handle_action(self, action: Action[Any]) -> T | list[T] | None:
373
+ """Handle an action and return result if selection complete."""
374
+ match action.action_type:
375
+ case ActionType.NAVIGATE_UP:
376
+ self.state.move_cursor(-1)
377
+
378
+ case ActionType.NAVIGATE_DOWN:
379
+ self.state.move_cursor(1)
380
+
381
+ case ActionType.SELECT:
382
+ if self.mode == ListMode.SINGLE_SELECT:
383
+ item = self.state.current_item
384
+ if item:
385
+ return item.value
386
+ elif self.mode == ListMode.MULTI_SELECT:
387
+ # In MULTI_SELECT, Enter confirms selection (same as CONFIRM)
388
+ # This prevents should_exit from returning None as "cancelled"
389
+ filtered = self.state.filtered_items
390
+ selected_indices = self.state.selected & set(range(len(filtered)))
391
+ return [filtered[i].value for i in sorted(selected_indices)]
392
+
393
+ case ActionType.TOGGLE:
394
+ if self.mode == ListMode.MULTI_SELECT:
395
+ self.state.toggle_selection()
396
+
397
+ case ActionType.TOGGLE_ALL:
398
+ if self.mode == ListMode.MULTI_SELECT:
399
+ self.state.toggle_all()
400
+
401
+ case ActionType.CONFIRM:
402
+ if self.mode == ListMode.MULTI_SELECT:
403
+ # Return all selected items
404
+ filtered = self.state.filtered_items
405
+ return [filtered[i].value for i in sorted(self.state.selected)]
406
+
407
+ case ActionType.FILTER_CHAR:
408
+ if action.filter_char:
409
+ self.state.add_filter_char(action.filter_char)
410
+
411
+ case ActionType.FILTER_DELETE:
412
+ self.state.delete_filter_char()
413
+
414
+ case ActionType.CUSTOM:
415
+ if action.custom_key and action.custom_key in self.custom_actions:
416
+ item = self.state.current_item
417
+ if item:
418
+ self.custom_actions[action.custom_key](item)
419
+
420
+ case ActionType.CANCEL | ActionType.QUIT:
421
+ # Will be handled by should_exit in caller
422
+ pass
423
+
424
+ case ActionType.TEAM_SWITCH:
425
+ # Bubble up to orchestrator for consistent team switching
426
+ raise TeamSwitchRequested()
427
+
428
+ case ActionType.HELP:
429
+ # Show mode-aware help overlay
430
+ from .help import HelpMode, show_help_overlay
431
+
432
+ help_mode = (
433
+ HelpMode.MULTI_SELECT if self.mode == ListMode.MULTI_SELECT else HelpMode.PICKER
434
+ )
435
+ show_help_overlay(help_mode, self._console)
436
+
437
+ return None