mirror of
https://github.com/wassname/pi-lgtm.git
synced 2026-06-27 17:01:35 +08:00
remove autocomplete
honor /new
This commit is contained in:
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.1] - 2026-03-22
|
||||
|
||||
### Added
|
||||
- **Auto-clear completed tasks** — new `autoClearCompleted` setting with three modes:
|
||||
- `never`: completed tasks stay visible until manually cleared
|
||||
- `on_list_complete` **(default)**: completed tasks are cleared after all tasks are done and a few turns pass — user sees the "all done" state before cleanup
|
||||
- `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
|
||||
|
||||
### 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`.
|
||||
|
||||
## [0.4.0] - 2026-03-22
|
||||
|
||||
### Added
|
||||
@@ -111,6 +128,7 @@ Initial release — Claude Code-style task tracking and coordination for pi.
|
||||
- **Background process tracker** — output buffering (stdout + stderr), waiter notification, graceful stop with timeout escalation (SIGTERM → 5s → SIGKILL).
|
||||
- **78 unit tests** — task store CRUD, dependencies, warnings, file persistence; widget rendering, icons, spinners, token/duration formatting; process tracker lifecycle.
|
||||
|
||||
[0.4.1]: https://github.com/tintinweb/pi-tasks/releases/tag/v0.4.1
|
||||
[0.4.0]: https://github.com/tintinweb/pi-tasks/releases/tag/v0.4.0
|
||||
[0.3.3]: https://github.com/tintinweb/pi-tasks/releases/tag/v0.3.3
|
||||
[0.3.2]: https://github.com/tintinweb/pi-tasks/releases/tag/v0.3.2
|
||||
|
||||
@@ -190,7 +190,19 @@ Task storage is controlled by the `taskScope` setting (`/tasks` → Settings →
|
||||
|
||||
On new session start, if all persisted tasks are completed they are auto-cleared for a clean slate. On session resume, all tasks (including completed) are shown so the user can review progress. Empty session files are automatically deleted when all tasks are cleared.
|
||||
|
||||
Settings (`taskScope`, `autoCascade`) are saved to `<cwd>/.pi/tasks-config.json`.
|
||||
### Auto-clear completed tasks
|
||||
|
||||
The `autoClearCompleted` setting controls automatic cleanup of completed tasks:
|
||||
|
||||
| Mode | Behaviour |
|
||||
|------|-----------|
|
||||
| `never` | Completed tasks stay visible until manually cleared via `/tasks` → Clear completed |
|
||||
| `on_list_complete` **(default)** | Cleared after all tasks are done and a few idle turns pass |
|
||||
| `on_task_complete` | Each completed task cleared individually after a few turns |
|
||||
|
||||
Both auto-clear modes use a turn-based delay for non-jarring UX — tasks linger briefly so you see the completion before they disappear.
|
||||
|
||||
Settings (`taskScope`, `autoCascade`, `autoClearCompleted`) are saved to `<cwd>/.pi/tasks-config.json`.
|
||||
|
||||
### Override via environment variables
|
||||
|
||||
@@ -232,7 +244,7 @@ Tasks
|
||||
- **Create task** — input prompts for subject and description
|
||||
- **Clear completed** — remove all completed tasks
|
||||
- **Clear all** — remove all tasks regardless of status
|
||||
- **Settings** — configure task storage mode and auto-cascade (saved to `tasks-config.json`)
|
||||
- **Settings** — configure task storage, auto-cascade, and auto-clear completed tasks (saved to `tasks-config.json`)
|
||||
|
||||
## Cross-extension Communication with [`@tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents)
|
||||
|
||||
@@ -291,7 +303,8 @@ src/
|
||||
├── index.ts # Extension entry: 7 tools + /tasks command + widget + subagent integration
|
||||
├── types.ts # Task, TaskStatus, BackgroundProcess types
|
||||
├── task-store.ts # File-backed store with CRUD, dependencies, locking
|
||||
├── tasks-config.ts # Config persistence (taskScope, autoCascade) → .pi/tasks-config.json
|
||||
├── auto-clear.ts # Turn-based auto-clearing of completed tasks (AutoClearManager)
|
||||
├── tasks-config.ts # Config persistence (taskScope, autoCascade, autoClearCompleted) → .pi/tasks-config.json
|
||||
├── process-tracker.ts # Background process output buffering and stop
|
||||
└── ui/
|
||||
├── task-widget.ts # Persistent widget with status icons and spinner
|
||||
@@ -307,7 +320,7 @@ src/
|
||||
```bash
|
||||
npm install
|
||||
npm run typecheck # TypeScript validation
|
||||
npm test # Run unit tests (125 tests)
|
||||
npm test # Run unit tests (143 tests)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tintinweb/pi-tasks",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "A pi extension that brings Claude Code-style task tracking and coordination to pi.",
|
||||
"author": "tintinweb",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* auto-clear.ts — Turn-based auto-clearing of completed tasks.
|
||||
*
|
||||
* Two modes:
|
||||
* - "on_task_complete": each completed task gets its own REMINDER_INTERVAL countdown, deleted individually
|
||||
* - "on_list_complete": countdown starts when ALL tasks are completed, cleared as a batch
|
||||
*
|
||||
* Both use the same turn delay (REMINDER_INTERVAL) for consistency.
|
||||
*/
|
||||
|
||||
import type { TaskStore } from "./task-store.js";
|
||||
|
||||
export type AutoClearMode = "never" | "on_list_complete" | "on_task_complete";
|
||||
|
||||
export class AutoClearManager {
|
||||
/** Per-task: turn when task was marked completed ("on_task_complete" mode). */
|
||||
private completedAtTurn = new Map<string, number>();
|
||||
/** Turn when ALL tasks became completed ("on_list_complete" mode). */
|
||||
private allCompletedAtTurn: number | null = null;
|
||||
|
||||
constructor(
|
||||
private store: TaskStore,
|
||||
private getMode: () => AutoClearMode,
|
||||
/** How many turns completed tasks linger before auto-clearing. */
|
||||
private clearDelayTurns = 4,
|
||||
) {}
|
||||
|
||||
/** Record a task completion. Call AFTER cascade logic. */
|
||||
trackCompletion(taskId: string, currentTurn: number): void {
|
||||
const mode = this.getMode();
|
||||
if (mode === "never") return;
|
||||
|
||||
if (mode === "on_task_complete") {
|
||||
this.completedAtTurn.set(taskId, currentTurn);
|
||||
} else if (mode === "on_list_complete") {
|
||||
this.checkAllCompleted(currentTurn);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if all tasks are completed and start/reset the batch countdown. */
|
||||
private checkAllCompleted(currentTurn: number): void {
|
||||
const tasks = this.store.list();
|
||||
if (tasks.length > 0 && tasks.every(t => t.status === "completed")) {
|
||||
if (this.allCompletedAtTurn === null) this.allCompletedAtTurn = currentTurn;
|
||||
} else {
|
||||
this.allCompletedAtTurn = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset batch countdown (e.g., when a new task is created or task goes non-completed). */
|
||||
resetBatchCountdown(): void {
|
||||
this.allCompletedAtTurn = null;
|
||||
}
|
||||
|
||||
/** Reset all tracking state (e.g., on new session). */
|
||||
reset(): void {
|
||||
this.completedAtTurn.clear();
|
||||
this.allCompletedAtTurn = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on each turn start. Deletes tasks whose linger period has expired.
|
||||
* Returns true if any tasks were cleared.
|
||||
*/
|
||||
onTurnStart(currentTurn: number): boolean {
|
||||
const mode = this.getMode();
|
||||
let cleared = false;
|
||||
|
||||
if (mode === "on_task_complete") {
|
||||
for (const [taskId, turn] of this.completedAtTurn) {
|
||||
const task = this.store.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.completedAtTurn.delete(taskId);
|
||||
cleared = true;
|
||||
}
|
||||
}
|
||||
} else if (mode === "on_list_complete" && this.allCompletedAtTurn !== null) {
|
||||
if (currentTurn - this.allCompletedAtTurn >= this.clearDelayTurns) {
|
||||
this.store.clearCompleted();
|
||||
this.allCompletedAtTurn = null;
|
||||
cleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
return cleared;
|
||||
}
|
||||
}
|
||||
+34
-3
@@ -18,6 +18,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { join, resolve } from "node:path";
|
||||
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { AutoClearManager } from "./auto-clear.js";
|
||||
import { ProcessTracker } from "./process-tracker.js";
|
||||
import { TaskStore } from "./task-store.js";
|
||||
import { loadTasksConfig } from "./tasks-config.js";
|
||||
@@ -43,6 +44,9 @@ const TASK_TOOL_NAMES = new Set(["TaskCreate", "TaskList", "TaskGet", "TaskUpdat
|
||||
/** How many turns without task tool usage before injecting a reminder. */
|
||||
const REMINDER_INTERVAL = 4;
|
||||
|
||||
/** How many turns completed tasks linger before auto-clearing. */
|
||||
const AUTO_CLEAR_DELAY = 4;
|
||||
|
||||
const SYSTEM_REMINDER = `<system-reminder>
|
||||
The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate to add new tasks and TaskUpdate to update task status (set to in_progress when starting, completed when done). Also consider cleaning up the task list if it has become stale. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable. Make sure that you NEVER mention this reminder to the user
|
||||
</system-reminder>`;
|
||||
@@ -163,6 +167,8 @@ export default function (pi: ExtensionAPI) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -203,6 +209,7 @@ export default function (pi: ExtensionAPI) {
|
||||
}
|
||||
}
|
||||
}
|
||||
autoClear.trackCompletion(task.id, currentTurn);
|
||||
widget.update();
|
||||
});
|
||||
|
||||
@@ -219,9 +226,11 @@ export default function (pi: ExtensionAPI) {
|
||||
if (status === "stopped") {
|
||||
// Intentional stop — mark completed, preserve partial result
|
||||
store.update(task.id, { status: "completed", metadata: { ...task.metadata, result: result || task.metadata?.result } });
|
||||
autoClear.trackCompletion(task.id, currentTurn);
|
||||
} else {
|
||||
// Actual error — revert to pending
|
||||
store.update(task.id, { status: "pending", metadata: { ...task.metadata, lastError: error || status } });
|
||||
autoClear.resetBatchCountdown();
|
||||
}
|
||||
widget.setActiveTask(task.id, false);
|
||||
widget.update();
|
||||
@@ -272,6 +281,7 @@ export default function (pi: ExtensionAPI) {
|
||||
latestCtx = ctx;
|
||||
widget.setUICtx(ctx.ui as UICtx);
|
||||
upgradeStoreIfNeeded(ctx);
|
||||
if (autoClear.onTurnStart(currentTurn)) widget.update();
|
||||
});
|
||||
|
||||
// ── Token usage tracking ──
|
||||
@@ -323,12 +333,25 @@ export default function (pi: ExtensionAPI) {
|
||||
}
|
||||
});
|
||||
|
||||
// session_switch fires on resume (reason: "resume") — reload persisted tasks.
|
||||
// session_switch fires on /new (reason: "new") and /resume (reason: "resume").
|
||||
// On /new: reset all session-scoped state so the store switches to the new session file.
|
||||
// On resume: reload persisted tasks from the existing session file.
|
||||
pi.on("session_switch" as any, async (event: any, ctx: ExtensionContext) => {
|
||||
latestCtx = ctx;
|
||||
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();
|
||||
}
|
||||
|
||||
upgradeStoreIfNeeded(ctx);
|
||||
showPersistedTasks(event?.reason === "resume");
|
||||
showPersistedTasks(isResume);
|
||||
});
|
||||
|
||||
// Keep latestCtx fresh on every tool execution as well.
|
||||
@@ -401,6 +424,7 @@ All tasks are created with status \`pending\`.
|
||||
}),
|
||||
|
||||
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
autoClear.resetBatchCountdown();
|
||||
const meta = params.metadata ?? {};
|
||||
if (params.agentType) meta.agentType = params.agentType;
|
||||
const task = store.create(params.subject, params.description, params.activeForm, Object.keys(meta).length > 0 ? meta : undefined);
|
||||
@@ -660,8 +684,12 @@ Set up task dependencies:
|
||||
// Update widget active task tracking
|
||||
if (fields.status === "in_progress") {
|
||||
widget.setActiveTask(taskId);
|
||||
autoClear.resetBatchCountdown();
|
||||
} else if (fields.status === "pending") {
|
||||
autoClear.resetBatchCountdown();
|
||||
} else if (fields.status === "completed" || fields.status === "deleted") {
|
||||
widget.setActiveTask(taskId, false);
|
||||
if (fields.status === "completed") autoClear.trackCompletion(taskId, currentTurn);
|
||||
}
|
||||
|
||||
widget.update();
|
||||
@@ -783,6 +811,7 @@ Set up task dependencies:
|
||||
const task = store.get(resolvedId);
|
||||
if (task?.metadata?.agentId && task.status === "in_progress") {
|
||||
store.update(taskId, { status: "completed" });
|
||||
autoClear.trackCompletion(taskId, currentTurn);
|
||||
await stopSubagent(task.metadata.agentId);
|
||||
widget.setActiveTask(taskId, false);
|
||||
widget.update();
|
||||
@@ -792,6 +821,7 @@ Set up task dependencies:
|
||||
}
|
||||
|
||||
store.update(taskId, { status: "completed" });
|
||||
autoClear.trackCompletion(taskId, currentTurn);
|
||||
widget.setActiveTask(taskId, false);
|
||||
widget.update();
|
||||
return textResult(`Task #${taskId} stopped successfully`);
|
||||
@@ -1006,6 +1036,7 @@ Set up task dependencies:
|
||||
return viewTasks();
|
||||
} else if (action === "✓ Complete") {
|
||||
store.update(taskId, { status: "completed" });
|
||||
autoClear.trackCompletion(taskId, currentTurn);
|
||||
widget.setActiveTask(taskId, false);
|
||||
widget.update();
|
||||
return viewTasks();
|
||||
@@ -1019,7 +1050,7 @@ Set up task dependencies:
|
||||
};
|
||||
|
||||
const settingsMenu = (): Promise<void> =>
|
||||
openSettingsMenu(ui, cfg, mainMenu);
|
||||
openSettingsMenu(ui, cfg, mainMenu, AUTO_CLEAR_DELAY);
|
||||
|
||||
const createTask = async (): Promise<void> => {
|
||||
const subject = await ui.input("Task subject");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { dirname, join } from "node:path";
|
||||
export interface TasksConfig {
|
||||
taskScope?: "memory" | "session" | "project"; // default: "session"
|
||||
autoCascade?: boolean; // default: false
|
||||
autoClearCompleted?: "never" | "on_list_complete" | "on_task_complete"; // default: "on_list_complete"
|
||||
}
|
||||
|
||||
const CONFIG_PATH = join(process.cwd(), ".pi", "tasks-config.json");
|
||||
|
||||
@@ -25,6 +25,7 @@ export async function openSettingsMenu(
|
||||
ui: SettingsUI,
|
||||
cfg: TasksConfig,
|
||||
onBack: () => Promise<void>,
|
||||
clearDelayTurns: number,
|
||||
): Promise<void> {
|
||||
await ui.custom((_tui, theme, _kb, done) => {
|
||||
const items: SettingItem[] = [
|
||||
@@ -48,6 +49,17 @@ export async function openSettingsMenu(
|
||||
currentValue: (cfg.autoCascade ?? false) ? "on" : "off",
|
||||
values: ["on", "off"],
|
||||
},
|
||||
{
|
||||
id: "autoClearCompleted",
|
||||
label: "Auto-clear completed tasks",
|
||||
description:
|
||||
"never: completed tasks stay visible until manually cleared. " +
|
||||
"on_list_complete: cleared automatically after all tasks are done. " +
|
||||
"on_task_complete: each task cleared shortly after it completes. " +
|
||||
`Clearing lags ~${clearDelayTurns} turns.`,
|
||||
currentValue: cfg.autoClearCompleted ?? "on_list_complete",
|
||||
values: ["never", "on_list_complete", "on_task_complete"],
|
||||
},
|
||||
];
|
||||
|
||||
const list = new SettingsList(
|
||||
@@ -63,6 +75,10 @@ export async function openSettingsMenu(
|
||||
cfg.taskScope = newValue as "memory" | "session" | "project";
|
||||
saveTasksConfig(cfg);
|
||||
}
|
||||
if (id === "autoClearCompleted") {
|
||||
cfg.autoClearCompleted = newValue as TasksConfig["autoClearCompleted"];
|
||||
saveTasksConfig(cfg);
|
||||
}
|
||||
},
|
||||
/* onCancel */ () => done(undefined),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { AutoClearMode } from "../src/auto-clear.js";
|
||||
import { AutoClearManager } from "../src/auto-clear.js";
|
||||
import { TaskStore } from "../src/task-store.js";
|
||||
|
||||
describe("auto-clear: on_task_complete mode", () => {
|
||||
let store: TaskStore;
|
||||
let manager: AutoClearManager;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new TaskStore();
|
||||
manager = new AutoClearManager(store, () => "on_task_complete");
|
||||
});
|
||||
|
||||
it("does not clear completed task before REMINDER_INTERVAL turns", () => {
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
// Turns 2, 3, 4 — not enough
|
||||
for (let turn = 2; turn <= 4; turn++) {
|
||||
manager.onTurnStart(turn);
|
||||
}
|
||||
expect(store.get("1")).toBeDefined();
|
||||
expect(store.get("1")!.status).toBe("completed");
|
||||
});
|
||||
|
||||
it("clears completed task after REMINDER_INTERVAL turns", () => {
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
// Turn 5 = turn 1 + 4 (REMINDER_INTERVAL)
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeUndefined();
|
||||
expect(store.list()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("clears each task independently based on its own completion turn", () => {
|
||||
store.create("Task A", "Desc");
|
||||
store.create("Task B", "Desc");
|
||||
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
store.update("2", { status: "completed" });
|
||||
manager.trackCompletion("2", 3);
|
||||
|
||||
// Turn 5: Task A expires (1+4), Task B still lingers (3+4=7)
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeUndefined();
|
||||
expect(store.get("2")).toBeDefined();
|
||||
|
||||
// Turn 7: Task B expires
|
||||
manager.onTurnStart(7);
|
||||
expect(store.get("2")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not clear pending or in_progress tasks", () => {
|
||||
store.create("Pending", "Desc");
|
||||
store.create("In Progress", "Desc");
|
||||
store.create("Completed", "Desc");
|
||||
store.update("2", { status: "in_progress" });
|
||||
store.update("3", { status: "completed" });
|
||||
manager.trackCompletion("3", 1);
|
||||
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeDefined(); // pending — untouched
|
||||
expect(store.get("2")).toBeDefined(); // in_progress — untouched
|
||||
expect(store.get("3")).toBeUndefined(); // completed — cleared
|
||||
});
|
||||
|
||||
it("cleans up dependency edges when auto-clearing", () => {
|
||||
store.create("Blocker", "Desc");
|
||||
store.create("Blocked", "Desc");
|
||||
store.update("1", { addBlocks: ["2"] });
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeUndefined();
|
||||
expect(store.get("2")!.blockedBy).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns true when tasks are cleared", () => {
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
expect(manager.onTurnStart(4)).toBe(false);
|
||||
expect(manager.onTurnStart(5)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto-clear: on_list_complete mode", () => {
|
||||
let store: TaskStore;
|
||||
let manager: AutoClearManager;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new TaskStore();
|
||||
manager = new AutoClearManager(store, () => "on_list_complete");
|
||||
});
|
||||
|
||||
it("does not clear when some tasks are still pending", () => {
|
||||
store.create("Done", "Desc");
|
||||
store.create("Pending", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
for (let turn = 2; turn <= 10; turn++) {
|
||||
manager.onTurnStart(turn);
|
||||
}
|
||||
expect(store.get("1")).toBeDefined();
|
||||
expect(store.list()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("does not clear immediately when all tasks complete", () => {
|
||||
store.create("A", "Desc");
|
||||
store.create("B", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
store.update("2", { status: "completed" });
|
||||
manager.trackCompletion("2", 1);
|
||||
|
||||
// Turns 2-4: not enough
|
||||
for (let turn = 2; turn <= 4; turn++) {
|
||||
manager.onTurnStart(turn);
|
||||
}
|
||||
expect(store.list()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("clears all completed tasks after REMINDER_INTERVAL turns when all are completed", () => {
|
||||
store.create("A", "Desc");
|
||||
store.create("B", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
store.update("2", { status: "completed" });
|
||||
manager.trackCompletion("2", 1);
|
||||
|
||||
manager.onTurnStart(5);
|
||||
expect(store.list()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("resets countdown when a new task is created before REMINDER_INTERVAL", () => {
|
||||
store.create("A", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
// Turn 3: new task created — reset countdown
|
||||
manager.onTurnStart(3);
|
||||
manager.resetBatchCountdown();
|
||||
store.create("B", "Desc");
|
||||
|
||||
// Turn 5 would have cleared, but countdown was reset at turn 3
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeDefined(); // still around — list isn't all completed
|
||||
});
|
||||
|
||||
it("resets countdown when a task goes back to in_progress", () => {
|
||||
store.create("A", "Desc");
|
||||
store.create("B", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
store.update("2", { status: "completed" });
|
||||
manager.trackCompletion("2", 1);
|
||||
|
||||
// Turn 3: task 2 goes back to in_progress
|
||||
manager.onTurnStart(3);
|
||||
store.update("2", { status: "in_progress" });
|
||||
manager.resetBatchCountdown();
|
||||
|
||||
// Turn 5: would have cleared, but countdown was reset
|
||||
manager.onTurnStart(5);
|
||||
expect(store.list()).toHaveLength(2); // both still here
|
||||
});
|
||||
|
||||
it("returns true when tasks are cleared", () => {
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
expect(manager.onTurnStart(4)).toBe(false);
|
||||
expect(manager.onTurnStart(5)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto-clear: never mode", () => {
|
||||
let store: TaskStore;
|
||||
let manager: AutoClearManager;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new TaskStore();
|
||||
manager = new AutoClearManager(store, () => "never");
|
||||
});
|
||||
|
||||
it("never clears completed tasks regardless of turns", () => {
|
||||
store.create("A", "Desc");
|
||||
store.create("B", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
store.update("2", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
manager.trackCompletion("2", 1);
|
||||
|
||||
for (let turn = 2; turn <= 20; turn++) {
|
||||
manager.onTurnStart(turn);
|
||||
}
|
||||
expect(store.list()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("trackCompletion is a no-op", () => {
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
manager.onTurnStart(100);
|
||||
expect(store.get("1")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
|
||||
// Track in never mode — no-op
|
||||
manager.trackCompletion("1", 1);
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeDefined();
|
||||
|
||||
// Switch to on_task_complete and re-track
|
||||
mode = "on_task_complete";
|
||||
manager.trackCompletion("1", 5);
|
||||
manager.onTurnStart(9);
|
||||
expect(store.get("1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
// Simulate /new — reset before the delay expires
|
||||
manager.reset();
|
||||
|
||||
// Old completion should NOT trigger after reset
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeDefined();
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
|
||||
// Simulate /new — reset before the delay expires
|
||||
manager.reset();
|
||||
|
||||
// Old batch countdown should NOT trigger after reset
|
||||
manager.onTurnStart(5);
|
||||
expect(store.get("1")).toBeDefined();
|
||||
});
|
||||
|
||||
it("tracking works normally after reset", () => {
|
||||
const store = new TaskStore();
|
||||
const manager = new AutoClearManager(store, () => "on_task_complete");
|
||||
|
||||
store.create("Task", "Desc");
|
||||
store.update("1", { status: "completed" });
|
||||
manager.trackCompletion("1", 1);
|
||||
manager.reset();
|
||||
|
||||
// Re-track after reset with new turn baseline
|
||||
manager.trackCompletion("1", 10);
|
||||
manager.onTurnStart(14);
|
||||
expect(store.get("1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user