@oisin-ee/pi-todo 0.2.0 → 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 CHANGED
@@ -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. There is no other state. So a resumed session,
31
- a `/tree` move, and calls made from inside codemode all see the list of the current branch. A widget shows the list
32
- while it has tasks, and `/todos` prints it.
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.2.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",
@@ -21,11 +21,13 @@
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": {
@@ -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
- * The widget lines for `list`, or `undefined` for an empty list.
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): string[] | undefined;
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
- * The widget lines for `list`, or `undefined` for an empty list.
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
- * @returns {string[] | undefined}
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, replay, settle, show, start } from "./lifecycle.ts";
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<ExtensionAPI, "appendEntry" | "getAllTools" | "on" | "registerCommand" | "registerTool">
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
- replay(ctx);
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
- replay(ctx);
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: "Show the todo list on this session branch.",
59
- async handler(_args, ctx) {
60
- show(ctx);
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
- const list = parse(latestSnapshot(ctx.sessionManager.getBranch()));
42
- ctx.ui.setWidget("pi-todo", widget(list));
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
@@ -28,4 +28,6 @@ export type {
28
28
  Status,
29
29
  Task,
30
30
  TodoList,
31
+ WidgetKind,
32
+ WidgetRow,
31
33
  } from "./generated/todo_core.js";
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 };