@gavin-hu/my-pi-agent 0.18.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.
- package/README.md +52 -44
- package/README.zh-CN.md +46 -41
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +554 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +61 -50
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +226 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +258 -162
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +3 -20
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/{html.ts → view/html.ts} +14 -12
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +147 -38
- package/extensions/plan/index.ts +6 -4
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +8 -11
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +21 -9
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/tools/exit.ts +11 -95
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +0 -8
- package/extensions/plan/web-client.ts +1 -1
- package/extensions/plan/web-page.ts +0 -7
- package/extensions/plan/web-router.ts +3 -57
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +4 -2
- package/extensions/session-bus/runtime.ts +129 -238
- package/extensions/session-bus/store.ts +42 -6
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +4 -4
- package/extensions/session-bus/types.ts +13 -13
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +76 -92
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/guard.ts +3 -5
- package/lib/README.md +8 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/policy.ts +12 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +5 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -11,37 +11,35 @@
|
|
|
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
|
|
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
|
-
|
|
30
|
-
type PendingPromptInfo,
|
|
31
|
-
type WechatInteractionChannel,
|
|
32
|
-
} from "./interaction.ts";
|
|
25
|
+
import { parseInbound, peerKey, peerLabel, peerLanguage, sanitizeInbound, type PeerLanguage } from "./text/format.ts";
|
|
26
|
+
import { notice } from "./text/notices.ts";
|
|
27
|
+
import { createPresence } from "./peer/presence.ts";
|
|
28
|
+
import { createPeerChannels, type PeerChannels } from "./dialog/channels.ts";
|
|
29
|
+
import type { PendingPromptInfo, WechatInteractionChannel } from "./dialog/interaction.ts";
|
|
33
30
|
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";
|
|
31
|
+
import { createPeerWorker, type PeerState, type PeerWorker } from "./peer/peer-worker.ts";
|
|
32
|
+
import type { PeerSession } from "./peer/peer-session.ts";
|
|
33
|
+
import { emptyState, findPeer, ownerTarget, recordInbound, rememberMessage, seenMessage } from "./state/state.ts";
|
|
37
34
|
import {
|
|
38
35
|
expandSnippet,
|
|
39
36
|
findSlashCommand,
|
|
40
37
|
parseSlash,
|
|
38
|
+
slashControls,
|
|
41
39
|
SLASH_COMMANDS,
|
|
42
40
|
type SlashCommand,
|
|
43
41
|
type SlashContext,
|
|
44
|
-
} from "./slash.ts";
|
|
42
|
+
} from "./commands/slash.ts";
|
|
45
43
|
import type { BotCredentials, InboundItem, WechatState, WeixinMessage } from "./types.ts";
|
|
46
44
|
import type { WechatConfig } from "./config.ts";
|
|
47
45
|
|
|
@@ -120,10 +118,10 @@ export interface BridgeStatus {
|
|
|
120
118
|
/** Labels of the senders refused since the bridge opened, newest last, capped. */
|
|
121
119
|
refusedPeers: string[];
|
|
122
120
|
mediaDropped: number;
|
|
123
|
-
/** Whether
|
|
121
|
+
/** Whether any peer's dialogs prompt is waiting for an answer. */
|
|
124
122
|
promptPending: boolean;
|
|
125
|
-
/**
|
|
126
|
-
|
|
123
|
+
/** Every waiting prompt, one per peer, with its kind, title, and age. */
|
|
124
|
+
prompts: PendingPromptInfo[];
|
|
127
125
|
/** When this connection opened. */
|
|
128
126
|
startedAt?: number;
|
|
129
127
|
/** When the last poll settled, successful or not. */
|
|
@@ -160,10 +158,10 @@ export interface Bridge {
|
|
|
160
158
|
/** What one peer's conversation is doing, for `/status`. */
|
|
161
159
|
peerState(peer: string): PeerState;
|
|
162
160
|
status(): BridgeStatus;
|
|
163
|
-
/**
|
|
164
|
-
channel(): WechatInteractionChannel;
|
|
165
|
-
/** Message the owner proactively
|
|
166
|
-
sendToOwner(text: string): Promise<SendResult>;
|
|
161
|
+
/** One peer's interaction channel: the channel its session routes its dialogs through. */
|
|
162
|
+
channel(peer: string): WechatInteractionChannel;
|
|
163
|
+
/** Message the owner proactively: one text, or one image, never a chosen recipient. */
|
|
164
|
+
sendToOwner(text: string | undefined, image?: string): Promise<SendResult>;
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
/** Create the bridge over an injected environment. */
|
|
@@ -225,8 +223,8 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
225
223
|
}
|
|
226
224
|
|
|
227
225
|
/** 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);
|
|
226
|
+
async function reply(target: MessageTarget, text: string, signal?: AbortSignal): Promise<boolean> {
|
|
227
|
+
return deliverer.reply(target, text, signal ?? abort?.signal);
|
|
230
228
|
}
|
|
231
229
|
|
|
232
230
|
/** The outbound surface, reading the live client and account at call time. */
|
|
@@ -257,18 +255,15 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
257
255
|
},
|
|
258
256
|
activeTurn: worker?.state(target.peer).status === "running",
|
|
259
257
|
language: languageOf(target.peer),
|
|
260
|
-
session: {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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;
|
|
258
|
+
session: slashControls(target.peer, {
|
|
259
|
+
state: (peer) => worker?.state(peer) ?? { status: "idle", queued: 0 },
|
|
260
|
+
prompt: (peer) => channels.pendingFor(peer)?.title,
|
|
261
|
+
reset: (peer) => {
|
|
262
|
+
worker?.reset(peer);
|
|
269
263
|
},
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
stop: (peer) => worker?.abort(peer) ?? false,
|
|
265
|
+
history: (peer, count) => deps.readHistory(peer, count),
|
|
266
|
+
}),
|
|
272
267
|
};
|
|
273
268
|
try {
|
|
274
269
|
await command.run(ctx);
|
|
@@ -279,31 +274,27 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
279
274
|
}
|
|
280
275
|
|
|
281
276
|
/**
|
|
282
|
-
*
|
|
277
|
+
* Every peer's dialogs, one channel each.
|
|
283
278
|
*
|
|
284
|
-
* A
|
|
285
|
-
*
|
|
286
|
-
*
|
|
279
|
+
* A prompt is sent through the peer's channel to the turn it belongs to, so a
|
|
280
|
+
* reply can only answer its own peer's question even when two turns run at once.
|
|
281
|
+
* A failure is reported once per peer by the deliverer; a silent failure would
|
|
282
|
+
* leave the peer with only the choices, as if nothing preceded them.
|
|
287
283
|
*/
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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 ?? ""),
|
|
284
|
+
const channels: PeerChannels = createPeerChannels({
|
|
285
|
+
flush: async (peer) => {
|
|
286
|
+
await worker?.flush(peer);
|
|
287
|
+
},
|
|
288
|
+
send: (peer, text, signal) => {
|
|
289
|
+
const item = worker?.running(peer);
|
|
290
|
+
return item ? deliverer.prompt(item, text, signal ?? abort?.signal) : Promise.resolve(false);
|
|
291
|
+
},
|
|
292
|
+
sendFile: (peer, filePath, fileName, signal) => {
|
|
293
|
+
const item = worker?.running(peer);
|
|
294
|
+
return item ? deliverer.file(item, filePath, fileName, signal ?? abort?.signal) : Promise.resolve(false);
|
|
295
|
+
},
|
|
296
|
+
active: (peer) => worker?.state(peer).status === "running",
|
|
297
|
+
language: (peer) => languageOf(peer),
|
|
307
298
|
armTimer: deps.armTimer,
|
|
308
299
|
});
|
|
309
300
|
|
|
@@ -316,11 +307,6 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
316
307
|
return languages.get(peer) ?? "en";
|
|
317
308
|
}
|
|
318
309
|
|
|
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
310
|
/** Typing, acknowledgement, and the slow/busy lines; one timer registry each. */
|
|
325
311
|
const presence = createPresence({
|
|
326
312
|
typingIndicator: () => deps.config.typingIndicator,
|
|
@@ -329,10 +315,10 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
329
315
|
client: () => client,
|
|
330
316
|
account: () => account,
|
|
331
317
|
signal: () => abort?.signal,
|
|
332
|
-
statusOf:
|
|
318
|
+
statusOf: (peer) => worker?.state(peer).status ?? "idle",
|
|
333
319
|
languageOf,
|
|
334
320
|
send: (peer, contextToken, text) => reply({ peer, contextToken }, text),
|
|
335
|
-
promptPending: () =>
|
|
321
|
+
promptPending: (peer) => channels.hasPending(peer),
|
|
336
322
|
});
|
|
337
323
|
|
|
338
324
|
/** Inbound media resolution and the hand-off to a peer session. */
|
|
@@ -410,7 +396,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
410
396
|
}
|
|
411
397
|
// A reply to a pending prompt is consumed here, before queueing, so it never
|
|
412
398
|
// starts a turn of its own. A duplicate answer was stopped by the id check.
|
|
413
|
-
if (
|
|
399
|
+
if (channels.handleInbound(peer, text)) return;
|
|
414
400
|
const language = languageOf(peer, text);
|
|
415
401
|
const item: InboundItem = { ...target, text, media, language };
|
|
416
402
|
if (!text && media.length === 0) {
|
|
@@ -477,7 +463,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
477
463
|
overflow.clear();
|
|
478
464
|
refused = 0;
|
|
479
465
|
refusedPeers.clear();
|
|
480
|
-
|
|
466
|
+
channels.reset();
|
|
481
467
|
deliverer.reset();
|
|
482
468
|
presence.reset();
|
|
483
469
|
budget.reset();
|
|
@@ -552,10 +538,10 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
552
538
|
// a login (or a reconnect after an expiry) starts from a clean slate while
|
|
553
539
|
// each peer's history stays on disk.
|
|
554
540
|
worker = createPeerWorker({
|
|
555
|
-
openSession: (peer, options) => deps.openSession(peer, { fresh: options.fresh, channel:
|
|
541
|
+
openSession: (peer, options) => deps.openSession(peer, { fresh: options.fresh, channel: channels.for(peer) }),
|
|
556
542
|
// Each part is delivered as its own bubble; the worker puts the
|
|
557
543
|
// attachment line on the first one. A false result stops the stream.
|
|
558
|
-
send: (item, text) => reply(item, text),
|
|
544
|
+
send: (item, text, options) => reply(item, text, options?.signal),
|
|
559
545
|
fallback: (item, reason) => notice(reason, item.language),
|
|
560
546
|
onStart: (item) => {
|
|
561
547
|
presence.cancelAck(item.peer);
|
|
@@ -602,7 +588,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
602
588
|
// worker, so close it unconditionally rather than leaking the sessions.
|
|
603
589
|
const closing = worker;
|
|
604
590
|
worker = undefined;
|
|
605
|
-
await closing?.close();
|
|
591
|
+
await closing?.close({ abandon: true });
|
|
606
592
|
resetEphemeral();
|
|
607
593
|
publishState();
|
|
608
594
|
releaseSafe();
|
|
@@ -618,7 +604,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
618
604
|
// not be cancelled by the poll abort that follows.
|
|
619
605
|
const stopping = abort;
|
|
620
606
|
abort = undefined;
|
|
621
|
-
|
|
607
|
+
await channels.cancelAll();
|
|
622
608
|
stopping?.abort();
|
|
623
609
|
try {
|
|
624
610
|
await loopPromise;
|
|
@@ -682,8 +668,8 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
682
668
|
refused,
|
|
683
669
|
refusedPeers: [...refusedPeers],
|
|
684
670
|
mediaDropped: inbound.dropped(),
|
|
685
|
-
promptPending:
|
|
686
|
-
|
|
671
|
+
promptPending: channels.hasPending(),
|
|
672
|
+
prompts: channels.prompts(),
|
|
687
673
|
startedAt,
|
|
688
674
|
lastPollAt,
|
|
689
675
|
lastPollError,
|
|
@@ -694,27 +680,25 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
694
680
|
};
|
|
695
681
|
},
|
|
696
682
|
|
|
697
|
-
channel(): WechatInteractionChannel {
|
|
698
|
-
return
|
|
683
|
+
channel(peer: string): WechatInteractionChannel {
|
|
684
|
+
return channels.for(peer);
|
|
699
685
|
},
|
|
700
686
|
|
|
701
|
-
async sendToOwner(text: string): Promise<SendResult> {
|
|
687
|
+
async sendToOwner(text: string | undefined, image?: string): Promise<SendResult> {
|
|
702
688
|
if (!running || !client || !account) return { ok: false, error: "WeChat bridge is not started." };
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
689
|
+
const target = ownerTarget(state, account);
|
|
690
|
+
if (text !== undefined && worker?.state(target.peer).status === "running") {
|
|
691
|
+
// The bridge answers this turn itself, so a text send would land twice.
|
|
692
|
+
return { ok: false, error: "The bridge replies to this turn automatically; do not repeat it." };
|
|
693
|
+
}
|
|
694
|
+
if (image !== undefined) {
|
|
695
|
+
// An image cannot duplicate the streamed answer, so a running turn is fine.
|
|
696
|
+
const sent = await deliverer.image(target, image, abort?.signal);
|
|
697
|
+
return sent ? { ok: true } : { ok: false, error: "The image could not be sent; see the terminal notice." };
|
|
711
698
|
}
|
|
712
|
-
const trimmed = sanitizeInbound(text);
|
|
699
|
+
const trimmed = sanitizeInbound(text ?? "");
|
|
713
700
|
if (!trimmed) return { ok: false, error: "Nothing to send." };
|
|
714
|
-
|
|
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);
|
|
701
|
+
return deliverer.toOwner(target.peer, target.contextToken, trimmed, abort?.signal);
|
|
718
702
|
},
|
|
719
703
|
};
|
|
720
704
|
}
|
|
@@ -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 "
|
|
22
|
-
import { sanitizeLine } from "
|
|
23
|
-
import { supportsCustomUI } from "
|
|
24
|
-
import { isAllowedPeer, type Bridge } from "
|
|
25
|
-
import type { WechatClient } from "
|
|
26
|
-
import type { WechatConfig } from "
|
|
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 "
|
|
29
|
-
import { peerLabel, peerKey } from "
|
|
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 "
|
|
32
|
-
import { LoginScreenComponent, type LoginViewState } from "
|
|
33
|
-
import { qrLines } from "
|
|
34
|
-
import {
|
|
35
|
-
|
|
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().
|
|
157
|
-
|
|
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 "
|
|
11
|
-
import type { BridgeStatus } from "
|
|
12
|
-
import type { WechatConfig } from "
|
|
13
|
-
import type { BotCredentials } from "
|
|
14
|
-
import { peerLabel } from "
|
|
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 "
|
|
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 "
|
|
10
|
-
import { messageOf } from "
|
|
11
|
-
import type { BotCredentials, QrResponse, QrStatus } from "
|
|
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";
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* Value-only: no module-level state.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { type PeerLanguage } from "
|
|
14
|
-
import { notice } from "
|
|
13
|
+
import { type PeerLanguage } from "../text/format.ts";
|
|
14
|
+
import { notice } from "../text/notices.ts";
|
|
15
15
|
|
|
16
16
|
/** The two command kinds. */
|
|
17
17
|
export type SlashKind = "snippet" | "action";
|
|
@@ -37,6 +37,42 @@ export interface SlashSessionControls {
|
|
|
37
37
|
history?: (count: number) => Promise<string>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** What a builder needs to bind the session controls to one sender. */
|
|
41
|
+
export interface SlashControlsDeps {
|
|
42
|
+
/** What that peer's conversation is doing. */
|
|
43
|
+
state: (peer: string) => SlashPeerState;
|
|
44
|
+
/** The title of a dialog waiting on that peer, if one waits. */
|
|
45
|
+
prompt: (peer: string) => string | undefined;
|
|
46
|
+
/** Drop the peer's conversation, so the next message starts empty. */
|
|
47
|
+
reset: (peer: string) => void;
|
|
48
|
+
/** Stop the peer's running turn; false when nothing was running. */
|
|
49
|
+
stop: (peer: string) => boolean;
|
|
50
|
+
/** The last `count` messages of the peer's own conversation. */
|
|
51
|
+
history: (peer: string, count: number) => Promise<string>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The controls one action runs against, for the peer that typed the command.
|
|
56
|
+
*
|
|
57
|
+
* The registry owns what a command may do; the bridge owns the queue and the
|
|
58
|
+
* channels, so it supplies them here and keeps no per-peer session shape of its
|
|
59
|
+
* own.
|
|
60
|
+
*/
|
|
61
|
+
export function slashControls(peer: string, deps: SlashControlsDeps): SlashSessionControls {
|
|
62
|
+
return {
|
|
63
|
+
newSession: () => {
|
|
64
|
+
deps.reset(peer);
|
|
65
|
+
},
|
|
66
|
+
stop: () => deps.stop(peer),
|
|
67
|
+
status: () => {
|
|
68
|
+
const waiting = deps.prompt(peer);
|
|
69
|
+
const state = deps.state(peer);
|
|
70
|
+
return waiting === undefined ? state : { ...state, prompt: waiting };
|
|
71
|
+
},
|
|
72
|
+
history: (count) => deps.history(peer, count),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
/** The environment an action handler runs in. */
|
|
41
77
|
export interface SlashContext {
|
|
42
78
|
/** Text after the command name, trimmed. */
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One interaction channel per peer.
|
|
3
|
+
*
|
|
4
|
+
* Two peers' turns can run at once ({@link PEER_CONCURRENCY}), and a forwarded
|
|
5
|
+
* dialog is only unambiguous if the channel that asks knows which peer it is
|
|
6
|
+
* asking — so each peer owns a channel, and a reply can only answer its own
|
|
7
|
+
* peer's prompt. This registry builds them lazily, routes an inbound message to
|
|
8
|
+
* that peer's channel, and aggregates what waits so the control screen and
|
|
9
|
+
* `/wechat status` can name each waiting peer.
|
|
10
|
+
*
|
|
11
|
+
* It also owns the dialogs' outbound path — the flush of buffered text, the
|
|
12
|
+
* prompt gate, and the deliverer call — because that path is per peer and belongs
|
|
13
|
+
* with the channel that uses it rather than with the bridge that wires it.
|
|
14
|
+
*
|
|
15
|
+
* Value-only: the map lives in the closure.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PendingPromptInfo, WechatInteractionChannel } from "./interaction.ts";
|
|
19
|
+
import { createWechatInteractionChannel } from "./interaction.ts";
|
|
20
|
+
import type { PeerLanguage } from "../text/format.ts";
|
|
21
|
+
import type { ArmTimer } from "../timer.ts";
|
|
22
|
+
|
|
23
|
+
/** What the registry needs from the bridge; the turn's item carries the token. */
|
|
24
|
+
export interface PeerChannelDeps {
|
|
25
|
+
/** Put buffered assistant text on the wire before a prompt or a file. */
|
|
26
|
+
flush: (peer: string) => Promise<void>;
|
|
27
|
+
/** Send one dialog message to a peer as that peer's next bubble. */
|
|
28
|
+
send: (peer: string, text: string, signal?: AbortSignal) => Promise<boolean>;
|
|
29
|
+
/** Send one local file to a peer. */
|
|
30
|
+
sendFile: (peer: string, path: string, name: string, signal?: AbortSignal) => Promise<boolean>;
|
|
31
|
+
/** Whether the peer owns a running turn, so a prompt has somewhere to land. */
|
|
32
|
+
active: (peer: string) => boolean;
|
|
33
|
+
/** The language the peer writes in, for the prompt and the timeout notices. */
|
|
34
|
+
language: (peer: string) => PeerLanguage;
|
|
35
|
+
/** Arm a cancellable one-shot timer. Defaults to a real, unref'd one. */
|
|
36
|
+
armTimer?: ArmTimer;
|
|
37
|
+
/** Clock for a prompt's start time; defaults to `Date.now`. */
|
|
38
|
+
now?: () => number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PeerChannels {
|
|
42
|
+
/** This peer's channel, created on first use. */
|
|
43
|
+
for(peer: string): WechatInteractionChannel;
|
|
44
|
+
/** Route an inbound message to the peer's pending prompt; true when it answered one. */
|
|
45
|
+
handleInbound(peer: string, text: string): boolean;
|
|
46
|
+
/** The prompt one peer is waiting on, if any. */
|
|
47
|
+
pendingFor(peer: string): PendingPromptInfo | undefined;
|
|
48
|
+
/** Every waiting prompt, oldest channel first. */
|
|
49
|
+
prompts(): PendingPromptInfo[];
|
|
50
|
+
/** Whether that peer — or with no argument, any peer — has a prompt waiting. */
|
|
51
|
+
hasPending(peer?: string): boolean;
|
|
52
|
+
/** Withdraw every waiting prompt, telling each peer. */
|
|
53
|
+
cancelAll(): Promise<void>;
|
|
54
|
+
/** Drop every channel and its waiting prompt, silently. */
|
|
55
|
+
reset(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createPeerChannels(deps: PeerChannelDeps): PeerChannels {
|
|
59
|
+
const channels = new Map<string, WechatInteractionChannel>();
|
|
60
|
+
|
|
61
|
+
function build(peer: string): WechatInteractionChannel {
|
|
62
|
+
let channel: WechatInteractionChannel | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Dialog text is only sent while its prompt is registered; the timeout,
|
|
65
|
+
* waiting, and withdrawal lines force their way through. Reading the channel
|
|
66
|
+
* this closure builds keeps one owner for "is a prompt waiting".
|
|
67
|
+
*/
|
|
68
|
+
const allowed = (options?: { force?: boolean }): boolean =>
|
|
69
|
+
options?.force === true || (channel?.hasPending() ?? false);
|
|
70
|
+
|
|
71
|
+
channel = createWechatInteractionChannel({
|
|
72
|
+
peer,
|
|
73
|
+
send: async (text, signal, options) => {
|
|
74
|
+
if (!allowed(options)) return false;
|
|
75
|
+
await deps.flush(peer);
|
|
76
|
+
return deps.send(peer, text, signal);
|
|
77
|
+
},
|
|
78
|
+
sendFile: async (path, name, signal, options) => {
|
|
79
|
+
if (!allowed(options)) return false;
|
|
80
|
+
await deps.flush(peer);
|
|
81
|
+
return deps.sendFile(peer, path, name, signal);
|
|
82
|
+
},
|
|
83
|
+
active: () => deps.active(peer),
|
|
84
|
+
language: () => deps.language(peer),
|
|
85
|
+
armTimer: deps.armTimer,
|
|
86
|
+
now: deps.now,
|
|
87
|
+
});
|
|
88
|
+
return channel;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
for(peer: string): WechatInteractionChannel {
|
|
93
|
+
const existing = channels.get(peer);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
const created = build(peer);
|
|
96
|
+
channels.set(peer, created);
|
|
97
|
+
return created;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
handleInbound(peer: string, text: string): boolean {
|
|
101
|
+
// Only a channel that exists can be waiting, so an inbound message never
|
|
102
|
+
// creates one for a peer that has not been asked anything.
|
|
103
|
+
return channels.get(peer)?.handleInbound(text) ?? false;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
pendingFor(peer: string): PendingPromptInfo | undefined {
|
|
107
|
+
return channels.get(peer)?.pendingInfo();
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
prompts(): PendingPromptInfo[] {
|
|
111
|
+
const waiting: PendingPromptInfo[] = [];
|
|
112
|
+
for (const channel of channels.values()) {
|
|
113
|
+
const info = channel.pendingInfo();
|
|
114
|
+
if (info) waiting.push(info);
|
|
115
|
+
}
|
|
116
|
+
return waiting;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
hasPending(peer?: string): boolean {
|
|
120
|
+
if (peer !== undefined) return channels.get(peer)?.hasPending() ?? false;
|
|
121
|
+
for (const channel of channels.values()) if (channel.hasPending()) return true;
|
|
122
|
+
return false;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async cancelAll(): Promise<void> {
|
|
126
|
+
for (const channel of channels.values()) {
|
|
127
|
+
if (channel.hasPending()) await channel.cancel();
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
reset(): void {
|
|
132
|
+
for (const channel of channels.values()) channel.reset();
|
|
133
|
+
channels.clear();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|