@gavin-hu/my-pi-agent 0.19.0 → 0.21.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 (208) hide show
  1. package/README.md +19 -12
  2. package/README.zh-CN.md +15 -9
  3. package/docs/extensions/README.md +2 -1
  4. package/docs/extensions/board.md +329 -151
  5. package/docs/extensions/bytefly.md +427 -273
  6. package/docs/extensions/file-browse.md +46 -44
  7. package/docs/extensions/job.md +1 -2
  8. package/docs/extensions/plan.md +86 -24
  9. package/docs/extensions/serve.md +129 -0
  10. package/docs/extensions/session-bus.md +23 -10
  11. package/docs/extensions/team.md +56 -28
  12. package/docs/extensions/wechat.md +2 -1
  13. package/docs/extensions/worktree.md +30 -15
  14. package/extensions/board/commands.ts +48 -17
  15. package/extensions/board/format.ts +107 -9
  16. package/extensions/board/index.ts +14 -2
  17. package/extensions/board/join.ts +31 -10
  18. package/extensions/board/page.ts +202 -156
  19. package/extensions/board/result.ts +10 -24
  20. package/extensions/board/rows.ts +1 -1
  21. package/extensions/board/schema.ts +31 -10
  22. package/extensions/board/seat.ts +9 -10
  23. package/extensions/board/snapshot.ts +7 -5
  24. package/extensions/board/tools.ts +211 -34
  25. package/extensions/board/transport/router.ts +27 -110
  26. package/extensions/board/transport/server.ts +76 -56
  27. package/extensions/board/types.ts +57 -0
  28. package/extensions/board/view/client.ts +24 -13
  29. package/extensions/board/view/dialog.ts +43 -0
  30. package/extensions/board/view/keys.ts +28 -24
  31. package/extensions/board/view/live.ts +6 -178
  32. package/extensions/board/view/names.ts +91 -0
  33. package/extensions/board/view/page.ts +60 -89
  34. package/extensions/board/view/render.ts +228 -200
  35. package/extensions/board/view/styles.ts +72 -49
  36. package/extensions/board/view/team.ts +63 -41
  37. package/extensions/bytefly/footer/budget.ts +6 -5
  38. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  39. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  40. package/extensions/bytefly/footer/config.ts +19 -11
  41. package/extensions/bytefly/footer/price/price.ts +30 -8
  42. package/extensions/bytefly/footer/register.ts +5 -5
  43. package/extensions/bytefly/footer/render/footer.ts +18 -4
  44. package/extensions/bytefly/footer/render/format.ts +18 -0
  45. package/extensions/bytefly/footer/render/key.ts +46 -0
  46. package/extensions/bytefly/footer/render/layout.ts +13 -3
  47. package/extensions/bytefly/footer/render/lines.ts +23 -7
  48. package/extensions/bytefly/footer/render/segments.ts +93 -31
  49. package/extensions/bytefly/footer/types.ts +29 -5
  50. package/extensions/bytefly/footer/unpriced.ts +42 -17
  51. package/extensions/bytefly/header/component.ts +4 -20
  52. package/extensions/bytefly/panel/commands.ts +28 -1
  53. package/extensions/bytefly/panel/filter.ts +68 -0
  54. package/extensions/bytefly/panel/format.ts +16 -15
  55. package/extensions/bytefly/panel/join.ts +56 -13
  56. package/extensions/bytefly/panel/prices.ts +43 -0
  57. package/extensions/bytefly/panel/settings.ts +72 -23
  58. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  59. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  60. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  61. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  62. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  63. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  64. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  65. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  66. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  67. package/extensions/bytefly/panel/tui.ts +306 -259
  68. package/extensions/check/commands.ts +2 -1
  69. package/extensions/check/render.ts +10 -1
  70. package/extensions/file-browse/config/config.ts +4 -19
  71. package/extensions/file-browse/files/classify.ts +136 -0
  72. package/extensions/file-browse/files/files.ts +8 -547
  73. package/extensions/file-browse/files/index.ts +90 -0
  74. package/extensions/file-browse/files/list.ts +261 -0
  75. package/extensions/file-browse/files/tree.ts +101 -0
  76. package/extensions/file-browse/index.ts +27 -48
  77. package/extensions/file-browse/live/live.ts +1 -1
  78. package/extensions/file-browse/transport/router.ts +12 -6
  79. package/extensions/file-browse/transport/server.ts +61 -28
  80. package/extensions/file-browse/view/chrome.ts +4 -4
  81. package/extensions/file-browse/view/client.ts +3 -1
  82. package/extensions/file-browse/view/emit.ts +51 -0
  83. package/extensions/file-browse/view/html.ts +78 -46
  84. package/extensions/file-browse/view/live-client.ts +2 -2
  85. package/extensions/file-browse/view/markdown.ts +23 -26
  86. package/extensions/file-browse/view/palette.ts +2 -2
  87. package/extensions/file-browse/view/raw-html.ts +27 -22
  88. package/extensions/file-browse/view/tree.ts +1 -1
  89. package/extensions/file-browse/view/urls.ts +20 -6
  90. package/extensions/goal/tui.ts +12 -2
  91. package/extensions/job/runtime.ts +4 -4
  92. package/extensions/job/tools.ts +19 -7
  93. package/extensions/job/tui.ts +2 -28
  94. package/extensions/plan/commands.ts +2 -2
  95. package/extensions/plan/feedback.ts +88 -18
  96. package/extensions/plan/format.ts +10 -3
  97. package/extensions/plan/index.ts +24 -2
  98. package/extensions/plan/plan-files.ts +88 -0
  99. package/extensions/plan/plan-text.ts +114 -0
  100. package/extensions/plan/plans.ts +19 -23
  101. package/extensions/plan/policy.ts +10 -5
  102. package/extensions/plan/runtime.ts +43 -4
  103. package/extensions/plan/todos.ts +51 -3
  104. package/extensions/plan/tools/exit.ts +10 -1
  105. package/extensions/plan/tools/todo-seed.ts +4 -2
  106. package/extensions/plan/tools/write.ts +12 -1
  107. package/extensions/plan/tools.ts +2 -2
  108. package/extensions/plan/types.ts +15 -0
  109. package/extensions/plan/web-blocks.ts +160 -0
  110. package/extensions/plan/web-chrome.ts +7 -5
  111. package/extensions/plan/web-client.ts +271 -325
  112. package/extensions/plan/web-collab.ts +7 -7
  113. package/extensions/plan/web-comments.ts +57 -16
  114. package/extensions/plan/web-composer.ts +91 -0
  115. package/extensions/plan/web-draft.ts +40 -0
  116. package/extensions/plan/web-keys.ts +258 -0
  117. package/extensions/plan/web-markdown.ts +1 -1
  118. package/extensions/plan/web-outline.ts +212 -0
  119. package/extensions/plan/web-page.ts +59 -86
  120. package/extensions/plan/web-payload.ts +188 -0
  121. package/extensions/plan/web-plan.ts +160 -0
  122. package/extensions/plan/web-router.ts +0 -1
  123. package/extensions/plan/web-settle.ts +201 -0
  124. package/extensions/plan/web-share-session.ts +24 -55
  125. package/extensions/plan/web-share.ts +19 -37
  126. package/extensions/plan/web-write.ts +112 -0
  127. package/extensions/plan/web.ts +77 -130
  128. package/extensions/rewind/commands/review.ts +2 -1
  129. package/extensions/serve/config.ts +37 -0
  130. package/extensions/serve/index.ts +71 -0
  131. package/extensions/serve/registry.ts +251 -0
  132. package/extensions/session-bus/join-request.ts +16 -9
  133. package/extensions/session-bus/result.ts +1 -27
  134. package/extensions/session-bus/rows.ts +7 -3
  135. package/extensions/session-bus/runtime.ts +100 -38
  136. package/extensions/session-bus/store.ts +14 -2
  137. package/extensions/session-bus/tools.ts +5 -4
  138. package/extensions/session-bus/tui.ts +1 -1
  139. package/extensions/session-bus/types.ts +10 -1
  140. package/extensions/subagent/commands.ts +2 -1
  141. package/extensions/team/commands.ts +44 -9
  142. package/extensions/team/format.ts +14 -0
  143. package/extensions/team/index.ts +2 -1
  144. package/extensions/team/tools.ts +30 -9
  145. package/extensions/team/types.ts +17 -0
  146. package/extensions/todo/index.ts +4 -1
  147. package/extensions/todo/nudge.ts +8 -2
  148. package/extensions/todo/tui.ts +21 -3
  149. package/extensions/wechat/bridge.ts +43 -172
  150. package/extensions/wechat/intake.ts +245 -0
  151. package/extensions/worktree/commands.ts +24 -17
  152. package/extensions/worktree/exit.ts +12 -6
  153. package/extensions/worktree/git.ts +22 -14
  154. package/extensions/worktree/guard.ts +15 -285
  155. package/extensions/worktree/handoff.ts +10 -2
  156. package/extensions/worktree/index.ts +2 -1
  157. package/extensions/worktree/lifecycle.ts +2 -0
  158. package/extensions/worktree/registry.ts +20 -11
  159. package/extensions/worktree/root-tools.ts +13 -3
  160. package/extensions/worktree/runtime.ts +8 -2
  161. package/extensions/worktree/screen.ts +15 -8
  162. package/extensions/worktree/shell.ts +303 -0
  163. package/extensions/worktree/state.ts +1 -1
  164. package/extensions/worktree/status.ts +42 -9
  165. package/extensions/worktree/tools.ts +19 -13
  166. package/lib/README.md +9 -7
  167. package/lib/board/card.ts +192 -3
  168. package/lib/board/links.ts +26 -2
  169. package/lib/board/page-pointer.ts +111 -32
  170. package/lib/board/round-text.ts +11 -2
  171. package/lib/board/round.ts +25 -20
  172. package/lib/board/rules.ts +144 -11
  173. package/lib/board/session.ts +84 -18
  174. package/lib/board/store.ts +268 -55
  175. package/lib/board/view.ts +114 -24
  176. package/lib/bus-request.ts +83 -0
  177. package/lib/format.ts +22 -0
  178. package/lib/http/index.ts +11 -7
  179. package/lib/http/mount.ts +142 -0
  180. package/lib/http/port.ts +25 -0
  181. package/lib/http/serve.ts +170 -43
  182. package/lib/http/server.ts +3 -3
  183. package/lib/http/sink.ts +34 -9
  184. package/lib/plan-entry.ts +20 -13
  185. package/lib/policy.ts +14 -1
  186. package/lib/requirements.ts +2 -2
  187. package/lib/session-label.ts +23 -7
  188. package/lib/session-roster.ts +74 -3
  189. package/lib/session-tool.ts +37 -21
  190. package/lib/spawn.ts +37 -3
  191. package/lib/stray-fields.ts +45 -0
  192. package/lib/tool-names.ts +13 -0
  193. package/lib/tui/README.md +6 -3
  194. package/lib/tui/glyphs.ts +3 -6
  195. package/lib/tui/render-cache.ts +29 -0
  196. package/lib/tui/tool.ts +30 -1
  197. package/lib/web/README.md +52 -2
  198. package/lib/web/client-runtime.ts +149 -0
  199. package/lib/web/index.ts +4 -2
  200. package/lib/web/markup.ts +106 -2
  201. package/lib/web/names.ts +42 -0
  202. package/package.json +3 -2
  203. package/extensions/board/config.ts +0 -36
  204. package/extensions/board/view/actions.ts +0 -306
  205. package/extensions/board/writes.ts +0 -99
  206. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  207. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  208. package/extensions/job/process.ts +0 -35
@@ -12,18 +12,85 @@
12
12
  */
13
13
 
14
14
  import { type Card, cardRef, DEFAULT_PRIORITY, dependsOf, priorityOf } from "../../lib/board/card.ts";
15
- import type { BoardIndex } from "../../lib/board/links.ts";
15
+ import { type BoardIndex, lanesText, openDependentsOf } from "../../lib/board/links.ts";
16
+ import type { Skip } from "../../lib/board/store.ts";
17
+ import { formatAge } from "../../lib/format.ts";
16
18
  import { plural } from "../../lib/board/round-text.ts";
17
19
 
18
20
  /** One card: the headline, then the state a reader needs in order to act on it. */
19
- export function formatCard(card: Card, board?: BoardIndex): string {
20
- return [`${cardRef(card.id)} ${card.status} · ${card.title}`, ...detailLines(card, board)].join("\n");
21
+ export function formatCard(card: Card, board?: BoardIndex, now?: number): string {
22
+ return [`${cardRef(card.id)} ${card.status} · ${card.title}`, ...detailLines(card, board, now)].join("\n");
21
23
  }
22
24
 
23
- /** A board, in lane order. */
24
- export function formatCards(cards: readonly Card[]): string {
25
- if (cards.length === 0) return "The board is empty.";
26
- return cards.map((card) => listLine(card)).join("\n");
25
+ /**
26
+ * What a listing cannot show, and therefore has to say.
27
+ *
28
+ * A footnote is the difference between a listing and the board: an omission nobody
29
+ * accounted for reads as a smaller board — or as history that was deleted.
30
+ */
31
+ export interface ListingNotes {
32
+ /** Finished cards the board holds that a lane-less listing left out. */
33
+ hiddenDone: number;
34
+ /** Cards set aside in the archive, which no board read opens. */
35
+ archived: number;
36
+ /** Card files this build could not read. */
37
+ skipped: readonly Skip[];
38
+ }
39
+
40
+ /**
41
+ * A board's listing: the cards, then the footnotes a reader cannot see.
42
+ *
43
+ * One body, because the footnotes belong to the listing: a card file this build cannot
44
+ * read, the finished cards a plain listing left out, and the cards an `archive` set aside.
45
+ */
46
+ export function formatBoard(shown: readonly Card[], notes: ListingNotes): string {
47
+ const lines =
48
+ shown.length === 0 ? [notes.hiddenDone === 0 ? "The board is empty." : "No open cards."] : shown.map(listLine);
49
+ // A finished card is history rather than work, so a listing that names no lane leaves
50
+ // them out; the count is what keeps the omission from reading as a smaller board.
51
+ if (notes.hiddenDone > 0) {
52
+ const them = notes.hiddenDone === 1 ? "it" : "them";
53
+ lines.push(`${plural(notes.hiddenDone, "done card")} hidden; pass status: "done" to read ${them}.`);
54
+ }
55
+ for (const note of [archiveNote(notes.archived), formatSkipped(notes.skipped)]) {
56
+ if (note !== "") lines.push(note);
57
+ }
58
+ return lines.join("\n");
59
+ }
60
+
61
+ /**
62
+ * The archive's listing: what is set aside, and the files there this build refused.
63
+ *
64
+ * The archive is read on request only, so this has no lane footnote of its own — every
65
+ * card in the pile is finished, and a `done` lane would be a second way to say it.
66
+ */
67
+ export function formatArchive(shown: readonly Card[], skipped: readonly Skip[]): string {
68
+ const lines = shown.length === 0 ? ["The archive is empty."] : shown.map(listLine);
69
+ const unreadable = formatSkipped(skipped);
70
+ if (unreadable !== "") lines.push(unreadable);
71
+ return lines.join("\n");
72
+ }
73
+
74
+ /**
75
+ * The one sentence a board listing adds so its archive is not mistaken for deleted history.
76
+ *
77
+ * Empty when nothing is set aside, so the ordinary board says nothing new.
78
+ */
79
+ export function archiveNote(count: number): string {
80
+ if (count === 0) return "";
81
+ return `${plural(count, "card")} ${count === 1 ? "is" : "are"} set aside in the archive; pass archived: true to read ${count === 1 ? "it" : "them"}.`;
82
+ }
83
+
84
+ /**
85
+ * The files the board could not read, or nothing when it read everything.
86
+ *
87
+ * A skipped file is not an absent one: a board written by a newer build would
88
+ * otherwise read as "nobody has planned anything" while the work sits on disk.
89
+ */
90
+ export function formatSkipped(skipped: readonly Skip[]): string {
91
+ if (skipped.length === 0) return "";
92
+ const files = skipped.map((entry) => `${entry.file} (${entry.why})`).join(", ");
93
+ return `${plural(skipped.length, "file")} this build cannot read: ${files}`;
27
94
  }
28
95
 
29
96
  /** The dropped id, which is the only thing left of the card. */
@@ -31,22 +98,53 @@ export function formatDropped(id: string): string {
31
98
  return `${cardRef(id)} dropped, with its notes.`;
32
99
  }
33
100
 
101
+ /**
102
+ * The ids an archive set aside, and how to read them again.
103
+ *
104
+ * A move has to name the way back or it reads like a deletion: the card is out of the
105
+ * board and its notes, plan, and landed commit are still in its file.
106
+ */
107
+ export function formatArchived(ids: readonly string[]): string {
108
+ const theirs = ids.length === 1 ? "its" : "their";
109
+ return `${plural(ids.length, "card")} archived: ${ids.map(cardRef).join(", ")} — out of the board, ${theirs} notes and plan still readable with archived: true.`;
110
+ }
111
+
34
112
  /** A one-line listing: the id, the lane, the title, and who holds it. */
35
113
  function listLine(card: Card): string {
36
114
  const holder = card.owner === undefined ? "unassigned" : card.owner.label;
37
115
  const urgency = priorityOf(card) === DEFAULT_PRIORITY ? "" : ` · ${priorityOf(card)}`;
38
116
  const waits = dependsOf(card).length === 0 ? "" : ` · waits on ${dependsOf(card).map(cardRef).join(", ")}`;
39
- return `${cardRef(card.id)} ${card.status} · ${card.title} · ${holder}${urgency}${waits}`;
117
+ // The two lanes that carry a fact of their own say it here: what a block waits on, and
118
+ // what closed a finished card. Both are one short phrase, so a listing stays one line.
119
+ const blocked = card.blockedBy === undefined ? "" : ` · blocked: ${card.blockedBy}`;
120
+ const landed = card.landed === undefined ? "" : ` · landed ${card.landed.commit}`;
121
+ return `${cardRef(card.id)} ${card.status} · ${card.title} · ${holder}${urgency}${waits}${blocked}${landed}`;
40
122
  }
41
123
 
42
124
  /** The state lines under a card's headline. */
43
- function detailLines(card: Card, board?: BoardIndex): string[] {
125
+ function detailLines(card: Card, board?: BoardIndex, now?: number): string[] {
44
126
  const lines: string[] = [];
45
127
  if (card.owner !== undefined) lines.push(` owner: ${card.owner.label}`);
128
+ // An offer's age is the fact a reader acts on, which is the one place an age belongs in
129
+ // this text: the value is whether anybody has had time to answer.
130
+ if (card.offeredAt !== undefined && now !== undefined) {
131
+ lines.push(` offered: ${formatAge(now, card.offeredAt)} ago, and nobody took it`);
132
+ }
133
+ if (card.blockedBy !== undefined) lines.push(` blocked: ${card.blockedBy}`);
134
+ if (card.landed !== undefined) lines.push(` landed: ${card.landed.commit}`);
46
135
  if (priorityOf(card) !== DEFAULT_PRIORITY) lines.push(` priority: ${priorityOf(card)}`);
47
136
  if (card.plan !== undefined) lines.push(` plan: ${card.plan}`);
48
137
  if (card.paths.length > 0) lines.push(` paths: ${card.paths.join(", ")}`);
49
138
  if (dependsOf(card).length > 0) lines.push(` waits on: ${waitingOf(card, board)}`);
139
+ // The mirror question, for the same board: what leaving this card undone holds up. Only
140
+ // a card with the board in hand can answer it, which is why a bare card read omits it.
141
+ if (board !== undefined) {
142
+ const dependents = openDependentsOf(card.id, board);
143
+ if (dependents.length > 0) {
144
+ const which = dependents.length === 1 ? "card" : "cards";
145
+ lines.push(` waited on by ${dependents.length} ${which}: ${lanesText(dependents)}`);
146
+ }
147
+ }
50
148
  if (card.notes.length > 0) {
51
149
  const last = card.notes.at(-1);
52
150
  if (last) lines.push(` last note (${last.by}): ${last.text}`);
@@ -15,6 +15,7 @@
15
15
 
16
16
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
17
  import { isExtensionEnabled } from "../../lib/env.ts";
18
+ import { requestMount } from "../../lib/http/index.ts";
18
19
  import { registerCommands } from "./commands.ts";
19
20
  import { type BoardPage, createBoardPage } from "./page.ts";
20
21
  import { type BoardSession, openBoardSession } from "../../lib/board/session.ts";
@@ -30,9 +31,20 @@ export interface BoardDeps {
30
31
  export default function board(pi: ExtensionAPI, deps: BoardDeps = {}): void {
31
32
  if (!isExtensionEnabled("board")) return;
32
33
  const session = deps.session ?? openBoardSession(pi);
33
- const page = deps.page ?? createBoardPage(session);
34
+ /** The session the page serves as; the mount lands on this session's one server. */
35
+ let sessionId = "";
36
+ const page =
37
+ deps.page ??
38
+ createBoardPage(session, {
39
+ mount: (spec) => requestMount(pi, spec),
40
+ session: () => sessionId,
41
+ });
34
42
  registerTools(pi, session);
35
- registerCommands(pi, page);
43
+ registerCommands(pi, session, page);
44
+
45
+ pi.on("session_start", async (_event, ctx) => {
46
+ sessionId = ctx.sessionManager.getSessionId();
47
+ });
36
48
 
37
49
  pi.on("session_shutdown", async () => {
38
50
  await page.closeAll();
@@ -2,30 +2,51 @@
2
2
  * Board's side of the team: the one call that puts this session on it.
3
3
  *
4
4
  * The user's surface is `/board` and `team`; the session bus is the transport under
5
- * them. So the board joins on the user's behalf — when the page opens, and again the
6
- * first time a card is worked — and never asks anybody to run a bus action.
5
+ * them. So the board joins on the user's behalf — when the page opens, and the first
6
+ * time this session claims a card — and never asks anybody to run a bus action.
7
+ *
8
+ * The label it asks for is the session's own team name (`BoardSession.who`), so the
9
+ * card, the bus record, and the roster say one word, and joining never renames the
10
+ * session: the footer keeps the name the user gave it.
7
11
  *
8
12
  * The seam exists for the same reason `team`'s dispatch does: a board suite drives a
9
13
  * recording join instead of a bus, and the shipped implementation is one in-process
10
14
  * request through `lib/session-tool.ts` (a command context cannot call a tool).
11
15
  */
12
16
 
13
- import type { JoinResult } from "../../lib/session-tool.ts";
14
- import { DEFAULT_SEAT } from "./seat.ts";
17
+ import { messageOf } from "../../lib/errors.ts";
18
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import type { JoinResult, TeamJoin } from "../../lib/session-tool.ts";
20
+ import type { TeamStanding } from "../../lib/board/session.ts";
15
21
 
16
22
  /**
17
23
  * The join itself is `lib/session-tool.ts`'s, because `team` asks for it too; this
18
- * module only holds what is the board's — the label it lands on, and how the outcome
19
- * reads on a `/board` line.
24
+ * module only holds what is the board's — when it asks, and how the outcome reads on a
25
+ * `/board` line.
20
26
  */
21
27
  export type { TeamJoin } from "../../lib/session-tool.ts";
22
28
 
23
29
  /**
24
- * The label `/board` lands on: the page's seat when this session serves it, and
25
- * nothing when it watches somebody else's page, so a viewer keeps its own name.
30
+ * Put this session on the team, once: the first card it claims.
31
+ *
32
+ * Asked only while the standing does not say this session is on the team, so a card
33
+ * write costs one request for the session rather than one per card. A failure is
34
+ * recorded and asked again by the next claim; a bus that is not loaded answers with a
35
+ * sentence, which is a result and not an exception — and a join that did throw still
36
+ * must not undo the card that was already written, so it is swallowed here.
26
37
  */
27
- export function joinLabel(served: boolean): string | undefined {
28
- return served ? DEFAULT_SEAT : undefined;
38
+ export async function joinOnClaim(
39
+ standing: TeamStanding,
40
+ team: TeamJoin,
41
+ ctx: ExtensionContext,
42
+ label: string,
43
+ ): Promise<void> {
44
+ if (standing.last()?.ok === true) return;
45
+ try {
46
+ standing.record(await team.join(ctx, { label }));
47
+ } catch (error) {
48
+ standing.record({ ok: false, error: messageOf(error) });
49
+ }
29
50
  }
30
51
 
31
52
  /**