@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Registers the `watch` tool and `/watch` command. Watchers read a file or a
|
|
5
5
|
* command's merged output, match each new line against a regular expression,
|
|
6
6
|
* and alert the user; matching lines are reported to the model — and shown as a
|
|
7
|
-
* bytefly notice — at the next turn, or wake a turn
|
|
8
|
-
* watcher opted into `wake
|
|
7
|
+
* bytefly notice — at the next turn, or wake a turn as soon as the agent is idle
|
|
8
|
+
* when the watcher opted into `wake`, including right after a running turn
|
|
9
|
+
* settles. Watchers are session-only and stopped on shutdown.
|
|
9
10
|
*
|
|
10
11
|
* Load with: pi --extension ./extensions/watch
|
|
11
12
|
*/
|
|
@@ -14,8 +15,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
14
15
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
16
|
import { WATCH_MATCH_CHANNEL, type WatchMatchEvent } from "../../lib/events.ts";
|
|
16
17
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
17
|
-
import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
18
|
-
import { createPendingReporter } from "../../lib/report.ts";
|
|
18
|
+
import { burstText, noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
19
|
+
import { createPendingReporter, registerPendingChannel } from "../../lib/report.ts";
|
|
19
20
|
import { registerCommands } from "./commands.ts";
|
|
20
21
|
import { clipLabel } from "../../lib/tui/columns.ts";
|
|
21
22
|
import { formatPendingMatch } from "./format.ts";
|
|
@@ -32,10 +33,14 @@ const MAX_CONTEXT_MATCHES = 20;
|
|
|
32
33
|
/** Build the model-facing text for the pending-match notice. */
|
|
33
34
|
export function buildWatchContext(matches: PendingMatch[]): string {
|
|
34
35
|
const shown = matches.slice(0, MAX_CONTEXT_MATCHES);
|
|
35
|
-
const lines = shown.map(formatPendingMatch);
|
|
36
36
|
const more = matches.length - shown.length;
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
return burstText({
|
|
38
|
+
marker: WATCH_CONTEXT_MARKER,
|
|
39
|
+
headline: `${matches.length} watched line${matches.length === 1 ? "" : "s"} matched`,
|
|
40
|
+
lines: shown.map((match) => formatPendingMatch(match)),
|
|
41
|
+
...(more > 0 ? { more: `… ${more} more match${more === 1 ? "" : "es"}` } : {}),
|
|
42
|
+
hint: "Read more with `watch matches <id>`.",
|
|
43
|
+
});
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/** The shared notice for pending matches: model text plus renderer details. */
|
|
@@ -64,15 +69,16 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
64
69
|
if (!isExtensionEnabled("watch")) return;
|
|
65
70
|
const runtime = deps.runtime ?? createWatchRuntime();
|
|
66
71
|
|
|
67
|
-
// Opt-in wake: a `wake` watcher matched
|
|
68
|
-
//
|
|
69
|
-
//
|
|
72
|
+
// Opt-in wake: a `wake` watcher matched. The reporter reports *all* pending
|
|
73
|
+
// matches, not just the waking one, so a non-wake match is not silently
|
|
74
|
+
// dropped, and it retries a burst that arrived mid-turn once the run settles.
|
|
70
75
|
const reporter = createPendingReporter<PendingMatch>({
|
|
71
76
|
wake: (notice) => pi.sendMessage(notice, { triggerTurn: true }),
|
|
72
77
|
pending: () => runtime.pendingMatches(),
|
|
73
78
|
markReported: (matches) => runtime.markReported(matches),
|
|
74
79
|
buildNotice: watchNotice,
|
|
75
80
|
source: "watch",
|
|
81
|
+
wakeRequested: () => runtime.pendingMatches().some((match) => runtime.get(match.watcherId)?.wake === true),
|
|
76
82
|
// A wake means the user is away from the terminal, so the alert also leaves
|
|
77
83
|
// the process. bytefly's notify capability owns delivery; this publishes the
|
|
78
84
|
// fact and never learns how it is sent. The newest match is the one an alert
|
|
@@ -97,41 +103,16 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
97
103
|
registerTools(pi, runtime);
|
|
98
104
|
registerCommands(pi, runtime);
|
|
99
105
|
|
|
100
|
-
pi
|
|
101
|
-
reporter
|
|
102
|
-
runtime
|
|
106
|
+
registerPendingChannel(pi, {
|
|
107
|
+
reporter,
|
|
108
|
+
runtime,
|
|
103
109
|
// `shutdown` clears this; a reload or session replacement reuses the
|
|
104
110
|
// runtime, so re-arm it on every session start.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
pi.on("agent_start", () => {
|
|
114
|
-
reporter.agentStart();
|
|
115
|
-
});
|
|
116
|
-
pi.on("agent_settled", () => {
|
|
117
|
-
reporter.agentSettled();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Report matches to the model at the start of the next turn.
|
|
121
|
-
pi.on("before_agent_start", (_event, ctx) => {
|
|
122
|
-
const reported = reporter.beforeAgentStart();
|
|
123
|
-
if (!reported) return undefined;
|
|
124
|
-
runtime.setStatus(ctx);
|
|
125
|
-
return reported;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Keep stale match notices out of later turns. Only this extension's notices
|
|
129
|
-
// are pruned; another source's are left for its own handler.
|
|
130
|
-
pi.on("context", (event) => reporter.filterContext(event.messages));
|
|
131
|
-
|
|
132
|
-
// `shutdown` detaches the UI controller, which clears both chips.
|
|
133
|
-
pi.on("session_shutdown", async () => {
|
|
134
|
-
runtime.onMatch = undefined;
|
|
135
|
-
await runtime.shutdown();
|
|
111
|
+
onSessionStart: () => {
|
|
112
|
+
runtime.onMatch = () => reporter.report();
|
|
113
|
+
},
|
|
114
|
+
onSessionShutdown: () => {
|
|
115
|
+
runtime.onMatch = undefined;
|
|
116
|
+
},
|
|
136
117
|
});
|
|
137
118
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
|
-
import { errorResult } from "../../lib/tool-result.ts";
|
|
12
|
+
import { errorResult, textResult } from "../../lib/tool-result.ts";
|
|
13
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
14
14
|
import { callLine, detailsOrLine, rejectionLine } from "../../lib/tui/tool.ts";
|
|
15
15
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
@@ -82,62 +82,45 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
82
82
|
ctx as ExtensionContext,
|
|
83
83
|
);
|
|
84
84
|
const flags = `${call.caseSensitive ? "" : "i"}${watch.wake ? ", wake" : ""}`;
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
text: `Watching ${watch.id}: ${sourceLabel(watch)} → /${sanitizeLine(watch.pattern)}/${flags}\nRead with \`watch matches ${watch.id}\` or \`watch logs ${watch.id}\`.`,
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
details: { action: "add", watch } satisfies WatchDetails,
|
|
93
|
-
};
|
|
85
|
+
return textResult(
|
|
86
|
+
`Watching ${watch.id}: ${sourceLabel(watch)} → /${sanitizeLine(watch.pattern)}/${flags}\nRead with \`watch matches ${watch.id}\` or \`watch logs ${watch.id}\`.`,
|
|
87
|
+
{ action: "add", watch } satisfies WatchDetails,
|
|
88
|
+
);
|
|
94
89
|
}
|
|
95
90
|
|
|
96
91
|
case "list": {
|
|
97
92
|
const watches = runtime.list();
|
|
98
|
-
return {
|
|
99
|
-
content: [{ type: "text" as const, text: formatWatchList(watches) }],
|
|
100
|
-
details: { action: "list", watches } satisfies WatchDetails,
|
|
101
|
-
};
|
|
93
|
+
return textResult(formatWatchList(watches), { action: "list", watches } satisfies WatchDetails);
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
case "status": {
|
|
105
97
|
const watch = runtime.get(call.id);
|
|
106
98
|
if (!watch) return errorResult("status", `no watcher "${call.id}".`);
|
|
107
|
-
return {
|
|
108
|
-
content: [{ type: "text" as const, text: formatWatchStatus(watch) }],
|
|
109
|
-
details: { action: "status", watch } satisfies WatchDetails,
|
|
110
|
-
};
|
|
99
|
+
return textResult(formatWatchStatus(watch), { action: "status", watch } satisfies WatchDetails);
|
|
111
100
|
}
|
|
112
101
|
|
|
113
102
|
case "matches": {
|
|
114
103
|
const result = runtime.matches(call.id, call.lines);
|
|
115
104
|
if (!result) return errorResult("matches", `no watcher "${call.id}".`);
|
|
116
105
|
const text = formatTail("matches", result.watch, result.lines, 4000);
|
|
117
|
-
return {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
truncated: text.truncated,
|
|
124
|
-
} satisfies WatchDetails,
|
|
125
|
-
};
|
|
106
|
+
return textResult(text.text, {
|
|
107
|
+
action: "matches",
|
|
108
|
+
watch: result.watch,
|
|
109
|
+
matches: result.lines.join("\n"),
|
|
110
|
+
truncated: text.truncated,
|
|
111
|
+
} satisfies WatchDetails);
|
|
126
112
|
}
|
|
127
113
|
|
|
128
114
|
case "logs": {
|
|
129
115
|
const result = runtime.logs(call.id, call.lines);
|
|
130
116
|
if (!result) return errorResult("logs", `no watcher "${call.id}".`);
|
|
131
117
|
const text = formatTail("output", result.watch, result.lines, 4000);
|
|
132
|
-
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
truncated: text.truncated,
|
|
139
|
-
} satisfies WatchDetails,
|
|
140
|
-
};
|
|
118
|
+
return textResult(text.text, {
|
|
119
|
+
action: "logs",
|
|
120
|
+
watch: result.watch,
|
|
121
|
+
logs: result.lines.join("\n"),
|
|
122
|
+
truncated: text.truncated,
|
|
123
|
+
} satisfies WatchDetails);
|
|
141
124
|
}
|
|
142
125
|
|
|
143
126
|
case "stop": {
|
|
@@ -145,34 +128,23 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
145
128
|
if (!before) return errorResult("stop", `no watcher "${call.id}".`);
|
|
146
129
|
const stopped = runtime.stop(call.id);
|
|
147
130
|
const watch = runtime.get(call.id) ?? before;
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
: `${watch.id} was not running; nothing to stop.`,
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
details: { action: "stop", watch, stopped } satisfies WatchDetails,
|
|
158
|
-
};
|
|
131
|
+
return textResult(
|
|
132
|
+
stopped
|
|
133
|
+
? `Stopped ${watch.id} (${sourceLabel(watch)}).`
|
|
134
|
+
: `${watch.id} was not running; nothing to stop.`,
|
|
135
|
+
{ action: "stop", watch, stopped } satisfies WatchDetails,
|
|
136
|
+
);
|
|
159
137
|
}
|
|
160
138
|
|
|
161
139
|
case "clear": {
|
|
162
140
|
const result = runtime.clear(call.id, call.all ?? false);
|
|
163
141
|
if (result.refused) return errorResult("clear", result.refused);
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
? "Nothing to clear."
|
|
171
|
-
: `Cleared ${result.cleared} watcher${result.cleared === 1 ? "" : "s"}.`,
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
details: { action: "clear", cleared: result.cleared, watches: runtime.list() } satisfies WatchDetails,
|
|
175
|
-
};
|
|
142
|
+
return textResult(
|
|
143
|
+
result.cleared === 0
|
|
144
|
+
? "Nothing to clear."
|
|
145
|
+
: `Cleared ${result.cleared} watcher${result.cleared === 1 ? "" : "s"}.`,
|
|
146
|
+
{ action: "clear", cleared: result.cleared, watches: runtime.list() } satisfies WatchDetails,
|
|
147
|
+
);
|
|
176
148
|
}
|
|
177
149
|
}
|
|
178
150
|
} catch (error) {
|
|
@@ -47,8 +47,9 @@ How the `send_wechat` call and its result look in the transcript — the call li
|
|
|
47
47
|
- **Peer feedback** — a burst that arrives while a turn is still running is
|
|
48
48
|
answered once, a message waiting on another conversation is acknowledged once,
|
|
49
49
|
a slow turn says so, and a turn that ends without text says so instead of going
|
|
50
|
-
quiet. Notices and `/help` follow the peer's language
|
|
51
|
-
|
|
50
|
+
quiet. Notices and `/help` follow the peer's language: a Chinese message sets it,
|
|
51
|
+
and a language-neutral reply (`1`, `ok`) does not switch it back. The peer can
|
|
52
|
+
also stop (`/stop`), restart (`/new`), and ask what is happening (`/status`).
|
|
52
53
|
- **Remote prompts** — the peer session's own UI is bound to that peer, so
|
|
53
54
|
`ask_user_question`, plan approval, and any other dialog raised inside a
|
|
54
55
|
WeChat turn is sent to WeChat and answered there. Terminal turns keep the TUI,
|
|
@@ -174,14 +175,14 @@ needs. Per action:
|
|
|
174
175
|
|---|---|
|
|
175
176
|
| Sends a message | The native typing indicator starts as soon as the message is picked up; the answer is sent when the run settles. |
|
|
176
177
|
| Sends several messages quickly | At most one acknowledgement, then a single answer that covers the burst in order. |
|
|
177
|
-
| Waits behind a long turn | The indicator keeps refreshing, and after four seconds one short
|
|
178
|
+
| Waits behind a long turn | The indicator keeps refreshing, and after four seconds one short `收到,正在排队,轮到就回你。` / “Got it — queued; I’ll answer when it’s my turn.”; a turn still running at 30 seconds adds one `还在处理这条,发送 /stop 可取消。` / “Still working — send /stop to cancel.”, so the way out is named when it is wanted. |
|
|
178
179
|
| Sends while another peer's turn runs | One line after four seconds, then one more at a minute (`前面还有一条正在处理…` / “Your message is queued behind another conversation…”), and the answer when the slot frees. |
|
|
179
180
|
| Sends a photo or a file | The model reads it; an attachment that could not be read (too large, a failed download, no key) is named in the answer. |
|
|
180
181
|
| Sends a voice note or video | One line: voice without a transcript and video are not supported. |
|
|
181
182
|
| Reads a long answer | Streamed as it is produced — one part per paragraph, per two silent seconds, or per 400 characters — and numbered (`[1/3] …`) only when a single part exceeds `maxReplyChars`. |
|
|
182
183
|
| Reads an answer | Plain chat text — Markdown fences, headings, and emphasis markers are flattened, code-block bodies survive. |
|
|
183
|
-
| Is asked a question | A numbered prompt; 确定/是/yes or 不是/否/no answers it, 取消 dismisses. Ignored, it is nudged at
|
|
184
|
-
| Types `/help`, `/new`, `/stop`, or `/status` | The command list, a fresh conversation, a stopped answer, or the current state; `/new` is declined while their run is in flight. |
|
|
184
|
+
| Is asked a question | A numbered prompt; 确定/是/yes or 不是/否/no answers it, 取消 dismisses. Ignored, it is nudged at the halfway point of its window and then cancelled: three minutes for a dialog, ten for a plan review, where the plan has to be read first. |
|
|
185
|
+
| Types `/help`, `/new`, `/stop`, or `/status` | The command list, a fresh conversation, a stopped answer, or the current state — including the dialog a blocked turn is waiting on; `/new` is declined while their run is in flight. A command works while a dialog waits: it is never read as an answer to it. |
|
|
185
186
|
| Types `/history` | The last eight messages of this conversation (or the requested number, up to 20), one line each. |
|
|
186
187
|
| Floods the chat | Past eight queued messages, one line asking to wait rather than an unbounded queue. |
|
|
187
188
|
| Sends the same message twice (a server replay) | Answered once: the WeChat message id is remembered for an hour, across a reconnect or reload. |
|
|
@@ -191,23 +192,23 @@ needs. Per action:
|
|
|
191
192
|
| Gets a turn that could not start | `处理失败,请稍后重试。` / “Something went wrong — please try again.”, and the terminal says why. |
|
|
192
193
|
| Keeps messaging an expired or logged-out bridge | Nothing — the bot goes quiet, and only the terminal chip says why. |
|
|
193
194
|
|
|
194
|
-
Notices are one line and in the peer's language (Chinese
|
|
195
|
-
|
|
196
|
-
chatter: WeChat notifies per message,
|
|
195
|
+
Notices are one line and in the peer's language (Chinese once their messages
|
|
196
|
+
contain Chinese; a bare `1` or `ok` does not switch it back). The bridge never
|
|
197
|
+
sends greetings, read receipts, or progress chatter: WeChat notifies per message,
|
|
198
|
+
so every bubble has to carry information.
|
|
197
199
|
|
|
198
200
|
### The reply contract
|
|
199
201
|
|
|
200
|
-
One
|
|
201
|
-
|
|
202
|
-
inside the answer it is already waiting for — not with a second “working on it”.
|
|
202
|
+
One message, one turn, one answer. A queued message is never merged into
|
|
203
|
+
another prompt; it waits for its own turn. One acknowledgement per wait episode.
|
|
203
204
|
|
|
204
205
|
| Situation | What the peer gets |
|
|
205
206
|
|---|---|
|
|
206
207
|
| 1st message, session idle | typing, then the answer (no acknowledgement: there is nothing to wait for) |
|
|
207
|
-
| 1st message, peer busy | typing,
|
|
208
|
-
| 2nd message
|
|
209
|
-
| 2nd message
|
|
210
|
-
| 3rd…Nth in a burst |
|
|
208
|
+
| 1st message, peer busy | typing, `收到,排队中,轮到就回。` at 4 s, `还在处理前一条,轮到就回。` at 60 s, the answer when the slot frees |
|
|
209
|
+
| 2nd message while a turn runs | its own turn behind the first; the 4 s ack and 60 s busy line; then its own answer |
|
|
210
|
+
| 2nd message already queued | behind the first in FIFO; no merge |
|
|
211
|
+
| 3rd…Nth in a burst | each is its own turn; past eight queued, one line asking to wait |
|
|
211
212
|
|
|
212
213
|
Two consequences worth knowing: an answer can take up to one long-poll cycle
|
|
213
214
|
(the server holds each `getupdates`; measured ~18 s) before the turn even
|
|
@@ -217,17 +218,30 @@ starts, and a reply is delivered when the run settles, not token by token.
|
|
|
217
218
|
|
|
218
219
|
A dialog raised while a WeChat turn is in flight is forwarded to the sender:
|
|
219
220
|
|
|
220
|
-
- `select` — a numbered list; reply with a number
|
|
221
|
-
|
|
221
|
+
- `select` — a numbered list; reply with a number or the option text. Free text
|
|
222
|
+
becomes the “Other” answer only when the list carries that entry; the hint says
|
|
223
|
+
so, so a plan review without it does not pretend free text is a choice.
|
|
222
224
|
- `confirm` — reply 是/否, 确定/不是, or yes/no.
|
|
223
225
|
- `input` / `editor` — reply with the text.
|
|
224
|
-
- Reply 取消 (or cancel) to dismiss
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
- Reply 取消 (or cancel) to dismiss; `退出`, `退出 plan 模式`, and
|
|
227
|
+
`exit plan mode` are recognized as cancel intent. In a plan review a dismissal
|
|
228
|
+
leaves plan mode without approving.
|
|
229
|
+
- Hints are rendered in the peer’s language, one language per prompt. A reply
|
|
230
|
+
that cannot be read re-sends the prompt with a short “didn’t recognize that”
|
|
231
|
+
notice instead of repeating it silently.
|
|
232
|
+
- A command is not an answer. While a dialog waits the model is blocked on it, so
|
|
233
|
+
the slash commands are matched first: `/status` names the waiting question and
|
|
234
|
+
`/stop` stops the turn, where before both were parsed as an attempt to answer
|
|
235
|
+
and bounced back as “didn’t recognize that”. Any other text is still the answer,
|
|
236
|
+
so a number, 是/否, or 取消 behaves as above.
|
|
228
237
|
- An unanswered prompt is bounded: a reminder goes out halfway and the question
|
|
229
|
-
is cancelled
|
|
230
|
-
cannot freeze the session.
|
|
238
|
+
is cancelled at the end of its window with a one-line notice, so an ignored
|
|
239
|
+
dialog cannot freeze the session. A dialog uses three minutes; the plan review
|
|
240
|
+
uses ten. A timed-out plan review is treated like a dismissal: plan mode ends
|
|
241
|
+
without approval, and the plan file remains.
|
|
242
|
+
- A prompt that goes away before it is answered — a pause, a `/stop`, a shutdown —
|
|
243
|
+
says so (`问题已撤回。` / “That question was withdrawn.”) instead of reusing the
|
|
244
|
+
timeout line: the peer was not slow, the question left.
|
|
231
245
|
|
|
232
246
|
Each peer session is created with its own UI context (`bindExtensions` with
|
|
233
247
|
`mode: "rpc"`), so its dialogs go to that peer by construction rather than by
|
|
@@ -239,21 +253,17 @@ to WeChat. See [Design notes](#design-notes).
|
|
|
239
253
|
|
|
240
254
|
## Outbound files
|
|
241
255
|
|
|
242
|
-
A remote prompt can deliver a local file first.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
answerable from the chat even if the file bubble is never opened, and a file or
|
|
249
|
-
digest the bridge cannot deliver is reported on the terminal instead of vanishing.
|
|
250
|
-
The choices also include **Send the full plan**, which posts the plan text and
|
|
251
|
-
asks again, so the whole plan is reachable without the attachment.
|
|
256
|
+
A remote prompt can deliver a local file first. The plan review uses this: the
|
|
257
|
+
plan file is sent before one compact editor prompt with the title, first steps,
|
|
258
|
+
mode status, and three actions. Reply 2 to keep planning or 3 to exit; any other
|
|
259
|
+
text is a refinement. If the upload fails, the plan text is sent instead. The
|
|
260
|
+
plan file stays on disk. After a Keep decision the user can say “exit plan mode”; the model
|
|
261
|
+
calls `cancel_plan_mode`, which needs no plan path and records no todo or goal.
|
|
252
262
|
The flow mirrors the
|
|
253
263
|
reference client: `getuploadurl` → AES-128-ECB encrypt → `POST` the ciphertext
|
|
254
264
|
(`Content-Type: application/octet-stream`) → read `x-encrypted-param` → send a
|
|
255
|
-
`type: 4` file item.
|
|
256
|
-
|
|
265
|
+
`type: 4` file item. No current flow uses it; images, video, and voice are not
|
|
266
|
+
sent outbound.
|
|
257
267
|
|
|
258
268
|
## Configuration
|
|
259
269
|
|
|
@@ -507,7 +517,6 @@ status, which is what the old start/stop guidance used to do.
|
|
|
507
517
|
| `media.ts` | Media key/encrypt/decrypt, padded size, upload/download URL, MIME sniff, filename sanitize, and save helpers. |
|
|
508
518
|
| `outbound.ts` | Upload and send a local file: hash, encrypt, `getUploadUrl`, upload, `sendFileMessage`. |
|
|
509
519
|
| `login.ts` | QR login state machine. |
|
|
510
|
-
| `qrcode.ts` | QR matrix → Unicode half-block terminal lines. |
|
|
511
520
|
| `login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`), drawn from the `lib/tui/` kit (`titleRule`, `clip`, `isCloseKey`). |
|
|
512
521
|
| `status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`): live facts, a focusable peer list with stop/reset/history keys, and the pure one-line/body renderers, drawn from the `lib/tui/` kit (`titleRule`, `keyValueRow`, `listRow`, `paint`, `IdCursor`). |
|
|
513
522
|
| `config.ts` | The `wechat` section of `bytefly.json` loading and normalization. |
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { WechatError, type WechatClient } from "./client.ts";
|
|
20
20
|
import { createAdmissionQueue, createTurnBudget } from "./admission.ts";
|
|
21
|
-
import { createDeliverer } from "./deliver.ts";
|
|
21
|
+
import { createDeliverer, type MessageTarget } from "./deliver.ts";
|
|
22
22
|
import { createInboundAdmission } from "./inbound.ts";
|
|
23
23
|
import { createPollLoop, defaultPollTimeout } from "./poll.ts";
|
|
24
24
|
import { messageOf } from "./errors.ts";
|
|
@@ -225,8 +225,8 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/** Send one bubble to a peer and report whether it was delivered. */
|
|
228
|
-
async function reply(
|
|
229
|
-
return deliverer.reply(
|
|
228
|
+
async function reply(target: MessageTarget, text: string): Promise<boolean> {
|
|
229
|
+
return deliverer.reply(target, text, abort?.signal);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/** The outbound surface, reading the live client and account at call time. */
|
|
@@ -246,57 +246,61 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
246
246
|
lastSendError = undefined;
|
|
247
247
|
},
|
|
248
248
|
});
|
|
249
|
-
|
|
250
249
|
/** Run a slash action out-of-band and reply to the sender; never starts a turn. */
|
|
251
|
-
async function runSlashAction(command: SlashCommand,
|
|
250
|
+
async function runSlashAction(command: SlashCommand, target: MessageTarget, args: string): Promise<void> {
|
|
252
251
|
if (!command.run) return;
|
|
253
252
|
const ctx: SlashContext = {
|
|
254
253
|
args,
|
|
255
|
-
peer:
|
|
254
|
+
peer: target.peer,
|
|
256
255
|
reply: async (text) => {
|
|
257
|
-
await reply(
|
|
256
|
+
await reply(target, text);
|
|
258
257
|
},
|
|
259
|
-
activeTurn: worker?.state(
|
|
260
|
-
language: languageOf(
|
|
258
|
+
activeTurn: worker?.state(target.peer).status === "running",
|
|
259
|
+
language: languageOf(target.peer),
|
|
261
260
|
session: {
|
|
262
261
|
newSession: () => {
|
|
263
|
-
worker?.reset(
|
|
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;
|
|
264
269
|
},
|
|
265
|
-
|
|
266
|
-
status: () => worker?.state(item.peer) ?? { status: "idle", queued: 0 },
|
|
267
|
-
history: (count) => deps.readHistory(item.peer, count),
|
|
270
|
+
history: (count) => deps.readHistory(target.peer, count),
|
|
268
271
|
},
|
|
269
272
|
};
|
|
270
273
|
try {
|
|
271
274
|
await command.run(ctx);
|
|
272
275
|
} catch (error) {
|
|
273
276
|
notify(`WeChat command /${command.name} failed: ${messageOf(error)}`, "warning");
|
|
274
|
-
await reply(
|
|
277
|
+
await reply(target, `Command /${command.name} failed.`);
|
|
275
278
|
}
|
|
276
279
|
}
|
|
277
280
|
|
|
278
281
|
/**
|
|
279
|
-
* Send a
|
|
282
|
+
* Send a prompt to the peer owning the in-flight turn.
|
|
280
283
|
*
|
|
281
284
|
* A failure is reported on the terminal once per peer by the deliverer; a silent
|
|
282
285
|
* failure would leave the peer with only the choices, as if nothing preceded
|
|
283
286
|
* them.
|
|
284
287
|
*/
|
|
285
|
-
async function sendPrompt(text: string, signal?: AbortSignal): Promise<boolean> {
|
|
288
|
+
async function sendPrompt(text: string, signal?: AbortSignal, options?: { force?: boolean }): Promise<boolean> {
|
|
286
289
|
const current = worker?.current();
|
|
287
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;
|
|
288
294
|
return deliverer.prompt(current, text, signal ?? abort?.signal);
|
|
289
295
|
}
|
|
290
|
-
|
|
291
|
-
/** Upload and send a local file to the peer owning the in-flight turn. */
|
|
292
296
|
async function sendFileToPeer(filePath: string, fileName: string, signal?: AbortSignal): Promise<boolean> {
|
|
293
297
|
const current = worker?.current();
|
|
294
298
|
if (!current) return false;
|
|
299
|
+
await worker?.flush(current.peer);
|
|
295
300
|
return deliverer.file(current, filePath, fileName, signal ?? abort?.signal);
|
|
296
301
|
}
|
|
297
|
-
|
|
298
302
|
const interaction = createWechatInteractionChannel({
|
|
299
|
-
send: (text, signal) => sendPrompt(text, signal),
|
|
303
|
+
send: (text, signal, options) => sendPrompt(text, signal, options),
|
|
300
304
|
sendFile: (path, name, signal) => sendFileToPeer(path, name, signal),
|
|
301
305
|
activePeer: () => worker?.current()?.peer,
|
|
302
306
|
language: () => languageOf(worker?.current()?.peer ?? ""),
|
|
@@ -305,11 +309,10 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
305
309
|
|
|
306
310
|
/** The peer's language: from this text, else the last one they wrote. */
|
|
307
311
|
function languageOf(peer: string, text?: string): PeerLanguage {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
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");
|
|
313
316
|
return languages.get(peer) ?? "en";
|
|
314
317
|
}
|
|
315
318
|
|
|
@@ -386,26 +389,11 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
386
389
|
if (typeof messageId === "number" && messageId !== 0) rememberMessage(state, peer, messageId, now);
|
|
387
390
|
deps.saveState(state);
|
|
388
391
|
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
peer,
|
|
395
|
-
contextToken: message.context_token ?? findPeer(state, peer)?.lastContextToken,
|
|
396
|
-
text,
|
|
397
|
-
media,
|
|
398
|
-
language,
|
|
399
|
-
};
|
|
400
|
-
if (!text && media.length === 0) {
|
|
401
|
-
// Nothing to read. A voice note without a transcript or a video is
|
|
402
|
-
// answered, so the peer knows it did not reach the agent.
|
|
403
|
-
if (unsupported > 0) void reply(item, notice("unsupported", language));
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
// Intercept a text slash command before it becomes a turn. A message with
|
|
407
|
-
// media is never a command, so an attachment is never dropped; an unknown
|
|
408
|
-
// command falls through to the model as ordinary text.
|
|
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.
|
|
409
397
|
const slash = media.length === 0 ? parseSlash(text) : undefined;
|
|
410
398
|
const command = slash ? findSlashCommand(slashCommands, slash.name) : undefined;
|
|
411
399
|
if (command && slash) {
|
|
@@ -415,12 +403,22 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
415
403
|
}
|
|
416
404
|
if (command.kind === "snippet") {
|
|
417
405
|
text = expandSnippet(command, slash.args);
|
|
418
|
-
item.text = text;
|
|
419
406
|
} else {
|
|
420
|
-
void runSlashAction(command,
|
|
407
|
+
void runSlashAction(command, target, slash.args);
|
|
421
408
|
return;
|
|
422
409
|
}
|
|
423
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
|
+
}
|
|
424
422
|
// A slot is reserved here, before the download, so a burst of media cannot
|
|
425
423
|
// all pass the check and then enqueue together.
|
|
426
424
|
if (!budget.allow(peer)) {
|
|
@@ -620,7 +618,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
620
618
|
// not be cancelled by the poll abort that follows.
|
|
621
619
|
const stopping = abort;
|
|
622
620
|
abort = undefined;
|
|
623
|
-
if (interaction.hasPending()) interaction.cancel();
|
|
621
|
+
if (interaction.hasPending()) await interaction.cancel();
|
|
624
622
|
stopping?.abort();
|
|
625
623
|
try {
|
|
626
624
|
await loopPromise;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { asNumber } from "../../lib/config.ts";
|
|
12
13
|
import { mediaDownloadUrl } from "./media.ts";
|
|
13
14
|
import type {
|
|
14
15
|
BotCredentials,
|
|
@@ -120,10 +121,6 @@ function parseJson(text: string): Record<string, unknown> {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
function asNumber(value: unknown): number | undefined {
|
|
124
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
124
|
function asString(value: unknown): string | undefined {
|
|
128
125
|
return typeof value === "string" && value !== "" ? value : undefined;
|
|
129
126
|
}
|
|
@@ -30,7 +30,7 @@ import { peerLabel, peerKey } from "./format.ts";
|
|
|
30
30
|
import { waitForLogin, type LoginOutcome } from "./login.ts";
|
|
31
31
|
import { defaultSleep } from "./timer.ts";
|
|
32
32
|
import { LoginScreenComponent, type LoginViewState } from "./login-screen.ts";
|
|
33
|
-
import { qrLines } from "
|
|
33
|
+
import { qrLines } from "../../lib/qr.ts";
|
|
34
34
|
import { statusLine, statusView, StatusScreenComponent, type StatusAction, type StatusView } from "./status-screen.ts";
|
|
35
35
|
import type { BotCredentials } from "./types.ts";
|
|
36
36
|
|
|
@@ -154,7 +154,7 @@ function resetMessage(bridge: Bridge, peer: string): VerbOutcome {
|
|
|
154
154
|
|
|
155
155
|
function cancelPromptMessage(bridge: Bridge): VerbOutcome {
|
|
156
156
|
if (!bridge.status().promptPending) return { message: "No prompt is waiting.", kind: "info" };
|
|
157
|
-
bridge.channel().cancel();
|
|
157
|
+
void bridge.channel().cancel();
|
|
158
158
|
return { message: "Cancelled the waiting prompt.", kind: "info" };
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -292,7 +292,7 @@ async function runLogin(ctx: ExtensionContext, deps: CommandDeps): Promise<void>
|
|
|
292
292
|
return;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
const state: LoginViewState = { status: "wait", lines: qrLines(qr.imgContent), url: qr.imgContent };
|
|
295
|
+
const state: LoginViewState = { status: "wait", lines: await qrLines(qr.imgContent), url: qr.imgContent };
|
|
296
296
|
const result = await ctx.ui.custom<LoginOutcome | undefined>((tui, theme, _keybindings, done) => {
|
|
297
297
|
const controller = new AbortController();
|
|
298
298
|
void (async () => {
|