@gavin-hu/my-pi-agent 0.11.3 → 0.12.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.
Files changed (86) hide show
  1. package/README.md +13 -8
  2. package/README.zh-CN.md +5 -3
  3. package/assets/chrome.png +0 -0
  4. package/assets/plan-review.png +0 -0
  5. package/extensions/bytefly/README.md +291 -207
  6. package/extensions/bytefly/doctor/runtime.ts +16 -0
  7. package/extensions/bytefly/doctor/tools.ts +1 -3
  8. package/extensions/bytefly/footer/price/config.ts +54 -15
  9. package/extensions/bytefly/footer/price/price.ts +20 -7
  10. package/extensions/bytefly/footer/price/types.ts +13 -7
  11. package/extensions/bytefly/footer/render/format.ts +3 -2
  12. package/extensions/bytefly/footer/render/segments.ts +3 -2
  13. package/extensions/bytefly/footer/types.ts +1 -0
  14. package/extensions/bytefly/header/component.ts +21 -6
  15. package/extensions/bytefly/header/config.ts +3 -0
  16. package/extensions/bytefly/header/register.ts +34 -3
  17. package/extensions/bytefly/index.ts +13 -3
  18. package/extensions/bytefly/panel/settings-runtime.ts +71 -28
  19. package/extensions/bytefly/panel/settings.ts +201 -38
  20. package/extensions/bytefly/panel/tui/entries.ts +58 -0
  21. package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
  22. package/extensions/bytefly/panel/tui/frame.ts +71 -0
  23. package/extensions/bytefly/panel/tui/nav.ts +75 -0
  24. package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
  25. package/extensions/bytefly/panel/tui.ts +205 -319
  26. package/extensions/bytefly/picker/runtime.ts +61 -2
  27. package/extensions/bytefly/picker/toggle.ts +56 -0
  28. package/extensions/bytefly/update/check.ts +59 -0
  29. package/extensions/bytefly/update/format.ts +32 -0
  30. package/extensions/bytefly/update/register.ts +72 -0
  31. package/extensions/bytefly/update/state.ts +28 -0
  32. package/extensions/bytefly/update/version.ts +83 -0
  33. package/extensions/document/README.md +2 -2
  34. package/extensions/document/tool.ts +1 -3
  35. package/extensions/file-open/README.md +30 -15
  36. package/extensions/file-open/index.ts +4 -3
  37. package/extensions/file-open/open.ts +52 -21
  38. package/extensions/goal/README.md +39 -13
  39. package/extensions/goal/clarify-prompt.ts +5 -36
  40. package/extensions/goal/clarify.ts +55 -86
  41. package/extensions/goal/commands.ts +41 -35
  42. package/extensions/goal/format.ts +12 -4
  43. package/extensions/goal/tools.ts +28 -8
  44. package/extensions/goal/tui.ts +1 -1
  45. package/extensions/memory/README.md +1 -1
  46. package/extensions/plan/README.md +24 -8
  47. package/extensions/plan/blocks.ts +1 -35
  48. package/extensions/plan/commands.ts +18 -12
  49. package/extensions/plan/plans.ts +1 -1
  50. package/extensions/plan/policy.ts +4 -3
  51. package/extensions/plan/review.ts +243 -0
  52. package/extensions/plan/runtime.ts +4 -0
  53. package/extensions/plan/steps.ts +34 -23
  54. package/extensions/plan/todos.ts +5 -58
  55. package/extensions/plan/tools.ts +66 -222
  56. package/extensions/plan/tui.ts +17 -9
  57. package/extensions/plan/types.ts +3 -0
  58. package/extensions/plan/web-client.ts +51 -18
  59. package/extensions/plan/web-page.ts +5 -0
  60. package/extensions/plan/web-router.ts +11 -3
  61. package/extensions/plan/web.ts +100 -36
  62. package/extensions/question/README.md +1 -1
  63. package/extensions/question/tools.ts +1 -1
  64. package/extensions/subagent/index.ts +0 -2
  65. package/extensions/todo/README.md +19 -11
  66. package/extensions/todo/index.ts +5 -3
  67. package/extensions/todo/nudge.ts +58 -9
  68. package/extensions/todo/runtime.ts +28 -17
  69. package/extensions/web-access/README.md +3 -3
  70. package/extensions/web-access/fetch/tool.ts +1 -3
  71. package/extensions/web-access/search/tool.ts +1 -3
  72. package/extensions/wechat/README.md +22 -11
  73. package/extensions/wechat/bridge.ts +90 -22
  74. package/extensions/wechat/commands.ts +45 -1
  75. package/extensions/wechat/doctor.ts +1 -0
  76. package/extensions/wechat/index.ts +3 -1
  77. package/extensions/wechat/status-screen.ts +32 -2
  78. package/extensions/wechat/tools.ts +0 -2
  79. package/lib/README.md +2 -1
  80. package/lib/config.ts +43 -1
  81. package/lib/env.ts +12 -0
  82. package/lib/requirements.ts +76 -2
  83. package/lib/todo-state.ts +62 -0
  84. package/lib/tui/glyphs.ts +1 -0
  85. package/package.json +1 -1
  86. package/assets/bytefly.schema.json +0 -785
@@ -24,7 +24,7 @@ import { peerLabel } from "./format.ts";
24
24
  import type { BotCredentials } from "./types.ts";
25
25
 
26
26
  /** User-facing connection word; the internal `BridgeState` stays running/stopped. */
27
- export type StatusConnection = "open" | "expired" | "closed";
27
+ export type StatusConnection = "open" | "expired" | "paused" | "closed";
28
28
 
29
29
  /**
30
30
  * What the status screen closed with: a global action, a peer history request,
@@ -69,7 +69,7 @@ const MAX_PEER_ROWS = 5;
69
69
 
70
70
  /** The connection word for a bridge status, or `closed` when there is none. */
71
71
  export function connectionOf(status: BridgeStatus | undefined): StatusConnection {
72
- return status?.open ? "open" : status?.expired ? "expired" : "closed";
72
+ return status?.paused ? "paused" : status?.open ? "open" : status?.expired ? "expired" : "closed";
73
73
  }
74
74
 
75
75
  /** Build the view from the stored account and the live bridge status. */
@@ -146,6 +146,8 @@ function connectionText(connection: StatusConnection): string {
146
146
  return "open";
147
147
  case "expired":
148
148
  return "expired";
149
+ case "paused":
150
+ return "paused";
149
151
  case "closed":
150
152
  return "closed";
151
153
  }
@@ -158,6 +160,8 @@ function connectionRole(connection: StatusConnection): TuiRole {
158
160
  return "ok";
159
161
  case "expired":
160
162
  return "bad";
163
+ case "paused":
164
+ return "warn";
161
165
  case "closed":
162
166
  return "meta";
163
167
  }
@@ -193,6 +197,9 @@ export function statusBody(view: StatusView, theme: Theme, width: number, select
193
197
  if (view.connection === "expired") {
194
198
  lines.push("");
195
199
  lines.push(clip(` ${paint(theme, "bad", "Session expired — press l to log in again.")}`, target));
200
+ } else if (view.connection === "paused") {
201
+ lines.push("");
202
+ lines.push(clip(` ${paint(theme, "warn", "Paused — press p to resume polling.")}`, target));
196
203
  } else if (view.connection === "closed" && view.account) {
197
204
  lines.push("");
198
205
  lines.push(clip(` ${paint(theme, "meta", "Not connected — press l to log in again.")}`, target));
@@ -251,6 +258,8 @@ export interface StatusActions {
251
258
  reset(peer: string): string;
252
259
  /** Withdraw the waiting prompt; returns the line to flash. */
253
260
  cancelPrompt(): string;
261
+ /** Pause or resume polling; async because it settles the poll loop first. */
262
+ togglePause(): string | Promise<string>;
254
263
  }
255
264
 
256
265
  export interface StatusScreenOptions {
@@ -325,6 +334,27 @@ export class StatusScreenComponent implements Component {
325
334
  this.setFlash(undefined);
326
335
  return;
327
336
  }
337
+ if (data === "p" || data === "P") {
338
+ if (!view.bridge) {
339
+ this.setFlash("WeChat is not running.");
340
+ return;
341
+ }
342
+ const outcome = this.options.actions?.togglePause();
343
+ if (!outcome) {
344
+ this.setFlash("Pause is unavailable here.");
345
+ return;
346
+ }
347
+ if (typeof outcome === "string") {
348
+ this.setFlash(outcome);
349
+ return;
350
+ }
351
+ this.setFlash(view.connection === "paused" ? "Resuming…" : "Pausing…");
352
+ void outcome.then(
353
+ (line) => this.setFlash(line),
354
+ () => this.setFlash("Pause action failed."),
355
+ );
356
+ return;
357
+ }
328
358
  if (data === "a" || data === "A") {
329
359
  this.options.onClose({ kind: "allow" });
330
360
  return;
@@ -68,8 +68,6 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
68
68
  ],
69
69
  parameters: WechatSendParams,
70
70
  outputSchema: WechatSendResult,
71
- exposure: "direct",
72
- defaultActive: true,
73
71
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
74
72
  executionMode: "sequential",
75
73
 
package/lib/README.md CHANGED
@@ -16,7 +16,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
16
16
  | Module | Responsibility |
17
17
  |---|---|
18
18
  | [`browser.ts`](./browser.ts) | Cross-platform default-browser opener shared by `file-browse` and `plan`: pure `browserCommand(url, platform)` plus best-effort `openInBrowser(url, { platform, spawn })` with an injectable spawn. |
19
- | [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection`. Includes `clampInteger` / `clampNumber` / `cleanString` / `asRecord` / `readJson` / `readJsonResult`. |
19
+ | [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` (the last two edit one leaf, flat or nested, preserving siblings and metadata). Includes `clampInteger` / `clampNumber` / `cleanString` / `asRecord` / `readJson` / `readJsonResult`. |
20
20
  | [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
21
21
  | [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
22
22
  | [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` and `JobFinishedEvent`, published by `job` and consumed by bytefly's notify capability. |
@@ -32,6 +32,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
32
32
  | [`rails.ts`](./rails.ts) | Above-editor widget ordering (`goal` → `todo`) and dock-screen suppression, carried on `pi.events`. |
33
33
  | [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. |
34
34
  | [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
35
+ | [`todo-state.ts`](./todo-state.ts) | The published `todo` state replayed from the session branch: `ExistingTodo` and `lastTodos(entries)` (the last valid list, skipping a rejected call), so `plan` can merge an approved plan's steps into it and `goal` can name the items still open when a goal is marked achieved — with no extension importing another's modules. |
35
36
  | [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
36
37
  | [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, and the `tok-*` map), [`web/base.ts`](./web/base.ts), [`web/components.ts`](./web/components.ts), [`web/layout.ts`](./web/layout.ts), [`web/runtime.ts`](./web/runtime.ts) (`WEB_THEME_STORAGE_KEY`, `THEME_JS`, `THEME_TOGGLE_JS`), [`web/markup.ts`](./web/markup.ts), and the [`web/index.ts`](./web/index.ts) barrel. |
37
38
 
package/lib/config.ts CHANGED
@@ -139,6 +139,22 @@ export function configSection(
139
139
  return asRecord(raw?.[key]);
140
140
  }
141
141
 
142
+ /**
143
+ * The value at `path` in a document or section, or `undefined` when a step is
144
+ * absent or not an object. A one-element path reads a top-level key, so this
145
+ * also answers "is this setting stated here" for the flat case. `raw` may be an
146
+ * untrusted JSON value or an already-normalized config object.
147
+ */
148
+ export function configValueAt(raw: unknown, path: readonly string[]): unknown {
149
+ let current: unknown = raw;
150
+ for (const key of path) {
151
+ const record = asRecord(current);
152
+ if (!record) return undefined;
153
+ current = record[key];
154
+ }
155
+ return current;
156
+ }
157
+
142
158
  /** Options for {@link loadConfigSection}. */
143
159
  export interface SectionOptions {
144
160
  /** Agent directory override; `worktree` resolves it explicitly. */
@@ -197,12 +213,38 @@ function readEditableDocument(path: string): Record<string, unknown> {
197
213
  * malformed document.
198
214
  */
199
215
  export function writeConfigValue(path: string, section: string, key: string, value: unknown): void {
216
+ writeConfigValueAt(path, section, [key], value);
217
+ }
218
+
219
+ /**
220
+ * Set the leaf at `keyPath` in one top-level section, with the same guarantees
221
+ * as {@link writeConfigValue}: siblings, other sections, and metadata survive,
222
+ * and the file and the section are created when absent. Intermediate objects
223
+ * are created as needed; a non-object in the way is replaced, because a refused
224
+ * edit would leave a hand-written typo unfixable from the screen. Refuses a
225
+ * malformed document and an empty path.
226
+ */
227
+ export function writeConfigValueAt(path: string, section: string, keyPath: readonly string[], value: unknown): void {
228
+ if (keyPath.length === 0) throw new Error(`Cannot update ${path}: empty key path`);
200
229
  const document = readEditableDocument(path);
201
230
  const current = configSection(document, section) ?? {};
202
- document[section] = { ...current, [key]: value };
231
+ document[section] = setPath(current, keyPath, value);
203
232
  writeConfigFile(path, document);
204
233
  }
205
234
 
235
+ /** Copy `source` with the leaf at `path` replaced; every sibling key is kept. */
236
+ function setPath(source: Record<string, unknown>, path: readonly string[], value: unknown): Record<string, unknown> {
237
+ const [head] = path;
238
+ const next = { ...source };
239
+ if (head === undefined) return next;
240
+ if (path.length === 1) {
241
+ next[head] = value;
242
+ return next;
243
+ }
244
+ next[head] = setPath(asRecord(source[head]) ?? {}, path.slice(1), value);
245
+ return next;
246
+ }
247
+
206
248
  /**
207
249
  * Delete one key from one top-level section, preserving its siblings and the
208
250
  * rest of the document. An emptied section is removed rather than left as `{}`.
package/lib/env.ts CHANGED
@@ -16,6 +16,9 @@ export const ENV_ROOT = "PI_WORKTREE_ROOT";
16
16
  */
17
17
  export const ENV_DISABLED_EXTENSIONS = "PI_DISABLED_EXTENSIONS";
18
18
 
19
+ /** Env var that turns off Pi's automatic network activity, including our own. */
20
+ export const ENV_OFFLINE = "PI_OFFLINE";
21
+
19
22
  /** False when `name` appears in `PI_DISABLED_EXTENSIONS`, true otherwise. */
20
23
  export function isExtensionEnabled(name: string): boolean {
21
24
  const raw = process.env[ENV_DISABLED_EXTENSIONS];
@@ -29,6 +32,15 @@ export function isExtensionEnabled(name: string): boolean {
29
32
  return !disabled.has(name.toLowerCase());
30
33
  }
31
34
 
35
+ /**
36
+ * Whether `PI_OFFLINE` is on, read the way Pi reads it: `1`, `true`, or `yes`
37
+ * (case-insensitive). Any other value, including empty, is off.
38
+ */
39
+ export function isOffline(): boolean {
40
+ const raw = process.env[ENV_OFFLINE]?.trim().toLowerCase();
41
+ return raw === "1" || raw === "true" || raw === "yes";
42
+ }
43
+
32
44
  /** The active worktree root from the environment, when set and still on disk. */
33
45
  export function worktreeRoot(): string | undefined {
34
46
  const root = process.env[ENV_ROOT];
@@ -61,7 +61,19 @@ export interface InvocationCheck {
61
61
  hint: string;
62
62
  }
63
63
 
64
- export type RequirementCheck = BinaryCheck | ModuleCheck | StateCheck | InvocationCheck;
64
+ /**
65
+ * The notification delivery bytefly is configured for.
66
+ *
67
+ * The configured method belongs to the notify capability, so it arrives through
68
+ * {@link ProbeSeams.notify}; whether this host can honour it is decided here.
69
+ */
70
+ export interface NotifyCheck {
71
+ kind: "notify";
72
+ enables: string;
73
+ hint: string;
74
+ }
75
+
76
+ export type RequirementCheck = BinaryCheck | ModuleCheck | StateCheck | InvocationCheck | NotifyCheck;
65
77
 
66
78
  /** What an extension needs, split by how badly it needs it. */
67
79
  export interface ExtensionRequirements {
@@ -173,7 +185,8 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
173
185
  "file-open": {
174
186
  notes: [
175
187
  "The `/open` command and its notification need an interactive UI; the `open_path` tool works without one.",
176
- "Launching the OS handler is best-effort: a failure is reported, and the tool result still names the target.",
188
+ "Launching the OS handler is best-effort: a launcher that fails to start, or exits non-zero on macOS and Linux, is reported; Windows `explorer` can exit non-zero after a successful open.",
189
+ "The tool result names the target even when the launch fails.",
177
190
  ],
178
191
  },
179
192
  memory: {
@@ -193,6 +206,13 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
193
206
  },
194
207
  bytefly: {
195
208
  configFiles: [SHARED_CONFIG_FILE],
209
+ optional: [
210
+ {
211
+ kind: "notify",
212
+ enables: "the configured notification delivery",
213
+ hint: 'set "notify.method" to "auto" in bytefly.json, or choose one this host can deliver.',
214
+ },
215
+ ],
196
216
  notes: [
197
217
  "TUI only for the header, footer, and turn rule; `/bytefly` prints a listing and `/bytefly doctor` the report in RPC, JSON, and print modes.",
198
218
  "The panel writes extension enablement through Pi's own settings; project scope requires a trusted project.",
@@ -229,6 +249,35 @@ export interface ProbeSeams {
229
249
  loadModule?: (name: string) => Promise<unknown>;
230
250
  /** Resolve the nested-pi command; defaults to {@link getPiInvocation}. */
231
251
  invocation?: () => PiInvocation;
252
+ /** The notify capability's configured delivery, or absent when it is not in play. */
253
+ notify?: () => { enabled: boolean; method: string } | undefined;
254
+ /** Whether stdout is a terminal, which the OSC methods need; defaults to true. */
255
+ isTTY?: boolean;
256
+ }
257
+
258
+ /**
259
+ * Whether `method` can deliver on this host.
260
+ *
261
+ * `auto` inspects the environment itself, so it always can; the OSC methods need
262
+ * a terminal to write to; the rest are platform-specific. An unknown id counts
263
+ * as deliverable because bytefly's own loader already rejects one.
264
+ */
265
+ export function canDeliver(method: string, env: { platform: NodeJS.Platform; isTTY: boolean }): boolean {
266
+ switch (method) {
267
+ case "auto":
268
+ return true;
269
+ case "osc777":
270
+ case "osc99":
271
+ return env.isTTY;
272
+ case "toast":
273
+ return env.platform === "win32";
274
+ case "osascript":
275
+ return env.platform === "darwin";
276
+ case "notify-send":
277
+ return env.platform === "linux";
278
+ default:
279
+ return true;
280
+ }
232
281
  }
233
282
 
234
283
  const defaultLoadModule = (name: string): Promise<unknown> => import(name);
@@ -317,6 +366,31 @@ async function runCheck(
317
366
  hint: check.hint,
318
367
  };
319
368
  }
369
+ case "notify": {
370
+ const notify = seams.notify?.();
371
+ if (!notify?.enabled) {
372
+ return { extension, check: "notify.method", severity: "ok", detail: "notifications are off" };
373
+ }
374
+ const env = { platform: seams.platform, isTTY: seams.isTTY ?? true };
375
+ if (canDeliver(notify.method, env)) {
376
+ return {
377
+ extension,
378
+ check: "notify.method",
379
+ severity: "ok",
380
+ detail:
381
+ notify.method === "auto"
382
+ ? "auto picks a route from the environment"
383
+ : `"${notify.method}" delivers on ${seams.platform}`,
384
+ };
385
+ }
386
+ return {
387
+ extension,
388
+ check: "notify.method",
389
+ severity: failSeverity,
390
+ detail: `"${notify.method}" cannot deliver on ${seams.platform} — ${check.enables}`,
391
+ hint: check.hint,
392
+ };
393
+ }
320
394
  }
321
395
  }
322
396
 
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The published `todo` state, read back from the session branch (pure).
3
+ *
4
+ * The todo list is a whole-list replacement stored in each `todo` tool result's
5
+ * `details`, so replaying the branch is how another extension reads it without
6
+ * importing the todo extension. `plan` merges an approved plan's steps into the
7
+ * last list, and `goal` mentions the items still open when a goal is marked
8
+ * achieved.
9
+ */
10
+
11
+ import { TODO_TOOL } from "./tool-names.ts";
12
+
13
+ /** One item in a `todo` list, as the todo tool stores it. */
14
+ export interface ExistingTodo {
15
+ content: string;
16
+ status: "pending" | "in_progress" | "completed";
17
+ /** Present-continuous label the todo tool requires for an in-progress item. */
18
+ activeForm?: string;
19
+ }
20
+
21
+ /** Minimal shape of a session entry, used for runtime narrowing. */
22
+ interface BranchEntryLike {
23
+ type?: string;
24
+ message?: {
25
+ role?: string;
26
+ toolName?: string;
27
+ details?: unknown;
28
+ };
29
+ }
30
+
31
+ function isStatus(value: unknown): value is ExistingTodo["status"] {
32
+ return value === "pending" || value === "in_progress" || value === "completed";
33
+ }
34
+
35
+ /**
36
+ * The last valid todo list written on the branch, or an empty list. A rejected
37
+ * call carries the unchanged list and is not a state write, so it is skipped;
38
+ * an entry that does not re-validate is ignored rather than wiping a good list.
39
+ */
40
+ export function lastTodos(entries: Iterable<unknown>): ExistingTodo[] {
41
+ let todos: ExistingTodo[] = [];
42
+ for (const raw of entries) {
43
+ const entry = raw as BranchEntryLike;
44
+ if (entry?.type !== "message") continue;
45
+ const message = entry.message;
46
+ if (message?.role !== "toolResult" || message.toolName !== TODO_TOOL) continue;
47
+ const details = message.details as { todos?: unknown; error?: unknown } | undefined;
48
+ if (!details || details.error) continue;
49
+ if (!Array.isArray(details.todos)) continue;
50
+ todos = details.todos.flatMap((item): ExistingTodo[] => {
51
+ if (typeof item !== "object" || item === null) return [];
52
+ const value = item as { content?: unknown; status?: unknown; activeForm?: unknown };
53
+ if (typeof value.content !== "string" || !isStatus(value.status)) return [];
54
+ const content = value.content.trim();
55
+ if (!content) return [];
56
+ const activeForm =
57
+ typeof value.activeForm === "string" && value.activeForm.trim() ? value.activeForm.trim() : undefined;
58
+ return [activeForm ? { content, status: value.status, activeForm } : { content, status: value.status }];
59
+ });
60
+ }
61
+ return todos;
62
+ }
package/lib/tui/glyphs.ts CHANGED
@@ -48,6 +48,7 @@ export const GLYPHS = {
48
48
  boxOff: "◻",
49
49
  prev: "‹",
50
50
  next: "›",
51
+ update: "↑",
51
52
  prompt: "❯",
52
53
  } as const;
53
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavin-hu/my-pi-agent",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, document, memory, wechat, and bytefly.",
5
5
  "keywords": [
6
6
  "pi-package",