pi-better-harness 0.3.5 → 0.3.6

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.
@@ -7,6 +7,7 @@
7
7
  - Gives models `update_plan` and `get_plan` tools for atomic, explicit progress updates.
8
8
  - Shows the complete checklist of completed, active, pending, and blocked steps above the editor.
9
9
  - Persists plan state and display preferences on the active Pi session branch.
10
+ - Keeps a completed plan visible for 30 seconds, then clears it automatically.
10
11
  - Opens the complete plan with `/plan`.
11
12
 
12
13
  Plan progress is checklist progress, not an estimate of effort. The extension never infers completion from prose or successful tool calls.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-plan",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Structured execution plans with persistent progress for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -70,4 +70,4 @@
70
70
  "engines": {
71
71
  "node": ">=22.0.0"
72
72
  }
73
- }
73
+ }
@@ -4,6 +4,7 @@ import { Text } from "@earendil-works/pi-tui";
4
4
  import { Type } from "typebox";
5
5
 
6
6
  import {
7
+ completedPlanClearDelay,
7
8
  planClearEntry,
8
9
  planDisplayEntry,
9
10
  planProgress,
@@ -42,21 +43,53 @@ export default function planExtension(pi: ExtensionAPI): void {
42
43
  let currentPlan: PlanSnapshot | null = null;
43
44
  let displayMode: PlanDisplayMode = "auto";
44
45
  let refreshWidget: ((force?: boolean) => void) | undefined;
46
+ let completedPlanClearTimer: ReturnType<typeof setTimeout> | undefined;
45
47
 
46
48
  const refresh = (force = false): void => refreshWidget?.(force);
47
49
 
50
+ const cancelCompletedPlanClear = (): void => {
51
+ if (completedPlanClearTimer) clearTimeout(completedPlanClearTimer);
52
+ completedPlanClearTimer = undefined;
53
+ };
54
+
55
+ const clearPlan = (): void => {
56
+ cancelCompletedPlanClear();
57
+ currentPlan = null;
58
+ pi.appendEntry(EXTENSION_NAME, planClearEntry());
59
+ refresh(true);
60
+ };
61
+
62
+ const scheduleCompletedPlanClear = (): void => {
63
+ cancelCompletedPlanClear();
64
+ if (!currentPlan) return;
65
+ const delay = completedPlanClearDelay(currentPlan);
66
+ if (delay === null) return;
67
+ const completedPlan = currentPlan;
68
+ const timer = setTimeout(() => {
69
+ if (completedPlanClearTimer !== timer) return;
70
+ completedPlanClearTimer = undefined;
71
+ if (currentPlan?.planId !== completedPlan.planId || currentPlan.revision !== completedPlan.revision) return;
72
+ clearPlan();
73
+ }, delay);
74
+ completedPlanClearTimer = timer;
75
+ timer.unref?.();
76
+ };
77
+
48
78
  const restore = (ctx: ExtensionContext): void => {
79
+ cancelCompletedPlanClear();
49
80
  const state = reconstructPlanState(ctx.sessionManager.getBranch());
50
81
  currentPlan = state.plan;
51
82
  displayMode = state.displayMode;
52
83
  if (ctx.hasUI) ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
53
84
  refresh(true);
85
+ scheduleCompletedPlanClear();
54
86
  };
55
87
 
56
88
  const persistPlan = (plan: PlanSnapshot): void => {
57
89
  currentPlan = plan;
58
90
  pi.appendEntry(EXTENSION_NAME, planSetEntry(plan));
59
91
  refresh(true);
92
+ scheduleCompletedPlanClear();
60
93
  };
61
94
 
62
95
  const setDisplayMode = (mode: PlanDisplayMode): void => {
@@ -171,9 +204,7 @@ export default function planExtension(pi: ExtensionAPI): void {
171
204
  const input = args.trim().toLowerCase();
172
205
  if (!input) return showFullPlan(ctx);
173
206
  if (input === "clear") {
174
- currentPlan = null;
175
- pi.appendEntry(EXTENSION_NAME, planClearEntry());
176
- refresh(true);
207
+ clearPlan();
177
208
  ctx.ui.notify("Plan cleared.", "info");
178
209
  return;
179
210
  }
@@ -209,6 +240,7 @@ export default function planExtension(pi: ExtensionAPI): void {
209
240
  };
210
241
  });
211
242
  pi.on("session_shutdown", async (_event, ctx) => {
243
+ cancelCompletedPlanClear();
212
244
  refreshWidget = undefined;
213
245
  try {
214
246
  ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
@@ -4,6 +4,7 @@ import { EXTENSION_NAME } from "./types.js";
4
4
  const MAX_STEPS = 50;
5
5
  const MAX_STEP_CHARS = 500;
6
6
  const MAX_EXPLANATION_CHARS = 2_000;
7
+ export const COMPLETED_PLAN_RETENTION_MS = 30_000;
7
8
 
8
9
  interface SessionEntryLike {
9
10
  type: string;
@@ -54,6 +55,7 @@ export function replacePlan(
54
55
  steps: readonly PlanStepInput[],
55
56
  explanation?: string,
56
57
  now = nowSeconds(),
58
+ completedAtMs = Date.now(),
57
59
  ): PlanSnapshot {
58
60
  const error = validatePlanInput(steps, explanation);
59
61
  if (error) throw new Error(error);
@@ -68,6 +70,8 @@ export function replacePlan(
68
70
  status: item.status,
69
71
  };
70
72
  });
73
+ const isComplete = normalizedSteps.every((item) => item.status === "completed");
74
+ const currentIsComplete = current?.steps.every((item) => item.status === "completed") === true;
71
75
 
72
76
  return {
73
77
  version: 1,
@@ -77,9 +81,18 @@ export function replacePlan(
77
81
  steps: normalizedSteps,
78
82
  createdAt: current?.createdAt ?? now,
79
83
  updatedAt: now,
84
+ ...(isComplete
85
+ ? { completedAtMs: currentIsComplete ? current.completedAtMs ?? current.updatedAt * 1_000 : completedAtMs }
86
+ : {}),
80
87
  };
81
88
  }
82
89
 
90
+ export function completedPlanClearDelay(plan: PlanSnapshot, now = Date.now()): number | null {
91
+ if (planProgress(plan).state !== "complete") return null;
92
+ const completedAtMs = plan.completedAtMs ?? plan.updatedAt * 1_000;
93
+ return Math.max(0, completedAtMs + COMPLETED_PLAN_RETENTION_MS - now);
94
+ }
95
+
83
96
  export function planProgress(plan: PlanSnapshot): PlanProgress {
84
97
  const completed = plan.steps.filter((item) => item.status === "completed").length;
85
98
  const pending = plan.steps.filter((item) => item.status === "pending").length;
@@ -149,6 +162,7 @@ function isPlanSnapshot(value: unknown): value is PlanSnapshot {
149
162
  typeof candidate.revision === "number" &&
150
163
  Array.isArray(candidate.steps) &&
151
164
  typeof candidate.createdAt === "number" &&
152
- typeof candidate.updatedAt === "number"
165
+ typeof candidate.updatedAt === "number" &&
166
+ (candidate.completedAtMs === undefined || typeof candidate.completedAtMs === "number")
153
167
  );
154
168
  }
@@ -20,6 +20,7 @@ export interface PlanSnapshot {
20
20
  steps: PlanStep[];
21
21
  createdAt: number;
22
22
  updatedAt: number;
23
+ completedAtMs?: number;
23
24
  }
24
25
 
25
26
  export interface PlanProgress {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,7 +52,7 @@
52
52
  "dependencies": {
53
53
  "pi-better-background-tasks": "0.2.11",
54
54
  "pi-better-goal": "0.2.1",
55
- "pi-better-plan": "0.1.3",
55
+ "pi-better-plan": "0.1.4",
56
56
  "pi-better-sandbox": "0.3.0",
57
57
  "pi-better-ssh": "0.1.1",
58
58
  "pi-better-subagents": "0.1.26"