pi-orchestrator-config 4.1.5

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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,512 @@
1
+ /**
2
+ * Shared fullscreen overlay list → detail dashboard (pi-tui ui.custom + overlay).
3
+ * Used by /async-status and /cron list.
4
+ */
5
+
6
+ import type {
7
+ ExtensionCommandContext,
8
+ KeybindingsManager,
9
+ Theme,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import type { Component, TUI } from "@earendil-works/pi-tui";
12
+ import {
13
+ Key,
14
+ matchesKey,
15
+ truncateToWidth,
16
+ visibleWidth,
17
+ wrapTextWithAnsi,
18
+ } from "@earendil-works/pi-tui";
19
+ import {
20
+ reconcileSelection,
21
+ type OverlayId,
22
+ type OverlaySelection,
23
+ } from "./overlay-dashboard-utils.js";
24
+
25
+ export {
26
+ reconcileSelection,
27
+ type OverlayId,
28
+ type OverlaySelection,
29
+ } from "./overlay-dashboard-utils.js";
30
+
31
+ export const OVERLAY_OPTS = {
32
+ overlay: true as const,
33
+ overlayOptions: {
34
+ anchor: "center" as const,
35
+ width: "100%" as const,
36
+ maxHeight: "100%" as const,
37
+ },
38
+ };
39
+
40
+ export interface OverlayRowParts {
41
+ /** Already themed status glyph (e.g. ■). */
42
+ glyph: string;
43
+ /** Plain title; dashboard applies accent/text. */
44
+ title: string;
45
+ /** Dim id label (already themed or plain). */
46
+ idLabel: string;
47
+ /** Already themed right-side segments. */
48
+ rightParts: string[];
49
+ }
50
+
51
+ export interface OverlayListSpec<
52
+ TId extends OverlayId,
53
+ TItem extends { id: TId },
54
+ > {
55
+ title: string;
56
+ countLabel: (items: readonly TItem[]) => string;
57
+ borderTitle: (items: readonly TItem[]) => string;
58
+ footerHints: string;
59
+ listItems: () => TItem[];
60
+ rowParts: (item: TItem, theme: Theme) => OverlayRowParts;
61
+ /** Optional destructive/action key (x). */
62
+ onX?: (item: TItem) => void;
63
+ }
64
+
65
+ export interface OverlayDetailSpec {
66
+ footerHints: string;
67
+ /** Stick scroll to bottom as body grows (live logs). */
68
+ followTail?: boolean;
69
+ getHeader: (theme: Theme) => string;
70
+ getBodyLines: (theme: Theme) => string[];
71
+ emptyBody?: string;
72
+ /** Return true to close after action. */
73
+ onX?: () => boolean | void;
74
+ tickMs?: number;
75
+ pollMs?: number;
76
+ /** Return true when content changed (triggers re-render). */
77
+ onPoll?: () => boolean | void;
78
+ }
79
+
80
+ export function padAnsi(text: string, width: number): string {
81
+ const truncated = truncateToWidth(text, width);
82
+ return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
83
+ }
84
+
85
+ export function borderSegment(theme: Theme, width: number, title: string): string {
86
+ const label = title
87
+ ? ` ${truncateToWidth(title, Math.max(0, width - 3))} `
88
+ : "";
89
+ const labelWidth = visibleWidth(label);
90
+ return (
91
+ theme.fg("border", "─") +
92
+ (label ? theme.fg("text", label) : "") +
93
+ theme.fg("border", "─".repeat(Math.max(0, width - 1 - labelWidth)))
94
+ );
95
+ }
96
+
97
+ /** Join left + right columns with gap, truncated to width. */
98
+ export function splitRow(left: string, right: string, width: number): string {
99
+ const rightWidth = visibleWidth(right);
100
+ const leftMax = Math.max(0, width - rightWidth - 2);
101
+ const leftTruncated = truncateToWidth(left, leftMax);
102
+ const gap = Math.max(2, width - visibleWidth(leftTruncated) - rightWidth);
103
+ return truncateToWidth(leftTruncated + " ".repeat(gap) + right, width);
104
+ }
105
+
106
+ export class OverlayListDashboard<
107
+ TId extends OverlayId,
108
+ TItem extends { id: TId },
109
+ > implements Component {
110
+ private closed = false;
111
+ private ticker: ReturnType<typeof setInterval>;
112
+
113
+ constructor(
114
+ private tui: TUI,
115
+ private theme: Theme,
116
+ private keybindings: KeybindingsManager,
117
+ private spec: OverlayListSpec<TId, TItem>,
118
+ private selection: OverlaySelection<TId>,
119
+ private done: (value: TId | null) => void,
120
+ ) {
121
+ this.ticker = setInterval(() => this.tui.requestRender(), 1000);
122
+ }
123
+
124
+ private items(): TItem[] {
125
+ return this.spec.listItems();
126
+ }
127
+
128
+ private cleanup(): boolean {
129
+ if (this.closed) return false;
130
+ this.closed = true;
131
+ clearInterval(this.ticker);
132
+ return true;
133
+ }
134
+
135
+ private close(result: TId | null): void {
136
+ if (this.cleanup()) this.done(result);
137
+ }
138
+
139
+ dispose(): void {
140
+ this.cleanup();
141
+ }
142
+
143
+ handleInput(data: string): void {
144
+ const items = this.items();
145
+ reconcileSelection(this.selection, items);
146
+
147
+ if (
148
+ this.keybindings.matches(data, "tui.select.cancel") ||
149
+ matchesKey(data, Key.escape) ||
150
+ matchesKey(data, Key.ctrl("c"))
151
+ ) {
152
+ this.close(null);
153
+ return;
154
+ }
155
+ if (
156
+ this.keybindings.matches(data, "tui.select.confirm") ||
157
+ matchesKey(data, Key.enter)
158
+ ) {
159
+ const item = items[this.selection.index];
160
+ if (item) this.close(item.id);
161
+ return;
162
+ }
163
+ if (
164
+ this.keybindings.matches(data, "tui.select.up") ||
165
+ matchesKey(data, Key.up) ||
166
+ data === "k"
167
+ ) {
168
+ if (items.length > 0) {
169
+ this.selection.index =
170
+ (this.selection.index - 1 + items.length) % items.length;
171
+ this.selection.id = items[this.selection.index]?.id;
172
+ this.tui.requestRender();
173
+ }
174
+ return;
175
+ }
176
+ if (
177
+ this.keybindings.matches(data, "tui.select.down") ||
178
+ matchesKey(data, Key.down) ||
179
+ data === "j"
180
+ ) {
181
+ if (items.length > 0) {
182
+ this.selection.index = (this.selection.index + 1) % items.length;
183
+ this.selection.id = items[this.selection.index]?.id;
184
+ this.tui.requestRender();
185
+ }
186
+ return;
187
+ }
188
+ if (data === "x") {
189
+ const item = items[this.selection.index];
190
+ if (item) {
191
+ this.spec.onX?.(item);
192
+ if (this.items().length === 0) {
193
+ this.close(null);
194
+ return;
195
+ }
196
+ this.tui.requestRender();
197
+ }
198
+ }
199
+ }
200
+
201
+ render(width: number): string[] {
202
+ const theme = this.theme;
203
+ const items = this.items();
204
+ reconcileSelection(this.selection, items);
205
+
206
+ const rows = this.tui.terminal.rows || 30;
207
+ const bodyHeight = Math.max(6, rows - 5);
208
+ const innerWidth = Math.max(0, width - 2);
209
+ const lines: string[] = [];
210
+
211
+ const headerLeft = theme.fg("accent", theme.bold(this.spec.title));
212
+ const headerRight = theme.fg("muted", this.spec.countLabel(items));
213
+ const headerPad = Math.max(
214
+ 1,
215
+ width - visibleWidth(headerLeft) - visibleWidth(headerRight) - 4,
216
+ );
217
+ lines.push(
218
+ truncateToWidth(
219
+ ` ${headerLeft}${" ".repeat(headerPad)}${headerRight} `,
220
+ width,
221
+ ),
222
+ );
223
+
224
+ lines.push(
225
+ theme.fg("border", "╭") +
226
+ borderSegment(theme, innerWidth, this.spec.borderTitle(items)) +
227
+ theme.fg("border", "╮"),
228
+ );
229
+
230
+ const divider = theme.fg("border", "│");
231
+ const rowLines = this.renderRows(items, innerWidth, bodyHeight);
232
+ for (let i = 0; i < bodyHeight; i++) {
233
+ lines.push(divider + padAnsi(rowLines[i] ?? "", innerWidth) + divider);
234
+ }
235
+
236
+ lines.push(
237
+ theme.fg("border", "╰") +
238
+ theme.fg("border", "─".repeat(innerWidth)) +
239
+ theme.fg("border", "╯"),
240
+ );
241
+
242
+ lines.push(
243
+ truncateToWidth(theme.fg("dim", ` ${this.spec.footerHints}`), width),
244
+ );
245
+
246
+ return lines;
247
+ }
248
+
249
+ private renderRows(
250
+ items: ReadonlyArray<TItem>,
251
+ width: number,
252
+ height: number,
253
+ ): string[] {
254
+ const theme = this.theme;
255
+ const out: string[] = [];
256
+
257
+ let start = 0;
258
+ if (items.length > height) {
259
+ start = Math.min(
260
+ Math.max(0, this.selection.index - Math.floor(height / 2)),
261
+ items.length - height,
262
+ );
263
+ }
264
+ const visible = items.slice(start, start + height);
265
+ const dot = theme.fg("dim", " · ");
266
+
267
+ for (let i = 0; i < visible.length; i++) {
268
+ const item = visible[i];
269
+ const index = start + i;
270
+ const isSelected = index === this.selection.index;
271
+ const parts = this.spec.rowParts(item, theme);
272
+ const marker = isSelected ? theme.fg("accent", "❯") : " ";
273
+ const title = isSelected
274
+ ? theme.fg("accent", parts.title)
275
+ : theme.fg("text", parts.title);
276
+ const left = ` ${marker} ${parts.glyph} ${title} ${parts.idLabel}`;
277
+ const right = `${parts.rightParts.join(dot)} `;
278
+ out.push(splitRow(left, right, width));
279
+ }
280
+
281
+ if (start > 0) {
282
+ out[0] = truncateToWidth(theme.fg("dim", ` ... ${start} more`), width);
283
+ }
284
+ if (start + height < items.length) {
285
+ out[out.length - 1] = truncateToWidth(
286
+ theme.fg("dim", ` ... ${items.length - start - height} more`),
287
+ width,
288
+ );
289
+ }
290
+ return out;
291
+ }
292
+
293
+ invalidate(): void {}
294
+ }
295
+
296
+ export class OverlayScrollDetail implements Component {
297
+ private scrollOffset = 0;
298
+ private maxScroll = 0;
299
+ private following: boolean;
300
+ private closed = false;
301
+ private cachedWidth: number | undefined;
302
+ private cachedLines: string[] | undefined;
303
+ private ticker: ReturnType<typeof setInterval>;
304
+ private poller: ReturnType<typeof setInterval> | undefined;
305
+
306
+ constructor(
307
+ private tui: TUI,
308
+ private theme: Theme,
309
+ private spec: OverlayDetailSpec,
310
+ private done: (value: null) => void,
311
+ ) {
312
+ this.following = !!spec.followTail;
313
+ // Tick always invalidates — headers may show live clocks / durations.
314
+ this.ticker = setInterval(() => {
315
+ this.invalidate();
316
+ this.tui.requestRender();
317
+ }, spec.tickMs ?? 1000);
318
+ if (spec.onPoll) {
319
+ this.poller = setInterval(() => {
320
+ if (spec.onPoll?.()) {
321
+ this.invalidate();
322
+ this.tui.requestRender();
323
+ }
324
+ }, spec.pollMs ?? 500);
325
+ if (spec.onPoll()) {
326
+ this.invalidate();
327
+ this.tui.requestRender();
328
+ }
329
+ }
330
+ }
331
+
332
+ private cleanup(): boolean {
333
+ if (this.closed) return false;
334
+ this.closed = true;
335
+ clearInterval(this.ticker);
336
+ if (this.poller) clearInterval(this.poller);
337
+ return true;
338
+ }
339
+
340
+ private close(): void {
341
+ if (this.cleanup()) this.done(null);
342
+ }
343
+
344
+ dispose(): void {
345
+ this.cleanup();
346
+ }
347
+
348
+ handleInput(data: string): void {
349
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
350
+ this.close();
351
+ return;
352
+ }
353
+ if (data === "x" && this.spec.onX) {
354
+ if (this.spec.onX()) this.close();
355
+ else {
356
+ this.invalidate();
357
+ this.tui.requestRender();
358
+ }
359
+ return;
360
+ }
361
+ if (matchesKey(data, Key.up) || data === "k") {
362
+ if (this.scrollOffset > 0) {
363
+ this.scrollOffset--;
364
+ this.following = false;
365
+ this.invalidate();
366
+ this.tui.requestRender();
367
+ }
368
+ return;
369
+ }
370
+ if (matchesKey(data, Key.down) || data === "j") {
371
+ if (this.scrollOffset < this.maxScroll) {
372
+ this.scrollOffset++;
373
+ this.invalidate();
374
+ this.tui.requestRender();
375
+ }
376
+ if (this.scrollOffset >= this.maxScroll) this.following = true;
377
+ return;
378
+ }
379
+ if (matchesKey(data, Key.pageUp)) {
380
+ this.scrollOffset = Math.max(0, this.scrollOffset - 10);
381
+ this.following = false;
382
+ this.invalidate();
383
+ this.tui.requestRender();
384
+ return;
385
+ }
386
+ if (matchesKey(data, Key.pageDown)) {
387
+ this.scrollOffset = Math.min(this.maxScroll, this.scrollOffset + 10);
388
+ if (this.scrollOffset >= this.maxScroll) this.following = true;
389
+ this.invalidate();
390
+ this.tui.requestRender();
391
+ return;
392
+ }
393
+ if (matchesKey(data, Key.home)) {
394
+ this.scrollOffset = 0;
395
+ this.following = false;
396
+ this.invalidate();
397
+ this.tui.requestRender();
398
+ return;
399
+ }
400
+ if (matchesKey(data, Key.end)) {
401
+ this.scrollOffset = this.maxScroll;
402
+ this.following = true;
403
+ this.invalidate();
404
+ this.tui.requestRender();
405
+ }
406
+ }
407
+
408
+ render(width: number): string[] {
409
+ if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
410
+
411
+ const theme = this.theme;
412
+ const header = this.spec.getHeader(theme);
413
+ const footer = theme.fg("dim", this.spec.footerHints);
414
+ const sep = theme.fg("border", "─".repeat(Math.max(1, width)));
415
+
416
+ const body = this.spec.getBodyLines(theme);
417
+ const wrapped: string[] = [];
418
+ for (const line of body) {
419
+ for (const wl of wrapTextWithAnsi(line, Math.max(10, width - 2))) {
420
+ wrapped.push(truncateToWidth(wl, width));
421
+ }
422
+ }
423
+ if (wrapped.length === 0) {
424
+ wrapped.push(theme.fg("dim", this.spec.emptyBody ?? "(empty)"));
425
+ }
426
+
427
+ const rows = this.tui.terminal.rows || 30;
428
+ const viewHeight = Math.max(6, rows - 8);
429
+ this.maxScroll = Math.max(0, wrapped.length - viewHeight);
430
+ if (this.following && this.spec.followTail) {
431
+ this.scrollOffset = this.maxScroll;
432
+ }
433
+ if (this.scrollOffset > this.maxScroll) this.scrollOffset = this.maxScroll;
434
+
435
+ const visible = wrapped.slice(
436
+ this.scrollOffset,
437
+ this.scrollOffset + viewHeight,
438
+ );
439
+ while (visible.length < viewHeight) visible.push("");
440
+
441
+ this.cachedLines = [
442
+ truncateToWidth(header, width),
443
+ sep,
444
+ ...visible,
445
+ sep,
446
+ truncateToWidth(footer, width),
447
+ ];
448
+ this.cachedWidth = width;
449
+ return this.cachedLines;
450
+ }
451
+
452
+ invalidate(): void {
453
+ this.cachedWidth = undefined;
454
+ this.cachedLines = undefined;
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Loop: list overlay → detail overlay until list closed / empty.
460
+ */
461
+ export async function openListDetailOverlay<
462
+ TId extends OverlayId,
463
+ TItem extends { id: TId },
464
+ >(
465
+ ctx: ExtensionCommandContext,
466
+ options: {
467
+ emptyMessage: string;
468
+ listSpec: OverlayListSpec<TId, TItem>;
469
+ createDetail: (
470
+ item: TItem,
471
+ tui: TUI,
472
+ theme: Theme,
473
+ done: (value: null) => void,
474
+ ) => Component;
475
+ },
476
+ ): Promise<void> {
477
+ if (!ctx.hasUI) return;
478
+
479
+ const selection: OverlaySelection<TId> = { index: 0 };
480
+
481
+ while (true) {
482
+ const items = options.listSpec.listItems();
483
+ if (items.length === 0) {
484
+ ctx.ui.notify(options.emptyMessage, "info");
485
+ return;
486
+ }
487
+
488
+ const picked = await ctx.ui.custom<TId | null>(
489
+ (tui, theme, keybindings, done) =>
490
+ new OverlayListDashboard(
491
+ tui,
492
+ theme,
493
+ keybindings,
494
+ options.listSpec,
495
+ selection,
496
+ done,
497
+ ),
498
+ OVERLAY_OPTS,
499
+ );
500
+
501
+ if (picked === null || picked === undefined) return;
502
+
503
+ const item = options.listSpec.listItems().find((i) => i.id === picked);
504
+ if (!item) continue;
505
+
506
+ await ctx.ui.custom<null>(
507
+ (tui, theme, _kb, done) =>
508
+ options.createDetail(item, tui, theme, done),
509
+ OVERLAY_OPTS,
510
+ );
511
+ }
512
+ }