@gavin-hu/my-pi-agent 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +561 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -2,68 +2,44 @@
2
2
  * The WeChat bridge: one connection per account, one session per peer.
3
3
  *
4
4
  * This is the composition root. The poll loop, outbound delivery, the typing and
5
- * wait notices, admission (dedupe/cap/budget), and inbound media each live in
6
- * their own module; the bridge wires them to the live client, account, cursor,
7
- * and state, and owns the connection lifecycle and the status snapshot. Each
8
- * admitted message is handed to the sender's own session through the peer
9
- * worker, so a WeChat turn never runs in the terminal's session.
5
+ * wait notices, admission (dedupe/cap/budget), inbound media, and the inbound
6
+ * path itself each live in their own module; the bridge wires them to the live
7
+ * client, account, cursor, and state, and owns the connection lifecycle and the
8
+ * status snapshot. Each admitted message is handed to the sender's own session
9
+ * through the peer worker, so a WeChat turn never runs in the terminal's session.
10
10
  *
11
11
  * The peer only has the chat window, so the bridge carries the signals the phone
12
12
  * needs: typing starts as soon as a message is picked up, a message waiting on
13
13
  * another conversation is acknowledged once and told why, a slow turn says so,
14
- * and a burst that arrives before its turn starts is answered once, in order.
14
+ * and every queued message is answered in its own turn, in order.
15
15
  *
16
16
  * The model may message the owner proactively through {@link Bridge.sendToOwner}.
17
17
  */
18
18
 
19
- import { WechatError, type WechatClient } from "./client.ts";
20
- import { createAdmissionQueue, createTurnBudget } from "./admission.ts";
21
- import { createDeliverer, type MessageTarget } from "./deliver.ts";
22
- import { createInboundAdmission } from "./inbound.ts";
23
- import { createPollLoop, defaultPollTimeout } from "./poll.ts";
19
+ import { WechatError, type WechatClient } from "./transport/client.ts";
20
+ import { createAdmissionQueue, createTurnBudget } from "./peer/admission.ts";
21
+ import { createDeliverer, type MessageTarget } from "./transport/deliver.ts";
22
+ import { createInboundAdmission } from "./transport/inbound.ts";
23
+ import { createPollLoop, defaultPollTimeout } from "./transport/poll.ts";
24
24
  import { messageOf } from "./errors.ts";
25
- import { parseInbound, peerKey, peerLabel, peerLanguage, sanitizeInbound, type PeerLanguage } from "./format.ts";
26
- import { notice } from "./notices.ts";
27
- import { createPresence } from "./presence.ts";
28
- import {
29
- createWechatInteractionChannel,
30
- type PendingPromptInfo,
31
- type WechatInteractionChannel,
32
- } from "./interaction.ts";
25
+ import { peerKey, sanitizeInbound, type PeerLanguage } from "./text/format.ts";
26
+ import { notice } from "./text/notices.ts";
27
+ import { createPresence } from "./peer/presence.ts";
28
+ import { createForgetToken, createIntake, createLanguageOf, type Counter } from "./intake.ts";
29
+
30
+ // The admission predicates, re-exported: they are the bridge's contract, and a caller
31
+ // should not have to know which module inside it answers them.
32
+ export { isAllowedPeer, isInboundMessage } from "./intake.ts";
33
+ import { createPeerChannels, type PeerChannels } from "./dialog/channels.ts";
34
+ import type { PendingPromptInfo, WechatInteractionChannel } from "./dialog/interaction.ts";
33
35
  import { defaultSleep, defaultTimer, type ArmTimer } from "./timer.ts";
34
- import { createPeerWorker, type PeerState, type PeerWorker } from "./peer-worker.ts";
35
- import type { PeerSession } from "./peer-session.ts";
36
- import { emptyState, findPeer, recordInbound, rememberMessage, seenMessage } from "./state.ts";
37
- import {
38
- expandSnippet,
39
- findSlashCommand,
40
- parseSlash,
41
- SLASH_COMMANDS,
42
- type SlashCommand,
43
- type SlashContext,
44
- } from "./slash.ts";
45
- import type { BotCredentials, InboundItem, WechatState, WeixinMessage } from "./types.ts";
36
+ import { createPeerWorker, type PeerState, type PeerWorker } from "./peer/peer-worker.ts";
37
+ import type { PeerSession } from "./peer/peer-session.ts";
38
+ import { emptyState, ownerTarget } from "./state/state.ts";
39
+ import { SLASH_COMMANDS, type SlashCommand } from "./commands/slash.ts";
40
+ import type { BotCredentials, WechatState } from "./types.ts";
46
41
  import type { WechatConfig } from "./config.ts";
47
42
 
48
- /** Inbound messages held while a turn runs; beyond this the peer is told to wait. */
49
- const MAX_QUEUE = 8;
50
-
51
- /**
52
- * Whether `peer` may drive the agent: the owner, or an explicit extra. Either id
53
- * form is accepted (with or without `@im.wechat`), so a label copied from the
54
- * control screen is not silently refused.
55
- */
56
- export function isAllowedPeer(peer: string, owner: string | undefined, allowedPeers: string[]): boolean {
57
- const key = peerKey(peer);
58
- if (owner !== undefined && peerKey(owner) === key) return true;
59
- return allowedPeers.some((allowed) => peerKey(allowed) === key);
60
- }
61
-
62
- /** True when a message is inbound from a human (not the bot's own echo). */
63
- export function isInboundMessage(message: WeixinMessage): message is WeixinMessage & { from_user_id: string } {
64
- return message.message_type !== 2 && typeof message.from_user_id === "string" && message.from_user_id !== "";
65
- }
66
-
67
43
  /** The bridge's connection state, published to the UI for the status chip. */
68
44
  export type BridgeState = "stopped" | "running" | "expired" | "paused";
69
45
 
@@ -120,10 +96,10 @@ export interface BridgeStatus {
120
96
  /** Labels of the senders refused since the bridge opened, newest last, capped. */
121
97
  refusedPeers: string[];
122
98
  mediaDropped: number;
123
- /** Whether a remote dialog prompt is waiting for an answer. */
99
+ /** Whether any peer's dialogs prompt is waiting for an answer. */
124
100
  promptPending: boolean;
125
- /** The waiting prompt's peer, kind, title, and age. */
126
- prompt?: PendingPromptInfo;
101
+ /** Every waiting prompt, one per peer, with its kind, title, and age. */
102
+ prompts: PendingPromptInfo[];
127
103
  /** When this connection opened. */
128
104
  startedAt?: number;
129
105
  /** When the last poll settled, successful or not. */
@@ -160,10 +136,10 @@ export interface Bridge {
160
136
  /** What one peer's conversation is doing, for `/status`. */
161
137
  peerState(peer: string): PeerState;
162
138
  status(): BridgeStatus;
163
- /** The remote interaction channel the peer sessions route their dialogs through. */
164
- channel(): WechatInteractionChannel;
165
- /** Message the owner proactively. Owner-only and text-only by design. */
166
- sendToOwner(text: string): Promise<SendResult>;
139
+ /** One peer's interaction channel: the channel its session routes its dialogs through. */
140
+ channel(peer: string): WechatInteractionChannel;
141
+ /** Message the owner proactively: one text, or one image, never a chosen recipient. */
142
+ sendToOwner(text: string | undefined, image?: string): Promise<SendResult>;
167
143
  }
168
144
 
169
145
  /** Create the bridge over an injected environment. */
@@ -183,7 +159,8 @@ export function createBridge(deps: BridgeDeps): Bridge {
183
159
  let client: WechatClient | undefined;
184
160
  let account: BotCredentials | undefined;
185
161
  let state: WechatState = { cursor: "", peers: {} };
186
- let refused = 0;
162
+ /** Senders refused since the connection opened; the inbound path bumps it. */
163
+ const refused: Counter = { value: 0 };
187
164
  /** Labels of refused senders, so the operator can allowlist one by name. */
188
165
  const refusedPeers = new Set<string>();
189
166
  /** The language each peer last wrote in, so a media-only message can answer. */
@@ -191,6 +168,9 @@ export function createBridge(deps: BridgeDeps): Bridge {
191
168
  /** Peers already told that the queue is full, until it drains. */
192
169
  const overflow = new Set<string>();
193
170
  const slashCommands = deps.slashCommands ?? SLASH_COMMANDS;
171
+ /** The peer's language memory: written here when a turn settles, read on every message. */
172
+ const languageOf = createLanguageOf(languages);
173
+ const forgetToken = createForgetToken({ state: () => state, saveState: deps.saveState });
194
174
 
195
175
  /** One worker per open connection: it owns the queues and the peer sessions. */
196
176
  let worker: PeerWorker | undefined;
@@ -216,17 +196,9 @@ export function createBridge(deps: BridgeDeps): Bridge {
216
196
  notify("WeChat session expired. Run /wechat login to reconnect.", "warning");
217
197
  }
218
198
 
219
- /** Drop a peer's cached reply token, so the next inbound message replaces it. */
220
- function forgetToken(peer: string): void {
221
- const entry = findPeer(state, peer);
222
- if (!entry?.lastContextToken) return;
223
- delete entry.lastContextToken;
224
- deps.saveState(state);
225
- }
226
-
227
199
  /** Send one bubble to a peer and report whether it was delivered. */
228
- async function reply(target: MessageTarget, text: string): Promise<boolean> {
229
- return deliverer.reply(target, text, abort?.signal);
200
+ async function reply(target: MessageTarget, text: string, signal?: AbortSignal): Promise<boolean> {
201
+ return deliverer.reply(target, text, signal ?? abort?.signal);
230
202
  }
231
203
 
232
204
  /** The outbound surface, reading the live client and account at call time. */
@@ -246,81 +218,31 @@ export function createBridge(deps: BridgeDeps): Bridge {
246
218
  lastSendError = undefined;
247
219
  },
248
220
  });
249
- /** Run a slash action out-of-band and reply to the sender; never starts a turn. */
250
- async function runSlashAction(command: SlashCommand, target: MessageTarget, args: string): Promise<void> {
251
- if (!command.run) return;
252
- const ctx: SlashContext = {
253
- args,
254
- peer: target.peer,
255
- reply: async (text) => {
256
- await reply(target, text);
257
- },
258
- activeTurn: worker?.state(target.peer).status === "running",
259
- language: languageOf(target.peer),
260
- session: {
261
- newSession: () => {
262
- worker?.reset(target.peer);
263
- },
264
- stop: () => worker?.abort(target.peer) ?? false,
265
- status: () => {
266
- const current = worker?.state(target.peer) ?? { status: "idle" as const, queued: 0 };
267
- const waiting = interaction.pendingInfo();
268
- return waiting?.peer === target.peer ? { ...current, prompt: waiting.title } : current;
269
- },
270
- history: (count) => deps.readHistory(target.peer, count),
271
- },
272
- };
273
- try {
274
- await command.run(ctx);
275
- } catch (error) {
276
- notify(`WeChat command /${command.name} failed: ${messageOf(error)}`, "warning");
277
- await reply(target, `Command /${command.name} failed.`);
278
- }
279
- }
280
-
281
221
  /**
282
- * Send a prompt to the peer owning the in-flight turn.
222
+ * Every peer's dialogs, one channel each.
283
223
  *
284
- * A failure is reported on the terminal once per peer by the deliverer; a silent
285
- * failure would leave the peer with only the choices, as if nothing preceded
286
- * them.
224
+ * A prompt is sent through the peer's channel to the turn it belongs to, so a
225
+ * reply can only answer its own peer's question even when two turns run at once.
226
+ * A failure is reported once per peer by the deliverer; a silent failure would
227
+ * leave the peer with only the choices, as if nothing preceded them.
287
228
  */
288
- async function sendPrompt(text: string, signal?: AbortSignal, options?: { force?: boolean }): Promise<boolean> {
289
- const current = worker?.current();
290
- if (!current) return false;
291
- // The model may have produced text before this prompt; put it on the wire first.
292
- await worker?.flush(current.peer);
293
- if (!options?.force && !interaction.hasPending()) return false;
294
- return deliverer.prompt(current, text, signal ?? abort?.signal);
295
- }
296
- async function sendFileToPeer(filePath: string, fileName: string, signal?: AbortSignal): Promise<boolean> {
297
- const current = worker?.current();
298
- if (!current) return false;
299
- await worker?.flush(current.peer);
300
- return deliverer.file(current, filePath, fileName, signal ?? abort?.signal);
301
- }
302
- const interaction = createWechatInteractionChannel({
303
- send: (text, signal, options) => sendPrompt(text, signal, options),
304
- sendFile: (path, name, signal) => sendFileToPeer(path, name, signal),
305
- activePeer: () => worker?.current()?.peer,
306
- language: () => languageOf(worker?.current()?.peer ?? ""),
229
+ const channels: PeerChannels = createPeerChannels({
230
+ flush: async (peer) => {
231
+ await worker?.flush(peer);
232
+ },
233
+ send: (peer, text, signal) => {
234
+ const item = worker?.running(peer);
235
+ return item ? deliverer.prompt(item, text, signal ?? abort?.signal) : Promise.resolve(false);
236
+ },
237
+ sendFile: (peer, filePath, fileName, signal) => {
238
+ const item = worker?.running(peer);
239
+ return item ? deliverer.file(item, filePath, fileName, signal ?? abort?.signal) : Promise.resolve(false);
240
+ },
241
+ active: (peer) => worker?.state(peer).status === "running",
242
+ language: (peer) => languageOf(peer),
307
243
  armTimer: deps.armTimer,
308
244
  });
309
245
 
310
- /** The peer's language: from this text, else the last one they wrote. */
311
- function languageOf(peer: string, text?: string): PeerLanguage {
312
- // A Chinese message is evidence of Chinese. Its absence is not evidence of
313
- // English, or a short reply ("ok", "1") would flip an established conversation
314
- // back to English; English is the fallback, so it needs no record of its own.
315
- if (text && peerLanguage(text) === "zh") languages.set(peer, "zh");
316
- return languages.get(peer) ?? "en";
317
- }
318
-
319
- /** The peer's conversation state, for the wait decisions in presence. */
320
- function peerStatus(peer: string): PeerState["status"] {
321
- return worker?.state(peer).status ?? "idle";
322
- }
323
-
324
246
  /** Typing, acknowledgement, and the slow/busy lines; one timer registry each. */
325
247
  const presence = createPresence({
326
248
  typingIndicator: () => deps.config.typingIndicator,
@@ -329,10 +251,10 @@ export function createBridge(deps: BridgeDeps): Bridge {
329
251
  client: () => client,
330
252
  account: () => account,
331
253
  signal: () => abort?.signal,
332
- statusOf: peerStatus,
254
+ statusOf: (peer) => worker?.state(peer).status ?? "idle",
333
255
  languageOf,
334
256
  send: (peer, contextToken, text) => reply({ peer, contextToken }, text),
335
- promptPending: () => interaction.hasPending(),
257
+ promptPending: (peer) => channels.hasPending(peer),
336
258
  });
337
259
 
338
260
  /** Inbound media resolution and the hand-off to a peer session. */
@@ -353,92 +275,27 @@ export function createBridge(deps: BridgeDeps): Bridge {
353
275
  const budget = createTurnBudget(deps.config.maxTurnsPerHour, deps.now);
354
276
  const admission = createAdmissionQueue({ admit: inbound.admit, notify });
355
277
 
356
- function handleMessage(message: WeixinMessage): void {
357
- if (!isInboundMessage(message)) return;
358
- const peer = message.from_user_id;
359
- const { text: rawText, media, unsupported } = parseInbound(message.item_list);
360
- const owner = state.ownerId ?? account?.ilinkUserId;
361
- // Admission comes first: a refused sender never reaches the state file, a
362
- // pending prompt, or the queue.
363
- if (!isAllowedPeer(peer, owner, deps.config.allowedPeers)) {
364
- refused += 1;
365
- const label = peerLabel(peer);
366
- // Name a new sender once, so the operator can allowlist it from the
367
- // terminal; the cap keeps a spam run from filling the list.
368
- if (refusedPeers.size < 5 && !refusedPeers.has(label)) {
369
- refusedPeers.add(label);
370
- notify(`WeChat refused a message from ${label}. Add it to allowedPeers to admit.`, "warning");
371
- }
372
- return;
373
- }
374
- // A server replay of an already handled message must not answer twice. The
375
- // id is persisted, so a reload or reconnect cannot resurrect it.
376
- const messageId = message.message_id;
377
- if (typeof messageId === "number" && messageId !== 0 && seenMessage(state, peer, messageId)) return;
378
-
379
- let text = rawText;
380
- const cap = deps.config.maxInboundChars;
381
- if (text.length > cap) {
382
- const total = text.length;
383
- text = `${text.slice(0, cap)}\n[WeChat message truncated: showing ${cap} of ${total} characters]`;
384
- notify(`WeChat message truncated to ${cap} characters.`, "warning");
385
- }
386
-
387
- const now = deps.now();
388
- recordInbound(state, peer, message.context_token, now);
389
- if (typeof messageId === "number" && messageId !== 0) rememberMessage(state, peer, messageId, now);
390
- deps.saveState(state);
391
-
392
- const target = { peer, contextToken: message.context_token ?? findPeer(state, peer)?.lastContextToken };
393
- // A command is not an answer: while a dialog waits the model is blocked on it,
394
- // so /stop and /status must reach their handlers or the peer's only way out is
395
- // the prompt timing out. A message with media is never a command, and an
396
- // unknown command falls through to the model as ordinary text.
397
- const slash = media.length === 0 ? parseSlash(text) : undefined;
398
- const command = slash ? findSlashCommand(slashCommands, slash.name) : undefined;
399
- if (command && slash) {
400
- if (command.ownerOnly && peerKey(peer) !== peerKey(owner ?? "")) {
401
- refused += 1;
402
- return;
403
- }
404
- if (command.kind === "snippet") {
405
- text = expandSnippet(command, slash.args);
406
- } else {
407
- void runSlashAction(command, target, slash.args);
408
- return;
409
- }
410
- }
411
- // A reply to a pending prompt is consumed here, before queueing, so it never
412
- // starts a turn of its own. A duplicate answer was stopped by the id check.
413
- if (interaction.handleInbound(peer, text)) return;
414
- const language = languageOf(peer, text);
415
- const item: InboundItem = { ...target, text, media, language };
416
- if (!text && media.length === 0) {
417
- // Nothing to read. A voice note without a transcript or a video is
418
- // answered, so the peer knows it did not reach the agent.
419
- if (unsupported > 0) void reply(item, notice("unsupported", language));
420
- return;
421
- }
422
- // A slot is reserved here, before the download, so a burst of media cannot
423
- // all pass the check and then enqueue together.
424
- if (!budget.allow(peer)) {
425
- void reply(item, notice("budget", language));
426
- return;
427
- }
428
- const busy = (worker?.state(peer).queued ?? 0) + admission.pending(peer);
429
- if (busy >= MAX_QUEUE) {
430
- // Refuse rather than grow without bound; one notice per episode, not per
431
- // dropped message.
432
- if (!overflow.has(peer)) {
433
- overflow.add(peer);
434
- void reply(item, notice("overflow", language));
435
- }
436
- return;
437
- }
438
- presence.keepTyping(peer, item.contextToken);
439
- presence.armAck(item);
440
- admission.enqueue(item);
441
- }
278
+ /** The inbound path: admission, commands, and the hand-off to a peer's session. */
279
+ const intake = createIntake({
280
+ state: () => state,
281
+ saveState: deps.saveState,
282
+ account: () => account,
283
+ config: deps.config,
284
+ now: deps.now,
285
+ notify,
286
+ worker: () => worker,
287
+ channels,
288
+ presence,
289
+ budget,
290
+ admission,
291
+ languageOf,
292
+ reply: (target, text) => reply(target, text),
293
+ readHistory: deps.readHistory,
294
+ refused,
295
+ refusedPeers,
296
+ overflow,
297
+ slashCommands,
298
+ });
442
299
 
443
300
  /** The long-poll loop; it reads the live client, cursor, and state. */
444
301
  const poll = createPollLoop({
@@ -454,7 +311,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
454
311
  notify,
455
312
  onExpired: handleExpired,
456
313
  onMessages: (messages) => {
457
- for (const message of messages) handleMessage(message);
314
+ for (const message of messages) intake.handleMessage(message);
458
315
  },
459
316
  refreshLock: deps.refreshLock,
460
317
  isRunning: () => running,
@@ -475,9 +332,9 @@ export function createBridge(deps: BridgeDeps): Bridge {
475
332
  function resetEphemeral(): void {
476
333
  languages.clear();
477
334
  overflow.clear();
478
- refused = 0;
335
+ refused.value = 0;
479
336
  refusedPeers.clear();
480
- interaction.reset();
337
+ channels.reset();
481
338
  deliverer.reset();
482
339
  presence.reset();
483
340
  budget.reset();
@@ -552,10 +409,10 @@ export function createBridge(deps: BridgeDeps): Bridge {
552
409
  // a login (or a reconnect after an expiry) starts from a clean slate while
553
410
  // each peer's history stays on disk.
554
411
  worker = createPeerWorker({
555
- openSession: (peer, options) => deps.openSession(peer, { fresh: options.fresh, channel: interaction }),
412
+ openSession: (peer, options) => deps.openSession(peer, { fresh: options.fresh, channel: channels.for(peer) }),
556
413
  // Each part is delivered as its own bubble; the worker puts the
557
414
  // attachment line on the first one. A false result stops the stream.
558
- send: (item, text) => reply(item, text),
415
+ send: (item, text, options) => reply(item, text, options?.signal),
559
416
  fallback: (item, reason) => notice(reason, item.language),
560
417
  onStart: (item) => {
561
418
  presence.cancelAck(item.peer);
@@ -602,7 +459,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
602
459
  // worker, so close it unconditionally rather than leaking the sessions.
603
460
  const closing = worker;
604
461
  worker = undefined;
605
- await closing?.close();
462
+ await closing?.close({ abandon: true });
606
463
  resetEphemeral();
607
464
  publishState();
608
465
  releaseSafe();
@@ -618,7 +475,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
618
475
  // not be cancelled by the poll abort that follows.
619
476
  const stopping = abort;
620
477
  abort = undefined;
621
- if (interaction.hasPending()) await interaction.cancel();
478
+ await channels.cancelAll();
622
479
  stopping?.abort();
623
480
  try {
624
481
  await loopPromise;
@@ -679,11 +536,11 @@ export function createBridge(deps: BridgeDeps): Bridge {
679
536
  peerCount: Object.keys(state.peers).length,
680
537
  queued: states.reduce((total, entry) => total + entry.state.queued, 0),
681
538
  running: states.some((entry) => entry.state.status === "running"),
682
- refused,
539
+ refused: refused.value,
683
540
  refusedPeers: [...refusedPeers],
684
541
  mediaDropped: inbound.dropped(),
685
- promptPending: interaction.hasPending(),
686
- prompt: interaction.pendingInfo(),
542
+ promptPending: channels.hasPending(),
543
+ prompts: channels.prompts(),
687
544
  startedAt,
688
545
  lastPollAt,
689
546
  lastPollError,
@@ -694,27 +551,25 @@ export function createBridge(deps: BridgeDeps): Bridge {
694
551
  };
695
552
  },
696
553
 
697
- channel(): WechatInteractionChannel {
698
- return interaction;
554
+ channel(peer: string): WechatInteractionChannel {
555
+ return channels.for(peer);
699
556
  },
700
557
 
701
- async sendToOwner(text: string): Promise<SendResult> {
558
+ async sendToOwner(text: string | undefined, image?: string): Promise<SendResult> {
702
559
  if (!running || !client || !account) return { ok: false, error: "WeChat bridge is not started." };
703
- const owner = state.ownerId ?? account.ilinkUserId;
704
- const current = worker?.current();
705
- if (current && peerKey(current.peer) === peerKey(owner)) {
706
- // The bridge answers this turn itself, so a tool send would land twice.
707
- return {
708
- ok: false,
709
- error: "The bridge replies to this turn automatically; do not repeat it.",
710
- };
560
+ const target = ownerTarget(state, account);
561
+ if (text !== undefined && worker?.state(target.peer).status === "running") {
562
+ // The bridge answers this turn itself, so a text send would land twice.
563
+ return { ok: false, error: "The bridge replies to this turn automatically; do not repeat it." };
564
+ }
565
+ if (image !== undefined) {
566
+ // An image cannot duplicate the streamed answer, so a running turn is fine.
567
+ const sent = await deliverer.image(target, image, abort?.signal);
568
+ return sent ? { ok: true } : { ok: false, error: "The image could not be sent; see the terminal notice." };
711
569
  }
712
- const trimmed = sanitizeInbound(text);
570
+ const trimmed = sanitizeInbound(text ?? "");
713
571
  if (!trimmed) return { ok: false, error: "Nothing to send." };
714
- // Either id form may be stored: the owner id comes from the login response
715
- // and the token from an inbound `from_user_id`, and they need not agree.
716
- const contextToken = findPeer(state, owner)?.lastContextToken;
717
- return deliverer.toOwner(owner, contextToken, trimmed, abort?.signal);
572
+ return deliverer.toOwner(target.peer, target.contextToken, trimmed, abort?.signal);
718
573
  },
719
574
  };
720
575
  }
@@ -18,21 +18,27 @@ import {
18
18
  type ExtensionContext,
19
19
  } from "@earendil-works/pi-coding-agent";
20
20
  import { join } from "node:path";
21
- import { writeConfigValue } from "../../lib/config.ts";
22
- import { sanitizeLine } from "../../lib/format.ts";
23
- import { supportsCustomUI } from "../../lib/interaction.ts";
24
- import { isAllowedPeer, type Bridge } from "./bridge.ts";
25
- import type { WechatClient } from "./client.ts";
26
- import type { WechatConfig } from "./config.ts";
21
+ import { writeConfigValue } from "../../../lib/config.ts";
22
+ import { sanitizeLine } from "../../../lib/format.ts";
23
+ import { supportsCustomUI } from "../../../lib/interaction.ts";
24
+ import { isAllowedPeer, type Bridge } from "../bridge.ts";
25
+ import type { WechatClient } from "../transport/client.ts";
26
+ import type { WechatConfig } from "../config.ts";
27
27
  import { doctorReport } from "./doctor.ts";
28
- import { messageOf } from "./errors.ts";
29
- import { peerLabel, peerKey } from "./format.ts";
28
+ import { messageOf } from "../errors.ts";
29
+ import { peerLabel, peerKey } from "../text/format.ts";
30
30
  import { waitForLogin, type LoginOutcome } from "./login.ts";
31
- import { defaultSleep } from "./timer.ts";
32
- import { LoginScreenComponent, type LoginViewState } from "./login-screen.ts";
33
- import { qrLines } from "../../lib/qr.ts";
34
- import { statusLine, statusView, StatusScreenComponent, type StatusAction, type StatusView } from "./status-screen.ts";
35
- import type { BotCredentials } from "./types.ts";
31
+ import { defaultSleep } from "../timer.ts";
32
+ import { LoginScreenComponent, type LoginViewState } from "../view/login-screen.ts";
33
+ import { qrLines } from "../../../lib/qr.ts";
34
+ import {
35
+ statusLine,
36
+ statusView,
37
+ StatusScreenComponent,
38
+ type StatusAction,
39
+ type StatusView,
40
+ } from "../view/status-screen.ts";
41
+ import type { BotCredentials } from "../types.ts";
36
42
 
37
43
  export interface CommandDeps {
38
44
  getBridge: () => Bridge | undefined;
@@ -152,9 +158,11 @@ function resetMessage(bridge: Bridge, peer: string): VerbOutcome {
152
158
  : { message: `${label} is running; stop it before starting a new conversation.`, kind: "info" };
153
159
  }
154
160
 
155
- function cancelPromptMessage(bridge: Bridge): VerbOutcome {
156
- if (!bridge.status().promptPending) return { message: "No prompt is waiting.", kind: "info" };
157
- void bridge.channel().cancel();
161
+ function cancelPromptMessage(bridge: Bridge, peer: string): VerbOutcome {
162
+ if (!bridge.status().prompts.some((prompt) => prompt.peer === peer)) {
163
+ return { message: "No prompt is waiting.", kind: "info" };
164
+ }
165
+ void bridge.channel(peer).cancel();
158
166
  return { message: "Cancelled the waiting prompt.", kind: "info" };
159
167
  }
160
168
 
@@ -364,9 +372,9 @@ async function runPanel(ctx: ExtensionCommandContext, deps: CommandDeps): Promis
364
372
  const bridge = deps.getBridge();
365
373
  return bridge ? resetMessage(bridge, peer).message : "WeChat bridge is not running.";
366
374
  },
367
- cancelPrompt: () => {
375
+ cancelPrompt: (focused) => {
368
376
  const bridge = deps.getBridge();
369
- return bridge ? cancelPromptMessage(bridge).message : "No prompt is waiting.";
377
+ return bridge ? cancelPromptMessage(bridge, focused).message : "No prompt is waiting.";
370
378
  },
371
379
  togglePause: async () => {
372
380
  const bridge = deps.getBridge();
@@ -7,11 +7,11 @@
7
7
  * the wording and the age math are unit-tested without a bridge.
8
8
  */
9
9
 
10
- import { formatDuration } from "../../lib/format.ts";
11
- import type { BridgeStatus } from "./bridge.ts";
12
- import type { WechatConfig } from "./config.ts";
13
- import type { BotCredentials } from "./types.ts";
14
- import { peerLabel } from "./format.ts";
10
+ import { formatDuration } from "../../../lib/format.ts";
11
+ import type { BridgeStatus } from "../bridge.ts";
12
+ import type { WechatConfig } from "../config.ts";
13
+ import type { BotCredentials } from "../types.ts";
14
+ import { peerLabel } from "../text/format.ts";
15
15
 
16
16
  export interface DoctorInput {
17
17
  /** Undefined when the bridge has never been built in this runtime. */
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { SessionManager, type SessionEntry, type SessionMessageEntry } from "@earendil-works/pi-coding-agent";
11
- import { peerSessionDir } from "./peer-session.ts";
11
+ import { peerSessionDir } from "../peer/peer-session.ts";
12
12
 
13
13
  /** Longest text one history line carries before it is elided. */
14
14
  export const HISTORY_LINE_CHARS = 160;
@@ -6,9 +6,9 @@
6
6
  * without a real timer, and the caller owns displaying the QR.
7
7
  */
8
8
 
9
- import { DEFAULT_BASE_URL, type WechatClient } from "./client.ts";
10
- import { messageOf } from "./errors.ts";
11
- import type { BotCredentials, QrResponse, QrStatus } from "./types.ts";
9
+ import { DEFAULT_BASE_URL, type WechatClient } from "../transport/client.ts";
10
+ import { messageOf } from "../errors.ts";
11
+ import type { BotCredentials, QrResponse, QrStatus } from "../types.ts";
12
12
 
13
13
  /** Categorical meaning of a `get_qrcode_status` value. */
14
14
  export type StatusKind = "pending" | "confirmed" | "expired" | "verify" | "blocked" | "binded";