@gavin-hu/my-pi-agent 0.17.0 → 0.19.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 (274) hide show
  1. package/README.md +61 -45
  2. package/README.zh-CN.md +54 -42
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
  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 +554 -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} +66 -53
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
  14. package/docs/extensions/plan.md +976 -0
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
  19. package/docs/extensions/team.md +226 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -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 +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/notify/types.ts +1 -1
  51. package/extensions/bytefly/panel/commands.ts +40 -7
  52. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  53. package/extensions/bytefly/panel/settings.ts +11 -21
  54. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  55. package/extensions/bytefly/picker/discovery.ts +0 -2
  56. package/extensions/bytefly/picker/types.ts +0 -2
  57. package/extensions/bytefly/update/check.ts +39 -0
  58. package/extensions/bytefly/update/format.ts +15 -0
  59. package/extensions/bytefly/update/register.ts +73 -34
  60. package/extensions/bytefly/update/state.ts +6 -3
  61. package/extensions/context/tui.ts +1 -1
  62. package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
  63. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  64. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  65. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  66. package/extensions/file-browse/index.ts +4 -4
  67. package/extensions/file-browse/{live.ts → live/live.ts} +54 -82
  68. package/extensions/file-browse/live/rev.ts +42 -0
  69. package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
  70. package/extensions/file-browse/transport/server.ts +110 -0
  71. package/extensions/file-browse/view/chrome.ts +83 -0
  72. package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
  73. package/extensions/file-browse/view/diff-html.ts +94 -0
  74. package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
  75. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  76. package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
  77. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  78. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  79. package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -5
  80. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  81. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  82. package/extensions/file-read/schema.ts +2 -1
  83. package/extensions/goal/plan-route.ts +8 -2
  84. package/extensions/job/index.ts +15 -7
  85. package/extensions/memory/store.ts +12 -5
  86. package/extensions/plan/blocks.ts +23 -2
  87. package/extensions/plan/commands.ts +77 -21
  88. package/extensions/plan/feedback.ts +409 -63
  89. package/extensions/plan/index.ts +7 -5
  90. package/extensions/plan/plan-index.ts +44 -10
  91. package/extensions/plan/plan-text.ts +51 -24
  92. package/extensions/plan/plans.ts +162 -57
  93. package/extensions/plan/policy.ts +20 -12
  94. package/extensions/plan/review-prompt.ts +93 -0
  95. package/extensions/plan/review.ts +51 -95
  96. package/extensions/plan/runtime.ts +22 -10
  97. package/extensions/plan/sections.ts +112 -0
  98. package/extensions/plan/steps.ts +63 -95
  99. package/extensions/plan/tools/enter.ts +8 -3
  100. package/extensions/plan/tools/exit.ts +13 -19
  101. package/extensions/plan/tools/write.ts +8 -3
  102. package/extensions/plan/tools.ts +1 -1
  103. package/extensions/plan/tui.ts +29 -15
  104. package/extensions/plan/types.ts +29 -3
  105. package/extensions/plan/web-chrome.ts +2 -1
  106. package/extensions/plan/web-client.ts +122 -178
  107. package/extensions/plan/web-collab.ts +207 -0
  108. package/extensions/plan/web-comments.ts +269 -0
  109. package/extensions/plan/web-page.ts +96 -17
  110. package/extensions/plan/web-router.ts +109 -47
  111. package/extensions/plan/web-share-session.ts +191 -0
  112. package/extensions/plan/web-share.ts +169 -0
  113. package/extensions/plan/web-streams.ts +124 -0
  114. package/extensions/plan/web-tokens.ts +63 -0
  115. package/extensions/plan/web.ts +245 -151
  116. package/extensions/question/dialogs.ts +91 -1
  117. package/extensions/question/tools.ts +2 -1
  118. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  119. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  120. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  121. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  122. package/extensions/rewind/index.ts +3 -3
  123. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  124. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  125. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  126. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  127. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  128. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  129. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  130. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  131. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  132. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  133. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  134. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  135. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  136. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  137. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  138. package/extensions/session-bus/commands.ts +113 -0
  139. package/extensions/session-bus/format.ts +101 -0
  140. package/extensions/session-bus/index.ts +75 -0
  141. package/extensions/session-bus/join-request.ts +40 -0
  142. package/extensions/session-bus/mail-watch.ts +134 -0
  143. package/extensions/session-bus/notice.ts +86 -0
  144. package/extensions/session-bus/overlap.ts +36 -0
  145. package/extensions/session-bus/protocol.ts +104 -0
  146. package/extensions/session-bus/resolve.ts +67 -0
  147. package/extensions/session-bus/result.ts +208 -0
  148. package/extensions/session-bus/rows.ts +171 -0
  149. package/extensions/session-bus/runtime.ts +466 -0
  150. package/extensions/session-bus/store.ts +365 -0
  151. package/extensions/session-bus/tools.ts +192 -0
  152. package/extensions/session-bus/tui.ts +394 -0
  153. package/extensions/session-bus/types.ts +98 -0
  154. package/extensions/subagent/agents.ts +9 -8
  155. package/extensions/subagent/invocation.ts +4 -2
  156. package/extensions/team/commands.ts +112 -0
  157. package/extensions/team/dispatch.ts +133 -0
  158. package/extensions/team/format.ts +105 -0
  159. package/extensions/team/index.ts +33 -0
  160. package/extensions/team/schema.ts +29 -0
  161. package/extensions/team/tools.ts +246 -0
  162. package/extensions/team/types.ts +48 -0
  163. package/extensions/watch/index.ts +15 -9
  164. package/extensions/web-access/fetch/page.ts +4 -1
  165. package/extensions/web-access/fetch/structured.ts +447 -0
  166. package/extensions/web-access/fetch/tool.ts +2 -1
  167. package/extensions/web-access/search/tool.ts +2 -1
  168. package/extensions/wechat/bridge.ts +76 -92
  169. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  170. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  171. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  172. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  173. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  174. package/extensions/wechat/dialog/channels.ts +136 -0
  175. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +66 -51
  176. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
  177. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  178. package/extensions/wechat/index.ts +10 -10
  179. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  180. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  181. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +119 -56
  182. package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
  183. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
  184. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  185. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  186. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  187. package/extensions/wechat/{notices.ts → text/notices.ts} +15 -7
  188. package/extensions/wechat/tools.ts +29 -12
  189. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  190. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +104 -40
  191. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  192. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  193. package/extensions/wechat/transport/outbound.ts +142 -0
  194. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  195. package/extensions/wechat/types.ts +10 -1
  196. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  197. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  198. package/extensions/worktree/git.ts +5 -0
  199. package/extensions/worktree/guard.ts +3 -5
  200. package/extensions/worktree/registry.ts +11 -5
  201. package/extensions/worktree/status.ts +1 -3
  202. package/lib/README.md +12 -6
  203. package/lib/board/card.ts +290 -0
  204. package/lib/board/links.ts +117 -0
  205. package/lib/board/page-pointer.ts +256 -0
  206. package/lib/board/round-text.ts +72 -0
  207. package/lib/board/round.ts +89 -0
  208. package/lib/board/rules.ts +304 -0
  209. package/lib/board/session.ts +105 -0
  210. package/lib/board/store.ts +310 -0
  211. package/lib/board/view.ts +150 -0
  212. package/lib/errors.ts +12 -4
  213. package/lib/format.ts +13 -0
  214. package/lib/http/guard.ts +112 -0
  215. package/lib/http/index.ts +16 -5
  216. package/lib/http/router.ts +40 -13
  217. package/lib/http/serve.ts +234 -0
  218. package/lib/http/server.ts +29 -13
  219. package/lib/http/sink.ts +110 -0
  220. package/lib/http/token.ts +38 -0
  221. package/lib/interaction.ts +10 -0
  222. package/lib/notice.ts +29 -2
  223. package/lib/plan-entry.ts +7 -2
  224. package/lib/policy.ts +12 -2
  225. package/lib/qr.ts +64 -0
  226. package/lib/repo-key.ts +74 -0
  227. package/lib/report.ts +52 -33
  228. package/lib/requirements.ts +7 -9
  229. package/lib/session-label.ts +64 -0
  230. package/lib/session-roster.ts +130 -0
  231. package/lib/session-tool.ts +184 -0
  232. package/lib/tool-names.ts +25 -2
  233. package/lib/tui/README.md +16 -2
  234. package/lib/tui/dock-screen.ts +85 -17
  235. package/lib/tui/glyphs.ts +2 -0
  236. package/lib/tui/line-pane.ts +15 -4
  237. package/lib/tui/tool.ts +23 -4
  238. package/lib/web/README.md +5 -2
  239. package/lib/web/components.ts +6 -1
  240. package/lib/web/runtime.ts +7 -1
  241. package/lib/web/tokens.ts +25 -2
  242. package/package.json +27 -14
  243. package/skills/init/SKILL.md +80 -45
  244. package/skills/init/references/agents-md-quality.md +26 -2
  245. package/skills/init/references/interview-questions.md +36 -14
  246. package/skills/next/SKILL.md +77 -51
  247. package/skills/project-docs/SKILL.md +133 -0
  248. package/skills/project-docs/references/audit.md +131 -0
  249. package/skills/project-docs/references/changelog.md +105 -0
  250. package/skills/project-docs/references/discovery.md +127 -0
  251. package/skills/project-docs/references/readme.md +80 -0
  252. package/skills/project-docs/references/release.md +84 -0
  253. package/skills/project-docs/references/roadmap.md +93 -0
  254. package/skills/project-docs/scripts/state.ts +316 -0
  255. package/skills/refactor/SKILL.md +100 -125
  256. package/skills/refactor/references/handoff.md +35 -0
  257. package/skills/refactor/references/signals.md +54 -0
  258. package/skills/refactor/scripts/inventory.ts +587 -0
  259. package/extensions/file-browse/README.md +0 -467
  260. package/extensions/file-browse/server.ts +0 -134
  261. package/extensions/plan/README.md +0 -709
  262. package/extensions/plan/config.ts +0 -27
  263. package/extensions/wechat/outbound.ts +0 -100
  264. package/extensions/wechat/qrcode.ts +0 -46
  265. package/skills/changelog/SKILL.md +0 -148
  266. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  267. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  268. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  269. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  270. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  271. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  272. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  273. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  274. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * The WeChat implementation of {@link InteractionChannel}: it forwards a dialog
3
- * prompt to the peer that owns the current turn and resolves when that peer
4
- * replies.
3
+ * prompt to one peer and resolves when that peer replies.
5
4
  *
6
- * It depends only on injected `send`/`sendFile`/`activePeer` accessors and a
7
- * timer seam, never on the bridge or HTTP client, so it is unit-tested without
8
- * I/O. The bridge owns the poll loop and hands each inbound message to
5
+ * One channel serves one peer. That is what lets two peers' turns run at once
6
+ * without a dialog becoming ambiguous: the channel knows who it is asking, so a
7
+ * reply can only answer its own peer's prompt. It depends only on injected
8
+ * `send`/`sendFile`/`active` accessors and a timer seam, never on the bridge or
9
+ * HTTP client, so it is unit-tested without I/O. The bridge owns the poll loop,
10
+ * keeps one channel per peer (`channels.ts`), and hands each inbound message to
9
11
  * {@link WechatInteractionChannel.handleInbound}.
10
12
  *
11
13
  * A prompt is bounded: a peer who never answers cannot hold the session open, so
@@ -18,23 +20,25 @@ import {
18
20
  type InteractionAnswer,
19
21
  type InteractionChannel,
20
22
  type InteractionRequest,
21
- } from "../../lib/interaction.ts";
22
- import { type PeerLanguage } from "./format.ts";
23
- import { notice } from "./notices.ts";
23
+ } from "../../../lib/interaction.ts";
24
+ import { type PeerLanguage } from "../text/format.ts";
25
+ import { notice } from "../text/notices.ts";
24
26
  import { formatPrompt, parsePromptAnswer } from "./prompt.ts";
25
- import { defaultTimer, type ArmTimer } from "./timer.ts";
27
+ import { defaultTimer, type ArmTimer } from "../timer.ts";
26
28
 
27
29
  /** How long a forwarded prompt waits before it is cancelled. */
28
30
  export const PROMPT_TIMEOUT_MS = 180_000;
29
31
 
30
32
  export interface WechatInteractionDeps {
31
- /** Send one text message to the owning peer; resolves false when it fails. */
32
- send: (text: string, signal?: AbortSignal) => Promise<boolean>;
33
- /** Send a local file to the owning peer; resolves false when it fails. */
34
- sendFile: (path: string, name: string, signal?: AbortSignal) => Promise<boolean>;
35
- /** The peer that owns the in-flight turn, or undefined when there is none. */
36
- activePeer: () => string | undefined;
37
- /** The language the owning peer writes in, for the timeout notices. */
33
+ /** The one peer this channel asks. */
34
+ peer: string;
35
+ /** Send one text message to the peer; `force` bypasses the prompt-active gate. */
36
+ send: (text: string, signal?: AbortSignal, options?: { force?: boolean }) => Promise<boolean>;
37
+ /** Send a local file to the peer; `force` bypasses the prompt-active gate. */
38
+ sendFile: (path: string, name: string, signal?: AbortSignal, options?: { force?: boolean }) => Promise<boolean>;
39
+ /** Whether the peer owns a turn right now, so a prompt has somewhere to land. */
40
+ active: () => boolean;
41
+ /** The language the peer writes in, for the timeout notices. */
38
42
  language: () => PeerLanguage;
39
43
  /** Arm a cancellable one-shot timer. Defaults to a real, unref'd `setTimeout`. */
40
44
  armTimer?: ArmTimer;
@@ -48,14 +52,16 @@ export interface WechatInteractionChannel extends InteractionChannel {
48
52
  * previous one when it ends: the optional channel member, made concrete here.
49
53
  */
50
54
  setPromptTimeout(ms: number): () => void;
51
- /** Consume an inbound message as a reply; true when it was one. */
52
- handleInbound(peer: string, text: string): boolean;
55
+ /** Consume an inbound message as a reply to this peer's prompt. */
56
+ handleInbound(text: string): boolean;
53
57
  /** Whether a prompt is currently waiting for a reply. */
54
58
  hasPending(): boolean;
55
59
  /** The waiting prompt, for the control screen; undefined when none waits. */
56
60
  pendingInfo(): PendingPromptInfo | undefined;
57
61
  /** Withdraw the waiting prompt: tell the peer and resolve it cancelled. */
58
- cancel(): void;
62
+ cancel(): Promise<void>;
63
+ /** The language prompts are rendered in. */
64
+ language(): PeerLanguage;
59
65
  /** Drop any pending prompt (turn settled or the bridge stopped). */
60
66
  reset(): void;
61
67
  }
@@ -71,7 +77,6 @@ export interface PendingPromptInfo {
71
77
 
72
78
  interface Pending {
73
79
  request: InteractionRequest;
74
- peer: string;
75
80
  startedAt: number;
76
81
  resolve: (answer: InteractionAnswer) => void;
77
82
  signal?: AbortSignal;
@@ -90,11 +95,11 @@ export function createWechatInteractionChannel(deps: WechatInteractionDeps): Wec
90
95
  */
91
96
  let scopedTimeoutMs: number | undefined;
92
97
  /**
93
- * Free text captured from a `select` "Other" reply, per peer: the immediately
94
- * following `input` for that peer returns it. Keyed so one peer's free text can
95
- * never become another peer's answer.
98
+ * Free text captured from a `select` "Other" reply: the immediately following
99
+ * `input` for this peer returns it. One value, because one channel asks one peer
100
+ * — another peer's free text cannot reach this channel at all.
96
101
  */
97
- const other = new Map<string, string>();
102
+ let other: string | undefined;
98
103
 
99
104
  function settle(answer: InteractionAnswer): void {
100
105
  const current = pending;
@@ -108,7 +113,11 @@ export function createWechatInteractionChannel(deps: WechatInteractionDeps): Wec
108
113
 
109
114
  return {
110
115
  isActive(): boolean {
111
- return deps.activePeer() !== undefined;
116
+ return deps.active();
117
+ },
118
+
119
+ language(): PeerLanguage {
120
+ return deps.language();
112
121
  },
113
122
 
114
123
  hasPending(): boolean {
@@ -120,28 +129,33 @@ export function createWechatInteractionChannel(deps: WechatInteractionDeps): Wec
120
129
  return current === undefined
121
130
  ? undefined
122
131
  : {
123
- peer: current.peer,
132
+ peer: deps.peer,
124
133
  kind: current.request.kind,
125
134
  title: current.request.title,
126
135
  startedAt: current.startedAt,
127
136
  };
128
137
  },
129
138
 
130
- cancel(): void {
131
- if (!pending) return;
139
+ async cancel(): Promise<void> {
140
+ const current = pending;
141
+ if (!current) return;
132
142
  // The peer saw the prompt; one line says it was withdrawn, which is not the
133
143
  // same as expiry — they were not slow, the question went away. `reset` is the
134
- // silent cleanup path and deliberately does not send this.
135
- void deps.send(notice("withdrawn", deps.language()));
136
- settle({ kind: "cancelled" });
144
+ // silent cleanup path and deliberately does not send this. Settle after the
145
+ // notice is on the wire, so a caller awaiting the answer knows it was sent.
146
+ try {
147
+ await deps.send(notice("withdrawn", deps.language()), undefined, { force: true });
148
+ } finally {
149
+ if (pending === current) settle({ kind: "cancelled" });
150
+ }
137
151
  },
138
152
 
139
153
  async post(text: string, signal?: AbortSignal): Promise<void> {
140
- await deps.send(text, signal);
154
+ await deps.send(text, signal, { force: true });
141
155
  },
142
156
 
143
157
  async postFile(path: string, name: string, signal?: AbortSignal): Promise<boolean> {
144
- return deps.sendFile(path, name, signal);
158
+ return deps.sendFile(path, name, signal, { force: true });
145
159
  },
146
160
 
147
161
  setPromptTimeout(ms: number): () => void {
@@ -153,54 +167,55 @@ export function createWechatInteractionChannel(deps: WechatInteractionDeps): Wec
153
167
  },
154
168
 
155
169
  async request(request: InteractionRequest, signal?: AbortSignal): Promise<InteractionAnswer> {
156
- // A `select` that resolved to "Other" stashes its text for that peer; the
157
- // immediately following `input` from the same peer returns it without
158
- // another round-trip.
159
- const asking = deps.activePeer();
160
- const stashed = asking === undefined ? undefined : other.get(asking);
161
- if (request.kind === "input" && asking !== undefined && stashed !== undefined) {
162
- other.delete(asking);
170
+ // A `select` that resolved to "Other" stashes its text; the immediately
171
+ // following `input` returns it without another round-trip.
172
+ const stashed = other;
173
+ if (request.kind === "input" && stashed !== undefined) {
174
+ other = undefined;
163
175
  return { kind: "value", value: stashed };
164
176
  }
165
- if (request.kind !== "input" && asking !== undefined) other.delete(asking);
177
+ if (request.kind !== "input") other = undefined;
166
178
 
167
179
  if (signal?.aborted || !this.isActive()) return { kind: "cancelled" };
168
180
  if (pending) return { kind: "cancelled" };
169
181
 
170
182
  // Register the pending prompt before the send await so a fast reply is
171
183
  // never lost to the round-trip, then send and fail closed.
172
- const peer = deps.activePeer() as string;
173
184
  const cancelTimers: Array<() => void> = [];
174
185
  const answer = new Promise<InteractionAnswer>((resolve) => {
175
186
  const onAbort = () => settle({ kind: "cancelled" });
176
- pending = { request, peer, startedAt: now(), resolve, signal, onAbort, cancelTimers };
187
+ pending = { request, startedAt: now(), resolve, signal, onAbort, cancelTimers };
177
188
  signal?.addEventListener("abort", onAbort, { once: true });
178
189
  // An ignored question must not freeze the session: nudge once, then
179
190
  // cancel so the model gets a clean answer instead of a hang.
180
191
  const bound = scopedTimeoutMs ?? PROMPT_TIMEOUT_MS;
181
- cancelTimers.push(arm(bound / 2, () => void deps.send(notice("promptWaiting", deps.language()))));
192
+ cancelTimers.push(
193
+ arm(bound / 2, () => void deps.send(notice("promptWaiting", deps.language()), undefined, { force: true })),
194
+ );
182
195
  cancelTimers.push(
183
196
  arm(bound, () => {
184
- void deps.send(notice("timeout", deps.language()));
197
+ void deps.send(notice("timeout", deps.language()), undefined, { force: true });
185
198
  settle({ kind: "cancelled" });
186
199
  }),
187
200
  );
188
201
  });
189
- const sent = await deps.send(formatPrompt(request), signal);
202
+ const sent = await deps.send(formatPrompt(request, { language: deps.language() }), signal);
190
203
  if (!sent) settle({ kind: "cancelled" });
191
204
  return await answer;
192
205
  },
193
206
 
194
- handleInbound(peer: string, text: string): boolean {
207
+ handleInbound(text: string): boolean {
195
208
  const current = pending;
196
- if (!current || current.peer !== peer) return false;
209
+ if (!current) return false;
197
210
  const parsed = parsePromptAnswer(current.request, text);
198
211
  switch (parsed.kind) {
199
212
  case "retry":
200
- void deps.send(formatPrompt(current.request, { retry: true }));
213
+ void deps.send(formatPrompt(current.request, { retry: true, language: deps.language() }), undefined, {
214
+ force: true,
215
+ });
201
216
  return true;
202
217
  case "other":
203
- other.set(peer, parsed.text);
218
+ other = parsed.text;
204
219
  settle({ kind: "value", value: INTERACTION_OTHER_LABEL });
205
220
  return true;
206
221
  case "value":
@@ -216,7 +231,7 @@ export function createWechatInteractionChannel(deps: WechatInteractionDeps): Wec
216
231
  },
217
232
 
218
233
  reset(): void {
219
- other.clear();
234
+ other = undefined;
220
235
  scopedTimeoutMs = undefined;
221
236
  settle({ kind: "cancelled" });
222
237
  },
@@ -2,15 +2,15 @@
2
2
  * Pure rendering and parsing of remote dialog prompts (no I/O).
3
3
  *
4
4
  * A prompt is rendered to a WeChat message by {@link formatPrompt}; the reply is
5
- * interpreted by {@link parsePromptAnswer}. Hints are bilingual (Chinese and
6
- * English) so either reader can answer, and {@link formatPrompt} can prepend a
7
- * short notice when a reply was not understood. The parser is deliberately total:
5
+ * interpreted by {@link parsePromptAnswer}. Hints are rendered in the peer's
6
+ * language, and {@link formatPrompt} can prepend a short notice when a reply was
7
+ * not understood. The parser is deliberately total:
8
8
  * anything it cannot resolve reports `retry`, and free text on a `select` becomes
9
9
  * the {@link INTERACTION_OTHER_LABEL} option so the caller can collect it once.
10
10
  */
11
11
 
12
- import { INTERACTION_OTHER_LABEL, type InteractionRequest } from "../../lib/interaction.ts";
13
- import { sanitizeInbound } from "./format.ts";
12
+ import { INTERACTION_OTHER_LABEL, type InteractionRequest } from "../../../lib/interaction.ts";
13
+ import { sanitizeInbound, type PeerLanguage } from "../text/format.ts";
14
14
 
15
15
  /** A parsed reply: a resolved answer, the "other" choice, or a retry. */
16
16
  export type PromptParse =
@@ -20,20 +20,30 @@ export type PromptParse =
20
20
  | { kind: "cancelled" }
21
21
  | { kind: "retry" };
22
22
 
23
- const SELECT_OTHER_HINT =
24
- "回复编号或选项文字;其它内容作为自定义回答;回复「取消」或 cancel 跳过。\n" +
25
- "Reply with a number or the option text; anything else becomes your own answer; reply cancel to dismiss.";
26
- const SELECT_CHOICE_HINT =
27
- "回复编号或选项文字;回复「取消」或 cancel 跳过。\n" +
28
- "Reply with a number or the option text; reply cancel to dismiss.";
29
- const CONFIRM_HINT = "回复「是」或「否」。\nReply yes or no.";
30
- const INPUT_HINT = "直接回复内容;回复「取消」或 cancel 跳过。\nReply with the text; reply cancel to skip.";
31
- const EDITOR_HINT =
32
- "把内容作为一条消息发回;回复「取消」或 cancel 取消。\nSend the text as one message; reply cancel to cancel.";
23
+ const SELECT_OTHER_HINT: Record<PeerLanguage, string> = {
24
+ zh: "回复编号或选项文字;其它内容作为自定义回答;回复「取消」跳过。",
25
+ en: "Reply with a number or the option text; anything else is your own answer; reply cancel to dismiss.",
26
+ };
27
+ const SELECT_CHOICE_HINT: Record<PeerLanguage, string> = {
28
+ zh: "回复编号或选项文字;回复「取消」跳过。",
29
+ en: "Reply with a number or the option text; reply cancel to dismiss.",
30
+ };
31
+ const CONFIRM_HINT: Record<PeerLanguage, string> = {
32
+ zh: "回复「是」或「否」。",
33
+ en: "Reply yes or no.",
34
+ };
35
+ const INPUT_HINT: Record<PeerLanguage, string> = {
36
+ zh: "直接回复内容;回复「取消」跳过。",
37
+ en: "Reply with the text; reply cancel to skip.",
38
+ };
39
+ const EDITOR_HINT: Record<PeerLanguage, string> = {
40
+ zh: "把内容作为一条消息发回;回复「取消」取消。",
41
+ en: "Send the text as one message; reply cancel to cancel.",
42
+ };
33
43
 
34
44
  /** Sent ahead of a re-prompt when a reply could not be interpreted. */
35
- export const RETRY_NOTICE =
36
- "没有识别到有效回复,请重新回答。\nSorry, I didn't recognize that reply — please answer again.";
45
+ export const RETRY_NOTICE = "Sorry, I didn't recognize that reply — please answer again.";
46
+ const RETRY_ZH = "没有识别到有效回复,请重新回答。";
37
47
 
38
48
  const CANCEL_WORDS = new Set(["取消", "退出", "cancel", "quit", "exit"]);
39
49
  const YES_WORDS = new Set(["是", "y", "yes", "1", "确认", "确定", "行", "ok", "好", "对"]);
@@ -44,24 +54,29 @@ function lines(parts: Array<string | undefined>): string {
44
54
  }
45
55
 
46
56
  /** Render a dialog prompt as a WeChat message body, optionally after a retry. */
47
- export function formatPrompt(request: InteractionRequest, options: { retry?: boolean } = {}): string {
48
- const body = promptBody(request);
49
- return options.retry ? `${RETRY_NOTICE}\n\n${body}` : body;
57
+ export function formatPrompt(
58
+ request: InteractionRequest,
59
+ options: { retry?: boolean; language?: PeerLanguage } = {},
60
+ ): string {
61
+ const language = options.language ?? "en";
62
+ const body = promptBody(request, language);
63
+ const retry = language === "zh" ? RETRY_ZH : RETRY_NOTICE;
64
+ return options.retry ? `${retry}\n\n${body}` : body;
50
65
  }
51
66
 
52
- function promptBody(request: InteractionRequest): string {
67
+ function promptBody(request: InteractionRequest, language: PeerLanguage): string {
53
68
  switch (request.kind) {
54
69
  case "select": {
55
70
  const options = (request.options ?? []).map((label, index) => `${index + 1}. ${label}`);
56
71
  const hasOther = (request.options ?? []).includes(INTERACTION_OTHER_LABEL);
57
- return lines([request.title, ...options, hasOther ? SELECT_OTHER_HINT : SELECT_CHOICE_HINT]);
72
+ return lines([request.title, ...options, hasOther ? SELECT_OTHER_HINT[language] : SELECT_CHOICE_HINT[language]]);
58
73
  }
59
74
  case "confirm":
60
- return lines([request.title, request.message, CONFIRM_HINT]);
75
+ return lines([request.title, request.message, CONFIRM_HINT[language]]);
61
76
  case "input":
62
- return lines([request.title, request.placeholder, INPUT_HINT]);
77
+ return lines([request.title, request.placeholder, INPUT_HINT[language]]);
63
78
  case "editor":
64
- return lines([request.title, request.prefill, EDITOR_HINT]);
79
+ return lines([request.title, request.prefill, EDITOR_HINT[language]]);
65
80
  }
66
81
  }
67
82
 
@@ -24,7 +24,7 @@ import {
24
24
  type InteractionAnswer,
25
25
  type InteractionChannel,
26
26
  type InteractionRequest,
27
- } from "../../lib/interaction.ts";
27
+ } from "../../../lib/interaction.ts";
28
28
 
29
29
  function valueOf(answer: InteractionAnswer): string | undefined {
30
30
  return answer.kind === "value" ? answer.value : undefined;
@@ -19,18 +19,18 @@ import { isPidAlive } from "../../lib/spawn.ts";
19
19
  import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
20
20
  import { paint } from "../../lib/tui/tokens.ts";
21
21
  import { createBridge, type Bridge, type BridgeState } from "./bridge.ts";
22
- import { createFetchRunner, WechatClient, type WechatClientOptions } from "./client.ts";
23
- import { registerCommands, type CommandDeps } from "./commands.ts";
22
+ import { createFetchRunner, WechatClient, type WechatClientOptions } from "./transport/client.ts";
23
+ import { registerCommands, type CommandDeps } from "./commands/commands.ts";
24
24
  import { messageOf } from "./errors.ts";
25
25
  import { loadWechatConfig } from "./config.ts";
26
- import { clearCredentials, primaryAccount, saveAccount, wechatDir } from "./credentials.ts";
27
- import { acquireLock, lockPath, readLock, refreshLock, releaseLock } from "./lock.ts";
28
- import { openPeerSession, type PeerSession, type PeerSessionOptions } from "./peer-session.ts";
29
- import { createPeerUIContext } from "./remote-ui.ts";
30
- import { peerKey } from "./format.ts";
31
- import { readPeerHistory } from "./history.ts";
32
- import { SLASH_COMMANDS } from "./slash.ts";
33
- import { readState, statePath, writeState } from "./state.ts";
26
+ import { clearCredentials, primaryAccount, saveAccount, wechatDir } from "./state/credentials.ts";
27
+ import { acquireLock, lockPath, readLock, refreshLock, releaseLock } from "./state/lock.ts";
28
+ import { openPeerSession, type PeerSession, type PeerSessionOptions } from "./peer/peer-session.ts";
29
+ import { createPeerUIContext } from "./dialog/remote-ui.ts";
30
+ import { peerKey } from "./text/format.ts";
31
+ import { readPeerHistory } from "./commands/history.ts";
32
+ import { SLASH_COMMANDS } from "./commands/slash.ts";
33
+ import { readState, statePath, writeState } from "./state/state.ts";
34
34
  import { registerTools } from "./tools.ts";
35
35
 
36
36
  /**
@@ -8,8 +8,8 @@
8
8
  * budget is in memory; a reload starts a fresh hour.
9
9
  */
10
10
 
11
- import type { InboundItem } from "./types.ts";
12
- import { peerKey } from "./format.ts";
11
+ import type { InboundItem } from "../types.ts";
12
+ import { peerKey } from "../text/format.ts";
13
13
 
14
14
  /** A rolling-hour turn budget per peer. */
15
15
  export interface TurnBudget {
@@ -32,8 +32,8 @@ import {
32
32
  SessionManager,
33
33
  } from "@earendil-works/pi-coding-agent";
34
34
  import { join } from "node:path";
35
- import { WECHAT_CHANNEL_PROMPT } from "./channel.ts";
36
- import { peerKey } from "./format.ts";
35
+ import { WECHAT_CHANNEL_PROMPT } from "../text/channel.ts";
36
+ import { peerKey } from "../text/format.ts";
37
37
 
38
38
  /** The SDK pieces a peer session needs; injected so tests never load resources. */
39
39
  export interface PeerSessionSeams {