@oisin-ee/pi-todo 0.1.1 → 0.3.0
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/README.md +15 -4
- package/package.json +5 -3
- package/src/generated/todo_core.d.ts +16 -3
- package/src/generated/todo_core.js +5 -4
- package/src/generated/todo_core_bg.wasm +0 -0
- package/src/generated/todo_core_bg.wasm.d.ts +1 -1
- package/src/index.ts +26 -8
- package/src/lifecycle.ts +18 -3
- package/src/presentation.ts +48 -0
- package/src/wasm.ts +2 -0
- package/src/widget.ts +57 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ The list rules run in Rust, compiled to WebAssembly.
|
|
|
7
7
|
pi install npm:@oisin-ee/pi-todo
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
To install it
|
|
10
|
+
To install it with the other packages, use [`@oisin-ee/oisinpi`](https://www.npmjs.com/package/@oisin-ee/oisinpi).
|
|
11
11
|
|
|
12
12
|
## The tool
|
|
13
13
|
|
|
@@ -27,9 +27,20 @@ and the list stays as it was.
|
|
|
27
27
|
|
|
28
28
|
## State
|
|
29
29
|
|
|
30
|
-
Each change is saved as a `todo-state` entry on the session branch.
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Each task change is saved as a `todo-state` entry on the session branch. A resumed session, a `/tree` move,
|
|
31
|
+
and calls made from inside codemode all see the list of the current branch.
|
|
32
|
+
|
|
33
|
+
## Widget
|
|
34
|
+
|
|
35
|
+
The widget shows only in-progress tasks by default. Active tasks use the theme accent and bold text.
|
|
36
|
+
Long task names wrap to the terminal width. An empty list has no widget.
|
|
37
|
+
|
|
38
|
+
Press **Alt+T** to expand Upcoming, Done, and Dropped sections beneath the active task. Press **Alt+T** again to collapse them.
|
|
39
|
+
`/todos` also toggles these sections. `/todos list` prints the full list.
|
|
40
|
+
|
|
41
|
+
Expansion stays open through task updates. Session changes and `/tree` navigation collapse it. Expansion does not change saved tasks.
|
|
42
|
+
When no task is active, a short status and the shortcut hint remain visible.
|
|
43
|
+
Outside the terminal UI, `/todos` prints the list instead.
|
|
33
44
|
|
|
34
45
|
## Reminder
|
|
35
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oisin-ee/pi-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A todo tool for pi with a Rust core: one active task, state on the session branch.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-extension",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/oisin-ee/
|
|
13
|
+
"url": "git+https://github.com/oisin-ee/oisinpi.git",
|
|
14
14
|
"directory": "packages/pi-todo"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
@@ -21,17 +21,19 @@
|
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@earendil-works/pi-ai": "1.3.0",
|
|
23
23
|
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
24
|
+
"@earendil-works/pi-tui": "1.3.0",
|
|
24
25
|
"typebox": "1.3.27"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
27
28
|
"@earendil-works/pi-ai": "*",
|
|
28
29
|
"@earendil-works/pi-coding-agent": "*",
|
|
30
|
+
"@earendil-works/pi-tui": "*",
|
|
29
31
|
"typebox": "*"
|
|
30
32
|
},
|
|
31
33
|
"pi": {
|
|
32
34
|
"extensions": [
|
|
33
35
|
"./src/index.ts"
|
|
34
36
|
],
|
|
35
|
-
"image": "https://raw.githubusercontent.com/oisin-ee/
|
|
37
|
+
"image": "https://raw.githubusercontent.com/oisin-ee/oisinpi/main/assets/prewalk.png"
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -24,6 +24,14 @@ export interface Request {
|
|
|
24
24
|
task: string | undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* One row before terminal wrapping and theme application.
|
|
29
|
+
*/
|
|
30
|
+
export interface WidgetRow {
|
|
31
|
+
kind: WidgetKind;
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
/**
|
|
28
36
|
* One task, addressed by its exact content.
|
|
29
37
|
*/
|
|
@@ -37,6 +45,11 @@ export interface Task {
|
|
|
37
45
|
*/
|
|
38
46
|
export type ReminderFact = { kind: "user" } | { kind: "snapshot"; list: TodoList } | { kind: "reminder" };
|
|
39
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Semantic styling for a terminal row.
|
|
50
|
+
*/
|
|
51
|
+
export type WidgetKind = "heading" | "active" | "upcoming" | "done" | "dropped" | "summary";
|
|
52
|
+
|
|
40
53
|
/**
|
|
41
54
|
* Supported model-facing operations.
|
|
42
55
|
*/
|
|
@@ -102,13 +115,13 @@ export function parse(snapshot: unknown): TodoList;
|
|
|
102
115
|
export function view(list: TodoList): string;
|
|
103
116
|
|
|
104
117
|
/**
|
|
105
|
-
*
|
|
118
|
+
* Human-facing rows for `list`, or `undefined` for an empty list.
|
|
106
119
|
*
|
|
107
120
|
* # Errors
|
|
108
121
|
*
|
|
109
122
|
* Throws when `list` is not a valid todo list.
|
|
110
123
|
*/
|
|
111
|
-
export function widget(list: TodoList):
|
|
124
|
+
export function widget(list: TodoList, expanded: boolean): WidgetRow[] | undefined;
|
|
112
125
|
|
|
113
126
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
114
127
|
|
|
@@ -119,7 +132,7 @@ export interface InitOutput {
|
|
|
119
132
|
readonly init: (a: number, b: number, c: number) => void;
|
|
120
133
|
readonly parse: (a: number, b: number) => void;
|
|
121
134
|
readonly view: (a: number, b: number) => void;
|
|
122
|
-
readonly widget: (a: number, b: number) => void;
|
|
135
|
+
readonly widget: (a: number, b: number, c: number) => void;
|
|
123
136
|
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
124
137
|
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
125
138
|
readonly __wbindgen_export3: (a: number) => void;
|
|
@@ -149,18 +149,19 @@ export function view(list) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
152
|
+
* Human-facing rows for `list`, or `undefined` for an empty list.
|
|
153
153
|
*
|
|
154
154
|
* # Errors
|
|
155
155
|
*
|
|
156
156
|
* Throws when `list` is not a valid todo list.
|
|
157
157
|
* @param {TodoList} list
|
|
158
|
-
* @
|
|
158
|
+
* @param {boolean} expanded
|
|
159
|
+
* @returns {WidgetRow[] | undefined}
|
|
159
160
|
*/
|
|
160
|
-
export function widget(list) {
|
|
161
|
+
export function widget(list, expanded) {
|
|
161
162
|
try {
|
|
162
163
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
163
|
-
wasm.widget(retptr, addBorrowedObject(list));
|
|
164
|
+
wasm.widget(retptr, addBorrowedObject(list), expanded);
|
|
164
165
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
165
166
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
166
167
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
|
Binary file
|
|
@@ -6,7 +6,7 @@ export const completion_reminder: (a: number, b: number) => void;
|
|
|
6
6
|
export const init: (a: number, b: number, c: number) => void;
|
|
7
7
|
export const parse: (a: number, b: number) => void;
|
|
8
8
|
export const view: (a: number, b: number) => void;
|
|
9
|
-
export const widget: (a: number, b: number) => void;
|
|
9
|
+
export const widget: (a: number, b: number, c: number) => void;
|
|
10
10
|
export const __wbindgen_export: (a: number, b: number) => number;
|
|
11
11
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
12
12
|
export const __wbindgen_export3: (a: number) => void;
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { ownsTodo,
|
|
1
|
+
import { ownsTodo, settle, show } from "./lifecycle.ts";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
|
+
import { TODO_SHORTCUT } from "./widget.ts";
|
|
5
|
+
import { TodoPresentation } from "./presentation.ts";
|
|
4
6
|
import { Type } from "typebox";
|
|
5
7
|
import { runTodo } from "./tool.ts";
|
|
6
8
|
|
|
7
9
|
type TodoAPI = Readonly<
|
|
8
|
-
Pick<
|
|
10
|
+
Pick<
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
"appendEntry" | "getAllTools" | "on" | "registerCommand" | "registerShortcut" | "registerTool"
|
|
13
|
+
>
|
|
9
14
|
>;
|
|
10
15
|
|
|
11
16
|
const items = Type.Immutable(
|
|
@@ -27,6 +32,13 @@ const items = Type.Immutable(
|
|
|
27
32
|
* @param {TodoAPI} pi The extension API methods used here.
|
|
28
33
|
*/
|
|
29
34
|
export default function piTodo(pi: TodoAPI): void {
|
|
35
|
+
const presentation = new TodoPresentation();
|
|
36
|
+
pi.registerShortcut(TODO_SHORTCUT, {
|
|
37
|
+
description: "Expand or collapse upcoming and done todos.",
|
|
38
|
+
handler(ctx) {
|
|
39
|
+
presentation.toggle(pi, ctx);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
30
42
|
pi.registerTool({
|
|
31
43
|
description:
|
|
32
44
|
"Keep the session todo list. Tasks are addressed by their exact content. One task is active; the next open task starts when it closes.",
|
|
@@ -37,7 +49,7 @@ export default function piTodo(pi: TodoAPI): void {
|
|
|
37
49
|
},
|
|
38
50
|
branch: () => ctx.sessionManager.getBranch(),
|
|
39
51
|
});
|
|
40
|
-
|
|
52
|
+
presentation.refresh(ctx);
|
|
41
53
|
return result;
|
|
42
54
|
},
|
|
43
55
|
executionMode: "sequential",
|
|
@@ -46,18 +58,24 @@ export default function piTodo(pi: TodoAPI): void {
|
|
|
46
58
|
parameters,
|
|
47
59
|
});
|
|
48
60
|
pi.on("session_start", (_event, ctx) => {
|
|
49
|
-
start(pi, ctx);
|
|
61
|
+
presentation.start(pi, ctx);
|
|
50
62
|
});
|
|
51
63
|
pi.on("session_tree", (_event, ctx) => {
|
|
52
64
|
if (ownsTodo(pi)) {
|
|
53
|
-
|
|
65
|
+
presentation.reset(ctx);
|
|
54
66
|
}
|
|
55
67
|
});
|
|
56
68
|
pi.on("agent_before_settle", (event, ctx) => settle(pi, event, ctx));
|
|
57
69
|
pi.registerCommand("todos", {
|
|
58
|
-
description: "
|
|
59
|
-
async handler(
|
|
60
|
-
|
|
70
|
+
description: "Toggle todo details; /todos list prints the full branch list.",
|
|
71
|
+
async handler(args, ctx) {
|
|
72
|
+
if (ownsTodo(pi)) {
|
|
73
|
+
if (args.trim() === "list" || ctx.mode !== "tui") {
|
|
74
|
+
show(ctx);
|
|
75
|
+
} else {
|
|
76
|
+
presentation.toggle(pi, ctx);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
61
79
|
},
|
|
62
80
|
});
|
|
63
81
|
}
|
package/src/lifecycle.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BoundaryResult,
|
|
3
|
+
ExtensionContext,
|
|
3
4
|
ExtensionUIContext,
|
|
4
5
|
SessionBoundaryDraft,
|
|
5
6
|
SessionEntry,
|
|
7
|
+
Theme,
|
|
6
8
|
} from "@earendil-works/pi-coding-agent";
|
|
7
9
|
import { REMINDER_ENTRY, reminder } from "./reminder.ts";
|
|
8
10
|
import { parse, view, widget } from "./wasm.ts";
|
|
11
|
+
import { TodoWidget } from "./widget.ts";
|
|
9
12
|
import { isOwnSource } from "./source.ts";
|
|
10
13
|
import { latestSnapshot } from "./state.ts";
|
|
11
14
|
|
|
12
15
|
interface TodoContext {
|
|
16
|
+
readonly mode: ExtensionContext["mode"];
|
|
13
17
|
readonly sessionManager: { readonly getBranch: () => readonly SessionEntry[] };
|
|
14
18
|
readonly ui: Readonly<Pick<ExtensionUIContext, "notify" | "setWidget">>;
|
|
15
19
|
}
|
|
@@ -36,10 +40,21 @@ const ownsTodo = (pi: TodoHost): boolean =>
|
|
|
36
40
|
/**
|
|
37
41
|
* Renders the selected branch snapshot, and removes the widget when the list is empty.
|
|
38
42
|
* @param {TodoContext} ctx Branch and UI methods.
|
|
43
|
+
* @param {boolean} expanded Whether to reveal secondary task groups.
|
|
39
44
|
*/
|
|
40
|
-
replay = (ctx: TodoContext): void => {
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
replay = (ctx: TodoContext, expanded = false): void => {
|
|
46
|
+
if (ctx.mode !== "tui") {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const list = parse(latestSnapshot(ctx.sessionManager.getBranch())),
|
|
50
|
+
rows = widget(list, expanded);
|
|
51
|
+
ctx.ui.setWidget(
|
|
52
|
+
"pi-todo",
|
|
53
|
+
rows === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: (_tui: unknown, theme: Readonly<Pick<Theme, "fg" | "bold">>): TodoWidget =>
|
|
56
|
+
new TodoWidget(rows, expanded, theme),
|
|
57
|
+
);
|
|
43
58
|
},
|
|
44
59
|
/**
|
|
45
60
|
* Applies the completion decision at pi's final actionable boundary.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { TodoContext, TodoHost } from "./lifecycle.ts";
|
|
2
|
+
import { ownsTodo, replay, start } from "./lifecycle.ts";
|
|
3
|
+
|
|
4
|
+
/** Owns transient expansion for one extension instance, never task state. */
|
|
5
|
+
class TodoPresentation {
|
|
6
|
+
#expanded = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Replays current branch tasks without changing expansion.
|
|
10
|
+
* @param {TodoContext} ctx Current branch and UI.
|
|
11
|
+
*/
|
|
12
|
+
public refresh(ctx: TodoContext): void {
|
|
13
|
+
replay(ctx, this.#expanded);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Collapses when the session starts or the selected branch changes.
|
|
18
|
+
* @param {TodoContext} ctx Current branch and UI.
|
|
19
|
+
*/
|
|
20
|
+
public reset(ctx: TodoContext): void {
|
|
21
|
+
this.#expanded = false;
|
|
22
|
+
this.refresh(ctx);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Starts collapsed, or leaves another todo owner untouched.
|
|
27
|
+
* @param {TodoHost} pi Registered tool metadata.
|
|
28
|
+
* @param {TodoContext} ctx Current branch and UI.
|
|
29
|
+
*/
|
|
30
|
+
public start(pi: TodoHost, ctx: TodoContext): void {
|
|
31
|
+
this.#expanded = false;
|
|
32
|
+
start(pi, ctx);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reveals or hides secondary groups without writing session state.
|
|
37
|
+
* @param {TodoHost} pi Registered tool metadata.
|
|
38
|
+
* @param {TodoContext} ctx Current branch and UI.
|
|
39
|
+
*/
|
|
40
|
+
public toggle(pi: TodoHost, ctx: TodoContext): void {
|
|
41
|
+
if (ownsTodo(pi)) {
|
|
42
|
+
this.#expanded = !this.#expanded;
|
|
43
|
+
this.refresh(ctx);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { TodoPresentation };
|
package/src/wasm.ts
CHANGED
package/src/widget.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { WidgetKind, WidgetRow } from "./wasm.ts";
|
|
4
|
+
|
|
5
|
+
const TODO_SHORTCUT = "alt+t",
|
|
6
|
+
horizontalPadding = 1,
|
|
7
|
+
verticalPadding = 0,
|
|
8
|
+
colors: Readonly<Record<WidgetKind, ThemeColor>> = {
|
|
9
|
+
active: "accent",
|
|
10
|
+
done: "success",
|
|
11
|
+
dropped: "dim",
|
|
12
|
+
heading: "muted",
|
|
13
|
+
summary: "muted",
|
|
14
|
+
upcoming: "text",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** The presentation-only component; task state stays on the session branch. */
|
|
18
|
+
class TodoWidget extends Text {
|
|
19
|
+
readonly #rows: readonly Readonly<WidgetRow>[];
|
|
20
|
+
readonly #expanded: boolean;
|
|
21
|
+
readonly #theme: Readonly<Pick<Theme, "fg" | "bold">>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Retains unstyled rows so each render uses the current theme.
|
|
25
|
+
* @param {readonly Readonly<WidgetRow>[]} rows Rust-owned status groups.
|
|
26
|
+
* @param {boolean} expanded Whether secondary groups are visible.
|
|
27
|
+
* @param {Readonly<Pick<Theme, "fg" | "bold">>} theme Pi theme helpers.
|
|
28
|
+
*/
|
|
29
|
+
public constructor(
|
|
30
|
+
rows: readonly Readonly<WidgetRow>[],
|
|
31
|
+
expanded: boolean,
|
|
32
|
+
theme: Readonly<Pick<Theme, "fg" | "bold">>,
|
|
33
|
+
) {
|
|
34
|
+
super("", horizontalPadding, verticalPadding);
|
|
35
|
+
this.#rows = rows;
|
|
36
|
+
this.#expanded = expanded;
|
|
37
|
+
this.#theme = theme;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wraps all task text with Pi's native terminal component.
|
|
42
|
+
* @param {number} width Available terminal columns.
|
|
43
|
+
* @returns {string[]} Width-bounded, themed terminal lines.
|
|
44
|
+
*/
|
|
45
|
+
public override render(width: number): string[] {
|
|
46
|
+
const lines = this.#rows.map((row) => {
|
|
47
|
+
const text = this.#theme.fg(colors[row.kind], row.text);
|
|
48
|
+
return row.kind === "active" ? this.#theme.bold(text) : text;
|
|
49
|
+
}),
|
|
50
|
+
action = this.#expanded ? "collapse" : "upcoming / done";
|
|
51
|
+
lines.push(this.#theme.fg("dim", `Alt+T · ${action}`));
|
|
52
|
+
this.setText(lines.join("\n"));
|
|
53
|
+
return super.render(width).map((line) => truncateToWidth(line, width));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { TODO_SHORTCUT, TodoWidget };
|