Files
pi-plan/src/notice-display.ts
T
wassnameandPI/OpenAI 0a35869be9 Show sampled fortune after substantive goal updates without model work
Append a UI-only saved entry after supervisor or solo final updates; omit short unchanged waits. Remove the prompt instructions to sample fortunes, and cover the non-context renderer and lifecycle.

Co-Authored-By: PI/OpenAI <288921227+claudypoo@users.noreply.github.com>
2026-09-23 12:07:14 +08:00

89 lines
4.2 KiB
TypeScript

// Pi/OpenAI: Pi converts routine custom notices to the same user-role model input; legacy mirrored prompts remain exact.
import { randomInt } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { type ExtensionAPI, type ExtensionContext, getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
import { Markdown, truncateToWidth } from "@earendil-works/pi-tui";
const NOTICE = "pi-goals-notice";
const PROMPT = "pi-goals-prompt";
const COMPACT = "pi-goals-compact-prompt";
const FORTUNE = "pi-goals-fortune";
const kaomoji = ["(˶ᵔ ᵕ ᵔ˶)", "( •̀ᴗ•́ )و", "( ̄▽ ̄)", "(。•̀ᴗ-)✧"];
const fortunePath = join(homedir(), ".pi/agent/skills/ml-debug/fortune.txt");
export function substantiveUpdate(text: string): boolean {
const update = text.trim();
return update.length > 30 && !/^(?:no (?:material )?change|still waiting|waiting for|nothing (?:new|changed))\b/i.test(update);
}
function noticeLabel(content: string) {
return content.includes("[pi-goals: plan activity]") ? "Plan activity recorded"
: content.includes("\nPlan requirements or goal status changed") ? "Plan changed · review requested"
: content.includes("\nPlan changed") ? "Plan changed · review requested"
: content.includes("## Selected worker-stop reviews") ? "Selected worker-stop reviews"
: content.includes("## Worker stop review:") ? "Worker stop review"
: content.includes("## Worker status:") ? "Worker status"
: "Goal instructions";
}
export function noticeDisplay(pi: ExtensionAPI) {
const mirrored = new Set<string>();
const compacted = new Set<string>();
pi.registerMarkdownTransformer((markdown, context) => {
if (context.messageType !== "user") return markdown;
if (compacted.has(markdown)) return `[pi-goals] ${noticeLabel(markdown)}`;
return mirrored.has(markdown) ? "" : markdown;
});
const render = (content: string, expanded: boolean, theme: { fg(color: string, text: string): string }) => {
if (expanded) return new Markdown(content, 0, 0, getMarkdownTheme());
return {
render: (width: number) => [truncateToWidth(theme.fg("muted", `[pi-goals] ${noticeLabel(content)} · ${keyHint("app.tools.expand", "expand")}`), width)],
invalidate() {},
};
};
pi.registerEntryRenderer(NOTICE, (entry, { expanded }, theme) => render((entry.data as { content: string }).content, expanded, theme));
pi.registerEntryRenderer(FORTUNE, (entry) => new Markdown((entry.data as { content: string }).content, 0, 0, getMarkdownTheme()));
pi.registerMessageRenderer(PROMPT, (message, { expanded }, theme) => render(typeof message.content === "string" ? message.content : message.content.filter(part => part.type === "text").map(part => part.text).join("\n"), expanded, theme));
return {
fortune() {
if (!existsSync(fortunePath)) return;
const lines = readFileSync(fortunePath, "utf8").split(/\r?\n/).filter(Boolean);
if (!lines.length) throw new Error(`Empty fortune file: ${fortunePath}`);
pi.appendEntry(FORTUNE, { content: `${kaomoji[randomInt(kaomoji.length)]} ${lines[randomInt(lines.length)]}` });
},
prompt(content: string) {
pi.sendMessage({ customType: PROMPT, content, display: true }, { triggerTurn: true, deliverAs: "followUp" });
},
passive(content: string) {
pi.sendMessage({ customType: PROMPT, content, display: true }, { deliverAs: "nextTurn" });
},
hide(content: string) {
mirrored.add(content);
},
mirror(content: string) {
mirrored.add(content);
pi.appendEntry(NOTICE, { content });
},
compact(content: string) {
compacted.add(content);
pi.appendEntry(COMPACT, { content });
},
restore(ctx: ExtensionContext, hiddenTypes: string[] = []) {
mirrored.clear();
compacted.clear();
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type !== "custom") continue;
const compact = entry.customType === COMPACT;
const mirroredType = entry.customType === NOTICE || hiddenTypes.includes(entry.customType);
if (!compact && !mirroredType) continue;
const content = (entry.data as { content?: unknown }).content;
if (typeof content !== "string") continue;
if (compact) compacted.add(content);
else mirrored.add(content);
}
},
};
}