diff --git a/CHANGELOG.md b/CHANGELOG.md index ca80f6c..15d362b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,13 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `on_task_complete`: each completed task is cleared individually after a few turns - Both auto-clear modes use a turn-based delay (matching `REMINDER_INTERVAL`) for consistent, non-jarring UX — tasks linger briefly so the user sees the completion before they disappear - **`AutoClearManager`** — extracted, testable class (`src/auto-clear.ts`) handling turn-based clearing logic with per-task and batch countdown tracking -- **15 new unit tests** — full coverage of all three auto-clear modes, turn delays, dependency cleanup, batch reset, and dynamic mode switching +- **20 new unit tests** — full coverage of all three auto-clear modes, turn delays, dependency cleanup, batch reset, dynamic mode switching, session reset, and store swap ### Changed - **Settings** — `/tasks` → Settings now shows "Auto-clear completed tasks" toggle with `never` / `on_list_complete` / `on_task_complete` values. Also configurable via `.pi/tasks-config.json`. ### Fixed -- **`/new` now correctly switches to a new session task store** — `storeUpgraded` and `persistedTasksShown` flags were never reset on `session_switch`, causing the store to stay pointed at the old session file. All session-scoped state (turn counters, reminder flags, auto-clear tracking) is now reset on `/new`. +- **`/new` and `/resume` now correctly switch session state** — `storeUpgraded` and `persistedTasksShown` flags were never reset on `session_switch`, causing the store to stay pointed at the old session file and the widget to not refresh. All session-scoped state (turn counters, reminder flags, auto-clear tracking) is now reset on both `/new` and `/resume`. Memory-mode tasks are explicitly cleared on `/new`. ## [0.4.0] - 2026-03-22 diff --git a/README.md b/README.md index 6ecd2ae..f6cc71d 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ src/ ```bash npm install npm run typecheck # TypeScript validation -npm test # Run unit tests (143 tests) +npm test # Run unit tests (145 tests) ``` ## License diff --git a/package-lock.json b/package-lock.json index 5f93b63..d60402d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tintinweb/pi-tasks", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tintinweb/pi-tasks", - "version": "0.4.0", + "version": "0.4.1", "license": "MIT", "dependencies": { "@mariozechner/pi-coding-agent": "^0.61.1", diff --git a/src/auto-clear.ts b/src/auto-clear.ts index b3344f9..08464ed 100644 --- a/src/auto-clear.ts +++ b/src/auto-clear.ts @@ -19,7 +19,7 @@ export class AutoClearManager { private allCompletedAtTurn: number | null = null; constructor( - private store: TaskStore, + private getStore: () => TaskStore, private getMode: () => AutoClearMode, /** How many turns completed tasks linger before auto-clearing. */ private clearDelayTurns = 4, @@ -39,7 +39,7 @@ export class AutoClearManager { /** Check if all tasks are completed and start/reset the batch countdown. */ private checkAllCompleted(currentTurn: number): void { - const tasks = this.store.list(); + const tasks = this.getStore().list(); if (tasks.length > 0 && tasks.every(t => t.status === "completed")) { if (this.allCompletedAtTurn === null) this.allCompletedAtTurn = currentTurn; } else { @@ -68,19 +68,19 @@ export class AutoClearManager { if (mode === "on_task_complete") { for (const [taskId, turn] of this.completedAtTurn) { - const task = this.store.get(taskId); + const task = this.getStore().get(taskId); if (!task || task.status !== "completed") { // Task was deleted or reverted — drop stale tracking entry this.completedAtTurn.delete(taskId); } else if (currentTurn - turn >= this.clearDelayTurns) { - this.store.delete(taskId); + this.getStore().delete(taskId); this.completedAtTurn.delete(taskId); cleared = true; } } } else if (mode === "on_list_complete" && this.allCompletedAtTurn !== null) { if (currentTurn - this.allCompletedAtTurn >= this.clearDelayTurns) { - this.store.clearCompleted(); + this.getStore().clearCompleted(); this.allCompletedAtTurn = null; cleared = true; } diff --git a/src/index.ts b/src/index.ts index 602720a..12327c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -167,7 +167,7 @@ export default function (pi: ExtensionAPI) { return prompt; } - const autoClear = new AutoClearManager(store, () => cfg.autoClearCompleted ?? "on_list_complete", AUTO_CLEAR_DELAY); + const autoClear = new AutoClearManager(() => store, () => cfg.autoClearCompleted ?? "on_list_complete", AUTO_CLEAR_DELAY); // ── Subagent completion listener ── // Listens for subagent lifecycle events to update task status and optionally cascade. @@ -341,13 +341,18 @@ export default function (pi: ExtensionAPI) { widget.setUICtx(ctx.ui as UICtx); const isResume = event?.reason === "resume"; - if (!isResume) { - storeUpgraded = false; - persistedTasksShown = false; - currentTurn = 0; - lastTaskToolUseTurn = 0; - reminderInjectedThisCycle = false; - autoClear.reset(); + + // Reset session-scoped state for both /new and /resume + storeUpgraded = false; + persistedTasksShown = false; + currentTurn = 0; + lastTaskToolUseTurn = 0; + reminderInjectedThisCycle = false; + autoClear.reset(); + + // Memory mode has no file-backed store to switch — clear explicitly on /new + if (!isResume && taskScope === "memory") { + store.clearAll(); } upgradeStoreIfNeeded(ctx); diff --git a/test/auto-clear.test.ts b/test/auto-clear.test.ts index 355821b..e956806 100644 --- a/test/auto-clear.test.ts +++ b/test/auto-clear.test.ts @@ -9,7 +9,7 @@ describe("auto-clear: on_task_complete mode", () => { beforeEach(() => { store = new TaskStore(); - manager = new AutoClearManager(store, () => "on_task_complete"); + manager = new AutoClearManager(() => store, () => "on_task_complete"); }); it("does not clear completed task before REMINDER_INTERVAL turns", () => { @@ -98,7 +98,7 @@ describe("auto-clear: on_list_complete mode", () => { beforeEach(() => { store = new TaskStore(); - manager = new AutoClearManager(store, () => "on_list_complete"); + manager = new AutoClearManager(() => store, () => "on_list_complete"); }); it("does not clear when some tasks are still pending", () => { @@ -187,7 +187,7 @@ describe("auto-clear: never mode", () => { beforeEach(() => { store = new TaskStore(); - manager = new AutoClearManager(store, () => "never"); + manager = new AutoClearManager(() => store, () => "never"); }); it("never clears completed tasks regardless of turns", () => { @@ -218,7 +218,7 @@ describe("auto-clear: dynamic mode switching", () => { it("respects mode changes via getMode callback", () => { const store = new TaskStore(); let mode: AutoClearMode = "never"; - const manager = new AutoClearManager(store, () => mode); + const manager = new AutoClearManager(() => store, () => mode); store.create("Task", "Desc"); store.update("1", { status: "completed" }); @@ -236,10 +236,45 @@ describe("auto-clear: dynamic mode switching", () => { }); }); +describe("auto-clear: store getter (session switch)", () => { + it("operates on the current store after swap", () => { + let store = new TaskStore(); + const manager = new AutoClearManager(() => store, () => "on_task_complete"); + + store.create("Old task", "Desc"); + store.update("1", { status: "completed" }); + manager.trackCompletion("1", 1); + + // Simulate session switch — swap store + store = new TaskStore(); + store.create("New task", "Desc"); + manager.reset(); + + // Old task tracking was reset, new store has no completed tasks + manager.onTurnStart(5); + expect(store.list()).toHaveLength(1); + expect(store.get("1")!.subject).toBe("New task"); + }); + + it("clears from new store, not old store", () => { + let store = new TaskStore(); + const manager = new AutoClearManager(() => store, () => "on_task_complete"); + + // Swap to new store with a completed task + store = new TaskStore(); + store.create("Task in new store", "Desc"); + store.update("1", { status: "completed" }); + manager.trackCompletion("1", 1); + + manager.onTurnStart(5); + expect(store.get("1")).toBeUndefined(); // cleared from new store + }); +}); + describe("auto-clear: reset (new session)", () => { it("reset clears per-task tracking so old completions don't fire", () => { const store = new TaskStore(); - const manager = new AutoClearManager(store, () => "on_task_complete"); + const manager = new AutoClearManager(() => store, () => "on_task_complete"); store.create("Task", "Desc"); store.update("1", { status: "completed" }); @@ -255,7 +290,7 @@ describe("auto-clear: reset (new session)", () => { it("reset clears batch countdown so old all-completed state doesn't fire", () => { const store = new TaskStore(); - const manager = new AutoClearManager(store, () => "on_list_complete"); + const manager = new AutoClearManager(() => store, () => "on_list_complete"); store.create("Task", "Desc"); store.update("1", { status: "completed" }); @@ -271,7 +306,7 @@ describe("auto-clear: reset (new session)", () => { it("tracking works normally after reset", () => { const store = new TaskStore(); - const manager = new AutoClearManager(store, () => "on_task_complete"); + const manager = new AutoClearManager(() => store, () => "on_task_complete"); store.create("Task", "Desc"); store.update("1", { status: "completed" });