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.
- package/README.md +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- 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
|
+
}
|