@oisin-ee/pi-todo 0.5.1 → 0.6.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 +57 -15
- package/package.json +1 -1
- package/src/display.ts +62 -0
- package/src/embedded.ts +105 -0
- package/src/generated/todo_core.d.ts +26 -11
- package/src/generated/todo_core_bg.wasm +0 -0
- package/src/guidance.ts +8 -0
- package/src/index.ts +23 -6
- package/src/plannotator.ts +4 -2
- package/src/tool.ts +13 -9
- package/src/wasm.ts +1 -0
- package/src/widget.ts +23 -3
- package/src/work.ts +13 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @oisin-ee/pi-todo
|
|
2
2
|
|
|
3
|
-
A `todo` tool for [pi](https://pi.dev). The model keeps one ordered
|
|
3
|
+
A `todo` tool for [pi](https://pi.dev). The model keeps one ordered list of outcomes and bounded subtasks for the session. Multiple units can be active together.
|
|
4
4
|
The list rules run in Rust, compiled to WebAssembly.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
@@ -13,22 +13,60 @@ To install it with the other packages, use [`@oisin-ee/oisinpi`](https://www.npm
|
|
|
13
13
|
|
|
14
14
|
The model calls `todo` with one `op`. Tasks are addressed by their exact content.
|
|
15
15
|
|
|
16
|
-
| `op` | Effect
|
|
17
|
-
| -------- |
|
|
18
|
-
| `init` | Replaces the list with `items`.
|
|
19
|
-
| `append` | Adds `items` to the end.
|
|
20
|
-
| `start` |
|
|
21
|
-
| `done` | Completes `task`. Without `task`, completes every open task.
|
|
22
|
-
| `drop` | Abandons `task`. Without `task`, abandons every open task.
|
|
23
|
-
| `view` | Returns the list. It does not change it.
|
|
16
|
+
| `op` | Effect |
|
|
17
|
+
| -------- | --------------------------------------------------------------- |
|
|
18
|
+
| `init` | Replaces the list with `items`. |
|
|
19
|
+
| `append` | Adds `items` to the end. |
|
|
20
|
+
| `start` | Explicitly activates `task` without demoting other active work. |
|
|
21
|
+
| `done` | Completes `task`. Without `task`, completes every open task. |
|
|
22
|
+
| `drop` | Abandons `task`. Without `task`, abandons every open task. |
|
|
23
|
+
| `view` | Returns the list. It does not change it. |
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
Creation and append leave tasks pending. Nothing starts automatically. This replaces the previous single-active, automatic-start behavior.
|
|
26
|
+
Use `start` to activate or reopen work explicitly. Starting a child also opens its parent. Starting a parent does not start its children.
|
|
27
|
+
|
|
28
|
+
`items` accepts strings or outcomes with string subtasks:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"op": "init",
|
|
33
|
+
"items": [
|
|
34
|
+
{
|
|
35
|
+
"content": "Password reset",
|
|
36
|
+
"subtasks": ["Implement tokens and behavior tests", "Implement form and interaction tests"]
|
|
37
|
+
},
|
|
38
|
+
"Update documentation"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To target a child, supply its exact `task` content and its exact root `parent` content:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{ "op": "start", "parent": "Password reset", "task": "Implement tokens and behavior tests" }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To add children, use `append` with `parent` and string `items`. Only two levels are permitted.
|
|
50
|
+
Root names are unique. Child names are unique within their parent. Different parents can use the same child name.
|
|
51
|
+
Blank names, duplicate siblings, unknown targets, grandchildren, and malformed requests fail without saving a change.
|
|
52
|
+
|
|
53
|
+
Each executable unit fits one agent session, with one deliverable, clear scope, and an acceptance condition.
|
|
54
|
+
Split independent outcomes or work that needs another session. Combine trivial steps with their deliverable and tests.
|
|
55
|
+
Delegate independent units separately. The lead can execute one unit. Keep dependent units ordered.
|
|
56
|
+
Do not delegate an oversized parent as one assignment.
|
|
57
|
+
|
|
58
|
+
A parent stays open after its children close and shows **Ready for review**.
|
|
59
|
+
Complete the parent explicitly after integration review. Completing a parent with open children fails, including bulk completion.
|
|
60
|
+
Dropping a parent abandons open children but preserves completed and abandoned children.
|
|
61
|
+
Untargeted `done` and `drop` affect open work only. A parent without `task` is not a bulk-child selector.
|
|
62
|
+
Appending children to a closed parent reopens it as pending. An active parent stays active.
|
|
63
|
+
Outcome and subtask progress remain separate. Abandoned work never counts as completed.
|
|
27
64
|
|
|
28
65
|
## State
|
|
29
66
|
|
|
30
67
|
Each task change is saved as a `todo-state` entry on the session branch. A resumed session, a `/tree` move,
|
|
31
68
|
and calls made from inside codemode all see the list of the current branch.
|
|
69
|
+
Old flat snapshots retain their recorded statuses and read with empty subtasks.
|
|
32
70
|
|
|
33
71
|
## Widget
|
|
34
72
|
|
|
@@ -45,12 +83,15 @@ Outside the terminal UI, `/todos` prints the list instead.
|
|
|
45
83
|
## Shared work display
|
|
46
84
|
|
|
47
85
|
When a compatible pi-wrangle extension is active, its `WORK` strip replaces the standalone todo widget.
|
|
48
|
-
Tasks remain on the current Pi session branch. Wrangle receives read-only
|
|
86
|
+
Tasks remain on the current Pi session branch. Wrangle receives a read-only presentation capability through `pi.events`.
|
|
87
|
+
Pi-todo owns the renderer in both standalone and shared views. Wrangle hosts it above the editor without opening a popup.
|
|
88
|
+
Alt+J/K scrolls the hosted rows. Alt+I toggles their expansion. Ordinary editor keys remain unchanged.
|
|
49
89
|
`/todos` and Alt+T request the shared Todos view. `/todos list` still prints the full list.
|
|
50
90
|
When shared presentation ends, the standalone widget returns without changing task state.
|
|
51
91
|
|
|
52
|
-
The [version
|
|
92
|
+
The [version 2 display contract](https://github.com/oisin-ee/wrangle/blob/main/docs/pi-work.md)
|
|
53
93
|
defines the snapshot request, acknowledgement, release, and read-only view events.
|
|
94
|
+
Incompatible versions leave the standalone widget available. Rendering failures restore it without changing saved tasks.
|
|
54
95
|
|
|
55
96
|
## Plannotator plans
|
|
56
97
|
|
|
@@ -74,7 +115,8 @@ Malformed events, a different working directory, or another owner of the `todo`
|
|
|
74
115
|
|
|
75
116
|
## Reminder
|
|
76
117
|
|
|
77
|
-
When the agent stops with open tasks, pi-todo adds a hidden reminder
|
|
118
|
+
When the agent stops with open tasks, pi-todo adds a hidden reminder and continues the turn.
|
|
119
|
+
The reminder names open children and parents that await integration review.
|
|
78
120
|
It sends at most 3 reminders per user message. After a reminder, it waits for the list to change before it sends
|
|
79
121
|
another. A new user message resets the count.
|
|
80
122
|
|
|
@@ -84,7 +126,7 @@ If another package already registers `todo`, pi-todo stays inactive and shows a
|
|
|
84
126
|
|
|
85
127
|
## Source
|
|
86
128
|
|
|
87
|
-
The
|
|
129
|
+
The original flat operations and the reminder derive from
|
|
88
130
|
[oh-my-pi](https://github.com/can1357/oh-my-pi/tree/e7fe318/packages/coding-agent/src) (`tools/todo`,
|
|
89
131
|
`todo-tracker.ts`), MIT. Saving a custom entry on every change follows
|
|
90
132
|
[rpiv-todo](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-todo).
|
package/package.json
CHANGED
package/src/display.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Subtask, TodoList } from "./wasm.ts";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { TodoView } from "./embedded.ts";
|
|
4
|
+
import { view } from "./wasm.ts";
|
|
5
|
+
|
|
6
|
+
/** Host-owned layout and rendering services for an inline todo view. */
|
|
7
|
+
interface ViewHost {
|
|
8
|
+
readonly theme: Readonly<Pick<Theme, "fg" | "bold">>;
|
|
9
|
+
readonly height: () => number;
|
|
10
|
+
readonly requestRender: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A provider-owned view. The host forwards updates without interpreting state. */
|
|
14
|
+
interface HostedView {
|
|
15
|
+
readonly render: (width: number) => string[];
|
|
16
|
+
readonly handleInput: (input: string) => void;
|
|
17
|
+
readonly invalidate: () => void;
|
|
18
|
+
readonly update: (state: unknown) => void;
|
|
19
|
+
readonly dispose: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Transient presentation data, never persisted in a session entry. */
|
|
23
|
+
interface TodoDisplay {
|
|
24
|
+
readonly empty: boolean;
|
|
25
|
+
readonly summary: string;
|
|
26
|
+
readonly current: string;
|
|
27
|
+
readonly text: string;
|
|
28
|
+
readonly state: unknown;
|
|
29
|
+
readonly createView: (host: ViewHost) => HostedView;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const emptyCount = 0,
|
|
33
|
+
headerIndex = 0,
|
|
34
|
+
/**
|
|
35
|
+
* Projects the core's summary and supplies its reusable renderer.
|
|
36
|
+
* @param {TodoList} list The validated current branch snapshot.
|
|
37
|
+
* @returns {TodoDisplay} An in-process presentation capability.
|
|
38
|
+
*/
|
|
39
|
+
display = (list: TodoList): TodoDisplay => {
|
|
40
|
+
const text = view(list),
|
|
41
|
+
active = list.tasks.flatMap((task) => [
|
|
42
|
+
...(task.status === "in_progress" ? [task.content] : []),
|
|
43
|
+
...(task.subtasks ?? [])
|
|
44
|
+
.filter((child: Readonly<Subtask>) => child.status === "in_progress")
|
|
45
|
+
.map((child: Readonly<Subtask>) => `${task.content} → ${child.content}`),
|
|
46
|
+
]),
|
|
47
|
+
current =
|
|
48
|
+
active.length > emptyCount
|
|
49
|
+
? active.join("; ")
|
|
50
|
+
: (list.tasks.find((task) => task.status === "pending")?.content ?? "");
|
|
51
|
+
return {
|
|
52
|
+
createView: (host) => new TodoView(list, host),
|
|
53
|
+
current,
|
|
54
|
+
empty: list.tasks.length === emptyCount,
|
|
55
|
+
state: list,
|
|
56
|
+
summary: text.split("\n").at(headerIndex) ?? "",
|
|
57
|
+
text,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export { display };
|
|
62
|
+
export type { HostedView, TodoDisplay, ViewHost };
|
package/src/embedded.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Key, ScrollView, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
import { parse, widget } from "./wasm.ts";
|
|
3
|
+
import type { TodoList } from "./wasm.ts";
|
|
4
|
+
import { TodoWidget } from "./widget.ts";
|
|
5
|
+
import type { ViewHost } from "./display.ts";
|
|
6
|
+
|
|
7
|
+
const minimumHeight = 1,
|
|
8
|
+
previousLine = -1,
|
|
9
|
+
nextLine = 1;
|
|
10
|
+
|
|
11
|
+
/** Inline viewport over the same renderer used by the standalone widget. */
|
|
12
|
+
class TodoView {
|
|
13
|
+
readonly #host: ViewHost;
|
|
14
|
+
#list: TodoList;
|
|
15
|
+
#expanded = true;
|
|
16
|
+
#disposed = false;
|
|
17
|
+
readonly #body: TodoWidget;
|
|
18
|
+
readonly #scroll: ScrollView;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates an expansion-local view without event subscriptions or task writes.
|
|
22
|
+
* @param {TodoList} list Validated branch tasks.
|
|
23
|
+
* @param {ViewHost} host Host layout and rendering services.
|
|
24
|
+
*/
|
|
25
|
+
public constructor(list: TodoList, host: ViewHost) {
|
|
26
|
+
this.#host = host;
|
|
27
|
+
this.#list = list;
|
|
28
|
+
this.#body = this.#content();
|
|
29
|
+
this.#scroll = new ScrollView(this.#body, { overscroll: "contain", scrollbar: "hidden" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates the shared widget content.
|
|
34
|
+
* @returns {TodoWidget} Theme-aware rows.
|
|
35
|
+
*/
|
|
36
|
+
#content(): TodoWidget {
|
|
37
|
+
const body = new TodoWidget(
|
|
38
|
+
widget(this.#list, this.#expanded) ?? [],
|
|
39
|
+
this.#expanded,
|
|
40
|
+
this.#host.theme,
|
|
41
|
+
);
|
|
42
|
+
body.setShortcut("Alt+I");
|
|
43
|
+
return body;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Applies a provider snapshot while preserving the viewport.
|
|
48
|
+
* @param {unknown} state The opaque state from the display capability.
|
|
49
|
+
*/
|
|
50
|
+
public update(state: unknown): void {
|
|
51
|
+
if (!this.#disposed) {
|
|
52
|
+
this.#list = parse(state);
|
|
53
|
+
this.#body.setRows(widget(this.#list, this.#expanded) ?? [], this.#expanded);
|
|
54
|
+
this.invalidate();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Renders a bounded, scrollable view with the active host theme.
|
|
60
|
+
* @param {number} width Available terminal columns.
|
|
61
|
+
* @returns {string[]} Visible terminal lines.
|
|
62
|
+
*/
|
|
63
|
+
public render(width: number): string[] {
|
|
64
|
+
if (this.#disposed) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const height = Math.max(minimumHeight, this.#host.height());
|
|
68
|
+
this.#scroll.updateLayout(this.#body.render(width).length, height, this.#host.requestRender);
|
|
69
|
+
return this.#scroll
|
|
70
|
+
.render(width)
|
|
71
|
+
.slice(this.#scroll.scrollTop, this.#scroll.scrollTop + height);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handles only navigation forwarded by the inline host.
|
|
76
|
+
* @param {string} input A translated host shortcut.
|
|
77
|
+
*/
|
|
78
|
+
public handleInput(input: string): void {
|
|
79
|
+
if (this.#disposed) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (matchesKey(input, Key.enter)) {
|
|
83
|
+
this.#expanded = !this.#expanded;
|
|
84
|
+
this.#body.setRows(widget(this.#list, this.#expanded) ?? [], this.#expanded);
|
|
85
|
+
} else if (matchesKey(input, Key.up)) {
|
|
86
|
+
this.#scroll.scrollBy(previousLine);
|
|
87
|
+
} else if (matchesKey(input, Key.down)) {
|
|
88
|
+
this.#scroll.scrollBy(nextLine);
|
|
89
|
+
}
|
|
90
|
+
this.#host.requestRender();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Invalidates both native components after a theme or data change. */
|
|
94
|
+
public invalidate(): void {
|
|
95
|
+
this.#body.invalidate();
|
|
96
|
+
this.#scroll.invalidate();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Ends this view's lifetime without modifying task state. */
|
|
100
|
+
public dispose(): void {
|
|
101
|
+
this.#disposed = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { TodoView };
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
+
/**
|
|
4
|
+
* A leaf work unit. Grandchildren are not accepted.
|
|
5
|
+
*/
|
|
6
|
+
export interface Subtask {
|
|
7
|
+
content: string;
|
|
8
|
+
status: Status;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
12
|
* A successful operation; views never persist a snapshot.
|
|
5
13
|
*/
|
|
@@ -8,6 +16,15 @@ export interface Change {
|
|
|
8
16
|
persist: boolean;
|
|
9
17
|
}
|
|
10
18
|
|
|
19
|
+
/**
|
|
20
|
+
* An outcome, optionally divided into bounded work units.
|
|
21
|
+
*/
|
|
22
|
+
export interface Task {
|
|
23
|
+
content: string;
|
|
24
|
+
status: Status;
|
|
25
|
+
subtasks?: Subtask[];
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
/**
|
|
12
29
|
* Branch facts supplied to the reminder boundary.
|
|
13
30
|
*/
|
|
@@ -16,12 +33,13 @@ export interface ReminderHistory {
|
|
|
16
33
|
}
|
|
17
34
|
|
|
18
35
|
/**
|
|
19
|
-
*
|
|
36
|
+
* Exact task content, scoped by an optional root parent.
|
|
20
37
|
*/
|
|
21
38
|
export interface Request {
|
|
22
39
|
op: Operation;
|
|
23
|
-
items
|
|
24
|
-
task
|
|
40
|
+
items?: readonly Item[] | undefined;
|
|
41
|
+
task?: string | undefined;
|
|
42
|
+
parent?: string | undefined;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
/**
|
|
@@ -33,17 +51,14 @@ export interface WidgetRow {
|
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
/**
|
|
36
|
-
*
|
|
54
|
+
* Relevant facts in branch order, including proposed boundary entries.
|
|
37
55
|
*/
|
|
38
|
-
export
|
|
39
|
-
content: string;
|
|
40
|
-
status: Status;
|
|
41
|
-
}
|
|
56
|
+
export type ReminderFact = { kind: "user" } | { kind: "snapshot"; list: TodoList } | { kind: "reminder" };
|
|
42
57
|
|
|
43
58
|
/**
|
|
44
|
-
*
|
|
59
|
+
* Root creation accepts standalone outcomes or outcomes with leaf work units.
|
|
45
60
|
*/
|
|
46
|
-
export type
|
|
61
|
+
export type Item = string | { content: string; subtasks: readonly string[] };
|
|
47
62
|
|
|
48
63
|
/**
|
|
49
64
|
* Semantic styling for a terminal row.
|
|
@@ -56,7 +71,7 @@ export type WidgetKind = "heading" | "active" | "upcoming" | "done" | "dropped"
|
|
|
56
71
|
export type Operation = "init" | "start" | "done" | "drop" | "append" | "view";
|
|
57
72
|
|
|
58
73
|
/**
|
|
59
|
-
* The state of one
|
|
74
|
+
* The state of one outcome or work unit.
|
|
60
75
|
*/
|
|
61
76
|
export type Status = "pending" | "in_progress" | "completed" | "abandoned";
|
|
62
77
|
|
|
Binary file
|
package/src/guidance.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Shared instructions for creation results, tool calls, and approved plans. */
|
|
2
|
+
const workUnitGuidance =
|
|
3
|
+
"Size each executable unit for one agent session, with one deliverable, clear scope, and an acceptance condition. " +
|
|
4
|
+
"Split independent outcomes or work that needs another session. Combine trivial steps with their deliverable and tests. " +
|
|
5
|
+
"Delegate independent units separately; the lead can execute one unit. Keep dependent units ordered. " +
|
|
6
|
+
"Do not delegate an oversized parent as one assignment. Close children before explicit parent integration review.";
|
|
7
|
+
|
|
8
|
+
export { workUnitGuidance };
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { TodoWork } from "./work.ts";
|
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import type { WorkHost } from "./work.ts";
|
|
9
9
|
import { runTodo } from "./tool.ts";
|
|
10
|
+
import { workUnitGuidance } from "./guidance.ts";
|
|
10
11
|
|
|
11
12
|
type TodoAPI = WorkHost &
|
|
12
13
|
Readonly<
|
|
@@ -22,17 +23,34 @@ type TodoAPI = WorkHost &
|
|
|
22
23
|
>
|
|
23
24
|
>;
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
-
|
|
26
|
+
const subtasks = Type.Immutable(Type.Array(Type.String())),
|
|
27
|
+
outcome = Type.Immutable(
|
|
28
|
+
Type.Object({ content: Type.String(), subtasks }, { additionalProperties: false }),
|
|
29
|
+
),
|
|
30
|
+
item = Type.Union([Type.String(), outcome]),
|
|
31
|
+
items = Type.Immutable(
|
|
32
|
+
Type.Array(item, {
|
|
33
|
+
description:
|
|
34
|
+
"Pending outcomes for init/append. With parent, append accepts string work units only. Two levels only.",
|
|
35
|
+
}),
|
|
27
36
|
),
|
|
28
37
|
parameters = Type.Object({
|
|
29
38
|
items: Type.Optional(items),
|
|
30
39
|
op: StringEnum(["init", "start", "done", "drop", "append", "view"] as const, {
|
|
31
40
|
description:
|
|
32
|
-
"init replaces; start
|
|
41
|
+
"init replaces; start explicitly activates without demoting others; done completes; drop abandons open work; append adds pending work; view reads.",
|
|
33
42
|
}),
|
|
43
|
+
parent: Type.Optional(
|
|
44
|
+
Type.String({
|
|
45
|
+
description:
|
|
46
|
+
"Exact root content to target a child. Child start/done/drop also require task.",
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
34
49
|
task: Type.Optional(
|
|
35
|
-
Type.String({
|
|
50
|
+
Type.String({
|
|
51
|
+
description:
|
|
52
|
+
"Exact task content within parent, or at root. Omit for done/drop to close open roots. Complete children before parents.",
|
|
53
|
+
}),
|
|
36
54
|
),
|
|
37
55
|
});
|
|
38
56
|
|
|
@@ -46,8 +64,7 @@ export default function piTodo(pi: TodoAPI): void {
|
|
|
46
64
|
work = new TodoWork(pi, presentation);
|
|
47
65
|
work.register(pi);
|
|
48
66
|
pi.registerTool({
|
|
49
|
-
description:
|
|
50
|
-
"Keep the session todo list. Tasks are addressed by their exact content. One task is active; the next open task starts when it closes.",
|
|
67
|
+
description: `Keep branch-local outcomes and optional bounded subtasks. Address exact content plus optional parent. Creation is pending. Start explicitly; parallel starts preserve other active work. Parents require explicit integration review after children close. ${workUnitGuidance}`,
|
|
51
68
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
52
69
|
const result = runTodo(params, {
|
|
53
70
|
append: (customType, list) => {
|
package/src/plannotator.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { TodoContext } from "./lifecycle.ts";
|
|
3
3
|
import type { WorkHost } from "./work.ts";
|
|
4
4
|
import { ownsTodo } from "./lifecycle.ts";
|
|
5
|
+
import { workUnitGuidance } from "./guidance.ts";
|
|
5
6
|
|
|
6
7
|
/** The supported Plannotator external-execution event. */
|
|
7
8
|
interface ApprovedPlan {
|
|
@@ -41,10 +42,11 @@ const isApprovedPlan = (value: unknown): value is ApprovedPlan =>
|
|
|
41
42
|
"The user approved this plan in Plannotator. Continue execution in this session.",
|
|
42
43
|
"Use the todo tool as the only task tracker. Do not maintain Plannotator [DONE:n] markers.",
|
|
43
44
|
"First call todo with op=view to inspect the current branch list.",
|
|
44
|
-
"Before implementation, represent
|
|
45
|
+
"Before implementation, represent remaining outcomes with optional bounded subtasks in that list. Use parent plus exact task for child operations.",
|
|
46
|
+
workUnitGuidance,
|
|
45
47
|
"If the list is empty, call todo with op=init and the remaining approved tasks in order.",
|
|
46
48
|
"Otherwise, reuse matching tasks and append missing tasks. Preserve unrelated work and completed progress.",
|
|
47
|
-
"If approved work remains,
|
|
49
|
+
"If approved work remains, explicitly start its open units with op=start. Independent units can stay active together. Never restart completed work because the plan was approved again.",
|
|
48
50
|
"If no approved work remains, report completion without changing tasks. Otherwise, update tasks with op=done or op=drop as work proceeds.",
|
|
49
51
|
"Resolve a relative planFilePath against cwd. The approved document and reviewer feedback follow as JSON:",
|
|
50
52
|
JSON.stringify({
|
package/src/tool.ts
CHANGED
|
@@ -2,11 +2,16 @@ import { STATE_ENTRY, latestSnapshot } from "./state.ts";
|
|
|
2
2
|
import { apply, parse, view } from "./wasm.ts";
|
|
3
3
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { TodoList } from "./wasm.ts";
|
|
5
|
+
import { workUnitGuidance } from "./guidance.ts";
|
|
5
6
|
|
|
6
7
|
/** One `todo` call, as the schema in `index.ts` admits it. */
|
|
7
8
|
interface TodoParams {
|
|
8
9
|
readonly op: "init" | "start" | "done" | "drop" | "append" | "view";
|
|
9
|
-
readonly items?: readonly
|
|
10
|
+
readonly items?: readonly (
|
|
11
|
+
| string
|
|
12
|
+
| { readonly content: string; readonly subtasks: readonly string[] }
|
|
13
|
+
)[];
|
|
14
|
+
readonly parent?: string;
|
|
10
15
|
readonly task?: string;
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -25,10 +30,13 @@ interface TodoResult {
|
|
|
25
30
|
/**
|
|
26
31
|
* The result for `list`.
|
|
27
32
|
* @param {TodoList} list A valid list.
|
|
33
|
+
* @param {boolean} creation Whether the call created work.
|
|
28
34
|
* @returns {TodoResult} The summary and the list.
|
|
29
35
|
*/
|
|
30
|
-
const result = (list: TodoList): TodoResult => ({
|
|
31
|
-
content: [
|
|
36
|
+
const result = (list: TodoList, creation: boolean): TodoResult => ({
|
|
37
|
+
content: [
|
|
38
|
+
{ text: creation ? `${view(list)}\n\n${workUnitGuidance}` : view(list), type: "text" },
|
|
39
|
+
],
|
|
32
40
|
details: list,
|
|
33
41
|
}),
|
|
34
42
|
/**
|
|
@@ -40,15 +48,11 @@ const result = (list: TodoList): TodoResult => ({
|
|
|
40
48
|
*/
|
|
41
49
|
runTodo = (params: TodoParams, session: TodoSession): TodoResult => {
|
|
42
50
|
const current = parse(latestSnapshot(session.branch())),
|
|
43
|
-
{ list, persist } = apply(current,
|
|
44
|
-
items: params.items ? [...params.items] : params.items,
|
|
45
|
-
op: params.op,
|
|
46
|
-
task: params.task,
|
|
47
|
-
});
|
|
51
|
+
{ list, persist } = apply(current, params);
|
|
48
52
|
if (persist) {
|
|
49
53
|
session.append(STATE_ENTRY, list);
|
|
50
54
|
}
|
|
51
|
-
return result(list);
|
|
55
|
+
return result(list, params.op === "init" || params.op === "append");
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
export { runTodo };
|
package/src/wasm.ts
CHANGED
package/src/widget.ts
CHANGED
|
@@ -16,8 +16,9 @@ const TODO_SHORTCUT = "alt+t",
|
|
|
16
16
|
|
|
17
17
|
/** The presentation-only component; task state stays on the session branch. */
|
|
18
18
|
class TodoWidget extends Text {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
#rows: readonly Readonly<WidgetRow>[];
|
|
20
|
+
#expanded: boolean;
|
|
21
|
+
#shortcut = "Alt+T";
|
|
21
22
|
readonly #theme: Readonly<Pick<Theme, "fg" | "bold">>;
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -37,6 +38,25 @@ class TodoWidget extends Text {
|
|
|
37
38
|
this.#theme = theme;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Updates rows without replacing the host's scrollable component.
|
|
43
|
+
* @param {readonly Readonly<WidgetRow>[]} rows Updated semantic rows.
|
|
44
|
+
* @param {boolean} expanded Whether secondary groups are visible.
|
|
45
|
+
*/
|
|
46
|
+
public setRows(rows: readonly Readonly<WidgetRow>[], expanded: boolean): void {
|
|
47
|
+
this.#rows = rows;
|
|
48
|
+
this.#expanded = expanded;
|
|
49
|
+
this.invalidate();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Uses the shortcut owned by the containing surface.
|
|
54
|
+
* @param {string} shortcut The host's display shortcut.
|
|
55
|
+
*/
|
|
56
|
+
public setShortcut(shortcut: string): void {
|
|
57
|
+
this.#shortcut = shortcut;
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
/**
|
|
41
61
|
* Wraps all task text with Pi's native terminal component.
|
|
42
62
|
* @param {number} width Available terminal columns.
|
|
@@ -48,7 +68,7 @@ class TodoWidget extends Text {
|
|
|
48
68
|
return row.kind === "active" ? this.#theme.bold(text) : text;
|
|
49
69
|
}),
|
|
50
70
|
action = this.#expanded ? "collapse" : "upcoming / done";
|
|
51
|
-
lines.push(this.#theme.fg("dim",
|
|
71
|
+
lines.push(this.#theme.fg("dim", `${this.#shortcut} · ${action}`));
|
|
52
72
|
this.setText(lines.join("\n"));
|
|
53
73
|
return super.render(width).map((line) => truncateToWidth(line, width));
|
|
54
74
|
}
|
package/src/work.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ownsTodo, show } from "./lifecycle.ts";
|
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { TODO_SHORTCUT } from "./widget.ts";
|
|
5
5
|
import type { TodoPresentation } from "./presentation.ts";
|
|
6
|
+
import { display } from "./display.ts";
|
|
6
7
|
import { latestSnapshot } from "./state.ts";
|
|
7
8
|
import { parse } from "./wasm.ts";
|
|
8
9
|
|
|
@@ -11,7 +12,7 @@ interface WorkHost extends TodoHost {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
type WorkCommands = Readonly<Pick<ExtensionAPI, "registerCommand" | "registerShortcut">>;
|
|
14
|
-
const protocolVersion =
|
|
15
|
+
const protocolVersion = 2,
|
|
15
16
|
initialRevision = 0;
|
|
16
17
|
|
|
17
18
|
interface WorkContext extends TodoContext {
|
|
@@ -129,7 +130,10 @@ class TodoWork {
|
|
|
129
130
|
}
|
|
130
131
|
}),
|
|
131
132
|
];
|
|
132
|
-
this.#host.events.emit("oisin.todo.ready", {
|
|
133
|
+
this.#host.events.emit("oisin.todo.ready", {
|
|
134
|
+
publisher: this.#publisher,
|
|
135
|
+
version: protocolVersion,
|
|
136
|
+
});
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
/**
|
|
@@ -145,13 +149,13 @@ class TodoWork {
|
|
|
145
149
|
const list = parse(latestSnapshot(ctx.sessionManager.getBranch()));
|
|
146
150
|
this.#host.events.emit("oisin.todo.snapshot", {
|
|
147
151
|
branch: ctx.sessionManager.getLeafId(),
|
|
152
|
+
display: display(list),
|
|
148
153
|
presenter: this.#presenter.presenter,
|
|
149
154
|
publisher: this.#publisher,
|
|
150
155
|
request: this.#presenter.request,
|
|
151
156
|
revision: this.#revision,
|
|
152
157
|
session: ctx.sessionManager.getSessionId(),
|
|
153
|
-
|
|
154
|
-
version: 1,
|
|
158
|
+
version: protocolVersion,
|
|
155
159
|
});
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -166,7 +170,7 @@ class TodoWork {
|
|
|
166
170
|
this.#host.events.emit("oisin.work.open", {
|
|
167
171
|
presenter: this.#presenter.presenter,
|
|
168
172
|
request: this.#presenter.request,
|
|
169
|
-
version:
|
|
173
|
+
version: protocolVersion,
|
|
170
174
|
view: "todos",
|
|
171
175
|
});
|
|
172
176
|
return true;
|
|
@@ -179,7 +183,10 @@ class TodoWork {
|
|
|
179
183
|
}
|
|
180
184
|
this.#subscriptions = [];
|
|
181
185
|
if (this.#context !== undefined) {
|
|
182
|
-
this.#host.events.emit("oisin.todo.stop", {
|
|
186
|
+
this.#host.events.emit("oisin.todo.stop", {
|
|
187
|
+
publisher: this.#publisher,
|
|
188
|
+
version: protocolVersion,
|
|
189
|
+
});
|
|
183
190
|
}
|
|
184
191
|
this.#release();
|
|
185
192
|
this.#context = undefined;
|