fix /new /resume

This commit is contained in:
tintinweb
2026-03-22 22:09:14 +01:00
parent 30c78e4eb8
commit e21350c794
6 changed files with 65 additions and 25 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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",
+5 -5
View File
@@ -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;
}
+7 -2
View File
@@ -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) {
// 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);
+42 -7
View File
@@ -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" });