beautiful-python-cli 0.0.0-stage → 1.0.0

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.
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """A compact Rich showcase: panels, tables, status, and measurable progress."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.progress import (
13
+ BarColumn,
14
+ Progress,
15
+ SpinnerColumn,
16
+ TaskProgressColumn,
17
+ TextColumn,
18
+ TimeElapsedColumn,
19
+ )
20
+ from rich.table import Table
21
+ from rich.theme import Theme
22
+
23
+ THEME = Theme(
24
+ {
25
+ "success": "bold green",
26
+ "warning": "bold yellow",
27
+ "error": "bold red",
28
+ "info": "cyan",
29
+ "muted": "dim",
30
+ "accent": "bold bright_magenta",
31
+ }
32
+ )
33
+
34
+
35
+ def make_console(no_color: bool = False) -> Console:
36
+ return Console(theme=THEME, no_color=no_color)
37
+
38
+
39
+ def render_summary(console: Console) -> None:
40
+ summary = Table.grid(padding=(0, 2))
41
+ summary.add_column(style="muted")
42
+ summary.add_column()
43
+ summary.add_row("Target", "demo.internal")
44
+ summary.add_row("Mode", "read-only demonstration")
45
+ summary.add_row("Result", "[success]COMPLETED[/success]")
46
+ console.print(
47
+ Panel(
48
+ summary,
49
+ title="[accent]Python CLI[/accent]",
50
+ subtitle="[muted]Rich interface showcase[/muted]",
51
+ border_style="cyan",
52
+ expand=False,
53
+ )
54
+ )
55
+
56
+
57
+ def render_results(console: Console) -> None:
58
+ table = Table(title="Example results", expand=True)
59
+ table.add_column("ID", style="muted", no_wrap=True)
60
+ table.add_column("Check", ratio=2)
61
+ table.add_column("Status", no_wrap=True)
62
+ table.add_column("Duration", justify="right", no_wrap=True)
63
+
64
+ table.add_row("CHK-001", "Configuration loaded", "[success]PASS[/success]", "0.02s")
65
+ table.add_row("CHK-002", "Dependency check", "[success]PASS[/success]", "0.11s")
66
+ table.add_row("CHK-003", "Optional integration", "[warning]SKIPPED[/warning]", "—")
67
+ console.print(table)
68
+
69
+
70
+ def run_progress(console: Console, count: int) -> None:
71
+ with Progress(
72
+ SpinnerColumn(),
73
+ TextColumn("[progress.description]{task.description}"),
74
+ BarColumn(),
75
+ TaskProgressColumn(),
76
+ TimeElapsedColumn(),
77
+ console=console,
78
+ transient=False,
79
+ disable=not console.is_terminal,
80
+ ) as progress:
81
+ task = progress.add_task("Processing demonstration items", total=count)
82
+ for _ in range(count):
83
+ time.sleep(0.08)
84
+ progress.advance(task)
85
+
86
+
87
+ def main() -> int:
88
+ parser = argparse.ArgumentParser(description="Showcase a polished Rich CLI.")
89
+ parser.add_argument("--no-color", action="store_true", help="Disable terminal colors.")
90
+ parser.add_argument("--items", type=int, default=20, help="Number of demo progress items.")
91
+ args = parser.parse_args()
92
+
93
+ if args.items < 1:
94
+ parser.error("--items must be at least 1")
95
+
96
+ console = make_console(no_color=args.no_color)
97
+ console.print()
98
+ render_summary(console)
99
+ console.print()
100
+ render_results(console)
101
+ console.print()
102
+
103
+ if console.is_terminal:
104
+ run_progress(console, args.items)
105
+ else:
106
+ console.print(f"Processing {args.items} demonstration items (progress animation disabled).")
107
+
108
+ console.print("\n[success]Demo finished.[/success]")
109
+ return 0
110
+
111
+
112
+ if __name__ == "__main__":
113
+ raise SystemExit(main())
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """Demonstration of a multi-pane Terminal Layout (TUI) with responsive panes."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ import time
9
+
10
+ from rich.align import Align
11
+ from rich.console import Console
12
+ from rich.layout import Layout
13
+ from rich.live import Live
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+ from rich.theme import Theme
17
+
18
+ THEME = Theme(
19
+ {
20
+ "success": "bold green",
21
+ "warning": "bold yellow",
22
+ "error": "bold red",
23
+ "info": "cyan",
24
+ "muted": "dim",
25
+ "accent": "bold magenta",
26
+ }
27
+ )
28
+
29
+ console = Console(theme=THEME)
30
+
31
+
32
+ def make_layout() -> Layout:
33
+ layout = Layout()
34
+ layout.split_column(
35
+ Layout(name="header", size=3),
36
+ Layout(name="main", ratio=1),
37
+ Layout(name="footer", size=3),
38
+ )
39
+ layout["main"].split_row(
40
+ Layout(name="sidebar", ratio=1),
41
+ Layout(name="content", ratio=2),
42
+ )
43
+ return layout
44
+
45
+
46
+ def render_sidebar(iteration: int) -> Panel:
47
+ table = Table.grid(padding=(0, 1))
48
+ table.add_column(style="muted")
49
+ table.add_column(justify="right")
50
+ table.add_row("Uptime:", f"{iteration * 0.5:.1f}s")
51
+ table.add_row("Memory:", "42.8 MB")
52
+ table.add_row("Active Tasks:", "[accent]4[/accent]")
53
+ table.add_row("Queue Depth:", "[info]12[/info]")
54
+ table.add_row("Health:", "[success]OPTIMAL[/success]")
55
+ return Panel(table, title="[bold]System Status[/bold]", border_style="cyan")
56
+
57
+
58
+ def render_content(iteration: int) -> Panel:
59
+ table = Table(expand=True, box=None)
60
+ table.add_column("Event ID", style="muted", no_wrap=True)
61
+ table.add_column("Source")
62
+ table.add_column("Level")
63
+ table.add_column("Message")
64
+
65
+ events = [
66
+ ("EVT-101", "ingest-service", "[success]INFO[/success]", "Payload batch parsed successfully"),
67
+ ("EVT-102", "auth-gateway", "[success]INFO[/success]", "TLS handshake verified with mutual cert"),
68
+ ("EVT-103", "worker-pool", "[warning]WARN[/warning]", "Memory buffer above 70% threshold"),
69
+ ("EVT-104", "storage-node", "[success]INFO[/success]", f"Snapshot #{iteration} committed to disk"),
70
+ ]
71
+ for eid, src, lvl, msg in events:
72
+ table.add_row(eid, src, lvl, msg)
73
+
74
+ return Panel(table, title="[bold]Real-Time Stream[/bold]", border_style="magenta")
75
+
76
+
77
+ def main() -> None:
78
+ parser = argparse.ArgumentParser(description="Demonstrate Rich Layout TUI.")
79
+ parser.add_argument("--seconds", type=float, default=2.0, help="Duration to run demonstration")
80
+ args = parser.parse_args()
81
+
82
+ layout = make_layout()
83
+ layout["header"].update(
84
+ Align.center("[accent]─── CLUSTER SURVEILLANCE & TELEMETRY TUI ───[/accent]")
85
+ )
86
+ layout["footer"].update(
87
+ Align.center("[muted]Press Ctrl+C to disconnect • Live stream frequency: 2 Hz[/muted]")
88
+ )
89
+
90
+ if not console.is_terminal:
91
+ layout["sidebar"].update(render_sidebar(1))
92
+ layout["content"].update(render_content(1))
93
+ console.print(layout)
94
+ return
95
+
96
+ with Live(layout, console=console, screen=False, refresh_per_second=4) as live:
97
+ start = time.time()
98
+ iteration = 0
99
+ try:
100
+ while time.time() - start < args.seconds:
101
+ iteration += 1
102
+ layout["sidebar"].update(render_sidebar(iteration))
103
+ layout["content"].update(render_content(iteration))
104
+ time.sleep(0.5)
105
+ except KeyboardInterrupt:
106
+ pass
107
+
108
+ console.print("[success]TUI session terminated cleanly.[/success]")
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
package/install.sh ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SKILL_NAME="beautiful-python-cli"
5
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+
7
+ INSTALL_OMP=0
8
+ INSTALL_PI=0
9
+ INSTALL_CODEX=0
10
+ INSTALL_CLAUDE=0
11
+ INSTALL_CURSOR=0
12
+ INSTALL_ANTIGRAVITY=0
13
+ INSTALL_WINDSURF=0
14
+ EXPLICIT_SELECTION=0
15
+
16
+ usage() {
17
+ cat <<'EOF'
18
+ Install the Beautiful Python CLI skill for AI coding agents.
19
+
20
+ Usage:
21
+ ./install.sh --all
22
+ ./install.sh --claude
23
+ ./install.sh --cursor
24
+ ./install.sh --antigravity
25
+ ./install.sh --codex
26
+ ./install.sh --windsurf
27
+ ./install.sh --pi
28
+ ./install.sh --omp
29
+
30
+ Targets:
31
+ Claude Code ~/.claude/skills/
32
+ Cursor ~/.cursor/skills/
33
+ Antigravity ~/.gemini/antigravity-cli/skills/
34
+ Codex ~/.agents/skills/
35
+ Windsurf ~/.codeium/windsurf/skills/
36
+ Pi ~/.pi/agent/skills/
37
+ OMP ~/.omp/agent/skills/
38
+
39
+ With no target flags, --all is used.
40
+ Existing installations are backed up before replacement.
41
+ EOF
42
+ }
43
+
44
+ for arg in "$@"; do
45
+ case "$arg" in
46
+ --all)
47
+ EXPLICIT_SELECTION=1
48
+ INSTALL_OMP=1
49
+ INSTALL_PI=1
50
+ INSTALL_CODEX=1
51
+ INSTALL_CLAUDE=1
52
+ INSTALL_CURSOR=1
53
+ INSTALL_ANTIGRAVITY=1
54
+ INSTALL_WINDSURF=1
55
+ ;;
56
+ --omp)
57
+ EXPLICIT_SELECTION=1
58
+ INSTALL_OMP=1
59
+ ;;
60
+ --pi)
61
+ EXPLICIT_SELECTION=1
62
+ INSTALL_PI=1
63
+ ;;
64
+ --codex)
65
+ EXPLICIT_SELECTION=1
66
+ INSTALL_CODEX=1
67
+ ;;
68
+ --claude)
69
+ EXPLICIT_SELECTION=1
70
+ INSTALL_CLAUDE=1
71
+ ;;
72
+ --cursor)
73
+ EXPLICIT_SELECTION=1
74
+ INSTALL_CURSOR=1
75
+ ;;
76
+ --antigravity|--gemini)
77
+ EXPLICIT_SELECTION=1
78
+ INSTALL_ANTIGRAVITY=1
79
+ ;;
80
+ --windsurf)
81
+ EXPLICIT_SELECTION=1
82
+ INSTALL_WINDSURF=1
83
+ ;;
84
+ -h|--help)
85
+ usage
86
+ exit 0
87
+ ;;
88
+ *)
89
+ printf 'Unknown option: %s\n\n' "$arg" >&2
90
+ usage >&2
91
+ exit 2
92
+ ;;
93
+ esac
94
+ done
95
+
96
+ if [[ "$EXPLICIT_SELECTION" -eq 0 ]]; then
97
+ INSTALL_OMP=1
98
+ INSTALL_PI=1
99
+ INSTALL_CODEX=1
100
+ INSTALL_CLAUDE=1
101
+ INSTALL_CURSOR=1
102
+ INSTALL_ANTIGRAVITY=1
103
+ INSTALL_WINDSURF=1
104
+ fi
105
+
106
+ install_to() {
107
+ local parent="$1"
108
+ local destination="${parent}/${SKILL_NAME}"
109
+ local temporary="${parent}/.${SKILL_NAME}.tmp.$$"
110
+ local backup=""
111
+
112
+ mkdir -p "$parent"
113
+ rm -rf "$temporary"
114
+ mkdir -p "$temporary"
115
+ cp -R "${SOURCE_DIR}/." "$temporary/"
116
+
117
+ if [[ -e "$destination" ]]; then
118
+ backup="${destination}.backup.$(date +%Y%m%d-%H%M%S)-$$"
119
+ mv "$destination" "$backup"
120
+ fi
121
+
122
+ if ! mv "$temporary" "$destination"; then
123
+ if [[ -n "$backup" && -e "$backup" ]]; then
124
+ mv "$backup" "$destination"
125
+ fi
126
+ printf 'Failed to install into %s\n' "$destination" >&2
127
+ return 1
128
+ fi
129
+
130
+ printf 'Installed: %s\n' "$destination"
131
+ if [[ -n "$backup" ]]; then
132
+ printf 'Previous copy backed up: %s\n' "$backup"
133
+ fi
134
+ }
135
+
136
+ if [[ "$INSTALL_CLAUDE" -eq 1 ]]; then
137
+ install_to "${HOME}/.claude/skills"
138
+ fi
139
+ if [[ "$INSTALL_CURSOR" -eq 1 ]]; then
140
+ install_to "${HOME}/.cursor/skills"
141
+ fi
142
+ if [[ "$INSTALL_ANTIGRAVITY" -eq 1 ]]; then
143
+ install_to "${HOME}/.gemini/antigravity-cli/skills"
144
+ fi
145
+ if [[ "$INSTALL_CODEX" -eq 1 ]]; then
146
+ install_to "${HOME}/.agents/skills"
147
+ fi
148
+ if [[ "$INSTALL_WINDSURF" -eq 1 ]]; then
149
+ install_to "${HOME}/.codeium/windsurf/skills"
150
+ fi
151
+ if [[ "$INSTALL_PI" -eq 1 ]]; then
152
+ install_to "${HOME}/.pi/agent/skills"
153
+ fi
154
+ if [[ "$INSTALL_OMP" -eq 1 ]]; then
155
+ install_to "${HOME}/.omp/agent/skills"
156
+ fi
157
+
158
+ printf '\nDone. Start a new agent session if the skill is not discovered automatically.\n'
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "beautiful-python-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Agent Skill for designing, implementing, and refactoring Python terminal UIs using Rich",
5
+ "keywords": [
6
+ "agent-skills",
7
+ "skills",
8
+ "skills-sh",
9
+ "rich",
10
+ "python",
11
+ "cli",
12
+ "terminal-ui",
13
+ "tui",
14
+ "claude-code",
15
+ "cursor",
16
+ "codex",
17
+ "windsurf",
18
+ "antigravity"
19
+ ],
20
+ "homepage": "https://github.com/syrex1013/beautiful-python-cli#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/syrex1013/beautiful-python-cli/issues"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/syrex1013/beautiful-python-cli.git"
27
+ },
28
+ "license": "MIT",
29
+ "author": "syrex1013",
30
+ "main": "SKILL.md",
31
+ "files": [
32
+ "SKILL.md",
33
+ "README.md",
34
+ "LICENSE",
35
+ "references",
36
+ "examples",
37
+ "install.sh",
38
+ "requirements.txt"
39
+ ],
40
+ "scripts": {
41
+ "test": "python3 -m unittest discover -s tests -p 'test_*.py'"
42
+ }
43
+ }
@@ -0,0 +1,34 @@
1
+ # Skill routing and quality checks
2
+
3
+ Use these prompts to evaluate whether the skill is discoverable and stays within its intended scope.
4
+
5
+ ## Prompts that should activate the skill
6
+
7
+ 1. "Make this Python CLI output look professional with Rich panels and tables."
8
+ 2. "Add a spinner while the API connection is being established."
9
+ 3. "Show real progress for this file-processing loop, including elapsed time."
10
+ 4. "Build a live terminal dashboard for my concurrent workers."
11
+ 5. "Improve the CLI's colors, status indicators, logging, and error messages."
12
+ 6. "Make this scanner's terminal output readable over SSH and in CI."
13
+ 7. "Add a clean JSON mode without progress bars contaminating stdout."
14
+ 8. "Refactor the CLI output into reusable Rich components."
15
+
16
+ ## Prompts that should not activate the skill by default
17
+
18
+ 1. "Build a React dashboard for this Python service." — this is a web UI.
19
+ 2. "Create a Tkinter desktop application." — this is a GUI.
20
+ 3. "Explain what a Python generator is." — no CLI presentation work.
21
+ 4. "Write a one-line Python script that prints hello." — plain output is sufficient.
22
+ 5. "Optimize this SQL query." — unrelated to terminal UX.
23
+
24
+ ## Review criteria
25
+
26
+ For an activation test, verify both routing and execution:
27
+
28
+ - Did the agent inspect the existing CLI and dependency setup first?
29
+ - Did it preserve the current CLI framework and output contract?
30
+ - Did it select a spinner for unknown progress and a bar only for measurable progress?
31
+ - Did it avoid fabricated progress, ETA, or throughput?
32
+ - Did it account for TTY, redirected output, JSON, and `NO_COLOR`?
33
+ - Did it add only necessary dependencies?
34
+ - Did it test errors, empty results, narrow widths, and cleanup?
@@ -0,0 +1,69 @@
1
+ # Concurrent and Multi-Threaded Progress
2
+
3
+ When tasks run across multiple threads, processes, or asynchronous coroutines, coordinate progress bars safely through a single `rich.progress.Progress` instance.
4
+
5
+ ## 1. Multi-Threaded Workers (`ThreadPoolExecutor`)
6
+
7
+ ```python
8
+ import concurrent.futures
9
+ import time
10
+ from rich.progress import (
11
+ BarColumn,
12
+ Progress,
13
+ SpinnerColumn,
14
+ TaskProgressColumn,
15
+ TextColumn,
16
+ TimeRemainingColumn,
17
+ )
18
+
19
+ def worker_task(progress: Progress, task_id: int, total_steps: int) -> str:
20
+ for _ in range(total_steps):
21
+ time.sleep(0.05) # Simulate real work
22
+ progress.advance(task_id, advance=1)
23
+ return "done"
24
+
25
+ with Progress(
26
+ SpinnerColumn(),
27
+ TextColumn("[progress.description]{task.description}"),
28
+ BarColumn(),
29
+ TaskProgressColumn(),
30
+ TimeRemainingColumn(),
31
+ console=console,
32
+ ) as progress:
33
+ with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
34
+ futures = []
35
+ for i in range(4):
36
+ task_id = progress.add_task(f"Worker {i+1}", total=20)
37
+ futures.append(executor.submit(worker_task, progress, task_id, 20))
38
+
39
+ # Wait for all workers to complete
40
+ for future in concurrent.futures.as_completed(futures):
41
+ _ = future.result()
42
+ ```
43
+
44
+ ## 2. Asyncio Workers
45
+
46
+ ```python
47
+ import asyncio
48
+ from rich.progress import Progress
49
+
50
+ async def async_worker(progress: Progress, task_id: int, count: int):
51
+ for _ in range(count):
52
+ await asyncio.sleep(0.1)
53
+ progress.advance(task_id)
54
+
55
+ async def main():
56
+ with Progress(console=console) as progress:
57
+ tasks = [
58
+ async_worker(progress, progress.add_task(f"Stream {i}", total=15), 15)
59
+ for i in range(3)
60
+ ]
61
+ await asyncio.gather(*tasks)
62
+
63
+ asyncio.run(main())
64
+ ```
65
+
66
+ ## 3. Important Rules
67
+ - **Never call `print()` inside workers**: Interleaved output will break the progress bar rendering lines. Use `progress.console.log()` or `progress.console.print()` if log output is required.
68
+ - **One `Progress` per console**: Do not create multiple active `Progress` context managers at the same time.
69
+ - **Update descriptions dynamically**: Use `progress.update(task_id, description="...")` to communicate changing sub-phases.
@@ -0,0 +1,109 @@
1
+ # Interaction patterns: loading, progress, and live output
2
+
3
+ ## 1. Indeterminate operation: spinner
4
+
5
+ Use when duration is unknown and there is no honest measurable denominator.
6
+
7
+ ```python
8
+ with console.status("[info]Connecting to service…[/info]", spinner="dots"):
9
+ client.connect()
10
+ ```
11
+
12
+ The context manager stops the spinner even if the operation raises. Keep the message action-oriented and update it when the phase changes.
13
+
14
+ ## 2. Measurable operation: progress bar
15
+
16
+ ```python
17
+ from rich.progress import (
18
+ BarColumn,
19
+ Progress,
20
+ SpinnerColumn,
21
+ TaskProgressColumn,
22
+ TextColumn,
23
+ TimeElapsedColumn,
24
+ TimeRemainingColumn,
25
+ )
26
+
27
+ with Progress(
28
+ SpinnerColumn(),
29
+ TextColumn("[progress.description]{task.description}"),
30
+ BarColumn(),
31
+ TaskProgressColumn(),
32
+ TimeElapsedColumn(),
33
+ TimeRemainingColumn(),
34
+ console=console,
35
+ transient=False,
36
+ disable=not console.is_terminal,
37
+ ) as progress:
38
+ task = progress.add_task("Processing files", total=len(files))
39
+ for path in files:
40
+ process(path)
41
+ progress.advance(task)
42
+ ```
43
+
44
+ Use `total=None` only when the total is genuinely unknown. Do not estimate progress by elapsed time unless the task is explicitly time-based and the estimate is clearly labeled.
45
+
46
+ ## 3. Multiple tasks
47
+
48
+ Use one `Progress` object with a task per independently progressing operation. Give each task a stable, concise description. Keep a separate final summary after the context manager exits.
49
+
50
+ For worker threads or async tasks, route progress events through a shared coordinator or lock if updates can race. Do not let workers write arbitrary text directly into the terminal.
51
+
52
+ ## 4. Download/transfer progress
53
+
54
+ For transfers, prefer `DownloadColumn`, `TransferSpeedColumn`, and `FileSizeColumn`. Update completed bytes from actual transfer callbacks. Do not display an ETA until there is enough observed throughput to make it meaningful.
55
+
56
+ ## 5. Live dashboard
57
+
58
+ ```python
59
+ from rich.live import Live
60
+ from rich.table import Table
61
+
62
+ def build_dashboard(snapshot):
63
+ table = Table(title="Worker status", expand=True)
64
+ table.add_column("Worker")
65
+ table.add_column("State")
66
+ table.add_column("Completed", justify="right")
67
+ for worker in snapshot:
68
+ table.add_row(worker.name, worker.state, str(worker.completed))
69
+ return table
70
+
71
+ with Live(
72
+ build_dashboard(initial_snapshot),
73
+ console=console,
74
+ refresh_per_second=4,
75
+ screen=False,
76
+ ) as live:
77
+ while running:
78
+ snapshot = read_current_state()
79
+ live.update(build_dashboard(snapshot), refresh=True)
80
+ ```
81
+
82
+ Use a dashboard only when live state helps the operator make decisions. Keep the refresh rate modest. Use `screen=True` only for a deliberately full-screen TUI, and ensure keyboard/cancellation behavior is designed accordingly.
83
+
84
+ While a `Live` display is active, use `console.log()` for log messages so Rich can preserve the live region. Do not mix in built-in `print()` calls.
85
+
86
+ ## 6. Async tasks
87
+
88
+ Use the project's existing async architecture. Update progress after awaited work completes, not when it is merely scheduled. Cancel child tasks cleanly and close the progress/live context in `finally` or with context managers.
89
+
90
+ ## 7. Stage-based work
91
+
92
+ For operations with named phases but no measurable subtask counts, show a concise stage indicator or spinner message:
93
+ - `1/4 Validate configuration`
94
+ - `2/4 Connect`
95
+ - `3/4 Execute`
96
+ - `4/4 Summarize`
97
+
98
+ Only use `N/M` when the number and order of stages are fixed and real.
99
+
100
+ ## 8. Final state
101
+
102
+ After loading/progress ends, show a durable summary:
103
+ - outcome (success, partial, failed, cancelled)
104
+ - actual completed/failed counts
105
+ - elapsed time if measured
106
+ - output paths or next action
107
+ - concise warnings
108
+
109
+ Do not leave the user with only a completed progress bar and no result.