pi-crew 0.11.0 → 0.11.2

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.
Files changed (174) hide show
  1. package/CHANGELOG.md +155 -9
  2. package/README.md +161 -1037
  3. package/agents/verifier.md +18 -7
  4. package/dist/index.mjs +744 -90644
  5. package/docs/README.md +57 -46
  6. package/docs/architecture.md +87 -33
  7. package/docs/commands-reference.md +9 -5
  8. package/docs/troubleshooting.md +3 -2
  9. package/package.json +1 -3
  10. package/schema.json +39 -0
  11. package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
  12. package/skills/real-test-pi-crew/SKILL.md +371 -34
  13. package/src/agents/agent-config.ts +1 -1
  14. package/src/agents/discover-agents.ts +1 -1
  15. package/src/config/config-validation.ts +15 -2
  16. package/src/config/config.ts +47 -13
  17. package/src/config/defaults.ts +0 -1
  18. package/src/config/env-vars.ts +35 -0
  19. package/src/config/types.ts +19 -5
  20. package/src/errors.ts +2 -2
  21. package/src/extension/async-notifier.ts +23 -0
  22. package/src/extension/crew-vibes/config.ts +0 -21
  23. package/src/extension/crew-vibes/index.ts +0 -2
  24. package/src/extension/crew-vibes/render.ts +1 -50
  25. package/src/extension/help.ts +21 -12
  26. package/src/extension/knowledge-injection.ts +2 -1
  27. package/src/extension/management.ts +8 -3
  28. package/src/extension/notification-sink.ts +17 -0
  29. package/src/extension/register.ts +7 -2
  30. package/src/extension/registration/command-utils.ts +28 -2
  31. package/src/extension/registration/commands/dashboard.ts +11 -1
  32. package/src/extension/registration/commands/manage.ts +36 -19
  33. package/src/extension/registration/commands/run.ts +24 -2
  34. package/src/extension/registration/commands/shared.ts +23 -1
  35. package/src/extension/registration/commands/status.ts +25 -2
  36. package/src/extension/registration/context-builder.ts +8 -2
  37. package/src/extension/registration/health-notify-policy.ts +100 -0
  38. package/src/extension/registration/lazy-configurers.ts +35 -0
  39. package/src/extension/registration/lifecycle-handlers.ts +91 -30
  40. package/src/extension/registration/lifecycle.ts +75 -10
  41. package/src/extension/registration/observability.ts +98 -35
  42. package/src/extension/registration/registration-types.ts +7 -5
  43. package/src/extension/registration/runtime-cleanup.ts +9 -3
  44. package/src/extension/registration/subagent-helpers.ts +38 -0
  45. package/src/extension/registration/subagent-tools.ts +16 -6
  46. package/src/extension/registration/team-tool.ts +10 -3
  47. package/src/extension/registration/terminal-status-wiring.ts +172 -0
  48. package/src/extension/registration/viewers.ts +6 -0
  49. package/src/extension/registration/wire-cross-extension.ts +28 -0
  50. package/src/extension/run-compare.ts +220 -0
  51. package/src/extension/run-export.ts +37 -5
  52. package/src/extension/run-maintenance.ts +155 -5
  53. package/src/extension/team-tool/dispatch/index.ts +3 -2
  54. package/src/extension/team-tool/dispatch/manage.ts +5 -2
  55. package/src/extension/team-tool/goal.ts +4 -1
  56. package/src/extension/team-tool/handle-settings.ts +33 -4
  57. package/src/extension/team-tool/health-monitor.ts +21 -7
  58. package/src/extension/team-tool/lifecycle-actions.ts +49 -1
  59. package/src/extension/team-tool/plan.ts +10 -0
  60. package/src/extension/team-tool/routing-hint.ts +63 -0
  61. package/src/extension/team-tool/status.ts +4 -0
  62. package/src/extension/team-tool.ts +52 -6
  63. package/src/extension/webhook-notify.ts +382 -0
  64. package/src/observability/metric-sink.ts +12 -2
  65. package/src/prompt/prompt-runtime.ts +82 -31
  66. package/src/prompt/worker-events-channel.ts +12 -0
  67. package/src/runtime/README.md +1 -1
  68. package/src/runtime/async-runner.ts +87 -1
  69. package/src/runtime/background-runner.ts +313 -234
  70. package/src/runtime/broker/crew-broker.ts +17 -11
  71. package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
  72. package/src/runtime/broker/wait-status-cache.ts +1 -1
  73. package/src/runtime/child-pi/child-pi-timers.ts +1 -1
  74. package/src/runtime/child-pi/mock-fixtures.ts +48 -0
  75. package/src/runtime/crew-agent-records.ts +337 -45
  76. package/src/runtime/deadletter.ts +43 -1
  77. package/src/runtime/delegate-spawn.ts +5 -1
  78. package/src/runtime/dispatch-batch.ts +72 -5
  79. package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
  80. package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
  81. package/src/runtime/model/model-fallback.ts +21 -1
  82. package/src/runtime/model/pi-args.ts +8 -10
  83. package/src/runtime/recovery/crash-recovery.ts +25 -1
  84. package/src/runtime/run-worker.ts +12 -1
  85. package/src/runtime/scheduling/global-worker-cap.ts +13 -6
  86. package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
  87. package/src/runtime/scheduling/scheduler.ts +49 -13
  88. package/src/runtime/scheduling/semaphore.ts +148 -20
  89. package/src/runtime/scratchpad/README.md +1 -1
  90. package/src/runtime/scratchpad/protocol.ts +1 -1
  91. package/src/runtime/settings-store.ts +1 -1
  92. package/src/runtime/skill-instructions.ts +22 -0
  93. package/src/runtime/stale-reconciler.ts +85 -13
  94. package/src/runtime/task-display.ts +1 -1
  95. package/src/runtime/task-runner/pre-execution.ts +26 -2
  96. package/src/runtime/task-runner/prompt-builder.ts +142 -45
  97. package/src/runtime/task-runner.ts +21 -1
  98. package/src/runtime/team-runner.ts +38 -1
  99. package/src/runtime/workspace-lock.ts +4 -1
  100. package/src/schema/config-schema.ts +18 -0
  101. package/src/schema/team-tool-schema.ts +17 -0
  102. package/src/state/atomic-write.ts +53 -0
  103. package/src/state/contracts.ts +109 -0
  104. package/src/state/coordination/locks.ts +191 -33
  105. package/src/state/coordination/mailbox.ts +140 -15
  106. package/src/state/crew-init.ts +87 -12
  107. package/src/state/event-log/cursor.ts +37 -1
  108. package/src/state/event-log/event-log-rotation.ts +72 -7
  109. package/src/state/stores/active-run-registry.ts +13 -1
  110. package/src/state/stores/state-store.ts +112 -22
  111. package/src/state/types.ts +4 -0
  112. package/src/ui/adaptive-card.ts +65 -0
  113. package/src/ui/agents-jobs-browser.ts +70 -64
  114. package/src/ui/card-colors.ts +36 -7
  115. package/src/ui/dashboard-panes/agents-pane.ts +55 -14
  116. package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
  117. package/src/ui/dashboard-panes/health-pane.ts +7 -5
  118. package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
  119. package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
  120. package/src/ui/dashboard-panes/pane-theme.ts +21 -0
  121. package/src/ui/dashboard-panes/plan-pane.ts +63 -30
  122. package/src/ui/dashboard-panes/progress-pane.ts +3 -2
  123. package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
  124. package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
  125. package/src/ui/dwf-phase-display.ts +3 -20
  126. package/src/ui/format-helpers.ts +22 -0
  127. package/src/ui/heartbeat-aggregator.ts +34 -0
  128. package/src/ui/inline-panel/crew-editor.ts +13 -3
  129. package/src/ui/inline-panel/index.ts +60 -4
  130. package/src/ui/keybinding-map.ts +251 -35
  131. package/src/ui/live-conversation-overlay.ts +180 -47
  132. package/src/ui/live-run-sidebar.ts +134 -55
  133. package/src/ui/mascot.ts +32 -16
  134. package/src/ui/overlays/agent-picker-overlay.ts +81 -26
  135. package/src/ui/overlays/confirm-overlay.ts +55 -29
  136. package/src/ui/overlays/help-overlay.ts +108 -53
  137. package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
  138. package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
  139. package/src/ui/powerbar-publisher.ts +0 -1
  140. package/src/ui/rail.ts +333 -0
  141. package/src/ui/run-dashboard.ts +193 -79
  142. package/src/ui/run-snapshot-cache.ts +18 -1
  143. package/src/ui/settings-overlay.ts +81 -39
  144. package/src/ui/spinner.ts +26 -2
  145. package/src/ui/terminal-status.ts +7 -1
  146. package/src/ui/theme-adapter.ts +0 -45
  147. package/src/ui/theme-discovery.ts +12 -6
  148. package/src/ui/tool-progress-formatter.ts +128 -9
  149. package/src/ui/tool-renderers/brief-mode.ts +10 -67
  150. package/src/ui/tool-renderers/index.ts +374 -523
  151. package/src/ui/transcript-viewer.ts +30 -12
  152. package/src/ui/widget/index.ts +32 -52
  153. package/src/ui/widget/task-list.ts +64 -32
  154. package/src/ui/widget/widget-formatters.ts +3 -402
  155. package/src/ui/widget/widget-model.ts +28 -7
  156. package/src/ui/widget/widget-renderer.ts +201 -128
  157. package/src/ui/widget/widget-types.ts +0 -2
  158. package/src/utils/incremental-reader.ts +11 -3
  159. package/src/utils/paths.ts +94 -12
  160. package/src/utils/project-markers.ts +40 -0
  161. package/src/utils/visual.ts +0 -4
  162. package/src/worktree/worktree-manager.ts +206 -26
  163. package/workflows/distill.workflow.md +3 -3
  164. package/workflows/fast-fix.workflow.md +1 -1
  165. package/workflows/plan-execute.workflow.md +1 -1
  166. package/workflows/review.workflow.md +1 -1
  167. package/workflows/strict-fast-fix.workflow.md +1 -1
  168. package/docs/migration-v0.4-v0.5.md +0 -208
  169. package/docs/runtime-flow.md +0 -148
  170. package/src/extension/crew-vibes/figures.ts +0 -22
  171. package/src/extension/crew-vibes/font-detect.ts +0 -71
  172. package/src/ui/dynamic-border.ts +0 -35
  173. package/src/ui/loaders.ts +0 -6
  174. package/src/ui/overlay-stack.ts +0 -148
@@ -1,148 +0,0 @@
1
- # pi-crew Runtime Flow
2
-
3
- This document is a compact map of the runtime paths used by `pi-crew`.
4
-
5
- ## Main sequence
6
-
7
- ```text
8
- User / model
9
- │ calls team({ action: "run", ... }) or /team-run
10
- ▼
11
- handleTeamTool()
12
- │ validates schema and routes action
13
- ▼
14
- handleRun()
15
- ├─ discoverTeams/discoverWorkflows/discoverAgents
16
- ├─ validateWorkflowForTeam
17
- ├─ expandParallelResearchWorkflow when applicable
18
- ├─ createRunManifest + tasks.json + goal artifact
19
- ├─ if async=true ─────────────────────────────────────────────┐
20
- │ spawnBackgroundTeamRun() │
21
- │ ├─ resolve jiti-register.mjs │
22
- │ ├─ fail-fast if jiti missing │
23
- │ ├─ node --import jiti-register.mjs background-runner.ts │
24
- │ └─ parent schedules early-exit guard │
25
- │ ▼
26
- │ background-runner.ts
27
- │ ├─ append async.started
28
- │ ├─ write async.pid startup marker
29
- │ ├─ rediscover team/workflow/agents
30
- │ └─ executeTeamRun()
31
- │
32
- └─ if foreground/default
33
- ├─ startForegroundRun schedules session-bound run, or
34
- └─ executeTeamRun inline for scaffold/non-scheduled paths
35
-
36
- executeTeamRun()
37
- ├─ write run.running
38
- ├─ materialize queued/running agent records lazily
39
- ├─ build task graph index
40
- ├─ while queued tasks exist
41
- │ ├─ taskGraphSnapshot
42
- │ ├─ resolveBatchConcurrency
43
- │ ├─ getReadyTasks
44
- │ ├─ append task.progress batch event
45
- │ ├─ mapConcurrent ready batch
46
- │ │ └─ runTeamTask()
47
- │ │ ├─ prepare workspace/worktree
48
- │ │ ├─ build task packet
49
- │ │ ├─ render prompt + dependency context
50
- │ │ ├─ choose model candidates from Pi config
51
- │ │ ├─ spawn child Pi process
52
- │ │ ├─ ChildPiLineObserver parses stdout/stderr
53
- │ │ ├─ append per-agent events/output
54
- │ │ ├─ update agent progress/task state
55
- │ │ ├─ parse final JSONL/session usage
56
- │ │ └─ write result/log/transcript/metadata artifacts
57
- │ ├─ merge task updates monotonically
58
- │ ├─ optional adaptive plan injection
59
- │ ├─ save tasks/agents/progress
60
- │ └─ write batch artifact
61
- ├─ policy closeout
62
- └─ run.completed / run.failed / run.blocked / run.cancelled
63
- ```
64
-
65
- ## Action router
66
-
67
- | Action | Handler | Purpose |
68
- |---|---|---|
69
- | `run` | `team-tool/run.ts` | Create and execute a run, foreground or async. |
70
- | `status` | `team-tool.ts` | Show manifest/tasks/agents/events and mark stale async runs failed. |
71
- | `summary` | `session-summary.ts`/summary handler | Write/read run summary artifact. |
72
- | `events` | `team-tool.ts` | Tail durable run events. |
73
- | `artifacts` | `team-tool.ts` | List run artifacts. |
74
- | `resume` | `team-tool.ts` | Requeue failed/cancelled/skipped/running tasks. |
75
- | `cancel` | `team-tool.ts` | Mark queued/running tasks cancelled and request foreground interrupt. |
76
- | `forget` | `run-maintenance.ts` | Delete run state/artifacts with confirmation. |
77
- | `prune` | `run-maintenance.ts` | Remove old finished runs with confirmation. |
78
- | `export` | `run-export.ts` | Create portable run bundle. |
79
- | `import` / `imports` | `run-import.ts` / `import-index.ts` | Store/list imported bundles. |
80
- | `config` | `config.ts` + config action | Show/update user/project config. |
81
- | `doctor` | `team-tool/doctor.ts` | Platform/resource/runtime diagnostics. |
82
- | `validate` | `validate-resources.ts` | Validate agents/teams/workflows. |
83
- | `recommend` | `team-recommendation.ts` | Suggest team/workflow/action for a goal. |
84
- | management | `management.ts` | Create/update/delete/rename teams, agents, workflows. |
85
- | API | `team-tool/api.ts` | File-backed observability/control/mailbox API. |
86
-
87
- ## Worker modes
88
-
89
- | Mode | Behavior |
90
- |---|---|
91
- | `child-process` | Default. Launches real child `pi` processes per task. |
92
- | `scaffold` | Explicit dry-run. No child Pi worker execution. |
93
- | `live-session` | Experimental/gated in-process/live agent path. |
94
- | `auto` | Resolves to child-process unless config/env requests otherwise. |
95
-
96
- ## Important files
97
-
98
- ```text
99
- src/extension/register.ts Pi extension entry/wiring
100
- src/extension/team-tool/run.ts run creation and foreground/async split
101
- src/runtime/background-runner.ts detached async entrypoint
102
- src/runtime/async-runner.ts background spawn command/options
103
- src/runtime/team-runner.ts workflow/task graph scheduler
104
- src/runtime/task-runner.ts single task execution
105
- src/runtime/child-pi.ts child Pi process and output observer
106
- src/runtime/model-fallback.ts configured model candidates/routing
107
- src/runtime/concurrency.ts batch concurrency decisions
108
- src/runtime/process-status.ts pid/liveness/stale detection
109
- src/state/state-store.ts manifest/tasks persistence
110
- src/state/event-log.ts JSONL run events
111
- src/runtime/crew-agent-records.ts aggregate + per-agent status files
112
- ```
113
-
114
- ## Environment variables
115
-
116
- | Env | Effect |
117
- |---|---|
118
- | `PI_CREW_EXECUTE_WORKERS=0` | Disable real workers, use scaffold behavior. |
119
- | `PI_TEAMS_EXECUTE_WORKERS=0` | Legacy alias for worker disable. |
120
- | `PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION=1` | Allow experimental live-session runtime. |
121
- | `PI_CREW_MOCK_LIVE_SESSION=success` | Test hook for live-session mock. |
122
- | `PI_TEAMS_MOCK_CHILD_PI` | Test hook for mocked child Pi execution. |
123
- | `PI_CREW_DEPTH`, `PI_CREW_MAX_DEPTH` | Canonical subagent recursion guard. |
124
- | `PI_TEAMS_DEPTH`, `PI_TEAMS_MAX_DEPTH` | Legacy recursion guard aliases. |
125
- | `PI_TEAMS_HOME` | Override user config/state home in tests. |
126
- | `PI_TEAMS_PI_BIN` | Override child `pi` executable. |
127
- | `PI_CODING_AGENT_DIR` | Override Pi settings/models directory for model discovery. |
128
- | `PI_CREW_ASYNC_EARLY_EXIT_GUARD=0` | Disable 3s background early-exit guard. |
129
-
130
- ## State transition summary
131
-
132
- ```text
133
- queued/planning/running ── completed
134
- ├─ failed
135
- ├─ blocked
136
- └─ cancelled
137
- ```
138
-
139
- Task states follow the same durable contract plus `skipped`. Terminal states are monotonic during parallel merge.
140
-
141
- ## Observability tips
142
-
143
- - Use `/team-dashboard` for a UI overview.
144
- - Use `team status runId=...` for canonical state and stale async detection.
145
- - Read `background.log` for early import/spawn errors.
146
- - Read `events.jsonl` for event chronology.
147
- - Read `agents/{taskId}/status.json` for per-agent model/progress/tool status.
148
- - Read `artifacts/{runId}/transcripts/{taskId}.jsonl` for raw child Pi transcript.
@@ -1,22 +0,0 @@
1
- /**
2
- * Capacity-stage helpers (shared by renderCapacity, which remains exported
3
- * for its unit tests after the footer retirement).
4
- *
5
- * The speed-UI parts of this module (braille/PUA spinner frames,
6
- * intervalForSpeed) were REMOVED with the tok/s speed UI (maintainer
7
- * decision 2026-09-14) — pi's built-in working indicator is always used.
8
- */
9
-
10
- /**
11
- * Pick the capacity stage index (0..levels-1) for a context-fill percent.
12
- * Ported from pi-chonk's chonkIndex.
13
- */
14
- export function capacityIndex(percent: number | null | undefined, levels = 6): number {
15
- if (percent === null || percent === undefined || !Number.isFinite(percent)) return 0;
16
- return Math.max(0, Math.min(levels - 1, Math.floor((Math.max(0, Math.min(100, percent)) / 100) * levels)));
17
- }
18
-
19
- /** The last two stages are "danger" stages and get the error color. */
20
- export function isDangerStage(index: number, levels: number): boolean {
21
- return index >= Math.max(0, levels - 2);
22
- }
@@ -1,71 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { homedir, platform } from "node:os";
3
- import { join } from "node:path";
4
-
5
- /**
6
- * Detect whether the crew-vibes PUA font (U+E700..U+E70F) file exists
7
- * on disk. This is a *best-effort* heuristic — file existence does NOT
8
- * guarantee the terminal can render PUA glyphs (many terminals have their
9
- * own font stacks). For reliable animation, braille fallback frames are
10
- * used by default; PUA frames are only activated when the user explicitly
11
- * (used by the capacity icons).
12
- */
13
-
14
- function fontPath(): string {
15
- const os = platform();
16
- const home = homedir();
17
- if (os === "darwin") return join(home, "Library", "Fonts", "crew-vibes.ttf");
18
- if (os === "linux") return join(home, ".local", "share", "fonts", "crew-vibes.ttf");
19
- if (os === "win32") {
20
- const local = process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
21
- return join(local, "Microsoft", "Windows", "Fonts", "crew-vibes.ttf");
22
- }
23
- return "";
24
- }
25
-
26
- let _hasFontFile: boolean | null = null;
27
-
28
- /** Returns true when crew-vibes.ttf exists in the platform font directory. */
29
- export function hasCrewFontFile(): boolean {
30
- if (_hasFontFile !== null) return _hasFontFile;
31
- const p = fontPath();
32
- _hasFontFile = p !== "" && existsSync(p);
33
- return _hasFontFile;
34
- }
35
-
36
- let _isWebTerminal: boolean | null = null;
37
-
38
- /** Returns true when running in a web-based terminal (gotty, wetty, etc.)
39
- * where system fonts are not available for PUA rendering. */
40
- export function isWebTerminal(): boolean {
41
- if (_isWebTerminal !== null) return _isWebTerminal;
42
- if (process.env.GOTTY || process.env.WEBTERM) {
43
- _isWebTerminal = true;
44
- return true;
45
- }
46
- if (process.env.TERM === "dumb") {
47
- _isWebTerminal = true;
48
- return true;
49
- }
50
- try {
51
- // Check current process and ancestors — gotty is often the grandparent
52
- // (gotty → tmux → pi), so /proc/self/cgroup may not contain it.
53
- let pid = process.pid;
54
- for (let i = 0; i < 6 && pid > 1; i++) {
55
- const cgroup = readFileSync(`/proc/${pid}/cgroup`, "utf8");
56
- if (cgroup.includes("gotty") || cgroup.includes("wetty")) {
57
- _isWebTerminal = true;
58
- return true;
59
- }
60
- const match = cgroup.match(/\d+:.*:(.*)/);
61
- // Read PPid from status
62
- const status = readFileSync(`/proc/${pid}/status`, "utf8");
63
- const ppid = status.match(/^PPid:\s+(\d+)/m);
64
- pid = ppid ? Number.parseInt(ppid[1], 10) : 1;
65
- }
66
- } catch {
67
- // not Linux or /proc not available
68
- }
69
- _isWebTerminal = false;
70
- return false;
71
- }
@@ -1,35 +0,0 @@
1
- import type { CrewTheme } from "./theme-adapter.ts";
2
-
3
- export interface DynamicCrewBorderOptions {
4
- color?: (value: string) => string;
5
- char?: string;
6
- }
7
-
8
- export class DynamicCrewBorder {
9
- private readonly theme: CrewTheme;
10
- private readonly color?: (value: string) => string;
11
- private readonly char: string;
12
- private cachedWidth = -1;
13
- private cachedLine = "";
14
-
15
- constructor(theme: CrewTheme, options: DynamicCrewBorderOptions = {}) {
16
- this.theme = theme;
17
- this.color = options.color;
18
- this.char = options.char && options.char.length > 0 ? options.char : "─";
19
- }
20
-
21
- render(width: number): string[] {
22
- const w = Math.max(0, width);
23
- if (w !== this.cachedWidth) {
24
- const line = this.char.repeat(w);
25
- this.cachedLine = this.color ? this.color(line) : this.theme.fg("border", line);
26
- this.cachedWidth = w;
27
- }
28
- return [this.cachedLine];
29
- }
30
-
31
- invalidate(): void {
32
- this.cachedWidth = -1;
33
- this.cachedLine = "";
34
- }
35
- }
package/src/ui/loaders.ts DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * CrewBorderedLoader and CountdownTimer were removed (UI-animation-audit C9).
3
- * They were dead production code — referenced only by test/unit/loaders.test.ts
4
- * (now deleted) and never instantiated in src/ or re-exported from the public
5
- * index. If a new loader component is needed, add it here.
6
- */
@@ -1,148 +0,0 @@
1
- /**
2
- * PR-G3 (UI-4) — minimal overlay stack for z-order, focus, and dismissal
3
- * chaining.
4
- *
5
- * Before this, src/ui/ had no central router for overlay z-order/focus/
6
- * dismissal. Overlays (ConfirmOverlay, MailboxDetailOverlay, AgentPickerOverlay,
7
- * HelpOverlay, …) each manage their own `handleInput`, and a caller that wants
8
- * two overlays up at once had no way to express "the top one gets input and
9
- * ESC dismisses it, restoring focus to the one below".
10
- *
11
- * The `OverlayStack` is a LIFO stack of overlays. The top of the stack owns
12
- * input focus and is rendered last (i.e. on top). Popping or dismissing the
13
- * top restores focus to the next overlay down. Every overlay already exposes
14
- * the `InteractiveComponent` contract (render + invalidate + handleInput), so
15
- * existing overlays opt in automatically with zero rewrite.
16
- *
17
- * This is intentionally additive: no existing overlay is required to use it.
18
- * Callers that want centralized z-order/focus management opt in by pushing
19
- * overlays here.
20
- */
21
-
22
- import type { InteractiveComponent } from "./component.ts";
23
-
24
- /**
25
- * An overlay is an interactive component occupying one z-layer. Anything with
26
- * `render` + `invalidate` + `handleInput` qualifies (ConfirmOverlay,
27
- * MailboxDetailOverlay, AgentPickerOverlay, RunDashboard, …).
28
- */
29
- export type Overlay = InteractiveComponent;
30
-
31
- export interface OverlayStackOptions {
32
- /**
33
- * Invoked after the top-of-stack changes (push / pop / dismissTop / clear).
34
- * Use it to (re)wire host focus or request a repaint. Optional.
35
- */
36
- onTopChanged?: () => void;
37
- }
38
-
39
- /**
40
- * LIFO overlay stack providing z-order, focus, and dismissal chaining.
41
- *
42
- * - `push(o)` → `o` becomes the new focus/top layer.
43
- * - `top()` → the focused overlay (or undefined when empty).
44
- * - `handleInput(data)` → routed to the top overlay only; returns false when
45
- * the stack is empty so callers can fall back to base input handling.
46
- * - `dismissTop()` / `pop()` → removes the top overlay, restoring focus below.
47
- * - `render(width)` → renders the top overlay by default; `{ composite: true }`
48
- * returns every layer bottom-to-top for custom compositing.
49
- */
50
- export class OverlayStack {
51
- private readonly stack: Overlay[] = [];
52
- private readonly onTopChanged?: () => void;
53
-
54
- constructor(options: OverlayStackOptions = {}) {
55
- this.onTopChanged = options.onTopChanged;
56
- }
57
-
58
- /** Number of overlays currently on the stack. */
59
- get size(): number {
60
- return this.stack.length;
61
- }
62
-
63
- /** Whether the stack currently holds any overlays. */
64
- isEmpty(): boolean {
65
- return this.stack.length === 0;
66
- }
67
-
68
- /** Push a new overlay on top. It becomes the input/render focus. */
69
- push(overlay: Overlay): void {
70
- this.stack.push(overlay);
71
- this.notifyTopChanged();
72
- }
73
-
74
- /** Remove and return the top overlay, or undefined if the stack is empty. */
75
- pop(): Overlay | undefined {
76
- const overlay = this.stack.pop();
77
- this.notifyTopChanged();
78
- return overlay;
79
- }
80
-
81
- /** The top-of-stack overlay (the focus/render target), or undefined when empty. */
82
- top(): Overlay | undefined {
83
- return this.stack.at(-1);
84
- }
85
-
86
- /** Peek at the overlay at a given depth (0 = bottom). */
87
- at(index: number): Overlay | undefined {
88
- return this.stack[index];
89
- }
90
-
91
- /**
92
- * Dismiss the top overlay. Equivalent to `pop()` but returns a boolean so
93
- * callers can chain dismissal (e.g. ESC closes the topmost, then the next).
94
- * Returns true when an overlay was dismissed, false when the stack is empty.
95
- */
96
- dismissTop(): boolean {
97
- if (this.stack.length === 0) return false;
98
- this.stack.pop();
99
- this.notifyTopChanged();
100
- return true;
101
- }
102
-
103
- /**
104
- * Route a raw input chunk to the top-of-stack overlay.
105
- * @returns true if an overlay handled it (stack non-empty); false if the
106
- * stack is empty so the caller may fall back to base input handling.
107
- */
108
- handleInput(data: string): boolean {
109
- const overlay = this.top();
110
- if (!overlay) return false;
111
- overlay.handleInput(data);
112
- return true;
113
- }
114
-
115
- /**
116
- * Render the stack. By default returns the top overlay's lines (the common
117
- * modal case where the top covers what is below). Pass `{ composite: true }`
118
- * to receive every layer's lines in bottom-to-top order for custom
119
- * compositing. Returns `[]` when the stack is empty.
120
- */
121
- render(width: number, options: { composite?: boolean } = {}): string[] {
122
- if (this.stack.length === 0) return [];
123
- if (options.composite) {
124
- const lines: string[] = [];
125
- for (const overlay of this.stack) {
126
- lines.push(...overlay.render(width));
127
- }
128
- return lines;
129
- }
130
- return this.stack.at(-1)!.render(width);
131
- }
132
-
133
- /** Invalidate every overlay on the stack (e.g. on theme change). */
134
- invalidateAll(): void {
135
- for (const overlay of this.stack) overlay.invalidate();
136
- }
137
-
138
- /** Remove every overlay from the stack. */
139
- clear(): void {
140
- const hadOverlays = this.stack.length > 0;
141
- this.stack.length = 0;
142
- if (hadOverlays) this.notifyTopChanged();
143
- }
144
-
145
- private notifyTopChanged(): void {
146
- this.onTopChanged?.();
147
- }
148
- }