Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pi-code-planner",
"version": "0.11.0",
"version": "0.10.0",
"description": "Structured planning, TDD, and Git worktrees for Pi local coding agents",
"keywords": [
"pi-package",
Expand Down
15 changes: 14 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
type PlannerContextBudgetDecision,
projectPlannerContextBudget,
shouldCancelOverlappingCompaction,
shouldClearStaleCompactIndicator,
shouldProactivelyCompact,
} from "./runtime/compact";
import {
Expand All @@ -101,6 +102,7 @@ import {
type PlannerContractToolName,
} from "./runtime/contracts";
import {
isWorkspaceCompactIndicatorActive,
openPlannerWorkspace,
registerPlannerDashboard,
setWorkspaceCompactIndicator,
Expand Down Expand Up @@ -3571,7 +3573,18 @@ function registerPlannerCompactEvents(
// safety cap. This can never clear a live compaction: summarization blocks the
// agent loop, so no turn can start while it is running.
const clearStaleCompactIndicator = (ctx: ExtensionContext) => {
if (!compactTimer) return;
// The banner (dashboard-mirrored, module-level) can outlive this closure's
// interval — e.g. a re-registration on /reload leaves a fresh timer at null
// while the "Compacting… 95%" line still shows over the resumed model. Clear
// on either signal so the stale banner never lingers past the resume.
if (
!shouldClearStaleCompactIndicator({
timerLive: compactTimer !== null,
bannerVisible: isWorkspaceCompactIndicatorActive(),
})
) {
return;
}
pendingCompact = null;
stopCompactIndicator(ctx);
};
Expand Down
33 changes: 33 additions & 0 deletions src/runtime/compact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
PLANNER_SYSTEM_INSTRUCTIONS_HEADER,
projectPlannerContextBudget,
shouldCancelOverlappingCompaction,
shouldClearStaleCompactIndicator,
shouldProactivelyCompact,
} from "./compact";
import type { PlannerPreflightResult } from "./preflight";
Expand Down Expand Up @@ -331,6 +332,38 @@ describe("planner compact runtime", () => {
});
});

describe("shouldClearStaleCompactIndicator", () => {
it("clears while the interval is still live", () => {
expect(
shouldClearStaleCompactIndicator({
timerLive: true,
bannerVisible: false,
}),
).toBe(true);
});

it("clears a banner that outlived its timer (the /reload-decoupled case)", () => {
// The regression: the closure interval was torn down (or re-created at
// null) while the module-level banner line still showed over the resumed
// model. Consulting the banner catches it.
expect(
shouldClearStaleCompactIndicator({
timerLive: false,
bannerVisible: true,
}),
).toBe(true);
});

it("does nothing when neither the timer nor the banner is up", () => {
expect(
shouldClearStaleCompactIndicator({
timerLive: false,
bannerVisible: false,
}),
).toBe(false);
});
});

describe("estimatePlannerInstructionTokens", () => {
it("mirrors Pi's conservative chars/4 heuristic", () => {
expect(estimatePlannerInstructionTokens("")).toBe(0);
Expand Down
21 changes: 21 additions & 0 deletions src/runtime/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ export function clearPlannerCompactionInFlight(
state.compactionInFlight = false;
}

/**
* Decide whether a resume signal (`agent_start` / `turn_start` / `message_start`)
* should tear down the compaction indicator. Summarization blocks the agent loop,
* so the loop only runs *between* compactions — any resume signal therefore means
* the compaction is over and an indicator still up is stale.
*
* We clear when EITHER our per-registration interval is live OR the shared,
* dashboard-mirrored banner line is still showing. Gating on the timer alone
* missed the case where the timer was already torn down (or re-created on
* `/reload`) without clearing the module-level banner: the interval was gone yet
* the "Compacting… 95%" line lingered over the resumed model. Consulting the
* banner line closes that gap for both the plain-chat widget and the
* /planner-dashboard mirror.
*/
export function shouldClearStaleCompactIndicator(input: {
timerLive: boolean;
bannerVisible: boolean;
}): boolean {
return input.timerLive || input.bannerVisible;
}

export function markPlannerControlledCompactStarted(
state: PlannerCompactRuntimeState,
): void {
Expand Down
15 changes: 15 additions & 0 deletions src/runtime/dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { visibleWidth } from "@earendil-works/pi-tui";
import { describe, expect, it } from "vitest";
import {
isWorkspaceCompactIndicatorActive,
PlannerWorkspaceComponent,
setWorkspaceCompactIndicator,
} from "./dashboard";
Expand Down Expand Up @@ -232,4 +233,18 @@ describe("PlannerWorkspaceComponent rendering", () => {
setWorkspaceCompactIndicator(null);
}
});

it("reports whether a compact banner is currently up", () => {
// The resume hooks in index.ts read this durable, module-level flag to clear
// a banner that outlived its per-registration timer.
try {
expect(isWorkspaceCompactIndicatorActive()).toBe(false);
setWorkspaceCompactIndicator("Compacting 68k tok (threshold) ▓▓░░ 40%");
expect(isWorkspaceCompactIndicatorActive()).toBe(true);
setWorkspaceCompactIndicator(null);
expect(isWorkspaceCompactIndicatorActive()).toBe(false);
} finally {
setWorkspaceCompactIndicator(null);
}
});
});
11 changes: 11 additions & 0 deletions src/runtime/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ export function setWorkspaceCompactIndicator(line: string | null): void {
compactIndicatorListener?.();
}

/**
* True while a compaction banner is currently showing anywhere. This is the one
* durable, module-level record of "an indicator is up" — the per-registration
* closure timer in index.ts can be torn down (or re-created on /reload) without
* clearing this line, so the resume hooks consult it to catch a banner that
* outlived its timer instead of gating solely on the timer.
*/
export function isWorkspaceCompactIndicatorActive(): boolean {
return compactIndicatorLine !== null;
}

export function registerPlannerDashboard(pi: ExtensionAPI): void {
pi.registerCommand("planner-dashboard", {
description:
Expand Down