@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -21,30 +21,25 @@ import { join } from "node:path";
|
|
|
21
21
|
import { writeConfigValue } from "../../../lib/config.ts";
|
|
22
22
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
23
23
|
import { supportsCustomUI } from "../../../lib/interaction.ts";
|
|
24
|
-
import {
|
|
24
|
+
import type { Bridge } from "../bridge.ts";
|
|
25
25
|
import type { WechatClient } from "../transport/client.ts";
|
|
26
26
|
import type { WechatConfig } from "../config.ts";
|
|
27
|
+
import type { StatusAction } from "../view/status-screen.ts";
|
|
28
|
+
import type { LoginViewState } from "../view/login-screen.ts";
|
|
29
|
+
import type { LoginOutcome } from "./login.ts";
|
|
27
30
|
import { doctorReport } from "./doctor.ts";
|
|
28
31
|
import { messageOf } from "../errors.ts";
|
|
29
32
|
import { peerLabel, peerKey } from "../text/format.ts";
|
|
30
|
-
import { waitForLogin, type LoginOutcome } from "./login.ts";
|
|
31
33
|
import { defaultSleep } from "../timer.ts";
|
|
32
|
-
import { LoginScreenComponent, type LoginViewState } from "../view/login-screen.ts";
|
|
33
34
|
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";
|
|
35
|
+
import { statusLine, statusView, type StatusView } from "../text/status.ts";
|
|
41
36
|
import type { BotCredentials } from "../types.ts";
|
|
42
37
|
|
|
43
38
|
export interface CommandDeps {
|
|
44
39
|
getBridge: () => Bridge | undefined;
|
|
45
|
-
ensureBridge: (ctx: ExtensionContext) => Bridge
|
|
40
|
+
ensureBridge: (ctx: ExtensionContext) => Promise<Bridge>;
|
|
46
41
|
loadConfig: (cwd: string) => WechatConfig;
|
|
47
|
-
createLoginClient: (cwd: string) => WechatClient
|
|
42
|
+
createLoginClient: (cwd: string) => Promise<WechatClient>;
|
|
48
43
|
saveAccount: (account: BotCredentials) => void;
|
|
49
44
|
primaryAccount: () => BotCredentials | undefined;
|
|
50
45
|
/** Read a peer's recent messages for `/wechat history`; read-only. */
|
|
@@ -202,6 +197,9 @@ async function runAllow(ctx: ExtensionContext, deps: CommandDeps, peerArg?: stri
|
|
|
202
197
|
const candidate = peerArg?.trim() || (await chooseRefused(ctx, deps));
|
|
203
198
|
if (!candidate) return;
|
|
204
199
|
const config = deps.loadConfig(ctx.cwd);
|
|
200
|
+
// The admission rule lives with the bridge's intake, which a session that never
|
|
201
|
+
// runs `/wechat allow` does not load.
|
|
202
|
+
const { isAllowedPeer } = await import("../intake.ts");
|
|
205
203
|
if (isAllowedPeer(candidate, undefined, config.allowedPeers)) {
|
|
206
204
|
ctx.ui.notify(`${sanitizeLine(peerLabel(candidate))} is already allowed.`);
|
|
207
205
|
return;
|
|
@@ -262,7 +260,7 @@ export async function finishLogin(result: LoginOutcome, ctx: ExtensionContext, d
|
|
|
262
260
|
// previous client and its token, and `open()` is a no-op while it runs, so
|
|
263
261
|
// it has to stop before the new credentials are used.
|
|
264
262
|
await deps.getBridge()?.close();
|
|
265
|
-
deps.ensureBridge(ctx).open();
|
|
263
|
+
(await deps.ensureBridge(ctx)).open();
|
|
266
264
|
ctx.ui.notify(`WeChat connected as ${label} and the bridge is running.`);
|
|
267
265
|
} catch (error) {
|
|
268
266
|
ctx.ui.notify(
|
|
@@ -289,7 +287,7 @@ async function runLogin(ctx: ExtensionContext, deps: CommandDeps): Promise<void>
|
|
|
289
287
|
return;
|
|
290
288
|
}
|
|
291
289
|
if (busyWithPeerTurn(ctx, deps, "login")) return;
|
|
292
|
-
const client = deps.createLoginClient(ctx.cwd);
|
|
290
|
+
const client = await deps.createLoginClient(ctx.cwd);
|
|
293
291
|
const config = deps.loadConfig(ctx.cwd);
|
|
294
292
|
|
|
295
293
|
let qr: { qrcode: string; imgContent: string };
|
|
@@ -300,6 +298,10 @@ async function runLogin(ctx: ExtensionContext, deps: CommandDeps): Promise<void>
|
|
|
300
298
|
return;
|
|
301
299
|
}
|
|
302
300
|
|
|
301
|
+
// The login screen and its poll loop are the TUI's: the refusal above and
|
|
302
|
+
// registration load neither.
|
|
303
|
+
const { waitForLogin } = await import("./login.ts");
|
|
304
|
+
const { LoginScreenComponent } = await import("../view/login-screen.ts");
|
|
303
305
|
const state: LoginViewState = { status: "wait", lines: await qrLines(qr.imgContent), url: qr.imgContent };
|
|
304
306
|
const result = await ctx.ui.custom<LoginOutcome | undefined>((tui, theme, _keybindings, done) => {
|
|
305
307
|
const controller = new AbortController();
|
|
@@ -357,6 +359,8 @@ async function runPanel(ctx: ExtensionCommandContext, deps: CommandDeps): Promis
|
|
|
357
359
|
ctx.ui.notify(statusLine(view()));
|
|
358
360
|
return;
|
|
359
361
|
}
|
|
362
|
+
// The control screen is the TUI's: the one-line summary above loads it never.
|
|
363
|
+
const { StatusScreenComponent } = await import("../view/status-screen.ts");
|
|
360
364
|
const action = await ctx.ui.custom<StatusAction>(
|
|
361
365
|
(tui, theme, _keybindings, done) =>
|
|
362
366
|
new StatusScreenComponent({
|
|
@@ -15,22 +15,18 @@
|
|
|
15
15
|
import { getAgentDir, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
18
|
+
import { lazy } from "../../lib/lazy.ts";
|
|
18
19
|
import { isPidAlive } from "../../lib/spawn.ts";
|
|
19
20
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
20
21
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
22
|
+
import type { Bridge, BridgeState } from "./bridge.ts";
|
|
23
|
+
import type { WechatClient, WechatClientOptions } from "./transport/client.ts";
|
|
23
24
|
import { registerCommands, type CommandDeps } from "./commands/commands.ts";
|
|
24
25
|
import { messageOf } from "./errors.ts";
|
|
25
|
-
import { loadWechatConfig } from "./config.ts";
|
|
26
|
+
import { loadWechatConfig, type WechatConfig } from "./config.ts";
|
|
26
27
|
import { clearCredentials, primaryAccount, saveAccount, wechatDir } from "./state/credentials.ts";
|
|
27
|
-
import {
|
|
28
|
-
import { openPeerSession, type PeerSession, type PeerSessionOptions } from "./peer/peer-session.ts";
|
|
29
|
-
import { createPeerUIContext } from "./dialog/remote-ui.ts";
|
|
28
|
+
import type { PeerSession, PeerSessionOptions } from "./peer/peer-session.ts";
|
|
30
29
|
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
30
|
import { registerTools } from "./tools.ts";
|
|
35
31
|
|
|
36
32
|
/**
|
|
@@ -62,14 +58,20 @@ export interface WechatDeps {
|
|
|
62
58
|
* call a model; production opens a real SDK session per peer.
|
|
63
59
|
*/
|
|
64
60
|
openSession?: (peer: string, options: PeerSessionOptions) => Promise<PeerSession>;
|
|
61
|
+
/** The bridge module; overridden in tests so a load can be counted. */
|
|
62
|
+
loadBridge?: () => Promise<typeof import("./bridge.ts")>;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
68
66
|
if (!isExtensionEnabled("wechat")) return;
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
/** The bridge and the transport, loaded on the first account, never at registration. */
|
|
69
|
+
const bridgeModule = lazy(deps.loadBridge ?? (() => import("./bridge.ts")));
|
|
70
|
+
const clientModule = lazy(() => import("./transport/client.ts"));
|
|
71
71
|
|
|
72
72
|
let bridge: Bridge | undefined;
|
|
73
|
+
/** A build in flight, so a command that races `session_start` joins it instead of loading twice. */
|
|
74
|
+
let building: Promise<Bridge> | undefined;
|
|
73
75
|
let ctxRef: ExtensionContext | undefined;
|
|
74
76
|
|
|
75
77
|
const lockDeps = () => ({
|
|
@@ -78,10 +80,36 @@ export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
|
78
80
|
isAlive: isPidAlive,
|
|
79
81
|
});
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
/** One iLink client over the fetched transport, from an already-loaded module. */
|
|
84
|
+
const clientOf = (
|
|
85
|
+
module: typeof import("./transport/client.ts"),
|
|
86
|
+
config: WechatConfig,
|
|
87
|
+
account?: { baseUrl: string },
|
|
88
|
+
): WechatClient =>
|
|
89
|
+
(deps.createClient ?? ((options: WechatClientOptions) => new module.WechatClient(options)))({
|
|
90
|
+
http: module.createFetchRunner(),
|
|
91
|
+
channelVersion: config.channelVersion,
|
|
92
|
+
botAgent: config.botAgent,
|
|
93
|
+
cdnBaseUrl: config.cdnBaseUrl,
|
|
94
|
+
...(account === undefined ? {} : { baseUrl: account.baseUrl }),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const build = async (ctx: ExtensionContext): Promise<Bridge> => {
|
|
82
98
|
ctxRef = ctx;
|
|
83
99
|
const config = loadWechatConfig(ctx.cwd);
|
|
84
100
|
const peerTools = new Map(Object.entries(config.peers).map(([id, peer]) => [peerKey(id), peer.tools] as const));
|
|
101
|
+
// Everything a live bridge composes loads here, so a session with no account
|
|
102
|
+
// never evaluates the bridge, the peer worker, the transport or the lock.
|
|
103
|
+
const [{ createBridge }, client, { openPeerSession }, { createPeerUIContext }] = await Promise.all([
|
|
104
|
+
bridgeModule.get(),
|
|
105
|
+
clientModule.get(),
|
|
106
|
+
import("./peer/peer-session.ts"),
|
|
107
|
+
import("./dialog/remote-ui.ts"),
|
|
108
|
+
]);
|
|
109
|
+
const { acquireLock, lockPath, readLock, refreshLock, releaseLock } = await import("./state/lock.ts");
|
|
110
|
+
const { readState, statePath, writeState } = await import("./state/state.ts");
|
|
111
|
+
const { SLASH_COMMANDS } = await import("./commands/slash.ts");
|
|
112
|
+
const { readPeerHistory } = await import("./commands/history.ts");
|
|
85
113
|
return createBridge({
|
|
86
114
|
config,
|
|
87
115
|
now: () => Date.now(),
|
|
@@ -111,14 +139,7 @@ export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
|
111
139
|
loadAccount: () => primaryAccount(),
|
|
112
140
|
loadState: () => readState(statePath()),
|
|
113
141
|
saveState: (state) => writeState(statePath(), state),
|
|
114
|
-
createClient: (account) =>
|
|
115
|
-
makeClient({
|
|
116
|
-
http: createFetchRunner(),
|
|
117
|
-
channelVersion: config.channelVersion,
|
|
118
|
-
botAgent: config.botAgent,
|
|
119
|
-
baseUrl: account.baseUrl,
|
|
120
|
-
cdnBaseUrl: config.cdnBaseUrl,
|
|
121
|
-
}),
|
|
142
|
+
createClient: (account) => clientOf(client, config, account),
|
|
122
143
|
sleep: deps.sleep,
|
|
123
144
|
acquireLock: () => acquireLock(lockPath(), lockDeps()),
|
|
124
145
|
refreshLock: () => refreshLock(lockPath(), lockDeps()),
|
|
@@ -147,13 +168,28 @@ export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
|
147
168
|
* no-op, so a host that binds one session twice (RPC emits `session_start`
|
|
148
169
|
* twice on a session switch) cannot start a second poller.
|
|
149
170
|
*/
|
|
150
|
-
|
|
171
|
+
/** Build the bridge once, sharing the load with a command that asks while it is in flight. */
|
|
172
|
+
const ensure = (ctx: ExtensionContext): Promise<Bridge> => {
|
|
173
|
+
building ??= build(ctx).then(
|
|
174
|
+
(built) => {
|
|
175
|
+
bridge = built;
|
|
176
|
+
return built;
|
|
177
|
+
},
|
|
178
|
+
(error: unknown) => {
|
|
179
|
+
// A failed build is not remembered, so the next command can retry.
|
|
180
|
+
building = undefined;
|
|
181
|
+
throw error;
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
return building;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const autoStart = async (ctx: ExtensionContext): Promise<void> => {
|
|
151
188
|
if (ctx.mode !== "tui" && ctx.mode !== "rpc") return;
|
|
152
189
|
if (!primaryAccount()) return;
|
|
153
190
|
if (bridge?.status().open || bridge?.status().paused) return;
|
|
154
191
|
try {
|
|
155
|
-
bridge
|
|
156
|
-
bridge.open();
|
|
192
|
+
(bridge ?? (await ensure(ctx))).open();
|
|
157
193
|
} catch (error) {
|
|
158
194
|
ctx.ui.notify(`WeChat did not connect: ${messageOf(error)}`, "warning");
|
|
159
195
|
}
|
|
@@ -161,24 +197,20 @@ export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
|
161
197
|
|
|
162
198
|
const commandDeps: CommandDeps = {
|
|
163
199
|
getBridge: () => bridge,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
},
|
|
200
|
+
// Both of these can be the first thing that needs the bridge: `/wechat login`
|
|
201
|
+
// runs before `session_start` on an RPC host, so the load is awaited here too.
|
|
202
|
+
ensureBridge: (ctx) => (bridge === undefined ? ensure(ctx) : Promise.resolve(bridge)),
|
|
168
203
|
loadConfig: (cwd) => loadWechatConfig(cwd),
|
|
169
|
-
createLoginClient: (cwd) => {
|
|
204
|
+
createLoginClient: async (cwd) => {
|
|
170
205
|
const config = loadWechatConfig(cwd);
|
|
171
|
-
return
|
|
172
|
-
http: createFetchRunner(),
|
|
173
|
-
channelVersion: config.channelVersion,
|
|
174
|
-
botAgent: config.botAgent,
|
|
175
|
-
cdnBaseUrl: config.cdnBaseUrl,
|
|
176
|
-
});
|
|
206
|
+
return clientOf(await clientModule.get(), config);
|
|
177
207
|
},
|
|
178
208
|
saveAccount,
|
|
179
209
|
primaryAccount,
|
|
180
|
-
readHistory: (peer, count) =>
|
|
181
|
-
readPeerHistory
|
|
210
|
+
readHistory: async (peer, count) => {
|
|
211
|
+
const { readPeerHistory } = await import("./commands/history.ts");
|
|
212
|
+
return readPeerHistory(ctxRef?.cwd ?? process.cwd(), join(wechatDir(), "sessions"), peer, count);
|
|
213
|
+
},
|
|
182
214
|
logout: () => {
|
|
183
215
|
clearCredentials();
|
|
184
216
|
// The account is the on/off switch: without one there is nothing to report,
|
|
@@ -186,6 +218,7 @@ export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
|
186
218
|
// counters. The next `ensureBridge`/`autoStart` builds a fresh bridge from
|
|
187
219
|
// the new account.
|
|
188
220
|
bridge = undefined;
|
|
221
|
+
building = undefined;
|
|
189
222
|
},
|
|
190
223
|
now: () => Date.now(),
|
|
191
224
|
};
|
|
@@ -193,11 +226,12 @@ export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
|
193
226
|
registerCommands(pi, commandDeps);
|
|
194
227
|
registerTools(pi, { getBridge: () => bridge });
|
|
195
228
|
|
|
196
|
-
pi.on("session_start", (_event, ctx) => {
|
|
229
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
197
230
|
ctxRef = ctx;
|
|
198
231
|
// Stored credentials mean the bridge should be polling, so a new session
|
|
199
232
|
// (startup, reload, `/new`, resume, fork) reconnects it without a command.
|
|
200
|
-
|
|
233
|
+
// The bridge module loads here, on the first account, not at registration.
|
|
234
|
+
await autoStart(ctx);
|
|
201
235
|
});
|
|
202
236
|
pi.on("session_shutdown", async () => {
|
|
203
237
|
// Shutdown is emitted before the host resets the UI, so the chip is still ours to clear.
|
|
@@ -207,6 +241,7 @@ export default function wechat(pi: ExtensionAPI, deps: WechatDeps = {}): void {
|
|
|
207
241
|
} finally {
|
|
208
242
|
// A throwing close must not leave a live handle on a dead runtime.
|
|
209
243
|
bridge = undefined;
|
|
244
|
+
building = undefined;
|
|
210
245
|
ctxRef = undefined;
|
|
211
246
|
}
|
|
212
247
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reading and writing `<
|
|
2
|
+
* Reading and writing `<agent-dir>/wechat/credentials.json`.
|
|
3
3
|
*
|
|
4
4
|
* The file is written `0600` and holds one entry per bot account, keyed by
|
|
5
5
|
* `ilinkBotId`. Malformed or partial entries are dropped on read rather than
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reading and writing `<
|
|
2
|
+
* Reading and writing `<agent-dir>/wechat/state.json`.
|
|
3
3
|
*
|
|
4
4
|
* Holds the long-poll cursor, the owner id, and per-peer reply tokens. It is
|
|
5
5
|
* validated on read; a corrupt file degrades to empty state rather than losing
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/wechat` status facts and their one-line summary (text only).
|
|
3
|
+
*
|
|
4
|
+
* `statusView` turns the stored account and the live bridge status into the
|
|
5
|
+
* facts the control screen and the text modes both read, and `statusLine` is
|
|
6
|
+
* what a non-TUI turn prints. They live in `text/` rather than beside the
|
|
7
|
+
* screen because the text modes need them and must never load the screen:
|
|
8
|
+
* `view/status-screen.ts` is a full-screen surface and loads on first use.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
12
|
+
import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
13
|
+
import type { BridgeStatus } from "../bridge.ts";
|
|
14
|
+
import { peerLabel } from "./format.ts";
|
|
15
|
+
import type { BotCredentials } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
/** User-facing connection word; the internal `BridgeState` stays running/stopped. */
|
|
18
|
+
export type StatusConnection = "open" | "expired" | "paused" | "closed";
|
|
19
|
+
|
|
20
|
+
/** The facts the status screen and the one-line summary render. */
|
|
21
|
+
export interface StatusView {
|
|
22
|
+
/** Whether a bridge exists, so the activity counters are meaningful. */
|
|
23
|
+
bridge: boolean;
|
|
24
|
+
account?: string;
|
|
25
|
+
connection: StatusConnection;
|
|
26
|
+
owner?: string;
|
|
27
|
+
peers: number;
|
|
28
|
+
queued: number;
|
|
29
|
+
/** Per-peer activity, so an operator can see who is waiting and for how long. */
|
|
30
|
+
peerRows: PeerRow[];
|
|
31
|
+
refused: number;
|
|
32
|
+
/** Labels of the refused senders, so an operator can allowlist one. */
|
|
33
|
+
refusedPeers: string[];
|
|
34
|
+
mediaDropped: number;
|
|
35
|
+
promptPending: boolean;
|
|
36
|
+
/** Every waiting prompt, one per peer that was asked something. */
|
|
37
|
+
prompts: Array<{ peer: string; title: string; startedAt: number }>;
|
|
38
|
+
/** The clock the view was built with, for the prompt age. */
|
|
39
|
+
now?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One peer's line in the control screen. */
|
|
43
|
+
export interface PeerRow {
|
|
44
|
+
/** The raw peer id, so an action targets the right person; falls back to `label`. */
|
|
45
|
+
id?: string;
|
|
46
|
+
label: string;
|
|
47
|
+
status: "idle" | "queued" | "running";
|
|
48
|
+
queued: number;
|
|
49
|
+
runningMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The connection word for a bridge status, or `closed` when there is none. */
|
|
53
|
+
export function connectionOf(status: BridgeStatus | undefined): StatusConnection {
|
|
54
|
+
return status?.paused ? "paused" : status?.open ? "open" : status?.expired ? "expired" : "closed";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build the view from the stored account and the live bridge status. */
|
|
58
|
+
export function statusView(
|
|
59
|
+
account: BotCredentials | undefined,
|
|
60
|
+
status: BridgeStatus | undefined,
|
|
61
|
+
now: number = Date.now(),
|
|
62
|
+
): StatusView {
|
|
63
|
+
return {
|
|
64
|
+
bridge: status !== undefined,
|
|
65
|
+
account: account ? peerLabel(account.ilinkUserId) : undefined,
|
|
66
|
+
connection: connectionOf(status),
|
|
67
|
+
owner: status?.owner ? peerLabel(status.owner) : undefined,
|
|
68
|
+
peers: status?.peerCount ?? 0,
|
|
69
|
+
queued: status?.queued ?? 0,
|
|
70
|
+
peerRows: (status?.peerStates ?? []).map((entry) => ({
|
|
71
|
+
id: entry.peer,
|
|
72
|
+
label: peerLabel(entry.peer),
|
|
73
|
+
status: entry.state.status,
|
|
74
|
+
queued: entry.state.queued,
|
|
75
|
+
runningMs: entry.state.runningMs,
|
|
76
|
+
})),
|
|
77
|
+
refused: status?.refused ?? 0,
|
|
78
|
+
refusedPeers: status?.refusedPeers ?? [],
|
|
79
|
+
mediaDropped: status?.mediaDropped ?? 0,
|
|
80
|
+
promptPending: status?.promptPending ?? false,
|
|
81
|
+
prompts: (status?.prompts ?? []).map((prompt) => ({
|
|
82
|
+
peer: sanitizeLine(peerLabel(prompt.peer)),
|
|
83
|
+
title: sanitizeLine(prompt.title),
|
|
84
|
+
startedAt: prompt.startedAt,
|
|
85
|
+
})),
|
|
86
|
+
now,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The one-line summary used outside the TUI. */
|
|
91
|
+
export function statusLine(view: StatusView): string {
|
|
92
|
+
const parts = [view.account ? `account ${view.account}` : "not logged in", view.connection];
|
|
93
|
+
if (view.bridge) {
|
|
94
|
+
parts.push(`${view.peers} peer(s)`, `${view.queued} queued`);
|
|
95
|
+
if (view.refused > 0) parts.push(`refused ${refusedValue(view)}`);
|
|
96
|
+
if (view.mediaDropped > 0) parts.push(`${view.mediaDropped} media dropped`);
|
|
97
|
+
if (view.owner) parts.push(`owner ${view.owner}`);
|
|
98
|
+
const active = view.peerRows.find((row) => row.status === "running");
|
|
99
|
+
if (active) parts.push(`running ${active.label}`);
|
|
100
|
+
}
|
|
101
|
+
const recovery = view.connection === "expired" ? " — run /wechat login to reconnect" : "";
|
|
102
|
+
return `wechat: ${parts.join(SEPARATORS.item)}${recovery}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The refusal count with the senders named: naming them is what makes
|
|
107
|
+
* `allowedPeers` usable, and either id form can be pasted into the config.
|
|
108
|
+
*/
|
|
109
|
+
export function refusedValue(view: StatusView): string {
|
|
110
|
+
const who = view.refusedPeers.length > 0 ? ` (${view.refusedPeers.join(", ")})` : "";
|
|
111
|
+
return `${view.refused}${who}`;
|
|
112
|
+
}
|
|
@@ -93,7 +93,7 @@ export interface BotCredentials {
|
|
|
93
93
|
createdAt: number;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** Persisted `<
|
|
96
|
+
/** Persisted `<agent-dir>/wechat/credentials.json`. */
|
|
97
97
|
export interface CredentialsFile {
|
|
98
98
|
accounts: Record<string, BotCredentials>;
|
|
99
99
|
/**
|
|
@@ -111,7 +111,7 @@ export interface PeerState {
|
|
|
111
111
|
lastSeen: number;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/** Persisted `<
|
|
114
|
+
/** Persisted `<agent-dir>/wechat/state.json`. */
|
|
115
115
|
export interface WechatState {
|
|
116
116
|
cursor: string;
|
|
117
117
|
ownerId?: string;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import type { Component } from "@earendil-works/pi-tui";
|
|
14
|
-
import { formatDuration
|
|
14
|
+
import { formatDuration } from "../../../lib/format.ts";
|
|
15
15
|
import { screenFooter, screenHint, type ScreenHint } from "../../../lib/tui/chrome.ts";
|
|
16
16
|
import { clip } from "../../../lib/tui/columns.ts";
|
|
17
17
|
import { formatRange, IdCursor, isCloseKey, navIntent } from "../../../lib/tui/cursor.ts";
|
|
@@ -19,12 +19,7 @@ import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
|
19
19
|
import { keyValueRow, listRow } from "../../../lib/tui/rows.ts";
|
|
20
20
|
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
21
21
|
import { paint, type TuiRole } from "../../../lib/tui/tokens.ts";
|
|
22
|
-
import type
|
|
23
|
-
import { peerLabel } from "../text/format.ts";
|
|
24
|
-
import type { BotCredentials } from "../types.ts";
|
|
25
|
-
|
|
26
|
-
/** User-facing connection word; the internal `BridgeState` stays running/stopped. */
|
|
27
|
-
export type StatusConnection = "open" | "expired" | "paused" | "closed";
|
|
22
|
+
import { type PeerRow, refusedValue, type StatusConnection, type StatusView } from "../text/status.ts";
|
|
28
23
|
|
|
29
24
|
/**
|
|
30
25
|
* What the status screen closed with: a global action, a peer history request,
|
|
@@ -32,106 +27,12 @@ export type StatusConnection = "open" | "expired" | "paused" | "closed";
|
|
|
32
27
|
*/
|
|
33
28
|
export type StatusAction = "close" | "login" | "new" | "logout" | { kind: "history"; peer: string } | { kind: "allow" };
|
|
34
29
|
|
|
35
|
-
/** The facts the status screen and the one-line summary render. */
|
|
36
|
-
export interface StatusView {
|
|
37
|
-
/** Whether a bridge exists, so the activity counters are meaningful. */
|
|
38
|
-
bridge: boolean;
|
|
39
|
-
account?: string;
|
|
40
|
-
connection: StatusConnection;
|
|
41
|
-
owner?: string;
|
|
42
|
-
peers: number;
|
|
43
|
-
queued: number;
|
|
44
|
-
/** Per-peer activity, so an operator can see who is waiting and for how long. */
|
|
45
|
-
peerRows: PeerRow[];
|
|
46
|
-
refused: number;
|
|
47
|
-
/** Labels of the refused senders, so an operator can allowlist one. */
|
|
48
|
-
refusedPeers: string[];
|
|
49
|
-
mediaDropped: number;
|
|
50
|
-
promptPending: boolean;
|
|
51
|
-
/** Every waiting prompt, one per peer that was asked something. */
|
|
52
|
-
prompts: Array<{ peer: string; title: string; startedAt: number }>;
|
|
53
|
-
/** The clock the view was built with, for the prompt age. */
|
|
54
|
-
now?: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** One peer's line in the control screen. */
|
|
58
|
-
export interface PeerRow {
|
|
59
|
-
/** The raw peer id, so an action targets the right person; falls back to `label`. */
|
|
60
|
-
id?: string;
|
|
61
|
-
label: string;
|
|
62
|
-
status: "idle" | "queued" | "running";
|
|
63
|
-
queued: number;
|
|
64
|
-
runningMs?: number;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
30
|
/** How many peer rows the screen lists before scrolling. */
|
|
68
31
|
const MAX_PEER_ROWS = 5;
|
|
69
32
|
|
|
70
|
-
/** The connection word for a bridge status, or `closed` when there is none. */
|
|
71
|
-
export function connectionOf(status: BridgeStatus | undefined): StatusConnection {
|
|
72
|
-
return status?.paused ? "paused" : status?.open ? "open" : status?.expired ? "expired" : "closed";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Build the view from the stored account and the live bridge status. */
|
|
76
|
-
export function statusView(
|
|
77
|
-
account: BotCredentials | undefined,
|
|
78
|
-
status: BridgeStatus | undefined,
|
|
79
|
-
now: number = Date.now(),
|
|
80
|
-
): StatusView {
|
|
81
|
-
return {
|
|
82
|
-
bridge: status !== undefined,
|
|
83
|
-
account: account ? peerLabel(account.ilinkUserId) : undefined,
|
|
84
|
-
connection: connectionOf(status),
|
|
85
|
-
owner: status?.owner ? peerLabel(status.owner) : undefined,
|
|
86
|
-
peers: status?.peerCount ?? 0,
|
|
87
|
-
queued: status?.queued ?? 0,
|
|
88
|
-
peerRows: (status?.peerStates ?? []).map((entry) => ({
|
|
89
|
-
id: entry.peer,
|
|
90
|
-
label: peerLabel(entry.peer),
|
|
91
|
-
status: entry.state.status,
|
|
92
|
-
queued: entry.state.queued,
|
|
93
|
-
runningMs: entry.state.runningMs,
|
|
94
|
-
})),
|
|
95
|
-
refused: status?.refused ?? 0,
|
|
96
|
-
refusedPeers: status?.refusedPeers ?? [],
|
|
97
|
-
mediaDropped: status?.mediaDropped ?? 0,
|
|
98
|
-
promptPending: status?.promptPending ?? false,
|
|
99
|
-
prompts: (status?.prompts ?? []).map((prompt) => ({
|
|
100
|
-
peer: sanitizeLine(peerLabel(prompt.peer)),
|
|
101
|
-
title: sanitizeLine(prompt.title),
|
|
102
|
-
startedAt: prompt.startedAt,
|
|
103
|
-
})),
|
|
104
|
-
now,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The one-line summary used outside the TUI. */
|
|
109
|
-
export function statusLine(view: StatusView): string {
|
|
110
|
-
const parts = [view.account ? `account ${view.account}` : "not logged in", view.connection];
|
|
111
|
-
if (view.bridge) {
|
|
112
|
-
parts.push(`${view.peers} peer(s)`, `${view.queued} queued`);
|
|
113
|
-
if (view.refused > 0) parts.push(`refused ${refusedValue(view)}`);
|
|
114
|
-
if (view.mediaDropped > 0) parts.push(`${view.mediaDropped} media dropped`);
|
|
115
|
-
if (view.owner) parts.push(`owner ${view.owner}`);
|
|
116
|
-
const active = view.peerRows.find((row) => row.status === "running");
|
|
117
|
-
if (active) parts.push(`running ${active.label}`);
|
|
118
|
-
}
|
|
119
|
-
const recovery = view.connection === "expired" ? " — run /wechat login to reconnect" : "";
|
|
120
|
-
return `wechat: ${parts.join(SEPARATORS.item)}${recovery}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
33
|
/** Label column width in the screen body. */
|
|
124
34
|
const LABEL_WIDTH = 11;
|
|
125
35
|
|
|
126
|
-
/**
|
|
127
|
-
* The refusal count with the senders named: naming them is what makes
|
|
128
|
-
* `allowedPeers` usable, and either id form can be pasted into the config.
|
|
129
|
-
*/
|
|
130
|
-
function refusedValue(view: StatusView): string {
|
|
131
|
-
const who = view.refusedPeers.length > 0 ? ` (${view.refusedPeers.join(", ")})` : "";
|
|
132
|
-
return `${view.refused}${who}`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
36
|
/** One body row: a `meta` label column and a value in `valueRole`. */
|
|
136
37
|
function row(theme: Theme, width: number, label: string, value: string, valueRole: TuiRole = "label"): string {
|
|
137
38
|
return keyValueRow(theme, width, { label, value, labelWidth: LABEL_WIDTH, labelRole: "meta", valueRole });
|
|
@@ -302,12 +203,12 @@ export class StatusScreenComponent implements Component {
|
|
|
302
203
|
private hints(view: StatusView): ScreenHint[] {
|
|
303
204
|
const hints: ScreenHint[] = ["Esc close"];
|
|
304
205
|
if (view.peerRows.length > 0) {
|
|
305
|
-
hints.push("j/k move", { text: "s stop", color: "
|
|
206
|
+
hints.push("j/k move", { text: "s stop", color: "warn" }, { text: "d reset", color: "warn" }, "h history");
|
|
306
207
|
}
|
|
307
208
|
hints.push(
|
|
308
209
|
{ text: "l login", color: "accent" },
|
|
309
210
|
{ text: "n new", color: "accent" },
|
|
310
|
-
{ text: "o logout", color: "
|
|
211
|
+
{ text: "o logout", color: "bad" },
|
|
311
212
|
"r refresh",
|
|
312
213
|
);
|
|
313
214
|
return hints;
|
|
@@ -16,19 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { messageOf } from "../../lib/errors.ts";
|
|
19
|
-
import { planModeRefusal } from "../../lib/plan-entry.ts";
|
|
20
|
-
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
21
|
-
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
22
19
|
import {
|
|
20
|
+
enteredWorktree,
|
|
23
21
|
enterWorktree,
|
|
24
22
|
exitWorktree,
|
|
25
23
|
formatWorktreeStatus,
|
|
26
24
|
isWorktreeCancelled,
|
|
27
25
|
pruneWorktrees,
|
|
28
|
-
enteredWorktree,
|
|
29
26
|
worktreeSnapshot,
|
|
30
27
|
} from "./lifecycle.ts";
|
|
31
|
-
import {
|
|
28
|
+
import type { WorktreeAction } from "./screen.ts";
|
|
32
29
|
|
|
33
30
|
interface Completion {
|
|
34
31
|
value: string;
|
|
@@ -45,8 +42,10 @@ const SUBCOMMANDS: Completion[] = [
|
|
|
45
42
|
const USAGE = "Usage: /worktree [enter [name]|exit|prune]";
|
|
46
43
|
|
|
47
44
|
/** Refuse a lifecycle change while plan mode is read-only, as `/check` does. */
|
|
48
|
-
function refuseInPlanMode(ctx: ExtensionContext): boolean {
|
|
45
|
+
async function refuseInPlanMode(ctx: ExtensionContext): Promise<boolean> {
|
|
46
|
+
const { isPlanModeEnabled } = await import("../../lib/plan-state.ts");
|
|
49
47
|
if (!isPlanModeEnabled(ctx.sessionManager.getBranch())) return false;
|
|
48
|
+
const { planModeRefusal } = await import("../../lib/plan-entry.ts");
|
|
50
49
|
ctx.ui.notify(planModeRefusal("changing worktrees"), "warning");
|
|
51
50
|
return true;
|
|
52
51
|
}
|
|
@@ -71,7 +70,7 @@ async function reportLifecycle(ctx: ExtensionContext, verb: string, run: () => P
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
async function runEnter(pi: ExtensionAPI, ctx: ExtensionContext, name: string | undefined): Promise<void> {
|
|
74
|
-
if (refuseInPlanMode(ctx)) return;
|
|
73
|
+
if (await refuseInPlanMode(ctx)) return;
|
|
75
74
|
// The path on its own line: a command's answer is read without a tool result beside it,
|
|
76
75
|
// and "which directory am I in now" is the question entering answers.
|
|
77
76
|
await reportLifecycle(ctx, "Enter", async () => {
|
|
@@ -81,12 +80,12 @@ async function runEnter(pi: ExtensionAPI, ctx: ExtensionContext, name: string |
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
async function runExit(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
84
|
-
if (refuseInPlanMode(ctx)) return;
|
|
83
|
+
if (await refuseInPlanMode(ctx)) return;
|
|
85
84
|
await reportLifecycle(ctx, "Exit", async () => (await exitWorktree(pi, ctx, {})).output);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
async function runPrune(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
89
|
-
if (refuseInPlanMode(ctx)) return;
|
|
88
|
+
if (await refuseInPlanMode(ctx)) return;
|
|
90
89
|
ctx.ui.notify(await pruneWorktrees(pi, ctx), "info");
|
|
91
90
|
}
|
|
92
91
|
|
|
@@ -100,6 +99,9 @@ async function showWorktrees(pi: ExtensionAPI, ctx: ExtensionContext): Promise<v
|
|
|
100
99
|
// The screen closes before an enter or exit runs: both change the session
|
|
101
100
|
// binding, and `exitWorktree` asks through `ctx.ui.select`, which cannot nest
|
|
102
101
|
// inside a mounted screen. Prune runs in place, so its verb stays on the screen.
|
|
102
|
+
const { withRailsSuppressed } = await import("../../lib/rails.ts");
|
|
103
|
+
const { WorktreeListComponent } = await import("./screen.ts");
|
|
104
|
+
const { isPlanModeEnabled } = await import("../../lib/plan-state.ts");
|
|
103
105
|
const action = await withRailsSuppressed(pi, () =>
|
|
104
106
|
ctx.ui.custom<WorktreeAction | undefined>((tui, theme, _keybindings, done) => {
|
|
105
107
|
return new WorktreeListComponent({
|
|
@@ -110,7 +110,7 @@ export async function enterWorktree(
|
|
|
110
110
|
}
|
|
111
111
|
if (current) {
|
|
112
112
|
throw new Error(
|
|
113
|
-
`Already working in a worktree (${worktreeLabel(current)}). Run /worktree exit, or call
|
|
113
|
+
`Already working in a worktree (${worktreeLabel(current)}). Run /worktree exit, or call \`worktree\` action \`exit\`, before entering another.`,
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -269,7 +269,7 @@ export async function enterWorktree(
|
|
|
269
269
|
setStatus(ctx, `${GLYPHS.worktree} ${worktreeLabel(state)}`);
|
|
270
270
|
const summary =
|
|
271
271
|
`Entered worktree.\n${stateSummary(state)}\n\n` +
|
|
272
|
-
`Relative paths now resolve inside the worktree. When finished, call
|
|
272
|
+
`Relative paths now resolve inside the worktree. When finished, call \`worktree\` action \`exit\`.` +
|
|
273
273
|
(copiedIncludes.length > 0 ? `\n\nCopied ${copiedIncludes.length} gitignored file(s).` : "");
|
|
274
274
|
return { state, summary };
|
|
275
275
|
}
|