scc-cli 1.4.0__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 (112) 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 +259 -0
  8. scc_cli/cli_admin.py +683 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1400 -0
  16. scc_cli/cli_org.py +1433 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +858 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +603 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1082 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1405 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/compute.py +377 -0
  46. scc_cli/marketplace/constants.py +87 -0
  47. scc_cli/marketplace/managed.py +135 -0
  48. scc_cli/marketplace/materialize.py +723 -0
  49. scc_cli/marketplace/normalize.py +548 -0
  50. scc_cli/marketplace/render.py +238 -0
  51. scc_cli/marketplace/resolve.py +459 -0
  52. scc_cli/marketplace/schema.py +502 -0
  53. scc_cli/marketplace/sync.py +257 -0
  54. scc_cli/marketplace/team_cache.py +195 -0
  55. scc_cli/marketplace/team_fetch.py +688 -0
  56. scc_cli/marketplace/trust.py +244 -0
  57. scc_cli/models/__init__.py +41 -0
  58. scc_cli/models/exceptions.py +273 -0
  59. scc_cli/models/plugin_audit.py +434 -0
  60. scc_cli/org_templates.py +269 -0
  61. scc_cli/output_mode.py +167 -0
  62. scc_cli/panels.py +113 -0
  63. scc_cli/platform.py +350 -0
  64. scc_cli/profiles.py +1034 -0
  65. scc_cli/remote.py +443 -0
  66. scc_cli/schemas/__init__.py +1 -0
  67. scc_cli/schemas/org-v1.schema.json +456 -0
  68. scc_cli/schemas/team-config.v1.schema.json +163 -0
  69. scc_cli/sessions.py +425 -0
  70. scc_cli/setup.py +582 -0
  71. scc_cli/source_resolver.py +470 -0
  72. scc_cli/stats.py +378 -0
  73. scc_cli/stores/__init__.py +13 -0
  74. scc_cli/stores/exception_store.py +251 -0
  75. scc_cli/subprocess_utils.py +88 -0
  76. scc_cli/teams.py +339 -0
  77. scc_cli/templates/__init__.py +2 -0
  78. scc_cli/templates/org/__init__.py +0 -0
  79. scc_cli/templates/org/minimal.json +19 -0
  80. scc_cli/templates/org/reference.json +74 -0
  81. scc_cli/templates/org/strict.json +38 -0
  82. scc_cli/templates/org/teams.json +42 -0
  83. scc_cli/templates/statusline.sh +75 -0
  84. scc_cli/theme.py +348 -0
  85. scc_cli/ui/__init__.py +124 -0
  86. scc_cli/ui/branding.py +68 -0
  87. scc_cli/ui/chrome.py +395 -0
  88. scc_cli/ui/dashboard/__init__.py +62 -0
  89. scc_cli/ui/dashboard/_dashboard.py +669 -0
  90. scc_cli/ui/dashboard/loaders.py +369 -0
  91. scc_cli/ui/dashboard/models.py +184 -0
  92. scc_cli/ui/dashboard/orchestrator.py +337 -0
  93. scc_cli/ui/formatters.py +443 -0
  94. scc_cli/ui/gate.py +350 -0
  95. scc_cli/ui/help.py +157 -0
  96. scc_cli/ui/keys.py +521 -0
  97. scc_cli/ui/list_screen.py +431 -0
  98. scc_cli/ui/picker.py +700 -0
  99. scc_cli/ui/prompts.py +200 -0
  100. scc_cli/ui/wizard.py +490 -0
  101. scc_cli/update.py +680 -0
  102. scc_cli/utils/__init__.py +39 -0
  103. scc_cli/utils/fixit.py +264 -0
  104. scc_cli/utils/fuzzy.py +124 -0
  105. scc_cli/utils/locks.py +101 -0
  106. scc_cli/utils/ttl.py +376 -0
  107. scc_cli/validate.py +455 -0
  108. scc_cli-1.4.0.dist-info/METADATA +369 -0
  109. scc_cli-1.4.0.dist-info/RECORD +112 -0
  110. scc_cli-1.4.0.dist-info/WHEEL +4 -0
  111. scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
  112. scc_cli-1.4.0.dist-info/licenses/LICENSE +21 -0
scc_cli/ui/gate.py ADDED
@@ -0,0 +1,350 @@
1
+ """Interactivity gate - policy enforcement for interactive UI.
2
+
3
+ This module implements the interactivity decision system that determines
4
+ whether interactive UI (pickers, lists, prompts) can be shown. It enforces
5
+ a strict priority order to ensure predictable behavior in all contexts.
6
+
7
+ Priority Order (highest to lowest):
8
+ 1. JSON mode (--json) → Always False
9
+ 2. Explicit --no-interactive flag → Always False
10
+ 3. CI environment detection → False
11
+ 4. Non-TTY stdin → False
12
+ 5. Explicit --interactive flag → True (if TTY available)
13
+ 6. Default → True (if TTY available)
14
+
15
+ Fast Fail Validation:
16
+ Conflicting flags (--json with --interactive/--select) raise UsageError
17
+ immediately rather than silently ignoring the interactive flag.
18
+
19
+ Example:
20
+ >>> ctx = InteractivityContext.create(json_mode=False)
21
+ >>> if ctx.allows_prompt():
22
+ ... result = pick_team(teams)
23
+ ... else:
24
+ ... raise UsageError("--team-name required")
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ import sys
31
+ from collections.abc import Callable
32
+ from dataclasses import dataclass
33
+ from enum import Enum, auto
34
+ from typing import TYPE_CHECKING, TypeVar
35
+
36
+ from rich.console import Console
37
+
38
+ from ..exit_codes import EXIT_SUCCESS, EXIT_USAGE
39
+
40
+ if TYPE_CHECKING:
41
+ pass
42
+
43
+ T = TypeVar("T")
44
+
45
+ # Console for error output
46
+ _stderr_console = Console(stderr=True)
47
+
48
+
49
+ class InteractivityMode(Enum):
50
+ """Resolved interactivity mode after gate evaluation."""
51
+
52
+ INTERACTIVE = auto() # Full interactive UI allowed
53
+ NON_INTERACTIVE = auto() # Text output only, fail on missing selection
54
+
55
+
56
+ def _is_ci_environment() -> bool:
57
+ """Detect if running in a CI environment.
58
+
59
+ Checks common CI environment variables set by various CI systems:
60
+ - CI (GitHub Actions, GitLab CI, CircleCI, Travis CI, etc.)
61
+ - CONTINUOUS_INTEGRATION (Travis CI)
62
+ - BUILD_NUMBER (Jenkins)
63
+ - GITHUB_ACTIONS (GitHub Actions specific)
64
+ - GITLAB_CI (GitLab CI specific)
65
+
66
+ Returns:
67
+ True if CI environment detected, False otherwise.
68
+ """
69
+ ci_vars = ["CI", "CONTINUOUS_INTEGRATION", "BUILD_NUMBER", "GITHUB_ACTIONS", "GITLAB_CI"]
70
+
71
+ for var in ci_vars:
72
+ value = os.getenv(var, "").lower()
73
+ if value in ("1", "true", "yes"):
74
+ return True
75
+
76
+ return False
77
+
78
+
79
+ def _is_tty_available() -> bool:
80
+ """Check if stdin is a TTY (terminal).
81
+
82
+ Returns:
83
+ True if stdin is a terminal, False if piped/redirected.
84
+ """
85
+ return sys.stdin.isatty()
86
+
87
+
88
+ def validate_mode_flags(
89
+ *,
90
+ json_mode: bool = False,
91
+ interactive: bool = False,
92
+ select: bool = False,
93
+ dashboard: bool = False,
94
+ ) -> None:
95
+ """Validate that mode flags don't conflict - fail fast if they do.
96
+
97
+ This validation should be called as early as possible after option parsing,
98
+ before any UI/gating code runs. It ensures users get immediate feedback
99
+ about conflicting flags rather than silent behavior.
100
+
101
+ Args:
102
+ json_mode: Whether --json flag is set.
103
+ interactive: Whether --interactive/-i flag is set.
104
+ select: Whether --select flag is set.
105
+ dashboard: Whether --dashboard flag is set.
106
+
107
+ Raises:
108
+ UsageError: If JSON mode is combined with any interactive flag.
109
+
110
+ Example:
111
+ >>> validate_mode_flags(json_mode=True, interactive=True)
112
+ Traceback (most recent call last):
113
+ ...
114
+ UsageError: Cannot use --json with --interactive
115
+ """
116
+ from ..errors import UsageError
117
+
118
+ if not json_mode:
119
+ return # No conflict possible without JSON mode
120
+
121
+ # Collect all conflicting flags
122
+ conflicts: list[str] = []
123
+ if interactive:
124
+ conflicts.append("--interactive")
125
+ if select:
126
+ conflicts.append("--select")
127
+ if dashboard:
128
+ conflicts.append("--dashboard")
129
+
130
+ if conflicts:
131
+ flags_str = ", ".join(conflicts)
132
+ raise UsageError(
133
+ user_message=f"Cannot use --json with {flags_str}",
134
+ suggested_action=(
135
+ "Remove one of the conflicting flags. "
136
+ "Use --json for machine-readable output OR interactive flags for user prompts, not both."
137
+ ),
138
+ )
139
+
140
+
141
+ def is_interactive_allowed(
142
+ *,
143
+ json_mode: bool = False,
144
+ no_interactive_flag: bool = False,
145
+ interactive_flag: bool = False,
146
+ ) -> bool:
147
+ """Check if interactive UI is allowed based on priority order.
148
+
149
+ Priority (from highest to lowest):
150
+ 1. JSON mode (--json) → False
151
+ 2. Explicit --no-interactive flag → False
152
+ 3. CI environment detection → False
153
+ 4. Non-TTY stdin → False
154
+ 5. Explicit --interactive flag → True (if TTY available)
155
+ 6. Default → True (if TTY available)
156
+
157
+ Args:
158
+ json_mode: Whether --json flag is set.
159
+ no_interactive_flag: Whether --no-interactive flag is set.
160
+ interactive_flag: Whether --interactive flag is set.
161
+
162
+ Returns:
163
+ True if interactive prompts are permitted, False otherwise.
164
+
165
+ Example:
166
+ >>> is_interactive_allowed(json_mode=True)
167
+ False
168
+ >>> is_interactive_allowed() # in TTY, no CI
169
+ True
170
+ """
171
+ # Priority 1: JSON mode always blocks
172
+ if json_mode:
173
+ return False
174
+
175
+ # Priority 2: Explicit --no-interactive blocks
176
+ if no_interactive_flag:
177
+ return False
178
+
179
+ # Priority 3: CI environment blocks
180
+ if _is_ci_environment():
181
+ return False
182
+
183
+ # Priority 4: Non-TTY blocks
184
+ if not _is_tty_available():
185
+ return False
186
+
187
+ # Priority 5 & 6: TTY available, allow interactive
188
+ # (--interactive flag doesn't change anything here since TTY is required anyway)
189
+ return True
190
+
191
+
192
+ @dataclass(frozen=True)
193
+ class InteractivityContext:
194
+ """Immutable context for interactivity decisions throughout a command.
195
+
196
+ Create once at command entry point, pass to all UI functions.
197
+ This ensures consistent behavior across all UI components.
198
+
199
+ Attributes:
200
+ mode: Resolved interactivity mode (INTERACTIVE or NON_INTERACTIVE).
201
+ is_json_output: Whether --json flag is set (for output formatting).
202
+ force_yes: Whether --yes flag is set (skip confirmations).
203
+ """
204
+
205
+ mode: InteractivityMode
206
+ is_json_output: bool
207
+ force_yes: bool
208
+
209
+ @classmethod
210
+ def create(
211
+ cls,
212
+ *,
213
+ json_mode: bool = False,
214
+ no_interactive: bool = False,
215
+ force_interactive: bool = False,
216
+ force_yes: bool = False,
217
+ ) -> InteractivityContext:
218
+ """Create context from command-line flags.
219
+
220
+ This is the primary factory method for creating InteractivityContext.
221
+ Call once at the entry point of a command and pass down to all UI functions.
222
+
223
+ Args:
224
+ json_mode: Whether --json flag is set.
225
+ no_interactive: Whether --no-interactive flag is set.
226
+ force_interactive: Whether --interactive/-i flag is set.
227
+ force_yes: Whether --yes/-y flag is set.
228
+
229
+ Returns:
230
+ Configured InteractivityContext with resolved mode.
231
+ """
232
+ allowed = is_interactive_allowed(
233
+ json_mode=json_mode,
234
+ no_interactive_flag=no_interactive,
235
+ interactive_flag=force_interactive,
236
+ )
237
+
238
+ mode = InteractivityMode.INTERACTIVE if allowed else InteractivityMode.NON_INTERACTIVE
239
+
240
+ return cls(
241
+ mode=mode,
242
+ is_json_output=json_mode,
243
+ force_yes=force_yes,
244
+ )
245
+
246
+ def allows_prompt(self) -> bool:
247
+ """Whether interactive prompts are permitted.
248
+
249
+ Returns:
250
+ True if interactive UI can be shown, False if must use explicit args.
251
+ """
252
+ return self.mode == InteractivityMode.INTERACTIVE and not self.is_json_output
253
+
254
+ def requires_confirmation(self) -> bool:
255
+ """Whether destructive actions need confirmation.
256
+
257
+ Returns:
258
+ True if confirmation dialog should be shown, False if --yes bypasses it.
259
+ """
260
+ return not self.force_yes and self.allows_prompt()
261
+
262
+
263
+ def require_selection_or_prompt(
264
+ selection: T | None,
265
+ picker_fn: Callable[[], T | None],
266
+ arg_name: str,
267
+ *,
268
+ ctx: InteractivityContext,
269
+ ) -> T:
270
+ """Get selection from explicit arg, picker, or fail with usage error.
271
+
272
+ This is the primary entry point for commands that support both
273
+ explicit arguments and interactive selection. It implements the
274
+ "graceful degradation" pattern:
275
+ 1. If explicit value provided, use it (works in all modes)
276
+ 2. If no value but interactive allowed, show picker
277
+ 3. If no value and not interactive, fail with helpful error
278
+
279
+ Args:
280
+ selection: Explicit value from command-line argument (None if not provided).
281
+ picker_fn: Function to call for interactive selection.
282
+ arg_name: Name of the argument for error messages (e.g., "team-name").
283
+ ctx: Interactivity context with mode and flags.
284
+
285
+ Returns:
286
+ The selected value (from arg or picker).
287
+
288
+ Raises:
289
+ SystemExit: With EXIT_USAGE if selection required but interactive forbidden.
290
+ SystemExit: With EXIT_SUCCESS (0) if user cancels picker.
291
+
292
+ Example:
293
+ >>> ctx = InteractivityContext.create(json_mode=False)
294
+ >>> team = require_selection_or_prompt(
295
+ ... selection=args.team_name,
296
+ ... picker_fn=lambda: pick_team(teams),
297
+ ... arg_name="team-name",
298
+ ... ctx=ctx,
299
+ ... )
300
+ """
301
+ # Case 1: Explicit selection provided - use it directly
302
+ if selection is not None:
303
+ return selection
304
+
305
+ # Case 2: No selection, but interactive allowed - show picker
306
+ if ctx.allows_prompt():
307
+ result = picker_fn()
308
+ if result is None:
309
+ # User cancelled - exit cleanly
310
+ raise SystemExit(EXIT_SUCCESS)
311
+ return result
312
+
313
+ # Case 3: No selection and not interactive - fail with helpful error
314
+ _print_missing_selection_error(arg_name, ctx)
315
+ raise SystemExit(EXIT_USAGE)
316
+
317
+
318
+ def _print_missing_selection_error(arg_name: str, ctx: InteractivityContext) -> None:
319
+ """Print helpful error message for missing selection.
320
+
321
+ This function implements the standard "what/why/next" error pattern:
322
+ - What: Clear problem statement (red Error: prefix)
323
+ - Why: Context-aware explanation (dim text explaining the cause)
324
+ - Next: Actionable steps (bold header with bullet points)
325
+
326
+ All user-facing validation errors in the UI module should follow
327
+ this pattern for consistency.
328
+
329
+ Args:
330
+ arg_name: Name of the missing argument.
331
+ ctx: Context for determining why interactive is blocked.
332
+ """
333
+ # Determine why interactive is blocked
334
+ if ctx.is_json_output:
335
+ why = "In JSON output mode, explicit arguments are required."
336
+ elif _is_ci_environment():
337
+ why = "In CI environment, interactive prompts are disabled."
338
+ elif not _is_tty_available():
339
+ why = "Input is not from a terminal, interactive prompts unavailable."
340
+ else:
341
+ why = "Interactive mode is disabled."
342
+
343
+ _stderr_console.print(f"[red]Error:[/red] Missing required argument: --{arg_name}")
344
+ _stderr_console.print()
345
+ _stderr_console.print(f"[dim]{why}[/dim]")
346
+ _stderr_console.print()
347
+ _stderr_console.print("[bold]Next steps:[/bold]")
348
+ _stderr_console.print(f" • Run with --{arg_name} <value> to specify explicitly")
349
+ _stderr_console.print(" • Run in a TTY without --json for interactive selection")
350
+ _stderr_console.print(f" • Run 'scc {arg_name.split('-')[0]} list' to see available options")
scc_cli/ui/help.py ADDED
@@ -0,0 +1,157 @@
1
+ """Help overlay for interactive UI screens.
2
+
3
+ Provides mode-aware help that shows only keys relevant to the current screen.
4
+ The overlay is triggered by pressing '?' and dismissed by any key.
5
+
6
+ Key categories shown per mode:
7
+ - ALL: Navigation (↑↓/j/k), typing to filter, backspace, t for teams
8
+ - PICKER: Enter to select, Esc to cancel
9
+ - MULTI_SELECT: Space to toggle, a to toggle all, Enter to confirm, Esc to cancel
10
+ - DASHBOARD: Tab/Shift+Tab for tabs, Enter for details, q to quit
11
+
12
+ Example:
13
+ >>> from scc_cli.ui.help import show_help_overlay
14
+ >>> from scc_cli.ui.list_screen import ListMode
15
+ >>> show_help_overlay(ListMode.SINGLE_SELECT)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from enum import Enum, auto
21
+ from typing import TYPE_CHECKING
22
+
23
+ from rich.panel import Panel
24
+ from rich.table import Table
25
+ from rich.text import Text
26
+
27
+ from ..theme import Indicators
28
+
29
+ if TYPE_CHECKING:
30
+ from rich.console import Console, RenderableType
31
+
32
+
33
+ class HelpMode(Enum):
34
+ """Screen mode for help overlay customization."""
35
+
36
+ PICKER = auto() # Single-select picker (team, worktree, etc.)
37
+ MULTI_SELECT = auto() # Multi-select list (containers, etc.)
38
+ DASHBOARD = auto() # Tabbed dashboard view
39
+
40
+
41
+ # Mapping from HelpMode enum to string mode names used in KEYBINDING_DOCS
42
+ _MODE_NAMES: dict[HelpMode, str] = {
43
+ HelpMode.PICKER: "PICKER",
44
+ HelpMode.MULTI_SELECT: "MULTI_SELECT",
45
+ HelpMode.DASHBOARD: "DASHBOARD",
46
+ }
47
+
48
+
49
+ def get_help_entries(mode: HelpMode) -> list[tuple[str, str]]:
50
+ """Get help entries filtered for a specific mode.
51
+
52
+ This function uses KEYBINDING_DOCS from keys.py as the single source
53
+ of truth for keybinding documentation.
54
+
55
+ Args:
56
+ mode: The current screen mode.
57
+
58
+ Returns:
59
+ List of (key, description) tuples for the given mode.
60
+ """
61
+ from .keys import get_keybindings_for_mode
62
+
63
+ mode_name = _MODE_NAMES[mode]
64
+ return get_keybindings_for_mode(mode_name)
65
+
66
+
67
+ def get_help_entries_grouped(mode: HelpMode) -> dict[str, list[tuple[str, str]]]:
68
+ """Get help entries grouped by section for a specific mode.
69
+
70
+ This function uses KEYBINDING_DOCS from keys.py as the single source
71
+ of truth for keybinding documentation.
72
+
73
+ Args:
74
+ mode: The current screen mode.
75
+
76
+ Returns:
77
+ Dict mapping section names to lists of (key, description) tuples.
78
+ """
79
+ from .keys import get_keybindings_grouped_by_section
80
+
81
+ mode_name = _MODE_NAMES[mode]
82
+ return get_keybindings_grouped_by_section(mode_name)
83
+
84
+
85
+ def render_help_content(mode: HelpMode) -> RenderableType:
86
+ """Render help content for a given mode with section headers.
87
+
88
+ Args:
89
+ mode: The current screen mode.
90
+
91
+ Returns:
92
+ A Rich renderable with the help content organized by section.
93
+ """
94
+ from rich.console import Group
95
+
96
+ grouped = get_help_entries_grouped(mode)
97
+
98
+ renderables: list[RenderableType] = []
99
+
100
+ for section_name, entries in grouped.items():
101
+ # Section header
102
+ section_header = Text()
103
+ sep = Indicators.get("HORIZONTAL_LINE")
104
+ section_header.append(f"{sep}{sep}{sep} {section_name} ", style="dim")
105
+ section_header.append(sep * max(0, 30 - len(section_name)), style="dim")
106
+ renderables.append(section_header)
107
+
108
+ # Section table
109
+ table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
110
+ table.add_column("Key", style="cyan bold", width=12)
111
+ table.add_column("Action", style="dim")
112
+
113
+ for key, desc in entries:
114
+ table.add_row(key, desc)
115
+
116
+ renderables.append(table)
117
+ renderables.append(Text("")) # Spacing between sections
118
+
119
+ # Mode indicator
120
+ mode_display = {
121
+ HelpMode.PICKER: "Picker",
122
+ HelpMode.MULTI_SELECT: "Multi-Select",
123
+ HelpMode.DASHBOARD: "Dashboard",
124
+ }.get(mode, "Unknown")
125
+
126
+ footer = Text()
127
+ footer.append("Press any key to dismiss", style="dim italic")
128
+ renderables.append(footer)
129
+
130
+ return Panel(
131
+ Group(*renderables),
132
+ title=f"[bold]Keyboard Shortcuts[/bold] {Indicators.get('VERTICAL_LINE')} {mode_display}",
133
+ title_align="left",
134
+ border_style="blue",
135
+ padding=(1, 2),
136
+ )
137
+
138
+
139
+ def show_help_overlay(mode: HelpMode, console: Console | None = None) -> None:
140
+ """Display help overlay and wait for any key to dismiss.
141
+
142
+ Args:
143
+ mode: The current screen mode (affects which keys are shown).
144
+ console: Optional console to use. If None, creates a new one.
145
+ """
146
+ if console is None:
147
+ from ..console import get_err_console
148
+
149
+ console = get_err_console()
150
+
151
+ content = render_help_content(mode)
152
+ console.print(content)
153
+
154
+ # Wait for any key to dismiss
155
+ from .keys import read_key
156
+
157
+ read_key()