@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 +8 -7
- package/src/command.ts +6 -3
- package/src/constants.ts +4 -7
- package/src/index.ts +3 -4
- package/src/state.ts +20 -40
- package/src/tool.ts +6 -3
- package/src/types.ts +0 -9
- package/src/utils.ts +41 -67
- package/src/widget.ts +23 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leo-alvarenga/pi-todo-list",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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
|
-
"
|
|
40
|
-
"
|
|
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 {
|
|
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(
|
|
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 {
|
|
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,
|
|
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 {
|
|
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 =
|
|
11
|
-
|
|
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 {
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
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(
|
|
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:
|
|
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 {
|
|
2
|
+
import { PANEL_STATE_ICON } from "@leo-alvarenga/pi-ext-core";
|
|
3
3
|
|
|
4
|
-
import { Todo, TodoStatus } from "./types";
|
|
5
|
-
import { MAX_PANEL_ROWS,
|
|
4
|
+
import type { Todo, TodoStatus } from "./types";
|
|
5
|
+
import { MAX_PANEL_ROWS, STATUS_STYLES } from "./constants";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Panel header: collapsed/expanded icon plus per-status counts. */
|
|
8
|
+
export function getHeader(
|
|
8
9
|
todos: Todo[],
|
|
9
10
|
th: Theme,
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 {
|
|
90
|
-
|
|
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 {
|
|
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 {
|
|
10
|
+
import { getHeader, getSortedTodos, getStyledTodo } from "./utils";
|
|
9
11
|
|
|
10
|
-
export function registerTodoWidget(
|
|
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:
|
|
14
|
-
emptyText: "No todos",
|
|
19
|
+
maxRows: MAX_PANEL_ROWS,
|
|
15
20
|
toggleChord: PANEL_TOGGLE_CHORD,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|