@gavin-hu/my-pi-agent 0.21.0 → 0.22.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 (254) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +40 -26
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -24
  9. package/docs/extensions/board.md +361 -147
  10. package/docs/extensions/bytefly.md +152 -111
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +30 -31
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +166 -126
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +34 -18
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +72 -33
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +56 -15
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +5 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +4 -40
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +13 -11
  38. package/extensions/board/result.ts +30 -16
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +42 -10
  41. package/extensions/board/snapshot.ts +2 -1
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +131 -44
  44. package/extensions/board/types.ts +36 -32
  45. package/extensions/board/view/card.ts +229 -0
  46. package/extensions/board/view/client.ts +33 -15
  47. package/extensions/board/view/dialog.ts +169 -6
  48. package/extensions/board/view/empty.ts +47 -0
  49. package/extensions/board/view/keys.ts +13 -2
  50. package/extensions/board/view/live.ts +89 -21
  51. package/extensions/board/view/names.ts +13 -4
  52. package/extensions/board/view/page.ts +23 -9
  53. package/extensions/board/view/render.ts +208 -271
  54. package/extensions/board/view/styles.ts +123 -31
  55. package/extensions/board/view/team.ts +297 -76
  56. package/extensions/bytefly/block/fallback.ts +1 -1
  57. package/extensions/bytefly/doctor/runtime.ts +5 -2
  58. package/extensions/bytefly/footer/collect/label.ts +60 -0
  59. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  60. package/extensions/bytefly/footer/config.ts +4 -2
  61. package/extensions/bytefly/footer/register.ts +33 -4
  62. package/extensions/bytefly/footer/render/lines.ts +7 -4
  63. package/extensions/bytefly/footer/render/segments.ts +37 -10
  64. package/extensions/bytefly/footer/render/status.ts +0 -11
  65. package/extensions/bytefly/footer/types.ts +7 -0
  66. package/extensions/bytefly/header/config.ts +1 -1
  67. package/extensions/bytefly/index.ts +32 -6
  68. package/extensions/bytefly/mode/editor.ts +115 -0
  69. package/extensions/bytefly/panel/commands.ts +210 -120
  70. package/extensions/bytefly/panel/format.ts +4 -4
  71. package/extensions/bytefly/panel/settings.ts +73 -6
  72. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  73. package/extensions/bytefly/update/cache.ts +1 -1
  74. package/extensions/bytefly/update/register.ts +4 -4
  75. package/extensions/bytefly/working/config.ts +5 -7
  76. package/extensions/bytefly/working/format.ts +10 -13
  77. package/extensions/bytefly/working/register.ts +22 -27
  78. package/extensions/check/commands.ts +3 -2
  79. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  80. package/extensions/context/commands.ts +2 -2
  81. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  82. package/extensions/file-browse/index.ts +67 -36
  83. package/extensions/file-read/render.ts +4 -4
  84. package/extensions/goal/plan-route.ts +2 -2
  85. package/extensions/job/child.ts +527 -0
  86. package/extensions/job/clock.ts +142 -0
  87. package/extensions/job/commands.ts +4 -2
  88. package/extensions/job/config.ts +8 -1
  89. package/extensions/job/format.ts +22 -1
  90. package/extensions/job/log-writer.ts +13 -2
  91. package/extensions/job/registry.ts +63 -7
  92. package/extensions/job/render.ts +34 -0
  93. package/extensions/job/runtime.ts +296 -655
  94. package/extensions/job/session.ts +132 -19
  95. package/extensions/job/store.ts +7 -7
  96. package/extensions/job/tools.ts +3 -3
  97. package/extensions/job/types.ts +47 -0
  98. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
  99. package/extensions/job/waiters.ts +44 -1
  100. package/extensions/memory/commands.ts +4 -2
  101. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  102. package/extensions/plan/commands.ts +12 -9
  103. package/extensions/plan/format.ts +8 -2
  104. package/extensions/plan/git-bash.ts +115 -16
  105. package/extensions/plan/index.ts +53 -52
  106. package/extensions/plan/plan-index.ts +4 -2
  107. package/extensions/plan/plans.ts +5 -54
  108. package/extensions/plan/policy.ts +8 -10
  109. package/extensions/plan/review-lazy.ts +85 -0
  110. package/extensions/plan/review.ts +2 -2
  111. package/extensions/plan/runtime.ts +49 -39
  112. package/extensions/plan/sections.ts +1 -1
  113. package/extensions/plan/steps.ts +3 -3
  114. package/extensions/plan/todos.ts +1 -1
  115. package/extensions/plan/tools/cancel.ts +27 -62
  116. package/extensions/plan/tools/enter.ts +55 -81
  117. package/extensions/plan/tools/exit.ts +221 -238
  118. package/extensions/plan/tools/refusal.ts +18 -0
  119. package/extensions/plan/tools/steps.ts +75 -92
  120. package/extensions/plan/tools/write.ts +77 -120
  121. package/extensions/plan/tools.ts +188 -30
  122. package/extensions/plan/tui.ts +5 -5
  123. package/extensions/plan/types.ts +48 -5
  124. package/extensions/plan/web-client.ts +3 -0
  125. package/extensions/plan/web-lazy.ts +53 -0
  126. package/extensions/plan/web-markdown.ts +9 -67
  127. package/extensions/plan/web-payload.ts +3 -3
  128. package/extensions/plan/web-router.ts +18 -9
  129. package/extensions/plan/web.ts +1 -12
  130. package/extensions/question/tools.ts +6 -2
  131. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  132. package/extensions/rewind/commands/commands.ts +6 -3
  133. package/extensions/rewind/state/runtime.ts +1 -1
  134. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  135. package/extensions/rewind/view/review-tui.ts +4 -4
  136. package/extensions/serve/config.ts +10 -8
  137. package/extensions/serve/index.ts +17 -3
  138. package/extensions/serve/registry.ts +26 -17
  139. package/extensions/session-bus/commands.ts +11 -6
  140. package/extensions/session-bus/join-request.ts +2 -1
  141. package/extensions/session-bus/mail-watch.ts +26 -17
  142. package/extensions/session-bus/protocol.ts +15 -3
  143. package/extensions/session-bus/resolve.ts +5 -7
  144. package/extensions/session-bus/runtime.ts +40 -12
  145. package/extensions/session-bus/store.ts +101 -19
  146. package/extensions/session-bus/types.ts +19 -0
  147. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  148. package/extensions/subagent/agents.ts +1 -1
  149. package/extensions/subagent/commands.ts +4 -2
  150. package/extensions/subagent/format.ts +15 -15
  151. package/extensions/subagent/index.ts +1 -1
  152. package/extensions/subagent/render.ts +3 -3
  153. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  154. package/extensions/team/commands.ts +24 -7
  155. package/extensions/team/dispatch.ts +18 -4
  156. package/extensions/team/format.ts +17 -2
  157. package/extensions/team/index.ts +9 -8
  158. package/extensions/team/schema.ts +6 -5
  159. package/extensions/team/tools.ts +107 -45
  160. package/extensions/team/types.ts +3 -2
  161. package/extensions/todo/commands.ts +4 -2
  162. package/extensions/todo/format.ts +11 -0
  163. package/extensions/todo/render.ts +57 -0
  164. package/extensions/todo/runtime.ts +1 -1
  165. package/extensions/todo/tools.ts +1 -1
  166. package/extensions/todo/view/todo-screen.ts +122 -0
  167. package/extensions/todo/widget.ts +47 -0
  168. package/extensions/watch/commands.ts +4 -2
  169. package/extensions/watch/format.ts +36 -7
  170. package/extensions/watch/render.ts +57 -0
  171. package/extensions/watch/runtime.ts +27 -22
  172. package/extensions/watch/tools.ts +4 -4
  173. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  174. package/extensions/web-access/fetch/tool.ts +12 -4
  175. package/extensions/wechat/commands/commands.ts +18 -14
  176. package/extensions/wechat/index.ts +72 -37
  177. package/extensions/wechat/state/credentials.ts +1 -1
  178. package/extensions/wechat/state/state.ts +1 -1
  179. package/extensions/wechat/text/status.ts +112 -0
  180. package/extensions/wechat/types.ts +2 -2
  181. package/extensions/wechat/view/status-screen.ts +4 -103
  182. package/extensions/worktree/commands.ts +11 -9
  183. package/extensions/worktree/enter.ts +2 -2
  184. package/extensions/worktree/exit.ts +2 -2
  185. package/extensions/worktree/index.ts +5 -5
  186. package/extensions/worktree/screen.ts +3 -3
  187. package/extensions/worktree/status.ts +2 -2
  188. package/extensions/worktree/tools.ts +113 -116
  189. package/lib/README.md +27 -13
  190. package/lib/board/actions.ts +111 -0
  191. package/lib/board/artifact.ts +149 -0
  192. package/lib/board/card.ts +163 -52
  193. package/lib/board/config.ts +58 -0
  194. package/lib/board/contract.ts +113 -0
  195. package/lib/board/join.ts +38 -0
  196. package/lib/board/lanes.ts +182 -0
  197. package/lib/board/lead.ts +130 -0
  198. package/lib/board/limits.ts +78 -0
  199. package/lib/board/links.ts +31 -10
  200. package/lib/board/page-pointer.ts +1 -1
  201. package/lib/board/read.ts +83 -0
  202. package/lib/board/reason.ts +102 -0
  203. package/lib/board/round-text.ts +68 -6
  204. package/lib/board/round.ts +31 -15
  205. package/lib/board/rules.ts +151 -307
  206. package/lib/board/seat.ts +35 -0
  207. package/lib/board/session.ts +136 -54
  208. package/lib/board/skipped.ts +22 -0
  209. package/lib/board/steps.ts +487 -0
  210. package/lib/board/store.ts +87 -30
  211. package/lib/board/tone.ts +92 -0
  212. package/lib/board/view.ts +70 -58
  213. package/{extensions/watch → lib}/child.ts +40 -21
  214. package/lib/env.ts +65 -7
  215. package/lib/format.ts +13 -0
  216. package/lib/http/index.ts +14 -12
  217. package/lib/http/port.ts +15 -10
  218. package/lib/lazy.ts +45 -0
  219. package/lib/plan-entry.ts +39 -10
  220. package/lib/plans/paths.ts +82 -0
  221. package/lib/policy.ts +9 -6
  222. package/lib/report.ts +3 -2
  223. package/lib/requirements.ts +8 -8
  224. package/lib/session-label.ts +111 -5
  225. package/lib/session-roster.ts +43 -1
  226. package/lib/session-tool.ts +5 -4
  227. package/lib/stray-fields.ts +7 -2
  228. package/lib/tool-names.ts +5 -10
  229. package/lib/tui/README.md +23 -8
  230. package/lib/tui/chrome.ts +16 -15
  231. package/lib/tui/cursor.ts +18 -3
  232. package/lib/tui/glyphs.ts +9 -7
  233. package/lib/tui/tokens.ts +33 -2
  234. package/lib/tui/tool.ts +3 -3
  235. package/lib/web/README.md +21 -0
  236. package/lib/web/components.ts +5 -1
  237. package/lib/web/index.ts +1 -0
  238. package/lib/web/markdown.ts +83 -0
  239. package/lib/web/markup.ts +2 -1
  240. package/lib/web/runtime.ts +9 -10
  241. package/package.json +6 -1
  242. package/skills/init/SKILL.md +5 -5
  243. package/skills/next/SKILL.md +3 -5
  244. package/extensions/board/seat.ts +0 -28
  245. package/extensions/todo/tui.ts +0 -212
  246. package/lib/tui/index.ts +0 -34
  247. package/skills/project-docs/SKILL.md +0 -133
  248. package/skills/project-docs/references/audit.md +0 -131
  249. package/skills/project-docs/references/changelog.md +0 -105
  250. package/skills/project-docs/references/discovery.md +0 -127
  251. package/skills/project-docs/references/readme.md +0 -80
  252. package/skills/project-docs/references/release.md +0 -84
  253. package/skills/project-docs/references/roadmap.md +0 -93
  254. package/skills/project-docs/scripts/state.ts +0 -316
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { homedir } from "node:os";
13
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
+ import { type ExtensionAPI, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
14
14
  import { SUBAGENT_FINISHED_CHANNEL } from "../../../lib/events.ts";
15
15
  import { budgetBody, budgetNotice, budgetState } from "./budget.ts";
16
16
  import { createSnapshotReader, type SnapshotReader } from "./collect/snapshot.ts";
@@ -32,6 +32,30 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
32
32
  const delegation = createDelegationLedger();
33
33
  pi.events.on(SUBAGENT_FINISHED_CHANNEL, (event) => delegation.note(event));
34
34
 
35
+ // The label a peer knows this session by, once the follower below reports one; `null` is
36
+ // no record, and the bar keeps Pi's own session name.
37
+ let rosterLabel: string | null = null;
38
+ /** Whether this session's label is being followed: a tree change re-installs, it is not new. */
39
+ let followed = false;
40
+ const followLabel = (ctx: ExtensionContext): void => {
41
+ if (followed) return;
42
+ followed = true;
43
+ // Loaded where it is used: the follower walks the bus's layout on disk and subscribes to
44
+ // its label, and the registration graph is a budget that may only fall.
45
+ void import("./collect/label.ts")
46
+ .then(({ followRosterLabel }) =>
47
+ followRosterLabel({
48
+ pi,
49
+ ctx,
50
+ agentDir: getAgentDir(),
51
+ sink: (label) => {
52
+ rosterLabel = label;
53
+ },
54
+ }),
55
+ )
56
+ .catch(() => {});
57
+ };
58
+
35
59
  /** The installed reader and the budget it was priced with, while a footer is up. */
36
60
  let budget: { limit: number; read: SnapshotReader } | undefined;
37
61
  /** One warning per session: the notice stays in scrollback, and a re-install must not repeat it. */
@@ -41,8 +65,11 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
41
65
 
42
66
  const install = (ctx: ExtensionContext): void => {
43
67
  if (ctx.mode !== "tui") return;
68
+ // Not awaited: a footer is installed synchronously, and the bar draws Pi's name until the
69
+ // label lands — which is what a session with no record shows anyway.
70
+ followLabel(ctx);
44
71
  const config = loadByteflyConfig(ctx.cwd);
45
- const read = createSnapshotReader(config, delegation);
72
+ const read = createSnapshotReader(config, delegation, () => rosterLabel);
46
73
  budget = { limit: config.budget, read };
47
74
 
48
75
  ctx.ui.setFooter((tui, _theme, footerData) =>
@@ -57,9 +84,11 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
57
84
  };
58
85
 
59
86
  pi.on("session_start", (_event, ctx) => {
60
- // A new session in this process starts from zero: the ledger is not branch state,
61
- // and a budget that was already reported is unreported again.
87
+ // A new session in this process starts from zero: the ledger is not branch state, the
88
+ // label belongs to the session, and a budget that was already reported is unreported again.
62
89
  delegation.reset();
90
+ followed = false;
91
+ rosterLabel = null;
63
92
  warned = false;
64
93
  warnedUnpriced = false;
65
94
  install(ctx);
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Turn a `StatusSnapshot` into the two footer lines.
3
3
  *
4
- * Line 1 — state: left `pwd` and the session name, right the alert, the mode
5
- * chips, then `⎇ branch · ⑂ worktree · ◉ 4780`. Line 2 — meters: left
6
- * the context gauge and usage, right the model and thinking level.
4
+ * Line 1 — state: left `pwd` and the session name, right the alert, the remaining
5
+ * status chips, then `⎇ branch · ⑂ worktree · ◉ 4780`. Line 2 — meters: left the
6
+ * mode, the context gauge, and the usage; right the model and thinking level.
7
7
  *
8
8
  * The split is one job per row. The state chips change while a session runs, so
9
9
  * they ride the line with slack; the meters are the dense row, and a fact that
10
- * moves onto the top row is one the bottom row stops dropping to fit.
10
+ * moves onto the top row is one the bottom row stops dropping to fit. The mode
11
+ * leads the meters because it qualifies every number after it.
11
12
  */
12
13
 
13
14
  import type { Theme } from "@earendil-works/pi-coding-agent";
@@ -19,6 +20,7 @@ import {
19
20
  costSegment,
20
21
  delegationSegment,
21
22
  modelSegment,
23
+ modeSegment,
22
24
  modesSegment,
23
25
  portsSegment,
24
26
  pwdSegment,
@@ -54,6 +56,7 @@ export function buildLines(snapshot: StatusSnapshot, theme: Theme, home: string
54
56
  };
55
57
 
56
58
  const line2Left = defined([
59
+ modeSegment(snapshot, theme),
57
60
  contextSegment(snapshot, theme),
58
61
  windowSegment(snapshot, theme),
59
62
  costSegment(snapshot, theme),
@@ -24,7 +24,7 @@ import {
24
24
  thinkingColor,
25
25
  truncateLabel,
26
26
  } from "./format.ts";
27
- import { compactStatus, dropStatusDetail } from "./status.ts";
27
+ import { compactStatus } from "./status.ts";
28
28
  import type { Segment, StatusSnapshot } from "../types.ts";
29
29
 
30
30
  /** Tertiary text: paths and de-emphasized meters. */
@@ -55,7 +55,11 @@ export function pwdSegment(snapshot: StatusSnapshot, theme: Theme, home: string
55
55
  }
56
56
 
57
57
  export function sessionSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
58
- const name = snapshot.sessionName ? sanitizeLine(snapshot.sessionName) : "";
58
+ // The roster's label first: Pi's session name is what the terminal calls this session, and
59
+ // the label is what a card, a handoff, and `/team` use. No record means no team, so the bar
60
+ // keeps the name the user knows.
61
+ const raw = snapshot.rosterLabel ?? snapshot.sessionName;
62
+ const name = raw ? sanitizeLine(raw) : "";
59
63
  if (!name) return null;
60
64
  const short = truncateLabel(name, CONFIG.sessionLabelMax);
61
65
  // Brackets and a brighter token keep the session name from reading as a
@@ -151,15 +155,36 @@ export function alertSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
151
155
  };
152
156
  }
153
157
 
158
+ /**
159
+ * The mode: `≡ plan` while planning, `▶ auto` otherwise.
160
+ *
161
+ * Plan owns the flag, so the presence of its status key is what the bar reads; the
162
+ * mode's word and colour are the bar's own, so neither the plan file name nor a
163
+ * truncated chip can reach it. Planning wears `warn`, the same role the composer's
164
+ * border takes while it is on. It leads the resource line, before the gauge, so the
165
+ * mode is read before how much room is left. A session without `plan` still reads
166
+ * `auto`: that is the mode it is in.
167
+ */
168
+ export function modeSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
169
+ if (snapshot.statuses.has(CONFIG.planStatusKey)) {
170
+ const plan = paint(theme, "warn", `${CONFIG.icons.planMode} ${CONFIG.labels.planMode}`);
171
+ return { id: "mode", weight: 1, droppable: false, separator: "", forms: [plan, compactStatus(plan)] };
172
+ }
173
+ const auto = paint(theme, "accent", `${CONFIG.icons.autoMode} ${CONFIG.labels.autoMode}`);
174
+ return { id: "mode", weight: 1, droppable: false, separator: "", forms: [auto] };
175
+ }
176
+
154
177
  export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
155
178
  const others = [...snapshot.statuses.entries()]
156
- .filter(([key]) => key !== CONFIG.worktreeStatusKey && key !== CONFIG.httpStatusKey && !isAlert(key))
179
+ .filter(
180
+ ([key]) =>
181
+ key !== CONFIG.planStatusKey &&
182
+ key !== CONFIG.worktreeStatusKey &&
183
+ key !== CONFIG.httpStatusKey &&
184
+ !isAlert(key),
185
+ )
157
186
  .sort(([a], [b]) => a.localeCompare(b))
158
- .map(([key, value]) => {
159
- const status = sanitize(value);
160
- // The plan chip trails the plan file name; the slot shows the mode only.
161
- return key === CONFIG.planStatusKey ? dropStatusDetail(status) : status;
162
- })
187
+ .map(([, value]) => sanitize(value))
163
188
  .filter(Boolean);
164
189
  if (others.length === 0) return null;
165
190
  const full = others.join(faint(theme, CONFIG.separators.item));
@@ -169,7 +194,7 @@ export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
169
194
  const icons = others.map((status) => compactStatus(status)).join(" ");
170
195
  return {
171
196
  id: "statuses",
172
- // Weight 2, ahead of the branch: a mode is stated again by the session, while
197
+ // Weight 2, ahead of the branch: a status is stated again by the session, while
173
198
  // `⎇` is the only thing on the bar that says where the work will land.
174
199
  weight: 2,
175
200
  droppable: true,
@@ -188,7 +213,9 @@ export function contextSegment(snapshot: StatusSnapshot, theme: Theme): Segment
188
213
  const forms = widths.map((blocks) =>
189
214
  blocks > 0 ? `${gaugeBar(theme, { percent, blocks, role, emptyRole: "faint" })} ${percentText}` : percentText,
190
215
  );
191
- return { id: "context", weight: 1, droppable: false, separator: "", forms };
216
+ // The mode always leads this segment, so the separator prints on every frame;
217
+ // the gauge's offset is the mode's width, not whether one is present.
218
+ return { id: "context", weight: 1, droppable: false, separator: faint(theme, CONFIG.separators.item), forms };
192
219
  }
193
220
 
194
221
  export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
@@ -29,14 +29,3 @@ export function compactStatus(status: string): string {
29
29
  const text = tokens.length === 2 && /^\d+$/.test(tokens[1]) ? `${icon}${tokens[1]}` : icon;
30
30
  return `${prefix}${text}${suffix}`;
31
31
  }
32
-
33
- /**
34
- * Drop a status's trailing ` · detail` while keeping its SGR styling.
35
- *
36
- * The `plan-mode` chip carries the plan file name as detail (`≡ plan · name`);
37
- * the bar shows only the mode. Matching `[^ESC]*` removes the visible suffix
38
- * without consuming the closing reset, so the chip's color stays balanced.
39
- */
40
- export function dropStatusDetail(status: string): string {
41
- return status.replace(/ · [^\x1b]*/, "");
42
- }
@@ -84,6 +84,13 @@ export interface StatusSnapshot {
84
84
  cwd: string;
85
85
  /** Session name set by the user, `null` when unnamed. */
86
86
  sessionName: string | null;
87
+ /**
88
+ * The name peers address this session by, when the bus has recorded one.
89
+ *
90
+ * `null` means no record — bytefly runs without the bus and the bar keeps Pi's own
91
+ * session name, which is all a session that never joined has.
92
+ */
93
+ rosterLabel: string | null;
87
94
  /** Branch name, `null` when not in a repository, `"detached"` on detached HEAD. */
88
95
  branch: string | null;
89
96
  /** Extension status texts (from `ctx.ui.setStatus`), including `worktree`. */
@@ -93,7 +93,7 @@ export const HEADER_CONFIG: HeaderConfig = {
93
93
  { binding: "app.thinking.toggle", description: "to expand thinking" },
94
94
  { binding: "app.model.select", description: "to select model" },
95
95
  { binding: "app.model.cycleForward", description: "to cycle models" },
96
- { binding: "app.thinking.cycle", description: "to cycle thinking" },
96
+ { raw: "shift+tab", description: "to toggle plan" },
97
97
  { binding: "app.editor.external", description: "for external editor" },
98
98
  ],
99
99
  [
@@ -1,14 +1,15 @@
1
1
  /**
2
- * bytefly — the package's terminal chrome and diagnostics, composed from eleven
2
+ * bytefly — the package's terminal chrome and diagnostics, composed from twelve
3
3
  * feature folders: `header/` (the themed startup mark and version), `footer/`
4
4
  * (the two-line, width-adaptive status bar with its price calculation),
5
5
  * `message/` (the user-message nameplate), `notice/` (the shared transcript
6
6
  * notice renderer), `block/` (the unified tool-block frame), `separator/` (the
7
- * transcript turn rule), `working/` (the live run line on Pi's working row),
8
- * `panel/` (the unified `/bytefly` screen and its
7
+ * transcript turn rule), `working/` (the live run line on the composer's top
8
+ * border), `panel/` (the unified `/bytefly` screen and its
9
9
  * `doctor` report), `doctor/` (the `run_doctor` tool and its session-scoped
10
- * probe runtime), `notify/` (OS notifications for unattended runs), and
11
- * `update/` (Pi's package list, the host suppression, and the header marker).
10
+ * probe runtime), `notify/` (OS notifications for unattended runs), `mode/`
11
+ * (the editor's Shift+Tab plan-mode toggle and its border), and `update/` (Pi's
12
+ * package list, the host suppression, and the header marker).
12
13
  *
13
14
  * The header, footer, turn rule, and working line are installed
14
15
  * only in interactive (`tui`) sessions, and are always on; the user-message
@@ -22,7 +23,7 @@
22
23
  * Load with: pi --extension ./extensions/bytefly
23
24
  */
24
25
 
25
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
26
27
  import { isExtensionEnabled } from "../../lib/env.ts";
27
28
  import { registerToolBlocks } from "./block/register.ts";
28
29
  import { registerChrome } from "./chrome.ts";
@@ -87,4 +88,29 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
87
88
  // the one repaint hook rather than installing a header of its own.
88
89
  registerPiRelease(pi, piUpdates, { ...deps.release, onUpdate: () => header.refresh() });
89
90
  registerPanel(pi, { doctor, update, discover: deps.extensions?.discover });
91
+
92
+ // Shift+Tab is a chord Pi reserves for the thinking cycle, so the mode key is
93
+ // an editor rather than a shortcut. It loads on the first TUI session and only
94
+ // when `plan` is loaded, so a session without it keeps Pi's key and pays for
95
+ // nothing; the mode module owns that availability check.
96
+ let detachModeKey: (() => void) | undefined;
97
+ let modeEpoch = 0;
98
+ const installModeKey = async (ctx: ExtensionContext): Promise<void> => {
99
+ const epoch = ++modeEpoch;
100
+ detachModeKey?.();
101
+ detachModeKey = undefined;
102
+ if (ctx.mode !== "tui") return;
103
+ const { attachModeKey } = await import("./mode/editor.ts");
104
+ // A shutdown or a newer session may land while the module loads; the stale
105
+ // reply must not install an editor nothing will release.
106
+ if (epoch !== modeEpoch) return;
107
+ detachModeKey = attachModeKey(pi, ctx);
108
+ };
109
+ pi.on("session_start", (_event, ctx) => installModeKey(ctx));
110
+ pi.on("session_tree", (_event, ctx) => installModeKey(ctx));
111
+ pi.on("session_shutdown", () => {
112
+ modeEpoch++;
113
+ detachModeKey?.();
114
+ detachModeKey = undefined;
115
+ });
90
116
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The Shift+Tab chord at the main editor: the plan-mode toggle and the border
3
+ * that says which mode the composer is in, with the live working line riding it.
4
+ *
5
+ * Pi reserves `app.thinking.cycle` on Shift+Tab, so an extension shortcut for the
6
+ * key is refused. The editor is the narrower place to own it: an overlay or a
7
+ * picker has focus instead, so a dialog keeps its own Shift+Tab. Attaching also
8
+ * unsets the built-in cycle binding, because a key that acts and a key the help
9
+ * still claims are two different answers to the same press.
10
+ *
11
+ * Replacing Pi's editor means opting back into what that editor did:
12
+ * `embedWorkingStatus` is what makes the host hand the working indicator over for
13
+ * the top border, as Pi's own editor asks for. Without it the indicator falls back
14
+ * to a row above the composer, and the border would carry the mode colour alone.
15
+ *
16
+ * The mode is plan's to own; this module only asks over the shared channel and
17
+ * reads the branch for the border. Attach once per session and release with the
18
+ * returned cleanup, which reverts only the editor and binding it still owns.
19
+ * Without `plan` there is nothing to toggle, so attach does nothing and Pi keeps
20
+ * its thinking-cycle chord.
21
+ */
22
+
23
+ import {
24
+ CustomEditor,
25
+ type ExtensionAPI,
26
+ type ExtensionContext,
27
+ type KeybindingsManager as AppKeybindingsManager,
28
+ type Theme,
29
+ } from "@earendil-works/pi-coding-agent";
30
+ import { type EditorTheme, getKeybindings, type KeybindingsManager, type TUI } from "@earendil-works/pi-tui";
31
+ import { isPlanExtensionLoaded, requestPlanToggle } from "../../../lib/plan-entry.ts";
32
+ import { isPlanModeEnabled } from "../../../lib/plan-state.ts";
33
+ import { isShiftTab } from "../../../lib/tui/cursor.ts";
34
+ import { paint } from "../../../lib/tui/tokens.ts";
35
+
36
+ /** What the editor reads on every input and frame. */
37
+ interface ModeEditorOptions {
38
+ /** Ask the mode's owner to flip it. */
39
+ onToggle: () => void;
40
+ /** The branch is the truth, whoever toggled it. */
41
+ isPlanning: () => boolean;
42
+ /** The role theme, read each frame so a theme change repaints. */
43
+ theme: () => Theme;
44
+ }
45
+
46
+ /** The composer with Shift+Tab bound to plan mode and its border to the mode's own colour. */
47
+ export class ModeEditor extends CustomEditor {
48
+ constructor(
49
+ tui: TUI,
50
+ theme: EditorTheme,
51
+ keybindings: KeybindingsManager,
52
+ private readonly options: ModeEditorOptions,
53
+ ) {
54
+ // Pi's manager carries the resolved keybindings config on top of the TUI one;
55
+ // the editor only calls `matches()`, which both provide. The option is the same
56
+ // one Pi's own editor sets, so the working line lands on this border instead of
57
+ // in a row of its own above the composer.
58
+ super(tui, theme, keybindings as unknown as AppKeybindingsManager, { embedWorkingStatus: true });
59
+ }
60
+
61
+ override handleInput(data: string): void {
62
+ if (isShiftTab(data)) {
63
+ this.options.onToggle();
64
+ return;
65
+ }
66
+ super.handleInput(data);
67
+ }
68
+
69
+ override render(width: number): string[] {
70
+ // The two roles the footer's mode segment draws, so the bar and the composer agree.
71
+ const role = this.options.isPlanning() ? "warn" : "accent";
72
+ this.borderColor = (text) => paint(this.options.theme(), role, text);
73
+ return super.render(width);
74
+ }
75
+ }
76
+
77
+ /** The binding the editor takes over; Pi's default chord for the thinking cycle. */
78
+ const THINKING_CYCLE = "app.thinking.cycle";
79
+
80
+ /**
81
+ * Install the mode editor and take Shift+Tab from the thinking cycle, or return
82
+ * `undefined` when the `plan` extension is not loaded.
83
+ *
84
+ * Returns an idempotent cleanup. It clears the editor only while ours is the
85
+ * installed factory, and restores the binding only while the empty override it
86
+ * wrote is still there, so a later extension's editor or binding is never undone.
87
+ */
88
+ export function attachModeKey(
89
+ pi: Pick<ExtensionAPI, "events" | "getAllTools">,
90
+ ctx: ExtensionContext,
91
+ ): (() => void) | undefined {
92
+ if (!isPlanExtensionLoaded(pi)) return undefined;
93
+ const factory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
94
+ new ModeEditor(tui, theme, keybindings, {
95
+ onToggle: () => {
96
+ void requestPlanToggle(pi, ctx);
97
+ },
98
+ isPlanning: () => isPlanModeEnabled(ctx.sessionManager.getBranch()),
99
+ theme: () => ctx.ui.theme,
100
+ });
101
+ const bindings = getKeybindings();
102
+ const previous = bindings.getUserBindings()[THINKING_CYCLE];
103
+ ctx.ui.setEditorComponent(factory);
104
+ bindings.setUserBindings({ ...bindings.getUserBindings(), [THINKING_CYCLE]: [] });
105
+
106
+ return () => {
107
+ if (ctx.ui.getEditorComponent() === factory) ctx.ui.setEditorComponent(undefined);
108
+ const current = bindings.getUserBindings()[THINKING_CYCLE];
109
+ if (!Array.isArray(current) || current.length > 0) return;
110
+ const restored = { ...bindings.getUserBindings() };
111
+ if (previous === undefined) delete restored[THINKING_CYCLE];
112
+ else restored[THINKING_CYCLE] = previous;
113
+ bindings.setUserBindings(restored);
114
+ };
115
+ }