@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 (200) hide show
  1. package/README.md +29 -15
  2. package/README.zh-CN.md +27 -15
  3. package/extensions/bytefly/README.md +195 -115
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/notify/types.ts +1 -1
  17. package/extensions/bytefly/panel/commands.ts +46 -25
  18. package/extensions/bytefly/panel/format.ts +49 -14
  19. package/extensions/bytefly/panel/join.ts +35 -9
  20. package/extensions/bytefly/panel/settings.ts +29 -39
  21. package/extensions/bytefly/panel/skills.ts +51 -0
  22. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  23. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  24. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  25. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  26. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  27. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  28. package/extensions/bytefly/panel/tui.ts +223 -331
  29. package/extensions/bytefly/panel/types.ts +26 -8
  30. package/extensions/bytefly/picker/discovery.ts +32 -27
  31. package/extensions/bytefly/picker/resources.ts +16 -8
  32. package/extensions/bytefly/picker/runtime.ts +62 -34
  33. package/extensions/bytefly/picker/toggle.ts +16 -10
  34. package/extensions/bytefly/picker/types.ts +31 -13
  35. package/extensions/bytefly/update/format.ts +66 -8
  36. package/extensions/bytefly/update/patch.ts +66 -0
  37. package/extensions/bytefly/update/register.ts +4 -11
  38. package/extensions/bytefly/update/release.ts +65 -0
  39. package/extensions/bytefly/update/state.ts +14 -0
  40. package/extensions/bytefly/update/suppress.ts +47 -29
  41. package/extensions/bytefly/update/types.ts +15 -5
  42. package/extensions/check/tui.ts +2 -0
  43. package/extensions/context/README.md +21 -11
  44. package/extensions/context/format.ts +10 -8
  45. package/extensions/context/report.ts +21 -8
  46. package/extensions/context/tui.ts +26 -10
  47. package/extensions/context/types.ts +5 -3
  48. package/extensions/file-browse/README.md +89 -22
  49. package/extensions/file-browse/chrome.ts +83 -0
  50. package/extensions/file-browse/client.ts +74 -459
  51. package/extensions/file-browse/config.ts +12 -3
  52. package/extensions/file-browse/diff-html.ts +94 -0
  53. package/extensions/file-browse/html.ts +26 -152
  54. package/extensions/file-browse/live-client.ts +47 -1
  55. package/extensions/file-browse/live.ts +35 -14
  56. package/extensions/file-browse/palette.ts +224 -0
  57. package/extensions/file-browse/rev.ts +42 -0
  58. package/extensions/file-browse/router.ts +27 -6
  59. package/extensions/file-browse/server.ts +7 -0
  60. package/extensions/file-browse/styles.ts +10 -3
  61. package/extensions/file-browse/tree.ts +381 -0
  62. package/extensions/file-read/README.md +4 -1
  63. package/extensions/file-read/extract/odt.ts +7 -5
  64. package/extensions/goal/plan-route.ts +8 -2
  65. package/extensions/goal/tools.ts +6 -11
  66. package/extensions/job/README.md +7 -5
  67. package/extensions/job/index.ts +35 -52
  68. package/extensions/job/tools.ts +51 -77
  69. package/extensions/memory/README.md +3 -3
  70. package/extensions/memory/tools.ts +4 -6
  71. package/extensions/memory/tui.ts +5 -1
  72. package/extensions/plan/README.md +333 -84
  73. package/extensions/plan/commands.ts +73 -7
  74. package/extensions/plan/feedback.ts +300 -63
  75. package/extensions/plan/index.ts +1 -1
  76. package/extensions/plan/plan-index.ts +4 -2
  77. package/extensions/plan/plan-text.ts +18 -1
  78. package/extensions/plan/plans.ts +184 -58
  79. package/extensions/plan/review-prompt.ts +93 -0
  80. package/extensions/plan/review.ts +95 -95
  81. package/extensions/plan/runtime.ts +4 -2
  82. package/extensions/plan/split.ts +87 -0
  83. package/extensions/plan/steps.ts +123 -107
  84. package/extensions/plan/tools/cancel.ts +79 -0
  85. package/extensions/plan/tools/enter.ts +17 -12
  86. package/extensions/plan/tools/exit.ts +148 -59
  87. package/extensions/plan/tools/steps.ts +7 -19
  88. package/extensions/plan/tools/write.ts +29 -31
  89. package/extensions/plan/tools.ts +6 -3
  90. package/extensions/plan/tui.ts +60 -15
  91. package/extensions/plan/types.ts +45 -3
  92. package/extensions/plan/web-chrome.ts +102 -0
  93. package/extensions/plan/web-client.ts +177 -446
  94. package/extensions/plan/web-collab.ts +207 -0
  95. package/extensions/plan/web-comments.ts +269 -0
  96. package/extensions/plan/web-markdown.ts +222 -0
  97. package/extensions/plan/web-page.ts +131 -19
  98. package/extensions/plan/web-router.ts +148 -32
  99. package/extensions/plan/web-share-session.ts +197 -0
  100. package/extensions/plan/web-share.ts +169 -0
  101. package/extensions/plan/web-streams.ts +124 -0
  102. package/extensions/plan/web-tokens.ts +63 -0
  103. package/extensions/plan/web.ts +193 -45
  104. package/extensions/question/README.md +3 -0
  105. package/extensions/question/dialogs.ts +91 -1
  106. package/extensions/question/tools.ts +2 -1
  107. package/extensions/rewind/checklist.ts +4 -0
  108. package/extensions/rewind/review-tui.ts +3 -1
  109. package/extensions/rewind/rewind-tui.ts +4 -0
  110. package/extensions/session-bus/README.md +247 -0
  111. package/extensions/session-bus/commands.ts +165 -0
  112. package/extensions/session-bus/format.ts +124 -0
  113. package/extensions/session-bus/index.ts +69 -0
  114. package/extensions/session-bus/notice.ts +58 -0
  115. package/extensions/session-bus/protocol.ts +134 -0
  116. package/extensions/session-bus/result.ts +243 -0
  117. package/extensions/session-bus/rows.ts +169 -0
  118. package/extensions/session-bus/runtime.ts +575 -0
  119. package/extensions/session-bus/store.ts +329 -0
  120. package/extensions/session-bus/team.ts +102 -0
  121. package/extensions/session-bus/tools.ts +227 -0
  122. package/extensions/session-bus/tui.ts +394 -0
  123. package/extensions/session-bus/types.ts +98 -0
  124. package/extensions/subagent/README.md +26 -13
  125. package/extensions/subagent/format.ts +72 -25
  126. package/extensions/subagent/index.ts +21 -47
  127. package/extensions/subagent/orchestrate.ts +18 -41
  128. package/extensions/subagent/render.ts +29 -42
  129. package/extensions/subagent/stream.ts +24 -18
  130. package/extensions/subagent/types.ts +7 -0
  131. package/extensions/todo/tools.ts +5 -6
  132. package/extensions/todo/tui.ts +5 -1
  133. package/extensions/watch/README.md +9 -5
  134. package/extensions/watch/index.ts +25 -44
  135. package/extensions/watch/tools.ts +31 -59
  136. package/extensions/wechat/README.md +45 -36
  137. package/extensions/wechat/bridge.ts +46 -48
  138. package/extensions/wechat/client.ts +1 -4
  139. package/extensions/wechat/commands.ts +3 -3
  140. package/extensions/wechat/deliver.ts +57 -37
  141. package/extensions/wechat/interaction.ts +54 -18
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +9 -7
  144. package/extensions/wechat/peer-worker.ts +19 -34
  145. package/extensions/wechat/presence.ts +3 -4
  146. package/extensions/wechat/prompt.ts +53 -23
  147. package/extensions/wechat/reply-pipeline.ts +12 -4
  148. package/extensions/wechat/slash.ts +14 -0
  149. package/extensions/wechat/status-screen.ts +2 -2
  150. package/extensions/wechat/tools.ts +3 -11
  151. package/extensions/worktree/README.md +48 -12
  152. package/extensions/worktree/commands.ts +109 -65
  153. package/extensions/worktree/enter.ts +1 -1
  154. package/extensions/worktree/exit.ts +11 -1
  155. package/extensions/worktree/git.ts +5 -0
  156. package/extensions/worktree/lifecycle.ts +9 -1
  157. package/extensions/worktree/registry.ts +11 -5
  158. package/extensions/worktree/screen.ts +263 -0
  159. package/extensions/worktree/status.ts +84 -40
  160. package/lib/README.md +12 -7
  161. package/lib/claims/overlap.ts +43 -0
  162. package/lib/config.ts +12 -0
  163. package/lib/format.ts +6 -2
  164. package/lib/http/router.ts +40 -13
  165. package/lib/http/server.ts +27 -13
  166. package/lib/interaction.ts +28 -1
  167. package/lib/notice.ts +29 -2
  168. package/lib/plan-entry.ts +7 -2
  169. package/lib/plans/sections.ts +172 -0
  170. package/lib/plans/setting.ts +52 -0
  171. package/lib/plans/split.ts +215 -0
  172. package/lib/plans/store.ts +19 -0
  173. package/lib/plans/team.ts +151 -0
  174. package/lib/qr.ts +64 -0
  175. package/lib/repo-key.ts +74 -0
  176. package/lib/report.ts +117 -25
  177. package/lib/requirements.ts +1 -2
  178. package/lib/tool-result.ts +54 -4
  179. package/lib/tui/README.md +30 -8
  180. package/lib/tui/chrome.ts +27 -6
  181. package/lib/tui/dock-screen.ts +91 -19
  182. package/lib/tui/gauge.ts +9 -5
  183. package/lib/tui/line-pane.ts +15 -4
  184. package/lib/tui/tool.ts +23 -4
  185. package/lib/usage.ts +26 -0
  186. package/lib/web/README.md +131 -0
  187. package/lib/web/base.ts +19 -2
  188. package/lib/web/components.ts +10 -6
  189. package/lib/web/layout.ts +2 -0
  190. package/lib/web/markup.ts +2 -1
  191. package/lib/web/runtime.ts +103 -25
  192. package/lib/web/tokens.ts +23 -7
  193. package/package.json +5 -2
  194. package/skills/changelog/SKILL.md +57 -45
  195. package/skills/init/SKILL.md +80 -45
  196. package/skills/init/references/agents-md-quality.md +26 -2
  197. package/skills/init/references/interview-questions.md +36 -14
  198. package/skills/next/SKILL.md +209 -0
  199. package/skills/parallel-dev/SKILL.md +150 -0
  200. package/extensions/wechat/qrcode.ts +0 -46
@@ -8,7 +8,7 @@
8
8
  * built-in footer.
9
9
  */
10
10
 
11
- import type { UsageRecord, UsageTotals } from "../types.ts";
11
+ import type { UsageRecord } from "../types.ts";
12
12
 
13
13
  interface UsageLike {
14
14
  input: number;
@@ -43,11 +43,6 @@ interface UsageSource {
43
43
  isAssistant: boolean;
44
44
  }
45
45
 
46
- /** Zeroed totals; the starting point for every accumulation. */
47
- export function emptyUsage(): UsageTotals {
48
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
49
- }
50
-
51
46
  /**
52
47
  * Every usage-bearing entry, as a record.
53
48
  *
@@ -17,8 +17,9 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
17
17
  import type { DelegationLedger } from "../delegation.ts";
18
18
  import { createPricer } from "../price/price.ts";
19
19
  import type { ByteflyConfig } from "../price/types.ts";
20
- import type { ContextInfo, FooterData, StatusSnapshot, UsageTotals } from "../types.ts";
21
- import { emptyUsage, usageRecords } from "./entries.ts";
20
+ import { zeroUsage } from "../../../../lib/usage.ts";
21
+ import type { ContextInfo, FooterData, PricedUsage, StatusSnapshot, UsageTotals } from "../types.ts";
22
+ import { usageRecords } from "./entries.ts";
22
23
 
23
24
  /**
24
25
  * Build a reader that memoizes both scans by branch leaf and model.
@@ -36,21 +37,33 @@ export interface SnapshotReader {
36
37
  *
37
38
  * The budget guardrail reads the number the footer already priced instead of
38
39
  * walking the session a second time; if no frame has rendered yet there is
39
- * nothing stale to act on.
40
+ * nothing stale to act on. The unpriced count rides along for the same reason.
40
41
  */
41
- peek(): UsageTotals | undefined;
42
+ peek(): PricedUsage | undefined;
43
+ }
44
+
45
+ /** The five counters, without the unpriced count that only the notice reads. */
46
+ function countsOf(priced: PricedUsage): UsageTotals {
47
+ return {
48
+ input: priced.input,
49
+ output: priced.output,
50
+ cacheRead: priced.cacheRead,
51
+ cacheWrite: priced.cacheWrite,
52
+ cost: priced.cost,
53
+ };
42
54
  }
43
55
 
44
56
  export function createSnapshotReader(config: ByteflyConfig, delegation: DelegationLedger): SnapshotReader {
45
57
  const price = createPricer(config);
46
58
  let key: string | null = null;
47
- let usage = emptyUsage();
48
- let delegationTotals = { runs: 0, usage: emptyUsage() };
59
+ let usage: PricedUsage = { ...zeroUsage(), unpriced: 0 };
60
+ let usageCounts: UsageTotals = zeroUsage();
61
+ let delegationTotals = { runs: 0, usage: zeroUsage() };
49
62
  let cacheHitRate: number | null = null;
50
63
  let context: ContextInfo = { tokens: null, contextWindow: 0, percent: null };
51
64
  let sessionName: string | null = null;
52
65
 
53
- let last: UsageTotals | undefined;
66
+ let last: PricedUsage | undefined;
54
67
 
55
68
  const reader = ((ctx: ExtensionContext, footerData: FooterData): StatusSnapshot => {
56
69
  const session = ctx.sessionManager;
@@ -60,10 +73,11 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
60
73
  const active = current ? { provider: current.provider, model: current.id } : undefined;
61
74
  const scanned = usageRecords(session.getEntries(), active);
62
75
  usage = price(scanned.records);
76
+ usageCounts = countsOf(usage);
63
77
  cacheHitRate = scanned.cacheHitRate;
64
78
  // The child runs are priced by the same pricer, so a currency or rate
65
79
  // change moves both numbers together — and neither is folded into the other.
66
- delegationTotals = { runs: delegation.runs(), usage: price(delegation.records()) };
80
+ delegationTotals = { runs: delegation.runs(), usage: countsOf(price(delegation.records())) };
67
81
 
68
82
  const usageInfo = ctx.getContextUsage();
69
83
  context = {
@@ -91,7 +105,7 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
91
105
  model,
92
106
  thinkingLevel: ctx.thinkingLevel ?? null,
93
107
  context,
94
- usage,
108
+ usage: usageCounts,
95
109
  currency: config.currency,
96
110
  cacheHitRate,
97
111
  providerCount: footerData.getAvailableProviderCount(),
@@ -2,15 +2,16 @@
2
2
  * Price usage records from configured rates.
3
3
  *
4
4
  * `createPricer` owns the two things that must not be rebuilt per record: the
5
- * matching rates and the timezone formatter. `currency.code` chooses the
6
- * algorithm: CNY prices matched records from `rates` and `peak`, while USD
7
- * totals Pi's recorded dollar cost and ignores both. A CNY-mode record with no
8
- * matching rate contributes nothing: an unpriced model shows no price at all.
5
+ * matching rates and the timezone formatter. Both currencies price a record
6
+ * from `rates` and `peak` when a key matches. They differ only for a model no
7
+ * key matches: `USD` adds Pi's recorded dollar amount (its catalog is already
8
+ * per model), while `CNY` adds nothing and counts the record as unpriced, so the
9
+ * session can say what its total leaves out.
9
10
  */
10
11
 
11
- import { emptyUsage } from "../collect/entries.ts";
12
+ import { zeroUsage } from "../../../../lib/usage.ts";
12
13
  import type { ByteflyConfig, ModelRates } from "./types.ts";
13
- import type { UsageRecord, UsageTotals } from "../types.ts";
14
+ import type { PricedUsage, UsageRecord } from "../types.ts";
14
15
  import { createRateLookup } from "./rates.ts";
15
16
  import { isPeak, peakFormatter } from "./schedule.ts";
16
17
 
@@ -33,7 +34,7 @@ function tokenCost(record: UsageRecord, rates: ModelRates): number {
33
34
  * The returned function totals tokens as well as cost, so the bar's meters and
34
35
  * its price always describe the same set of records.
35
36
  */
36
- export function createPricer(config: ByteflyConfig): (records: readonly UsageRecord[]) => UsageTotals {
37
+ export function createPricer(config: ByteflyConfig): (records: readonly UsageRecord[]) => PricedUsage {
37
38
  // Both currencies price from `rates` and the tariff; they differ only in what
38
39
  // an unmatched model falls back to, and Pi's recorded cost is already dollars.
39
40
  const dollars = config.currency.code === "USD";
@@ -43,7 +44,7 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
43
44
  const format = config.peak.windows.length > 0 ? peakFormatter(config.peak.timezone) : null;
44
45
 
45
46
  return (records) => {
46
- const totals = emptyUsage();
47
+ const totals: PricedUsage = { ...zeroUsage(), unpriced: 0 };
47
48
  for (const record of records) {
48
49
  totals.input += record.input;
49
50
  totals.output += record.output;
@@ -54,7 +55,10 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
54
55
  totals.cost += ratedCost(config, format, record, rates);
55
56
  continue;
56
57
  }
58
+ // No rate: USD still has Pi's own per-model amount, CNY has nothing to add
59
+ // and only a count to report.
57
60
  if (dollars) totals.cost += record.recordedCost;
61
+ else totals.unpriced += 1;
58
62
  }
59
63
  return totals;
60
64
  };
@@ -17,6 +17,7 @@ import { createSnapshotReader, type SnapshotReader } from "./collect/snapshot.ts
17
17
  import { createDelegationLedger } from "./delegation.ts";
18
18
  import { loadByteflyConfig } from "./price/config.ts";
19
19
  import { createFooter } from "./render/footer.ts";
20
+ import { unpricedBody, unpricedNotice } from "./unpriced.ts";
20
21
 
21
22
  /** Host seams, injected by the composition root and by tests. */
22
23
  export interface FooterDeps {
@@ -35,6 +36,8 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
35
36
  let budget: { limit: number; read: SnapshotReader } | undefined;
36
37
  /** One warning per session: the notice stays in scrollback, and a re-install must not repeat it. */
37
38
  let warned = false;
39
+ /** The unpriced notice is once per session too, and independent of the budget. */
40
+ let warnedUnpriced = false;
38
41
 
39
42
  const install = (ctx: ExtensionContext): void => {
40
43
  if (ctx.mode !== "tui") return;
@@ -58,6 +61,7 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
58
61
  // and a budget that was already reported is unreported again.
59
62
  delegation.reset();
60
63
  warned = false;
64
+ warnedUnpriced = false;
61
65
  install(ctx);
62
66
  });
63
67
  pi.on("session_tree", (_event, ctx) => install(ctx));
@@ -65,9 +69,17 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
65
69
  // A turn's cost is final when the run settles, and that is when the user can act
66
70
  // on it. Rendering never posts anything: the footer is derived state.
67
71
  pi.on("agent_settled", (_event, ctx) => {
68
- if (warned || !budget || budget.limit <= 0 || ctx.mode !== "tui") return;
72
+ if (!budget || ctx.mode !== "tui") return;
69
73
  const usage = budget.read.peek();
70
74
  if (!usage) return;
75
+ // CNY records no rate for a model, so its spend is simply absent from the
76
+ // total. USD never counts one: Pi's recorded amount covers every model.
77
+ if (!warnedUnpriced && usage.unpriced > 0) {
78
+ warnedUnpriced = true;
79
+ pi.sendMessage(unpricedNotice(usage.unpriced));
80
+ deps.notify?.(unpricedBody(usage.unpriced));
81
+ }
82
+ if (warned || budget.limit <= 0) return;
71
83
  const state = budgetState(budget.limit, usage.cost);
72
84
  if (!state.exceeded) return;
73
85
  warned = true;
@@ -36,6 +36,17 @@ export interface UsageTotals {
36
36
  cost: number;
37
37
  }
38
38
 
39
+ /**
40
+ * The totals plus how many records the mode had no rate for.
41
+ *
42
+ * CNY has no fallback, so those records contribute no cost at all; the count is
43
+ * what lets the session say so instead of quietly showing a total that omits
44
+ * them. USD falls back to Pi's recorded amount, so it never counts any.
45
+ */
46
+ export interface PricedUsage extends UsageTotals {
47
+ unpriced: number;
48
+ }
49
+
39
50
  /** Context-window usage for the active model. */
40
51
  export interface ContextInfo {
41
52
  /** Estimated tokens, or `null` when unknown (for example right after compaction). */
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The one notice a CNY session posts when records had no rate.
3
+ *
4
+ * CNY mode has no fallback, so a model the table cannot price contributes no
5
+ * cost at all; the footer's total is then smaller than the work done, with
6
+ * nothing on screen saying why. This is the one sentence that says it, once per
7
+ * session, and names the two honest fixes rather than picking one: switch to
8
+ * `USD` (Pi's catalog is already per model, tiers included) or state yuan rates
9
+ * under `price.rates`.
10
+ *
11
+ * Pure: the caller has the count, this shapes the answer. No figure is quoted,
12
+ * because there is none to quote.
13
+ */
14
+
15
+ import { noticeMessage, type NoticeMessage } from "../../../lib/notice.ts";
16
+
17
+ /** The notice naming what the total leaves out, and how to price it. */
18
+ export function unpricedNotice(count: number): NoticeMessage {
19
+ const subject = count === 1 ? "1 record has" : `${count} records have`;
20
+ return noticeMessage({
21
+ content:
22
+ `${subject} no CNY rate, so ${count === 1 ? "it is" : "they are"} not in this total. ` +
23
+ "Switch `price.currency.code` to USD to use Pi's recorded cost, or add yuan `price.rates` entries.",
24
+ details: {
25
+ source: "bytefly",
26
+ title: "Records with no CNY rate",
27
+ severity: "info",
28
+ items: [{ label: "records", detail: String(count) }],
29
+ },
30
+ });
31
+ }
32
+
33
+ /** The one-line body an OS notification carries. */
34
+ export function unpricedBody(count: number): string {
35
+ return `${count} record${count === 1 ? "" : "s"} with no CNY rate · use USD or add price.rates`;
36
+ }
@@ -17,8 +17,8 @@ import { keyHintText } from "../../../lib/tui/glyphs.ts";
17
17
  import { BODY_INDENT } from "../../../lib/tui/indent.ts";
18
18
  import { RULE_GLYPH } from "../../../lib/tui/rules.ts";
19
19
  import { paint } from "../../../lib/tui/tokens.ts";
20
- import { updateMarker } from "../update/format.ts";
21
20
  import { HEADER_CONFIG, type HeaderHeight, type HelpEntry } from "./config.ts";
21
+ import { identityCells } from "./identity.ts";
22
22
 
23
23
  /** The header's shape: everything but the live terminal height is resolved once per apply. */
24
24
  export interface HeaderView {
@@ -32,6 +32,8 @@ export interface HeaderView {
32
32
  rows?: number;
33
33
  /** A newer published version of this package, when one is known. */
34
34
  latest?: string;
35
+ /** A newer published version of Pi itself, when one is known. */
36
+ piLatest?: string;
35
37
  }
36
38
 
37
39
  /** The mark block is four columns on both rows, so the identity column aligns. */
@@ -121,40 +123,24 @@ export class ByteflyHeader implements Component {
121
123
  }
122
124
 
123
125
  private build(w: number, theme: Theme, view: HeaderView): string[] {
124
- const { nameRole, chromeRole, detailRole, updateRole, chromeTagline, chromeName, chromeVersion, help } =
125
- HEADER_CONFIG;
126
+ const { help } = HEADER_CONFIG;
126
127
  const shape = heightShape(view.rows);
127
128
  const padWidth = Math.max(0, Math.min(Math.floor(view.pad), Math.max(0, w - 1)));
128
129
  const pad = " ".repeat(padWidth);
129
130
  const [markTop, markBottom] = markLines(theme, view.halfBlocks);
130
131
 
131
- const version = view.showVersion && this.version ? paint(theme, detailRole, `v${this.version}`) : "";
132
- // The wordmark mark spells the app name, so the agent cell carries only the
133
- // version there; the half-block mark does not, so the name stays beside it.
134
- const agent = view.halfBlocks
135
- ? version
136
- ? `${theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))} ${version}`
137
- : theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))
138
- : version;
139
- const chrome = `${paint(theme, detailRole, chromeTagline)} ${paint(theme, chromeRole, chromeName)}`;
140
- const chromeWithVersion = chromeVersion ? `${chrome} ${paint(theme, detailRole, `v${chromeVersion}`)}` : chrome;
141
- // The published version follows the loaded one: `v0.11.4 ↑0.12.0`.
142
- const chromeWithMarker =
143
- chromeVersion && view.latest
144
- ? `${chromeWithVersion} ${paint(theme, updateRole, updateMarker(view.latest))}`
145
- : chromeWithVersion;
146
-
147
- // The identity rows shed as one block: the update marker first, then the
148
- // chrome version, the chrome line, and finally the agent.
149
- const states: Array<readonly [string, string]> = [
150
- [agent, chromeWithMarker],
151
- [agent, chromeWithVersion],
152
- [agent, chrome],
153
- [agent, ""],
154
- ["", ""],
155
- ];
132
+ // The identity rows shed as one block, widest pair first: see `identity.ts`.
156
133
  const lineWidth = (cell: string): number => pad.length + MARK_WIDTH + (cell ? 1 + visibleWidth(cell) : 0);
157
- const [agentCell, chromeCell] = states.find(([a, c]) => Math.max(lineWidth(a), lineWidth(c)) <= w) ?? ["", ""];
134
+ const [agentCell, chromeCell] = identityCells(
135
+ {
136
+ halfBlocks: view.halfBlocks,
137
+ showVersion: view.showVersion,
138
+ version: this.version,
139
+ piLatest: view.piLatest,
140
+ latest: view.latest,
141
+ },
142
+ theme,
143
+ ).find(([a, c]) => Math.max(lineWidth(a), lineWidth(c)) <= w) ?? ["", ""];
158
144
 
159
145
  // The header owns its leading blank: Pi mounts the built-in header inside
160
146
  // `Spacer(1)`s, but the `quietStartup: true` branch that hands this header the
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The header's two identity cells and the ladder that sheds them.
3
+ *
4
+ * The nameplate's first row names Pi and its version, the second the tagline,
5
+ * bytefly, and its version, and either row may end with the `↑<version>` marker
6
+ * of a newer published release. The cells shed as one block when the terminal
7
+ * narrows, so the order is data here rather than a state table inside `build()`:
8
+ * the widest pair first, then the chrome marker, the chrome version, the chrome
9
+ * line, and finally the agent's own marker with the agent row itself.
10
+ *
11
+ * Pure: the theme is passed in and every returned string is painted, so a caller
12
+ * only measures and mounts them.
13
+ */
14
+
15
+ import type { Theme } from "@earendil-works/pi-coding-agent";
16
+ import { paint } from "../../../lib/tui/tokens.ts";
17
+ import { updateMarker } from "../update/format.ts";
18
+ import { HEADER_CONFIG } from "./config.ts";
19
+
20
+ /** What the cells are built from: the live view's switches and the known versions. */
21
+ export interface IdentityInput {
22
+ /** Whether the terminal renders half blocks; the wordmark mark spells the app name. */
23
+ halfBlocks: boolean;
24
+ /** Print Pi's version on the agent line. */
25
+ showVersion: boolean;
26
+ /** Pi's running version. */
27
+ version: string;
28
+ /** Pi's newer published version, when one is known. */
29
+ piLatest?: string;
30
+ /** This package's newer published version, when one is known. */
31
+ latest?: string;
32
+ }
33
+
34
+ /** A marker cell suffix, or nothing when no newer version is known. */
35
+ function markerSuffix(theme: Theme, version: string | undefined): string {
36
+ return version ? ` ${paint(theme, HEADER_CONFIG.updateRole, updateMarker(version))}` : "";
37
+ }
38
+
39
+ /**
40
+ * The identity pairs, widest first, with each pair that repeats its predecessor
41
+ * dropped so the ladder names a state only when it is actually narrower.
42
+ */
43
+ export function identityCells(input: IdentityInput, theme: Theme): Array<readonly [string, string]> {
44
+ const { nameRole, chromeRole, detailRole, appName, chromeTagline, chromeName, chromeVersion } = HEADER_CONFIG;
45
+ const version = input.showVersion && input.version ? paint(theme, detailRole, `v${input.version}`) : "";
46
+ // The wordmark mark spells the app name, so the agent cell carries only the
47
+ // version there; the half-block mark does not, so the name stays beside it.
48
+ const agent = input.halfBlocks
49
+ ? version
50
+ ? `${theme.bold(paint(theme, nameRole, appName))} ${version}`
51
+ : theme.bold(paint(theme, nameRole, appName))
52
+ : version;
53
+ const chrome = `${paint(theme, detailRole, chromeTagline)} ${paint(theme, chromeRole, chromeName)}`;
54
+ const chromeWithVersion = chromeVersion ? `${chrome} ${paint(theme, detailRole, `v${chromeVersion}`)}` : chrome;
55
+ const chromeWithMarker = `${chromeWithVersion}${markerSuffix(theme, input.latest)}`;
56
+ const agentWithMarker = `${agent}${markerSuffix(theme, input.piLatest)}`;
57
+
58
+ const states: Array<readonly [string, string]> = [];
59
+ const shed = (agentCell: string, chromeCell: string): void => {
60
+ const last = states.at(-1);
61
+ if (last && last[0] === agentCell && last[1] === chromeCell) return;
62
+ states.push([agentCell, chromeCell]);
63
+ };
64
+ shed(agentWithMarker, chromeWithMarker);
65
+ shed(agentWithMarker, chromeWithVersion);
66
+ shed(agentWithMarker, chrome);
67
+ shed(agentWithMarker, "");
68
+ shed(agent, "");
69
+ shed("", "");
70
+ return states;
71
+ }
@@ -32,10 +32,15 @@ export interface HeaderHandle {
32
32
  /**
33
33
  * Register the install/restore lifecycle for the custom header.
34
34
  *
35
- * `updates` is the store the update check writes, so the marker appears when a
36
- * newer version is known. A test that does not exercise the marker may omit it.
35
+ * `updates` is the store the package-update check writes and `piUpdates` the one
36
+ * Pi's own release notice writes, so either marker appears when a newer version
37
+ * is known. A test that does not exercise a marker may omit its store.
37
38
  */
38
- export function registerHeader(pi: ExtensionAPI, updates: UpdateSource = createUpdateState()): HeaderHandle {
39
+ export function registerHeader(
40
+ pi: ExtensionAPI,
41
+ updates: UpdateSource = createUpdateState(),
42
+ piUpdates?: UpdateSource,
43
+ ): HeaderHandle {
39
44
  // Half blocks misalign in Apple Terminal, so the mark falls back to a wordmark
40
45
  // there, mirroring Pi's own built-in header.
41
46
  const halfBlocks = !isAppleTerminalSession();
@@ -60,6 +65,7 @@ export function registerHeader(pi: ExtensionAPI, updates: UpdateSource = createU
60
65
  halfBlocks,
61
66
  pad,
62
67
  latest: updates.latest(),
68
+ piLatest: piUpdates?.latest(),
63
69
  rows,
64
70
  });
65
71
  ctx.ui.setHeader(createHeader(() => ctx.ui.theme, view));
@@ -10,9 +10,9 @@
10
10
  * probe runtime), `notify/` (OS notifications for unattended runs), and
11
11
  * `update/` (Pi's package list, the host suppression, and the header marker).
12
12
  *
13
- * The header, footer, turn rule, and working line are installed only in
14
- * interactive (`tui`) sessions, and are always on; the user-message marker,
15
- * notice, and tool blocks register at load and take effect wherever a
13
+ * The header, footer, turn rule, and working line are installed
14
+ * only in interactive (`tui`) sessions, and are always on; the user-message
15
+ * marker, notice, and tool blocks register at load and take effect wherever a
16
16
  * transcript renders. `pi config` turns bytefly's chrome off. The panel and doctor work
17
17
  * in every mode, and notify works wherever the environment can deliver it. The
18
18
  * footer is priced from DeepSeek's published rates by default and from
@@ -35,6 +35,7 @@ import { type NotifyDeps, registerNotify } from "./notify/register.ts";
35
35
  import { createPadStore } from "./pad.ts";
36
36
  import { type PanelCommandDeps, registerPanel } from "./panel/commands.ts";
37
37
  import { registerTurnSeparator } from "./separator/register.ts";
38
+ import { registerPiRelease, type ReleaseDeps } from "./update/release.ts";
38
39
  import { registerUpdate, type UpdateDeps } from "./update/register.ts";
39
40
  import { createUpdateState } from "./update/state.ts";
40
41
  import { registerWorkingTimer } from "./working/register.ts";
@@ -49,6 +50,8 @@ export interface ByteflyDeps {
49
50
  notify?: NotifyDeps;
50
51
  /** Seams for the package-update surface. */
51
52
  update?: UpdateDeps;
53
+ /** Seams for Pi's own release notice. */
54
+ release?: ReleaseDeps;
52
55
  }
53
56
 
54
57
  /** Register every bytefly surface; the factory only registers. */
@@ -56,9 +59,11 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
56
59
  if (!isExtensionEnabled("bytefly")) return;
57
60
 
58
61
  // One store per load: the update check writes this package's new version and
59
- // the header reads it, so the marker and every re-install agree.
62
+ // the header reads it, so the marker and every re-install agree. Pi's own
63
+ // release notice writes the second store the same way.
60
64
  const updates = createUpdateState();
61
- const header = registerHeader(pi, updates);
65
+ const piUpdates = createUpdateState();
66
+ const header = registerHeader(pi, updates, piUpdates);
62
67
  // Notify first, so the footer's budget guardrail can raise one OS notification
63
68
  // through the same capability the triggers use instead of a second dispatch path.
64
69
  const notifications = registerNotify(pi, deps.notify);
@@ -78,5 +83,8 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
78
83
  // The returned handle is what `/bytefly update` re-runs, so the update surface is
79
84
  // registered before the panel that offers the command.
80
85
  const update = registerUpdate(pi, updates, { ...deps.update, onUpdate: () => header.refresh() });
86
+ // Pi's own release notice is the second source of a header marker, so it shares
87
+ // the one repaint hook rather than installing a header of its own.
88
+ registerPiRelease(pi, piUpdates, { ...deps.release, onUpdate: () => header.refresh() });
81
89
  registerPanel(pi, { doctor, update, discover: deps.extensions?.discover });
82
90
  }
@@ -1,18 +1,17 @@
1
1
  /**
2
- * The user-message nameplate: a two-row heading, the `You` label over a rule
3
- * that spans the width, above the message.
2
+ * The message transformer: the user-message nameplate.
4
3
  *
5
- * Pi's user message component parses the transformed string as Markdown and
6
- * owns its colors, so shaping text is the only lever: the surface can add no
7
- * component and no color of its own. The nameplate is a level-2 heading, which
8
- * Pi renders bold in `mdHeading` and prints without its `#` prefix; the bytefly
9
- * themes point `mdHeading` at the accent, so the label and rule carry the accent
10
- * while the message itself stays in the default text color. Markdown cannot
11
- * express a two-line heading, and Pi puts a blank line after a heading block,
12
- * so the label is padded to the width and the rule is appended: the heading's
13
- * text wraps, putting the rule on its own row with no blank between. A blank
14
- * line separates the nameplate from the message, so the message's own Markdown
15
- * (a fence, a list, a second paragraph) parses exactly as it would without it.
4
+ * Pi's message components parse the transformed string as Markdown and own their
5
+ * colors, so shaping text is the only lever: this surface adds no component and
6
+ * no color of its own. The nameplate is a level-2 heading, which Pi renders
7
+ * bold in `mdHeading` and prints without its `#` prefix; the bytefly themes point
8
+ * `mdHeading` at the accent, so the label and rule carry the accent while the
9
+ * message itself stays in the default text color. Markdown cannot express a
10
+ * two-line heading, and Pi puts a blank line after a heading block, so the label
11
+ * is padded to the width and the rule is appended: the heading's text wraps,
12
+ * putting the rule on its own row with no blank between. A blank line separates
13
+ * the nameplate from the message, so the message's own Markdown (a fence, a list,
14
+ * a second paragraph) parses exactly as it would without it.
16
15
  */
17
16
 
18
17
  import type { MarkdownTransformContext } from "@earendil-works/pi-coding-agent";
@@ -40,3 +39,11 @@ export function styleUserMessage(markdown: string, context: MarkdownTransformCon
40
39
  const head = nameplate(context.availableWidth);
41
40
  return head ? `${head}\n\n${markdown}` : markdown;
42
41
  }
42
+
43
+ /**
44
+ * Dispatch the single transformer Pi keeps per extension on the message type: a
45
+ * user message gains the nameplate, and every other message passes through.
46
+ */
47
+ export function styleMessage(markdown: string, context: MarkdownTransformContext): string {
48
+ return context.messageType === "user" ? styleUserMessage(markdown, context) : markdown;
49
+ }
@@ -1,15 +1,16 @@
1
1
  /**
2
- * Registers bytefly's user-message markdown transformer.
2
+ * Registers bytefly's single Markdown transformer.
3
3
  *
4
- * Registration happens at extension load, like the turn separator's entry
5
- * renderer: the transformer only matters where a transcript renders, and Pi
6
- * calls it for every message, so no session lifecycle is needed.
4
+ * Pi keeps one transformer per extension (`loader.js`), and the extension
5
+ * spends it on the user-message nameplate. A second
6
+ * `registerMarkdownTransformer` call would silently replace the first, so there
7
+ * is exactly one, here.
7
8
  */
8
9
 
9
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
- import { styleUserMessage } from "./format.ts";
11
+ import { styleMessage } from "./format.ts";
11
12
 
12
- /** Register the transformer that marks each user message. */
13
+ /** Register the transformer that marks user messages. */
13
14
  export function registerUserMessage(pi: ExtensionAPI): void {
14
- pi.registerMarkdownTransformer((markdown, context) => styleUserMessage(markdown, context));
15
+ pi.registerMarkdownTransformer((markdown, context) => styleMessage(markdown, context));
15
16
  }
@@ -21,7 +21,7 @@ export interface NotifyConfig {
21
21
  needsInput: boolean;
22
22
  /** Notify when a background `job` finishes. */
23
23
  jobFinished: boolean;
24
- /** Notify when a `watch` wake match fires while the agent is idle. */
24
+ /** Notify when a `watch` wake match fires, once the agent is idle. */
25
25
  matchFound: boolean;
26
26
  /** Delivery method; `auto` chooses by terminal and platform. */
27
27
  method: NotifyMethod;