mirror of
https://github.com/wassname/pi-plan.git
synced 2026-08-05 13:20:28 +08:00
goals widget: fold path into header + add 'prune completed goals'
Two space-savers for a display that grows across sessions: - widget header now carries the clickable .pi/goals.md path (with title), dropping the separate muted footer -- one vertical line instead of two. - /goals clear now offers 'Prune completed goals' (pure pruneCompleted: drops done+cancelled goal blocks, keeps active/open, title, and the log) alongside the existing full clear. Gives a way to shed old goals without losing the audit trail. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
+20
-7
@@ -39,7 +39,7 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@e
|
||||
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
||||
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { counts, findGoal, type Goal, type PlanDoc, parse, recordSignOff, type SignOff } from "./plan-file.js";
|
||||
import { counts, findGoal, type Goal, type PlanDoc, parse, pruneCompleted, recordSignOff, type SignOff } from "./plan-file.js";
|
||||
import {
|
||||
completeGoalDescription,
|
||||
completeGoalParamDescription,
|
||||
@@ -124,12 +124,15 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
}
|
||||
const c = counts(doc);
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", `◷ ${c.done}/${doc.goals.length} goals`));
|
||||
ctx.ui.setWidget(WIDGET_KEY, [...goalWidgetLines(doc), ctx.ui.theme.fg("muted", PLAN_REL)]);
|
||||
ctx.ui.setWidget(WIDGET_KEY, goalWidgetLines(doc, ctx));
|
||||
}
|
||||
|
||||
function goalWidgetLines(doc: PlanDoc): string[] {
|
||||
function goalWidgetLines(doc: PlanDoc, ctx: ExtensionContext): string[] {
|
||||
const mark: Record<Goal["status"], string> = { done: "✔", active: "▸", open: "◻", cancelled: "✗" };
|
||||
const lines = [`Goals: ${doc.title || "(untitled)"}`];
|
||||
// Header doubles as the file path (clickable) so we don't spend a second line on a "Goals:" label
|
||||
// plus a footer path -- one line carries both. Title trails the path when set.
|
||||
const header = ctx.ui.theme.fg("muted", doc.title ? `${PLAN_REL}: ${doc.title}` : PLAN_REL);
|
||||
const lines = [header];
|
||||
for (const g of doc.goals) {
|
||||
// Show every goal with its status glyph (✔ done, ▸ active, ◻ open, ✗ cancelled) so finished
|
||||
// goals read as checked off rather than vanishing. Plans are small, so this stays readable.
|
||||
@@ -143,7 +146,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
// --- plan mode: setup -------------------------------------------------------------------------
|
||||
|
||||
pi.registerCommand("goals", {
|
||||
description: "Plan mode: set up goals (with evidence) in goals.md, then work them. /goals <objective>",
|
||||
description: "Plan mode: set up goals (with evidence) in goals.md, then work them. /goals <objective> | /goals clear",
|
||||
handler: async (args, ctx) => {
|
||||
savedCmdCtx = ctx; // ctx here is an ExtensionCommandContext (has newSession); keep it for later
|
||||
const arg = args.trim();
|
||||
@@ -182,8 +185,18 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
return;
|
||||
}
|
||||
if (ctx.hasUI) {
|
||||
const ok = await ctx.ui.select(`Clear ${PLAN_REL}?`, ["Cancel", "Clear goals.md"]);
|
||||
if (ok !== "Clear goals.md") return;
|
||||
const choice = await ctx.ui.select(`Clear ${PLAN_REL}?`, [
|
||||
"Cancel",
|
||||
"Prune completed goals (keep active/open + log)",
|
||||
"Clear everything",
|
||||
]);
|
||||
if (!choice || choice.startsWith("Cancel")) return;
|
||||
if (choice.startsWith("Prune")) {
|
||||
writePlan(ctx, pruneCompleted(readPlan(ctx)));
|
||||
updateWidget(ctx);
|
||||
ctx.ui.notify(`Pruned completed goals from ${PLAN_REL}.`, "info");
|
||||
return;
|
||||
}
|
||||
}
|
||||
writePlan(ctx, "");
|
||||
state = { ...state, isPlanMode: false, objective: null };
|
||||
|
||||
@@ -228,6 +228,43 @@ export function setGoalStatus(text: string, subject: string, status: GoalStatus)
|
||||
throw new Error(`Goal "${subject}" not found`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop done+cancelled goal blocks (their header line through the last of their subtask/section lines)
|
||||
* from goals.md, keeping the title, context, active/open goals, and the ## Log. Pure. Lets the human
|
||||
* prune old finished goals so the widget/file stay short across sessions, without losing the log trail.
|
||||
*/
|
||||
export function pruneCompleted(text: string): string {
|
||||
const lines = text.split("\n");
|
||||
const out: string[] = [];
|
||||
let inGoals = false;
|
||||
let dropping = false;
|
||||
for (const line of lines) {
|
||||
if (GOALS_HEADER.test(line)) {
|
||||
inGoals = true;
|
||||
dropping = false;
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
if (ANY_HEADER.test(line)) {
|
||||
// Any other header (## Log, "# Future work") ends the goals section.
|
||||
inGoals = false;
|
||||
dropping = false;
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
if (inGoals) {
|
||||
const m = GOAL_ITEM.exec(line);
|
||||
if (m) {
|
||||
const status = CHAR_TO_STATUS[m[1].toLowerCase()] ?? "open";
|
||||
dropping = status === "done" || status === "cancelled";
|
||||
}
|
||||
if (dropping) continue; // skip the goal line and everything under it until the next goal/header
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* The outcome of a sign-off attempt, decided by CompleteGoal (which runs verify + the judge). Kept
|
||||
* separate from the I/O so the record logic below is pure and testable.
|
||||
|
||||
+38
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { appendLog, counts, findGoal, parse, recordSignOff, setGoalStatus } from "../src/plan-file.js";
|
||||
import { appendLog, counts, findGoal, parse, pruneCompleted, recordSignOff, setGoalStatus } from "../src/plan-file.js";
|
||||
|
||||
const SAMPLE = `# papers audit
|
||||
|
||||
@@ -169,3 +169,40 @@ describe("recordSignOff (CompleteGoal's pure record logic)", () => {
|
||||
expect(r.content).toBe(SAMPLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pruneCompleted (drop finished goals, keep the rest)", () => {
|
||||
// SAMPLE has one active + one open goal; add a done and a cancelled one to prune.
|
||||
const WITH_FINISHED = `# papers audit
|
||||
|
||||
Context line kept.
|
||||
|
||||
## Goals
|
||||
|
||||
1. [x] goal: Old finished thing
|
||||
- discriminator: shipped
|
||||
- tasks:
|
||||
1. [x] did it
|
||||
- evidence:
|
||||
- > done.log
|
||||
2. [/] goal: Implement cache layer
|
||||
- discriminator: hit-rate > 0.8
|
||||
3. [-] goal: Abandoned idea
|
||||
- discriminator: n/a
|
||||
4. [ ] goal: Document the API
|
||||
- discriminator: docstrings
|
||||
|
||||
## Log
|
||||
- 2026-06-15 14:02 note
|
||||
`;
|
||||
|
||||
it("removes done and cancelled goals, keeps active/open + title + log", () => {
|
||||
const out = parse(pruneCompleted(WITH_FINISHED));
|
||||
expect(out.goals.map((g) => g.subject)).toEqual(["Implement cache layer", "Document the API"]);
|
||||
expect(out.title).toBe("papers audit");
|
||||
expect(out.log.at(-1)).toBe("- 2026-06-15 14:02 note");
|
||||
});
|
||||
|
||||
it("is a no-op when nothing is finished", () => {
|
||||
expect(pruneCompleted(SAMPLE)).toBe(SAMPLE);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user