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,36 @@
1
+ /**
2
+ * Shared helper for waiting on async result files.
3
+ * Extracted so it can be tested without loading the full async-agents module.
4
+ */
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ /**
9
+ * Wait up to deadlineMs for result files to appear.
10
+ * Uses a single group-level deadline (not per-file).
11
+ */
12
+ export async function waitForResultFiles(
13
+ resultDir: string,
14
+ jobIds: string[],
15
+ deadlineMs: number,
16
+ ): Promise<Set<string>> {
17
+ const found = new Set<string>();
18
+ const deadline = Date.now() + deadlineMs;
19
+ // Always scan at least once (handles deadlineMs=0)
20
+ do {
21
+ let allFound = true;
22
+ for (const id of jobIds) {
23
+ if (found.has(id)) continue;
24
+ if (existsSync(join(resultDir, `${id}.json`))) {
25
+ found.add(id);
26
+ } else {
27
+ allFound = false;
28
+ }
29
+ }
30
+ if (allFound) break;
31
+ const remaining = deadline - Date.now();
32
+ if (remaining <= 0) break;
33
+ await new Promise(r => setTimeout(r, Math.min(250, remaining)));
34
+ } while (Date.now() < deadline);
35
+ return found;
36
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * /btw command — quick side questions without polluting conversation history.
3
+ */
4
+
5
+ import { complete, type UserMessage } from "@earendil-works/pi-ai";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import { tryGetSystemPromptOptions } from "./utils.js";
8
+ import {
9
+ BorderedLoader,
10
+ convertToLlm,
11
+ serializeConversation,
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import {
14
+ Key,
15
+ matchesKey,
16
+ visibleWidth,
17
+ wrapTextWithAnsi,
18
+ } from "@earendil-works/pi-tui";
19
+
20
+ export function registerBtw(pi: ExtensionAPI): void {
21
+ pi.registerCommand("btw", {
22
+ description: "Ask a quick side question without polluting conversation history",
23
+ handler: async (args, ctx) => {
24
+ const question = args?.trim();
25
+ if (!question) {
26
+ if (ctx.hasUI) ctx.ui.notify("Usage: /btw <question>", "error");
27
+ return;
28
+ }
29
+
30
+ if (!ctx.model) {
31
+ if (ctx.hasUI) ctx.ui.notify("No model selected. Use /model to select one first.", "error");
32
+ return;
33
+ }
34
+
35
+ // Build conversation context
36
+ const branch = ctx.sessionManager.getBranch();
37
+ const messages = branch
38
+ .filter((e: any) => e.type === "message")
39
+ .map((e: any) => e.message);
40
+ let conversationText = "";
41
+ if (messages.length > 0) {
42
+ const llmMessages = convertToLlm(messages);
43
+ conversationText = serializeConversation(llmMessages);
44
+ }
45
+
46
+ // Build context-aware system prompt using loaded resources
47
+ let projectContext = "";
48
+ const opts = tryGetSystemPromptOptions(ctx);
49
+ if (opts?.contextFiles?.length) {
50
+ const names = opts.contextFiles.map((f: any) => f.path?.split("/").pop() || f.path || "(unknown)").join(", ");
51
+ projectContext = `\n- Project context files loaded: ${names}`;
52
+ }
53
+ if (opts?.skills?.length) {
54
+ const names = opts.skills.map((s: any) => s.name || s).join(", ");
55
+ projectContext += `\n- Available skills: ${names}`;
56
+ }
57
+
58
+ const systemPrompt = `You are answering a quick "by the way" side question during a coding session.
59
+
60
+ Rules:
61
+ - Answer concisely and directly based on the conversation context provided.
62
+ - You have NO tool access — you cannot read files, run commands, or make changes.
63
+ - Only answer based on the conversation and loaded project context.${projectContext}
64
+ - Keep your response brief and to the point.
65
+ - Use markdown formatting where helpful (code blocks, lists, bold).
66
+ - If the conversation context doesn't contain enough information to answer, say so honestly.`;
67
+
68
+ const userMessage: UserMessage = {
69
+ role: "user",
70
+ content: [{
71
+ type: "text",
72
+ text: `<conversation_context>\n${conversationText}\n</conversation_context>\n\n<side_question>\n${question}\n</side_question>\n\nAnswer the side question above based on the conversation context. Be concise.`,
73
+ }],
74
+ timestamp: Date.now(),
75
+ };
76
+
77
+ // Step 1: Get the answer with a loading spinner
78
+ const answerResult = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
79
+ const loader = new BorderedLoader(tui, theme, `Thinking (${ctx.model!.id})...`);
80
+ loader.onAbort = () => done(null);
81
+
82
+ const doQuery = async () => {
83
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
84
+ if (!auth.ok || !auth.apiKey) {
85
+ throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
86
+ }
87
+ const response = await complete(
88
+ ctx.model!,
89
+ { systemPrompt, messages: [userMessage] },
90
+ { apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
91
+ );
92
+ if (response.stopReason === "aborted") return null;
93
+ if (response.stopReason === "error") return null;
94
+ return response.content
95
+ .filter((c: any) => c.type === "text" && typeof c.text === "string")
96
+ .map((c: any) => c.text)
97
+ .join("\n");
98
+ };
99
+
100
+ doQuery().then(done).catch(() => done(null));
101
+ return loader;
102
+ });
103
+
104
+ if (answerResult === null) {
105
+ ctx.ui.notify("Cancelled", "info");
106
+ return;
107
+ }
108
+ if (answerResult.trim() === "") {
109
+ ctx.ui.notify("No answer received", "warning");
110
+ return;
111
+ }
112
+
113
+ // Step 2: Show answer in a scrollable overlay
114
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
115
+ let scrollOffset = 0;
116
+ let cachedWidth: number | undefined;
117
+ let cachedLines: string[] | undefined;
118
+
119
+ return {
120
+ handleInput(data: string) {
121
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || data === " " || data.toLowerCase() === "q") {
122
+ done(undefined);
123
+ return;
124
+ }
125
+ if (matchesKey(data, Key.up) || data === "k") {
126
+ if (scrollOffset > 0) { scrollOffset--; cachedWidth = undefined; tui.requestRender(); }
127
+ return;
128
+ }
129
+ if (matchesKey(data, Key.down) || data === "j") {
130
+ scrollOffset++; cachedWidth = undefined; tui.requestRender();
131
+ return;
132
+ }
133
+ if (matchesKey(data, Key.pageUp)) {
134
+ scrollOffset = Math.max(0, scrollOffset - 10); cachedWidth = undefined; tui.requestRender();
135
+ return;
136
+ }
137
+ if (matchesKey(data, Key.pageDown)) {
138
+ scrollOffset += 10; cachedWidth = undefined; tui.requestRender();
139
+ return;
140
+ }
141
+ },
142
+
143
+ invalidate() { cachedWidth = undefined; cachedLines = undefined; },
144
+
145
+ render(width: number): string[] {
146
+ if (cachedLines && cachedWidth === width) return cachedLines;
147
+
148
+ const boxWidth = Math.min(width - 2, 120);
149
+ const contentWidth = boxWidth - 6;
150
+ const hLine = "─".repeat(boxWidth - 2);
151
+
152
+ const boxLine = (content: string, pad = 2): string => {
153
+ const padded = " ".repeat(pad) + content;
154
+ const right = Math.max(0, boxWidth - visibleWidth(padded) - 2);
155
+ return theme.fg("border", "│") + padded + " ".repeat(right) + theme.fg("border", "│");
156
+ };
157
+ const emptyLine = () => theme.fg("border", "│") + " ".repeat(boxWidth - 2) + theme.fg("border", "│");
158
+ const pad = (line: string) => { const len = visibleWidth(line); return line + " ".repeat(Math.max(0, width - len)); };
159
+
160
+ const lines: string[] = [];
161
+
162
+ // Header
163
+ lines.push(pad(theme.fg("accent", "╭" + hLine + "╮")));
164
+ lines.push(pad(boxLine(theme.fg("accent", theme.bold("btw")))));
165
+ lines.push(pad(theme.fg("accent", "├" + hLine + "┤")));
166
+
167
+ // Question
168
+ const qLabel = theme.fg("muted", "Q: ") + theme.fg("text", question);
169
+ for (const l of wrapTextWithAnsi(qLabel, contentWidth)) lines.push(pad(boxLine(l)));
170
+ lines.push(pad(emptyLine()));
171
+ lines.push(pad(theme.fg("border", "├" + hLine + "┤")));
172
+ lines.push(pad(emptyLine()));
173
+
174
+ // Answer lines
175
+ const answerLines: string[] = [];
176
+ for (const paragraph of answerResult.split("\n")) {
177
+ if (paragraph.trim() === "") { answerLines.push(""); }
178
+ else { answerLines.push(...wrapTextWithAnsi(paragraph, contentWidth)); }
179
+ }
180
+
181
+ // Scrolling
182
+ const termHeight = tui.height ?? 24;
183
+ const headerCount = lines.length;
184
+ const footerCount = 3;
185
+ const maxVisible = Math.max(1, termHeight - headerCount - footerCount - 2);
186
+ if (scrollOffset > Math.max(0, answerLines.length - maxVisible)) {
187
+ scrollOffset = Math.max(0, answerLines.length - maxVisible);
188
+ }
189
+ const visible = answerLines.slice(scrollOffset, scrollOffset + maxVisible);
190
+ for (const l of visible) lines.push(pad(boxLine(l)));
191
+
192
+ if (answerLines.length > maxVisible) {
193
+ const info = theme.fg("dim", `[${scrollOffset + 1}-${Math.min(scrollOffset + maxVisible, answerLines.length)}/${answerLines.length}]`);
194
+ lines.push(pad(boxLine(info)));
195
+ }
196
+
197
+ lines.push(pad(emptyLine()));
198
+ lines.push(pad(theme.fg("accent", "├" + hLine + "┤")));
199
+ lines.push(pad(boxLine(theme.fg("dim", "Esc/Space/q dismiss · ↑↓/j/k scroll · PgUp/PgDn"))));
200
+ lines.push(pad(theme.fg("accent", "╰" + hLine + "╯")));
201
+
202
+ cachedWidth = width;
203
+ cachedLines = lines;
204
+ return lines;
205
+ },
206
+ };
207
+ });
208
+ },
209
+ });
210
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pure cron status display helpers — no TUI deps (unit-testable).
3
+ */
4
+
5
+ export interface CronStatusTaskView {
6
+ /** Unique overlay row id (e.g. "1" or "cron-123-abc.json:1"). */
7
+ id: string;
8
+ /** Numeric cron task id within its session file. */
9
+ taskId: number;
10
+ description: string;
11
+ task: string;
12
+ intervalMs?: number;
13
+ atHour?: number;
14
+ atMinute?: number;
15
+ createdAt: number;
16
+ lastRun?: number;
17
+ nextRun?: number;
18
+ /** Shown in list-all rows (e.g. "this session", "PID 42"). */
19
+ sessionLabel?: string;
20
+ isLocal?: boolean;
21
+ cronFile?: string;
22
+ }
23
+
24
+ export function formatCronSchedule(task: {
25
+ intervalMs?: number;
26
+ atHour?: number;
27
+ atMinute?: number;
28
+ }): string {
29
+ if (task.intervalMs) {
30
+ if (task.intervalMs < 60000) return `every ${Math.round(task.intervalMs / 1000)}s`;
31
+ if (task.intervalMs < 3600000) {
32
+ return `every ${Math.round(task.intervalMs / 60000)}m`;
33
+ }
34
+ const h = Math.floor(task.intervalMs / 3600000);
35
+ const m = Math.round((task.intervalMs % 3600000) / 60000);
36
+ return m > 0 ? `every ${h}h${m}m` : `every ${h}h`;
37
+ }
38
+ if (task.atHour !== undefined && task.atMinute !== undefined) {
39
+ return `daily at ${String(task.atHour).padStart(2, "0")}:${String(task.atMinute).padStart(2, "0")}`;
40
+ }
41
+ return "unknown";
42
+ }
43
+
44
+ /** Best-effort next-fire timestamp for display. */
45
+ export function resolveNextRunAt(
46
+ task: CronStatusTaskView,
47
+ now = Date.now(),
48
+ ): number | undefined {
49
+ if (task.nextRun != null) return task.nextRun;
50
+ if (task.intervalMs != null) {
51
+ if (task.lastRun != null) return task.lastRun + task.intervalMs;
52
+ return task.createdAt + task.intervalMs;
53
+ }
54
+ if (task.atHour !== undefined && task.atMinute !== undefined) {
55
+ const target = new Date(now);
56
+ target.setHours(task.atHour, task.atMinute, 0, 0);
57
+ if (target.getTime() <= now) target.setDate(target.getDate() + 1);
58
+ return target.getTime();
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ export function formatRelativeMs(ms: number): string {
64
+ const abs = Math.abs(ms);
65
+ if (abs < 60000) return `${Math.round(abs / 1000)}s`;
66
+ if (abs < 3600000) return `${Math.round(abs / 60000)}m`;
67
+ const h = Math.floor(abs / 3600000);
68
+ const m = Math.round((abs % 3600000) / 60000);
69
+ return m > 0 ? `${h}h${m}m` : `${h}h`;
70
+ }
71
+
72
+ export function formatLastRunLabel(
73
+ task: CronStatusTaskView,
74
+ now = Date.now(),
75
+ ): string {
76
+ if (task.lastRun == null) return "never";
77
+ return `${formatRelativeMs(now - task.lastRun)} ago`;
78
+ }
79
+
80
+ export function formatNextRunLabel(
81
+ task: CronStatusTaskView,
82
+ now = Date.now(),
83
+ ): string {
84
+ const at = resolveNextRunAt(task, now);
85
+ if (at == null) return "—";
86
+ const delta = at - now;
87
+ if (delta <= 0) return "soon";
88
+ return `in ${formatRelativeMs(delta)}`;
89
+ }
90
+
91
+ /** Map a CronTask-like object into an overlay row view. */
92
+ export function toCronStatusTaskView(
93
+ task: {
94
+ id: number;
95
+ description: string;
96
+ task: string;
97
+ intervalMs?: number;
98
+ atHour?: number;
99
+ atMinute?: number;
100
+ createdAt: number;
101
+ lastRun?: number;
102
+ nextRun?: number;
103
+ },
104
+ opts: {
105
+ overlayId: string;
106
+ sessionLabel?: string;
107
+ isLocal?: boolean;
108
+ cronFile?: string;
109
+ },
110
+ ): CronStatusTaskView {
111
+ return {
112
+ id: opts.overlayId,
113
+ taskId: task.id,
114
+ description: task.description,
115
+ task: task.task,
116
+ intervalMs: task.intervalMs,
117
+ atHour: task.atHour,
118
+ atMinute: task.atMinute,
119
+ createdAt: task.createdAt,
120
+ lastRun: task.lastRun,
121
+ nextRun: task.nextRun,
122
+ sessionLabel: opts.sessionLabel,
123
+ isLocal: opts.isLocal,
124
+ cronFile: opts.cronFile,
125
+ };
126
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * /cron list + list-all fullscreen overlay — list dashboard → task detail.
3
+ */
4
+
5
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ formatCronSchedule,
8
+ formatLastRunLabel,
9
+ formatNextRunLabel,
10
+ type CronStatusTaskView,
11
+ } from "./cron-status-format.js";
12
+ import {
13
+ openListDetailOverlay,
14
+ OverlayScrollDetail,
15
+ } from "./overlay-dashboard.js";
16
+
17
+ export type { CronStatusTaskView } from "./cron-status-format.js";
18
+ export {
19
+ formatCronSchedule,
20
+ formatLastRunLabel,
21
+ formatNextRunLabel,
22
+ resolveNextRunAt,
23
+ toCronStatusTaskView,
24
+ } from "./cron-status-format.js";
25
+
26
+ export interface CronStatusUiDeps {
27
+ listTasks: () => CronStatusTaskView[];
28
+ /** Remove by overlay row id. Return true if removed. */
29
+ removeTask: (id: string) => boolean;
30
+ title?: string;
31
+ emptyMessage?: string;
32
+ borderTitle?: (tasks: readonly CronStatusTaskView[]) => string;
33
+ }
34
+
35
+ /**
36
+ * Open fullscreen cron list. Loops list → detail until user closes list.
37
+ */
38
+ export async function openCronStatusOverlay(
39
+ ctx: ExtensionCommandContext,
40
+ deps: CronStatusUiDeps,
41
+ ): Promise<void> {
42
+ const showSession = () =>
43
+ deps.listTasks().some((t) => !!t.sessionLabel);
44
+
45
+ await openListDetailOverlay(ctx, {
46
+ emptyMessage: deps.emptyMessage ?? "No scheduled tasks.",
47
+ listSpec: {
48
+ title: deps.title ?? "Cron tasks",
49
+ countLabel: (tasks) =>
50
+ `${tasks.length} task${tasks.length === 1 ? "" : "s"}`,
51
+ borderTitle:
52
+ deps.borderTitle ??
53
+ ((tasks) => `scheduled · ${tasks.length}`),
54
+ footerHints: "↑↓/jk select · Enter view · x remove · Esc close",
55
+ listItems: () => deps.listTasks(),
56
+ rowParts: (task, theme) => {
57
+ const now = Date.now();
58
+ const rightParts = [
59
+ theme.fg("muted", formatCronSchedule(task)),
60
+ theme.fg("dim", formatLastRunLabel(task, now)),
61
+ theme.fg("muted", formatNextRunLabel(task, now)),
62
+ ];
63
+ if (showSession() && task.sessionLabel) {
64
+ rightParts.unshift(theme.fg("dim", task.sessionLabel));
65
+ }
66
+ return {
67
+ glyph: theme.fg("warning", "■"),
68
+ title: task.description,
69
+ idLabel: theme.fg("dim", `#${task.taskId}`),
70
+ rightParts,
71
+ };
72
+ },
73
+ onX: (task) => {
74
+ deps.removeTask(task.id);
75
+ },
76
+ },
77
+ createDetail: (task, tui, theme, done) => {
78
+ const rowId = task.id;
79
+ return new OverlayScrollDetail(
80
+ tui,
81
+ theme,
82
+ {
83
+ footerHints: "↑↓/jk scroll · x remove · Esc back",
84
+ onX: () => deps.removeTask(rowId),
85
+ getHeader: (t) => {
86
+ const current = deps.listTasks().find((x) => x.id === rowId);
87
+ if (!current) return t.fg("error", "Task removed.");
88
+ const session = current.sessionLabel
89
+ ? t.fg("dim", ` · ${current.sessionLabel}`)
90
+ : "";
91
+ return (
92
+ `${t.fg("warning", "■")} ` +
93
+ t.fg(
94
+ "accent",
95
+ t.bold(`#${current.taskId} · ${current.description}`),
96
+ ) +
97
+ t.fg("muted", ` · ${formatCronSchedule(current)}`) +
98
+ session
99
+ );
100
+ },
101
+ getBodyLines: (t) => {
102
+ const current = deps.listTasks().find((x) => x.id === rowId);
103
+ if (!current) {
104
+ return [t.fg("error", "Task removed."), t.fg("dim", "Esc back")];
105
+ }
106
+ const now = Date.now();
107
+ const lines = [
108
+ `${t.fg("muted", "Schedule")} ${formatCronSchedule(current)}`,
109
+ `${t.fg("muted", "Last run")} ${formatLastRunLabel(current, now)}` +
110
+ (current.lastRun
111
+ ? t.fg(
112
+ "dim",
113
+ ` · ${new Date(current.lastRun).toLocaleString()}`,
114
+ )
115
+ : ""),
116
+ `${t.fg("muted", "Next run")} ${formatNextRunLabel(current, now)}`,
117
+ `${t.fg("muted", "Created")} ${new Date(current.createdAt).toLocaleString()}`,
118
+ ];
119
+ if (current.sessionLabel) {
120
+ lines.push(
121
+ `${t.fg("muted", "Session")} ${current.sessionLabel}`,
122
+ );
123
+ }
124
+ lines.push("", t.fg("muted", "Task"), current.task);
125
+ return lines;
126
+ },
127
+ },
128
+ done,
129
+ );
130
+ },
131
+ });
132
+ }