@gavin-hu/my-pi-agent 0.19.0 → 0.20.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 (158) hide show
  1. package/README.md +17 -11
  2. package/README.zh-CN.md +13 -8
  3. package/docs/extensions/README.md +1 -1
  4. package/docs/extensions/board.md +286 -123
  5. package/docs/extensions/bytefly.md +381 -223
  6. package/docs/extensions/file-browse.md +14 -7
  7. package/docs/extensions/job.md +1 -2
  8. package/docs/extensions/plan.md +56 -11
  9. package/docs/extensions/session-bus.md +23 -10
  10. package/docs/extensions/team.md +45 -17
  11. package/docs/extensions/wechat.md +2 -1
  12. package/docs/extensions/worktree.md +30 -15
  13. package/extensions/board/commands.ts +23 -5
  14. package/extensions/board/format.ts +107 -9
  15. package/extensions/board/join.ts +1 -1
  16. package/extensions/board/page.ts +170 -94
  17. package/extensions/board/result.ts +9 -0
  18. package/extensions/board/schema.ts +22 -7
  19. package/extensions/board/seat.ts +9 -10
  20. package/extensions/board/snapshot.ts +1 -0
  21. package/extensions/board/tools.ts +186 -21
  22. package/extensions/board/transport/router.ts +27 -108
  23. package/extensions/board/transport/server.ts +18 -29
  24. package/extensions/board/types.ts +67 -0
  25. package/extensions/board/view/client.ts +23 -13
  26. package/extensions/board/view/dialog.ts +43 -0
  27. package/extensions/board/view/keys.ts +28 -24
  28. package/extensions/board/view/live.ts +5 -171
  29. package/extensions/board/view/names.ts +91 -0
  30. package/extensions/board/view/page.ts +48 -88
  31. package/extensions/board/view/render.ts +213 -190
  32. package/extensions/board/view/styles.ts +65 -48
  33. package/extensions/board/view/team.ts +63 -41
  34. package/extensions/bytefly/footer/budget.ts +6 -5
  35. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  36. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  37. package/extensions/bytefly/footer/config.ts +19 -8
  38. package/extensions/bytefly/footer/price/price.ts +30 -8
  39. package/extensions/bytefly/footer/register.ts +5 -5
  40. package/extensions/bytefly/footer/render/format.ts +18 -0
  41. package/extensions/bytefly/footer/render/lines.ts +23 -7
  42. package/extensions/bytefly/footer/render/segments.ts +117 -31
  43. package/extensions/bytefly/footer/types.ts +29 -5
  44. package/extensions/bytefly/footer/unpriced.ts +42 -17
  45. package/extensions/bytefly/panel/commands.ts +28 -1
  46. package/extensions/bytefly/panel/filter.ts +68 -0
  47. package/extensions/bytefly/panel/format.ts +16 -15
  48. package/extensions/bytefly/panel/join.ts +56 -13
  49. package/extensions/bytefly/panel/prices.ts +43 -0
  50. package/extensions/bytefly/panel/settings.ts +72 -23
  51. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  52. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  53. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  54. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  55. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  56. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  57. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  58. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  59. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  60. package/extensions/bytefly/panel/tui.ts +306 -259
  61. package/extensions/check/commands.ts +2 -1
  62. package/extensions/file-browse/files/classify.ts +136 -0
  63. package/extensions/file-browse/files/files.ts +8 -547
  64. package/extensions/file-browse/files/index.ts +90 -0
  65. package/extensions/file-browse/files/list.ts +261 -0
  66. package/extensions/file-browse/files/tree.ts +101 -0
  67. package/extensions/file-browse/transport/router.ts +0 -2
  68. package/extensions/file-browse/view/emit.ts +51 -0
  69. package/extensions/file-browse/view/html.ts +6 -7
  70. package/extensions/file-browse/view/markdown.ts +11 -13
  71. package/extensions/file-browse/view/raw-html.ts +18 -13
  72. package/extensions/job/runtime.ts +4 -4
  73. package/extensions/plan/feedback.ts +40 -16
  74. package/extensions/plan/format.ts +10 -3
  75. package/extensions/plan/index.ts +17 -1
  76. package/extensions/plan/plan-files.ts +88 -0
  77. package/extensions/plan/plans.ts +19 -23
  78. package/extensions/plan/policy.ts +10 -5
  79. package/extensions/plan/runtime.ts +31 -2
  80. package/extensions/plan/todos.ts +51 -3
  81. package/extensions/plan/tools/exit.ts +10 -1
  82. package/extensions/plan/tools/todo-seed.ts +4 -2
  83. package/extensions/plan/tools/write.ts +12 -1
  84. package/extensions/plan/types.ts +15 -0
  85. package/extensions/plan/web-blocks.ts +89 -0
  86. package/extensions/plan/web-chrome.ts +3 -5
  87. package/extensions/plan/web-client.ts +179 -330
  88. package/extensions/plan/web-collab.ts +3 -3
  89. package/extensions/plan/web-comments.ts +18 -8
  90. package/extensions/plan/web-composer.ts +80 -0
  91. package/extensions/plan/web-draft.ts +40 -0
  92. package/extensions/plan/web-keys.ts +134 -0
  93. package/extensions/plan/web-outline.ts +192 -0
  94. package/extensions/plan/web-page.ts +4 -1
  95. package/extensions/plan/web-plan.ts +150 -0
  96. package/extensions/plan/web-router.ts +0 -1
  97. package/extensions/plan/web-settle.ts +194 -0
  98. package/extensions/rewind/commands/review.ts +2 -1
  99. package/extensions/session-bus/rows.ts +7 -3
  100. package/extensions/session-bus/runtime.ts +100 -38
  101. package/extensions/session-bus/store.ts +14 -2
  102. package/extensions/session-bus/tui.ts +1 -1
  103. package/extensions/session-bus/types.ts +10 -1
  104. package/extensions/subagent/commands.ts +2 -1
  105. package/extensions/team/commands.ts +41 -8
  106. package/extensions/team/format.ts +14 -0
  107. package/extensions/team/index.ts +2 -1
  108. package/extensions/team/tools.ts +30 -9
  109. package/extensions/team/types.ts +17 -0
  110. package/extensions/todo/index.ts +4 -1
  111. package/extensions/todo/nudge.ts +8 -2
  112. package/extensions/wechat/bridge.ts +43 -172
  113. package/extensions/wechat/intake.ts +245 -0
  114. package/extensions/worktree/commands.ts +24 -17
  115. package/extensions/worktree/exit.ts +12 -6
  116. package/extensions/worktree/git.ts +22 -14
  117. package/extensions/worktree/guard.ts +15 -285
  118. package/extensions/worktree/handoff.ts +10 -2
  119. package/extensions/worktree/index.ts +2 -1
  120. package/extensions/worktree/lifecycle.ts +2 -0
  121. package/extensions/worktree/registry.ts +20 -11
  122. package/extensions/worktree/root-tools.ts +13 -3
  123. package/extensions/worktree/runtime.ts +8 -2
  124. package/extensions/worktree/screen.ts +15 -8
  125. package/extensions/worktree/shell.ts +303 -0
  126. package/extensions/worktree/state.ts +1 -1
  127. package/extensions/worktree/status.ts +42 -9
  128. package/extensions/worktree/tools.ts +19 -13
  129. package/lib/README.md +6 -5
  130. package/lib/board/card.ts +192 -3
  131. package/lib/board/links.ts +26 -2
  132. package/lib/board/page-pointer.ts +86 -32
  133. package/lib/board/round-text.ts +11 -2
  134. package/lib/board/round.ts +25 -20
  135. package/lib/board/rules.ts +144 -11
  136. package/lib/board/store.ts +239 -52
  137. package/lib/board/view.ts +94 -10
  138. package/lib/format.ts +22 -0
  139. package/lib/http/serve.ts +3 -0
  140. package/lib/http/sink.ts +34 -9
  141. package/lib/plan-entry.ts +11 -0
  142. package/lib/policy.ts +14 -1
  143. package/lib/session-roster.ts +74 -3
  144. package/lib/spawn.ts +37 -3
  145. package/lib/stray-fields.ts +45 -0
  146. package/lib/tool-names.ts +13 -0
  147. package/lib/tui/glyphs.ts +2 -3
  148. package/lib/web/README.md +52 -2
  149. package/lib/web/client-runtime.ts +123 -0
  150. package/lib/web/index.ts +4 -2
  151. package/lib/web/markup.ts +88 -1
  152. package/lib/web/names.ts +42 -0
  153. package/package.json +1 -1
  154. package/extensions/board/view/actions.ts +0 -306
  155. package/extensions/board/writes.ts +0 -99
  156. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  157. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  158. package/extensions/job/process.ts +0 -35
@@ -22,6 +22,16 @@ export interface TodoMerge {
22
22
  skipped: number;
23
23
  /** Existing items kept as they were. */
24
24
  kept: number;
25
+ /**
26
+ * Untouched items dropped because they were seeded by an earlier approval of this
27
+ * plan and are no longer one of its steps: the plan was renamed or rewritten.
28
+ */
29
+ replaced: number;
30
+ /**
31
+ * Older seeds kept because someone had already started or finished them. Deleting one
32
+ * would erase work a reader did, so they are reported instead of replaced.
33
+ */
34
+ stale: number;
25
35
  /** Plan steps left out because the list reached its cap. */
26
36
  deferred: number;
27
37
  /**
@@ -89,9 +99,38 @@ function contentKey(content: string): string {
89
99
  * (still subject to the same duplicate and cap rules) but is not counted as a
90
100
  * step, because it is not one.
91
101
  */
92
- export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: number, trailing?: PlanStep): TodoMerge {
102
+ export function mergeTodos(
103
+ existing: ExistingTodo[],
104
+ steps: PlanStep[],
105
+ max: number,
106
+ trailing?: PlanStep,
107
+ /** What an earlier approval of this plan seeded, when there was one. */
108
+ previous?: readonly string[],
109
+ ): TodoMerge {
93
110
  const limit = Number.isInteger(max) && max > 0 ? max : DEFAULT_TODO_MAX;
94
- const todos: ExistingTodo[] = existing.map((todo) =>
111
+ // A plan refined after approval is approved again over the list its first approval
112
+ // made. An item this plan seeded that is no longer one of its steps is a leftover, and
113
+ // only an item *this plan seeded* is a candidate: a user's own item is never touched.
114
+ const ours = new Set((previous ?? []).map(contentKey));
115
+ const current = new Set(steps.map((step) => contentKey(step.content)));
116
+ if (trailing) current.add(contentKey(trailing.content));
117
+ let replaced = 0;
118
+ let stale = 0;
119
+ const kept: ExistingTodo[] = [];
120
+ for (const todo of existing) {
121
+ const key = contentKey(todo.content);
122
+ if (ours.has(key) && !current.has(key)) {
123
+ // An untouched leftover is bookkeeping and goes; one someone acted on is
124
+ // evidence of work, and is named rather than deleted.
125
+ if (todo.status === "pending") {
126
+ replaced += 1;
127
+ continue;
128
+ }
129
+ stale += 1;
130
+ }
131
+ kept.push(todo);
132
+ }
133
+ const todos: ExistingTodo[] = kept.map((todo) =>
95
134
  todo.status === "in_progress" && !todo.activeForm ? { ...todo, activeForm: todo.content } : todo,
96
135
  );
97
136
  const seen = new Set(todos.map((todo) => contentKey(todo.content)));
@@ -126,5 +165,14 @@ export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: num
126
165
  }
127
166
  }
128
167
 
129
- return { todos, added, skipped, kept: existing.length, deferred, trailing: trailingOutcome };
168
+ return {
169
+ todos,
170
+ added,
171
+ skipped,
172
+ kept: existing.length - replaced,
173
+ replaced,
174
+ stale,
175
+ deferred,
176
+ trailing: trailingOutcome,
177
+ };
130
178
  }
@@ -213,7 +213,16 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
213
213
  }
214
214
 
215
215
  runtime.disable(ctx);
216
- const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
216
+ // A plan refined and approved again seeds over the list its first approval made:
217
+ // the steps that approval appended are the ones this one may replace.
218
+ const previous = runtime.seeded(decision.plan.path);
219
+ const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content, previous);
220
+ if (error === undefined && steps.length > 0) {
221
+ runtime.markSeeded(
222
+ decision.plan.path,
223
+ steps.map((step) => step.content),
224
+ );
225
+ }
217
226
  const goal = await finalizeGoal(pi, ctx, decision.plan);
218
227
  const listing =
219
228
  steps.length > 0
@@ -36,14 +36,16 @@ export async function seedTodos(
36
36
  pi: ExtensionAPI,
37
37
  ctx: ExtensionToolContext,
38
38
  plan: string,
39
+ /** What an earlier approval of the same plan appended, when it was approved before. */
40
+ previous?: readonly string[],
39
41
  ): Promise<{ steps: PlanStep[]; merged?: TodoMerge; error?: string }> {
40
42
  const steps = extractPlanSteps(plan);
41
43
  const verification = verificationItem(plan);
42
44
  if (steps.length === 0 && !verification) return { steps };
43
- const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification);
45
+ const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification, previous);
44
46
  // Nothing new to write: every step (and the verification item) is already in
45
47
  // the list, or the list is full, so a rewrite would only churn the widget.
46
- if (merged.added === 0 && merged.trailing !== "added") return { steps, merged };
48
+ if (merged.added === 0 && merged.trailing !== "added" && merged.replaced === 0) return { steps, merged };
47
49
  const outcome = await ctx.executeTool(TODO_TOOL, { todos: merged.todos });
48
50
  if (!outcome.isError) return { steps, merged };
49
51
  // The nested call fails both when the tool is unavailable and when it rejects
@@ -93,8 +93,19 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
93
93
  // write; say so once, where the user is looking.
94
94
  const moved = runtime.plans.takeNotice();
95
95
  if (moved) ctx.ui.notify(moved, "info");
96
+ // A refinement says where the text it replaced went: a reviewer's comment that was
97
+ // carried to a block that moved can be checked against what she actually read.
98
+ const kept =
99
+ file.replaced === undefined ? "" : `, keeping the revision it replaced at ${file.replaced.relativePath}`;
100
+ // A plan that was approved before is now out of step with the todo list, and
101
+ // the list is only re-seeded by approving it again. Say so here, where the
102
+ // refinement is made, instead of letting the two disagree in silence.
103
+ const reseed =
104
+ runtime.seeded(file.path) === undefined
105
+ ? ""
106
+ : " This plan was approved before: its todo items are the ones that approval seeded, and approving it again replaces the steps that changed (an item you started or finished is left alone and named).";
96
107
  return textResult(
97
- `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
108
+ `Wrote the plan to ${file.relativePath}${kept}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}${reseed}`,
98
109
  { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
99
110
  );
100
111
  } catch (error) {
@@ -50,6 +50,21 @@ export interface ReviewBaselineEntry {
50
50
  sections: SectionDigest[];
51
51
  }
52
52
 
53
+ /**
54
+ * What one approval put into the todo list, so the next approval can replace it.
55
+ *
56
+ * A plan refined after it was approved is approved again, and the list still holds the
57
+ * items seeded the first time. Their texts are the only record of which items came from
58
+ * this plan, so a re-approval keeps them to recognise — and replace — its own leftovers
59
+ * without touching an item the user was already tracking.
60
+ */
61
+ export interface SeededStepsEntry {
62
+ /** Resolved path of the approved plan file. */
63
+ path: string;
64
+ /** The step texts the approval appended, in order. */
65
+ steps: string[];
66
+ }
67
+
53
68
  /** Structured result carried in `enter_plan_mode`'s `details`. */
54
69
  export interface EnterPlanModeDetails {
55
70
  /** Whether the user agreed to enter plan mode. */
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The review page's block navigation: which block is active, and how a key walks them.
3
+ *
4
+ * The fourth capability out of the review client's template literal. Each function takes
5
+ * the document it reads and returns the index it computed, so the client keeps the one
6
+ * variable (`activeIndex`) and these own the rules: clamp to the blocks that exist, start
7
+ * at the first or last when nothing is active, and land beside the reader rather than
8
+ * jumping to the middle of the page.
9
+ *
10
+ * `el` comes from the runtime, declared here because this source is spliced into the same
11
+ * bundle; the declaration emits nothing.
12
+ */
13
+
14
+ import type { ClientNode } from "../../lib/web/client-runtime.ts";
15
+
16
+ /** The island's view of one block, as far as navigation needs it. */
17
+ export interface IslandBlock {
18
+ start: number;
19
+ kind?: string;
20
+ }
21
+
22
+ /**
23
+ * The rendered blocks.
24
+ *
25
+ * A rebuild replaces them, so this is read fresh rather than remembered: the array a
26
+ * caller holds goes stale the moment `renderPlan` throws the DOM away.
27
+ */
28
+ export function blockElements(planEl: ClientNode | null): ClientNode[] {
29
+ if (!planEl) return [];
30
+ const nodes = planEl.querySelectorAll(".bf-block");
31
+ const blocks: ClientNode[] = [];
32
+ for (let i = 0; i < nodes.length; i++) {
33
+ const block = nodes[i];
34
+ if (block) blocks.push(block);
35
+ }
36
+ return blocks;
37
+ }
38
+
39
+ /**
40
+ * Make one block active and bring it into view, or say nothing when there are no blocks.
41
+ *
42
+ * `null` means "nothing changed": a page with no blocks has no active block, and the
43
+ * caller keeps the index it had rather than being reset to zero.
44
+ */
45
+ export function setActive(planEl: ClientNode | null, index: number): number | null {
46
+ const blocks = blockElements(planEl);
47
+ if (blocks.length === 0) return null;
48
+ const next = Math.max(0, Math.min(blocks.length - 1, index));
49
+ for (let i = 0; i < blocks.length; i++) {
50
+ const block = blocks[i];
51
+ if (block) block.classList.toggle("bf-block--active", i === next);
52
+ }
53
+ const current = blocks[next];
54
+ if (current && current.scrollIntoView) current.scrollIntoView({ block: "nearest" });
55
+ return next;
56
+ }
57
+
58
+ /**
59
+ * The index one step from `activeIndex`, or `null` when there is nothing to walk.
60
+ *
61
+ * With nothing active yet, `j` starts at the top and `k` at the bottom, which is what a
62
+ * reader pressing either key on a fresh page expects.
63
+ */
64
+ export function moveActive(planEl: ClientNode | null, activeIndex: number, delta: number): number | null {
65
+ const blocks = blockElements(planEl);
66
+ if (blocks.length === 0) return null;
67
+ const next = activeIndex < 0 ? (delta < 0 ? blocks.length - 1 : 0) : activeIndex + delta;
68
+ return setActive(planEl, next);
69
+ }
70
+
71
+ /**
72
+ * The rendered block and its island block for a source line, when both exist.
73
+ *
74
+ * Both halves are required: a rendered block with no island entry has nothing to comment
75
+ * on, and an island entry with no rendered block is a comment the page cannot place.
76
+ */
77
+ export function blockAtLine(
78
+ planEl: ClientNode | null,
79
+ blocks: readonly IslandBlock[],
80
+ line: number,
81
+ ): { wrap: ClientNode; block: IslandBlock } | null {
82
+ const wrap = planEl ? planEl.querySelector('.bf-block[data-line="' + line + '"]') : null;
83
+ let block: IslandBlock | undefined;
84
+ for (const candidate of blocks) if (candidate.start === line) block = candidate;
85
+ return wrap && block ? { wrap, block } : null;
86
+ }
87
+
88
+ /** The block rules as source, for the client bundle. */
89
+ export const WEB_BLOCKS_JS = [blockElements, setActive, moveActive, blockAtLine].map(String).join("\n\n");
@@ -8,10 +8,7 @@
8
8
  */
9
9
 
10
10
  export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
11
- if (!statusEl) return;
12
- statusEl.textContent = text || "";
13
- statusEl.title = text || "";
14
- statusEl.className = kind ? "bf-status bf-status--" + kind : "bf-status";
11
+ if (statusEl) paintStatus(statusEl, text, kind);
15
12
  }
16
13
 
17
14
  // The always-visible header chip: connection, comment count, and whether
@@ -56,7 +53,8 @@ export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
56
53
  titleEl.textContent = data.title;
57
54
  titleEl.title = data.title;
58
55
  }
59
- if (data.title) document.title = "Plan review \u00b7 " + data.title;
56
+ // The document's own <title> already names the plan, rendered with the page: a second
57
+ // copy of the same sentence in script is a tab title that can disagree with the head.
60
58
  }
61
59
 
62
60
  function updateProgress() {