@leo-alvarenga/pi-todo-list 0.5.3 → 0.6.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo-alvarenga/pi-todo-list",
3
- "version": "0.5.3",
3
+ "version": "0.6.3",
4
4
  "description": "Session-aware todo list overlay for Pi: todo tool, /todos command, and a live TUI panel",
5
5
  "license": "MIT",
6
6
  "repository": "github.com/leo-alvarenga/pi-mono",
@@ -15,10 +15,10 @@
15
15
  ],
16
16
  "files": [
17
17
  "src",
18
+ "!src/test",
18
19
  "skills",
19
20
  "README.md",
20
- "LICENSE",
21
- "!src/*.test.ts"
21
+ "LICENSE"
22
22
  ],
23
23
  "peerDependencies": {
24
24
  "@earendil-works/pi-ai": "*",
@@ -27,7 +27,7 @@
27
27
  "typebox": "*"
28
28
  },
29
29
  "dependencies": {
30
- "@leo-alvarenga/pi-ext-core": "^0.3.2"
30
+ "@leo-alvarenga/pi-ext-core": "^0.4.3"
31
31
  },
32
32
  "pi": {
33
33
  "extensions": [
@@ -36,11 +36,12 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.0.0",
39
- "tsx": "^4.19.0",
40
- "typescript": "^7.0.2"
39
+ "typescript": "^7.0.2",
40
+ "vitest": "^3.2.4"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsc --noEmit",
44
- "typecheck": "tsc --noEmit"
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run"
45
46
  }
46
47
  }
package/src/command.ts CHANGED
@@ -4,8 +4,8 @@ import { Text } from "@earendil-works/pi-tui";
4
4
  import { applyAction } from "./core";
5
5
  import { groupByStatus } from "./query";
6
6
  import { REPORT_ENTRY } from "./constants";
7
- import type { TodoStore } from "./state";
8
- import type { Todo } from "./types";
7
+ import type { SessionRecordStore } from "@leo-alvarenga/pi-ext-core";
8
+ import type { Todo, TodoState } from "./types";
9
9
  import { getStyledTodo } from "./utils";
10
10
 
11
11
  const renderList = (todos: Todo[], theme: Theme): string => {
@@ -30,7 +30,10 @@ const renderList = (todos: Todo[], theme: Theme): string => {
30
30
  return lines.join("\n");
31
31
  };
32
32
 
33
- export function registerTodosCommand(pi: ExtensionAPI, store: TodoStore): void {
33
+ export function registerTodosCommand(
34
+ pi: ExtensionAPI,
35
+ store: SessionRecordStore<TodoState>,
36
+ ): void {
34
37
  pi.registerCommand("todos", {
35
38
  description: "Show all todos grouped by status",
36
39
  handler: async (_args, ctx) => {
package/src/constants.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { KeyId } from "@earendil-works/pi-tui";
2
- import { TodoStatus, TodoStatusUi } from "./types";
2
+ import type { StatusUi } from "@leo-alvarenga/pi-ext-core";
3
+
4
+ import type { TodoStatus } from "./types";
3
5
 
4
6
  /** Task statuses, in lifecycle order. All limits are hardcoded — no config files. */
5
7
  export const STATUSES = ["pending", "in-progress", "completed"] as const;
6
8
 
7
- export const STATUS_STYLES: Record<TodoStatus, TodoStatusUi> = {
9
+ export const STATUS_STYLES: Record<TodoStatus, StatusUi> = {
8
10
  pending: { icon: " ", fg: "text", color: "text" },
9
11
  "in-progress": { icon: "󱥸 ", fg: "accent", color: "accent", bold: true },
10
12
  completed: { icon: " ", fg: "dim", color: "success", strikethrough: true },
@@ -27,8 +29,3 @@ export const REPORT_ENTRY = "todos.report";
27
29
 
28
30
  /** Chord that toggles the panel. Chosen in setup: free in pi's default keybindings. */
29
31
  export const PANEL_TOGGLE_CHORD: KeyId = "alt+t";
30
-
31
- export const PANEL_STATE_ICON = {
32
- collapsed: "󰅂",
33
- expanded: "󰅀",
34
- };
package/src/index.ts CHANGED
@@ -2,14 +2,13 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  import { registerTodosCommand } from "./command";
4
4
  import { STATE_ENTRY, WIDGET_KEY } from "./constants";
5
- import { TodoStore } from "./state";
5
+ import { createTodoStore } from "./state";
6
6
  import { registerTodoTool, registerTodoCompleteAllTool } from "./tool";
7
7
  import { registerTodoWidget } from "./widget";
8
8
 
9
9
  export default function (pi: ExtensionAPI): void {
10
- const store = new TodoStore(
11
- (snapshot) => pi.appendEntry(STATE_ENTRY, snapshot),
12
- undefined,
10
+ const store = createTodoStore((snapshot) =>
11
+ pi.appendEntry(STATE_ENTRY, snapshot),
13
12
  );
14
13
 
15
14
  const panelControls = registerTodoWidget(pi, store);
package/src/state.ts CHANGED
@@ -1,45 +1,25 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { createSessionStore, SessionRecordStore } from "@leo-alvarenga/pi-ext-core";
3
+ import {
4
+ createSessionStore,
5
+ type SessionRecordStore,
6
+ } from "@leo-alvarenga/pi-ext-core";
4
7
 
5
8
  import { STATE_ENTRY } from "./constants";
6
- import type { TodoDetails, TodoState } from "./types";
7
-
8
- export class TodoStore {
9
- private store: SessionRecordStore<TodoState>;
10
-
11
- constructor(
12
- private readonly persist: (snapshot: TodoState) => void,
13
- private readonly onRefresh?: (ctx: ExtensionContext) => void,
14
- ) {
15
- this.store = createSessionStore<TodoState>({
16
- empty: () => ({ todos: [], nextId: 1 }),
17
- entryType: STATE_ENTRY,
18
- snapshotOf: (entry) => {
19
- if (entry.type === "custom" && entry.customType === STATE_ENTRY) {
20
- return entry.data as TodoState | undefined;
21
- }
22
- },
23
- onChange: (state, ctx) => {
24
- this.persist(state);
25
- this.onRefresh?.(ctx);
26
- },
27
- });
28
- }
29
-
30
- getState(ctx: ExtensionContext): TodoState {
31
- return this.store.getState(ctx);
32
- }
33
-
34
- commit(ctx: ExtensionContext, state: TodoState): void {
35
- this.store.commit(ctx, state);
36
- }
37
-
38
- replay(ctx: ExtensionContext): void {
39
- this.store.replay(ctx);
40
- }
41
-
42
- persistSnapshot(ctx: ExtensionContext): void {
43
- this.store.persistSnapshot(ctx);
44
- }
9
+ import type { TodoState } from "./types";
10
+
11
+ /** Session-scoped todo state, committed to the `todos.state` custom entry. */
12
+ export function createTodoStore(
13
+ persist: (snapshot: TodoState) => void,
14
+ onRefresh?: (ctx: ExtensionContext) => void,
15
+ ): SessionRecordStore<TodoState> {
16
+ return createSessionStore<TodoState>({
17
+ empty: () => ({ todos: [], nextId: 1 }),
18
+ entryType: STATE_ENTRY,
19
+ snapshotOf: (entry) => entry.data as TodoState | undefined,
20
+ onChange: (state, ctx) => {
21
+ persist(state);
22
+ onRefresh?.(ctx);
23
+ },
24
+ });
45
25
  }
package/src/tool.ts CHANGED
@@ -5,7 +5,7 @@ import { Type } from "typebox";
5
5
 
6
6
  import { STATUSES } from "./constants";
7
7
  import { applyAction } from "./core";
8
- import type { TodoStore } from "./state";
8
+ import type { SessionRecordStore } from "@leo-alvarenga/pi-ext-core";
9
9
  import type { TodoDetails, TodoState } from "./types";
10
10
 
11
11
  const TodoParams = Type.Object({
@@ -38,7 +38,10 @@ const TodoParams = Type.Object({
38
38
  ),
39
39
  });
40
40
 
41
- export function registerTodoTool(pi: ExtensionAPI, store: TodoStore): void {
41
+ export function registerTodoTool(
42
+ pi: ExtensionAPI,
43
+ store: SessionRecordStore<TodoState>,
44
+ ): void {
42
45
  pi.registerTool({
43
46
  name: "todo",
44
47
  label: "Todo",
@@ -106,7 +109,7 @@ export function registerTodoTool(pi: ExtensionAPI, store: TodoStore): void {
106
109
 
107
110
  export function registerTodoCompleteAllTool(
108
111
  pi: ExtensionAPI,
109
- store: TodoStore,
112
+ store: SessionRecordStore<TodoState>,
110
113
  ): void {
111
114
  pi.registerTool({
112
115
  name: "todo_complete_all",
package/src/types.ts CHANGED
@@ -1,16 +1,7 @@
1
- import { ThemeColor } from "@earendil-works/pi-coding-agent";
2
1
  import { STATUSES } from "./constants";
3
2
 
4
3
  export type TodoStatus = (typeof STATUSES)[number];
5
4
 
6
- export type TodoStatusUi = {
7
- icon: string;
8
- fg: ThemeColor;
9
- bold?: boolean;
10
- color: ThemeColor;
11
- strikethrough?: boolean;
12
- };
13
-
14
5
  export interface Todo {
15
6
  id: number;
16
7
  text: string;
package/src/utils.ts CHANGED
@@ -1,63 +1,14 @@
1
1
  import { Theme } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { PANEL_STATE_ICON } from "@leo-alvarenga/pi-ext-core";
3
3
 
4
- import { Todo, TodoStatus } from "./types";
5
- import { MAX_PANEL_ROWS, PANEL_STATE_ICON, STATUS_STYLES } from "./constants";
4
+ import type { Todo, TodoStatus } from "./types";
5
+ import { MAX_PANEL_ROWS, STATUS_STYLES } from "./constants";
6
6
 
7
- export function getStyledTodoList(
7
+ /** Panel header: collapsed/expanded icon plus per-status counts. */
8
+ export function getHeader(
8
9
  todos: Todo[],
9
10
  th: Theme,
10
- width: number,
11
- isCollapsed?: boolean,
12
- ): string[] {
13
- const indent = (str: string, level = 1) => " ".repeat(Math.abs(level)) + str;
14
-
15
- const lines: string[] = [
16
- truncateToWidth(
17
- indent(getStyledTodoListHeader(todos, th, isCollapsed)),
18
- width,
19
- ),
20
- ];
21
-
22
- if (!isCollapsed) {
23
- lines.push("");
24
-
25
- if (todos.length === 0) {
26
- lines.push(
27
- truncateToWidth(
28
- indent(th.fg("dim", "No todos yet. Ask the agent to add some!"), 2),
29
- width,
30
- ),
31
- );
32
- } else {
33
- const visible = todos.slice(0, MAX_PANEL_ROWS);
34
-
35
- for (const t of visible) {
36
- lines.push(
37
- truncateToWidth(indent(getStyledTodo(t, todos, th), 2), width),
38
- );
39
- }
40
-
41
- if (todos.length > visible.length) {
42
- lines.push(
43
- truncateToWidth(
44
- indent(th.fg("dim", `… +${todos.length - visible.length} more`), 3),
45
- width,
46
- ),
47
- );
48
- }
49
- }
50
- }
51
-
52
- lines.push("");
53
-
54
- return lines;
55
- }
56
-
57
- export function getStyledTodoListHeader(
58
- todos: Todo[],
59
- th: Theme,
60
- isCollapsed?: boolean,
11
+ isCollapsed: boolean,
61
12
  ): string {
62
13
  const count = todos.reduce<Record<TodoStatus, number>>(
63
14
  (acc, t) => {
@@ -67,27 +18,50 @@ export function getStyledTodoListHeader(
67
18
  { pending: 0, "in-progress": 0, completed: 0 },
68
19
  );
69
20
 
70
- const collapseState = isCollapsed ? "collapsed" : "expanded";
71
-
72
- const statuses: TodoStatus[] = ["pending", "in-progress", "completed"];
73
-
74
- const counter = statuses
21
+ const counter = (["pending", "in-progress", "completed"] as TodoStatus[])
75
22
  .map((s) => {
76
- const { icon, color } = STATUS_STYLES[s] ?? STATUS_STYLES.pending;
23
+ const { icon, color = "text" } =
24
+ STATUS_STYLES[s] ?? STATUS_STYLES.pending;
77
25
 
78
26
  return th.fg(color, `${icon}${count[s] ?? 0}`);
79
27
  })
80
28
  .join(th.fg("dim", " / "));
81
29
 
82
- return th.fg(
83
- "accent",
84
- `${PANEL_STATE_ICON[collapseState]} Todos - ${counter}`,
85
- );
30
+ const state = isCollapsed ? "collapsed" : "expanded";
31
+
32
+ return th.fg("accent", `${PANEL_STATE_ICON[state]} 󰄲 Todos - ${counter}`);
33
+ }
34
+
35
+ export function getSortedTodos(todos: Todo[]): Todo[] {
36
+ if (todos.length <= MAX_PANEL_ROWS) return todos;
37
+
38
+ const weights: Record<TodoStatus, number> = {
39
+ pending: 0,
40
+ completed: 2,
41
+ "in-progress": 1,
42
+ };
43
+
44
+ const sorted = todos.sort((a, b) => {
45
+ const wa = weights[a.status];
46
+ const wb = weights[b.status];
47
+
48
+ if (wa === wb) return a.id - b.id;
49
+
50
+ return wa - wb;
51
+ });
52
+
53
+ return sorted;
86
54
  }
87
55
 
56
+ /** One panel/report row for a todo. */
88
57
  export function getStyledTodo(t: Todo, todos: Todo[], th: Theme): string {
89
- const { icon, fg, bold, strikethrough, color } =
90
- STATUS_STYLES[t.status] ?? STATUS_STYLES.pending;
58
+ const {
59
+ fg,
60
+ bold,
61
+ icon,
62
+ strikethrough,
63
+ color = "text",
64
+ } = STATUS_STYLES[t.status] ?? STATUS_STYLES.pending;
91
65
 
92
66
  const check = th.fg(color, icon);
93
67
  const id = th.fg("accent", `#${t.id}`);
package/src/widget.ts CHANGED
@@ -1,28 +1,36 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ createPanelWidget,
4
+ type PanelWidgetSpec,
5
+ type SessionRecordStore,
6
+ } from "@leo-alvarenga/pi-ext-core";
2
7
 
3
- import { createPanelWidget, PanelWidgetSpec } from "@leo-alvarenga/pi-ext-core";
4
-
5
- import { PANEL_TOGGLE_CHORD, WIDGET_KEY } from "./constants";
6
- import type { TodoStore } from "./state";
8
+ import { MAX_PANEL_ROWS, PANEL_TOGGLE_CHORD, WIDGET_KEY } from "./constants";
7
9
  import type { TodoState } from "./types";
8
- import { getStyledTodoList } from "./utils";
10
+ import { getHeader, getSortedTodos, getStyledTodo } from "./utils";
9
11
 
10
- export function registerTodoWidget(pi: ExtensionAPI, store: TodoStore) {
12
+ export function registerTodoWidget(
13
+ pi: ExtensionAPI,
14
+ store: SessionRecordStore<TodoState>,
15
+ ) {
11
16
  const spec: PanelWidgetSpec<TodoState> = {
17
+ store,
12
18
  widgetKey: WIDGET_KEY,
13
- maxRows: 20,
14
- emptyText: "No todos",
19
+ maxRows: MAX_PANEL_ROWS,
15
20
  toggleChord: PANEL_TOGGLE_CHORD,
16
- isEmpty: (s) => s.todos.length === 0,
17
- store,
18
- moreLabel: (n) => `…and ${n} more`,
19
- header: (s, theme, isCollapsed) =>
20
- theme.fg("muted", isCollapsed ? "▶ Todos" : "▼ Todos"),
21
- rows: (s, theme) => getStyledTodoList(s.todos, theme, 80, false),
21
+ moreLabel: (n) => `… +${n} more`,
22
+ emptyText: "No todos yet. Ask the agent to add some!",
23
+ header: (s, theme, isCollapsed) => getHeader(s.todos, theme, isCollapsed),
24
+
25
+ rows: (s, theme) =>
26
+ getSortedTodos(s.todos).map((t) => getStyledTodo(t, s.todos, theme)),
27
+
28
+ isEmpty: (s) =>
29
+ s.todos.length === 0 || s.todos.every((t) => t.status === "completed"),
22
30
  };
23
31
 
24
32
  const controls = createPanelWidget(pi, spec);
25
33
  controls.register();
34
+
26
35
  return controls;
27
36
  }
28
-