@gavin-hu/my-pi-agent 0.21.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 -31
- package/README.zh-CN.md +40 -26
- 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 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- 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 +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- 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 +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- 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 +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- 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 +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- 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/{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/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -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 +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- 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/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- 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 +49 -39
- 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-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- 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 +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- 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/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- 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 -7
- 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/view/todo-screen.ts +122 -0
- 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 +27 -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 +1 -1
- 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 +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- 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 +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- 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 +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- 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
package/lib/env.ts
CHANGED
|
@@ -12,24 +12,82 @@ export const ENV_ROOT = "PI_WORKTREE_ROOT";
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Env var listing extension names to skip. Entries are separated by commas
|
|
15
|
-
* and/or whitespace. Unset or empty means every extension loads
|
|
15
|
+
* and/or whitespace. Unset or empty means every extension loads except the
|
|
16
|
+
* opt-in ones.
|
|
16
17
|
*/
|
|
17
18
|
export const ENV_DISABLED_EXTENSIONS = "PI_DISABLED_EXTENSIONS";
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Env var listing the opt-in extensions to load. Entries are separated by
|
|
22
|
+
* commas and/or whitespace. Only `OPT_IN_EXTENSIONS` reads it; it never
|
|
23
|
+
* switches off an extension that loads by default.
|
|
24
|
+
*/
|
|
25
|
+
export const ENV_ENABLED_EXTENSIONS = "PI_ENABLED_EXTENSIONS";
|
|
26
|
+
|
|
19
27
|
/** Env var that turns off Pi's automatic network activity, including our own. */
|
|
20
28
|
export const ENV_OFFLINE = "PI_OFFLINE";
|
|
21
29
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Extensions that stay unloaded unless `PI_ENABLED_EXTENSIONS` names them.
|
|
32
|
+
*
|
|
33
|
+
* `board` and `team` are one workflow — a work list, and the lead's half of it
|
|
34
|
+
* — so a session opts into both or neither. They stay independent at runtime:
|
|
35
|
+
* naming one never loads the other.
|
|
36
|
+
*
|
|
37
|
+
* They are declared in the package manifest like every other extension, so Pi
|
|
38
|
+
* still discovers them and their docs stay in the pack; the surface is simply
|
|
39
|
+
* not registered until the session asks for it. `new Set` rather than an array
|
|
40
|
+
* literal because `test/boundaries.test.ts` rejects a module-level array in
|
|
41
|
+
* `lib/`, and a fixed keyword table is the one container that rule allows.
|
|
42
|
+
*/
|
|
43
|
+
export const OPT_IN_EXTENSIONS: ReadonlySet<string> = new Set(["board", "team"]);
|
|
44
|
+
|
|
45
|
+
/** What the environment says about one extension this session. */
|
|
46
|
+
export type ExtensionEnablement = "on" | "disabled" | "opt-in";
|
|
47
|
+
|
|
48
|
+
/** The names in an extension-list variable, lowercased; empty when it is unset. */
|
|
49
|
+
function listedExtensions(variable: string): Set<string> {
|
|
50
|
+
const raw = process.env[variable];
|
|
51
|
+
if (!raw) return new Set();
|
|
52
|
+
return new Set(
|
|
27
53
|
raw
|
|
28
54
|
.split(/[\s,]+/)
|
|
29
55
|
.map((entry) => entry.trim().toLowerCase())
|
|
30
56
|
.filter(Boolean),
|
|
31
57
|
);
|
|
32
|
-
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How this session treats `name`: `disabled` when `PI_DISABLED_EXTENSIONS`
|
|
62
|
+
* names it, `opt-in` when only `PI_ENABLED_EXTENSIONS` can switch it on and has
|
|
63
|
+
* not, and `on` otherwise. An explicit disable wins over an opt-in name.
|
|
64
|
+
*/
|
|
65
|
+
export function extensionEnablement(name: string): ExtensionEnablement {
|
|
66
|
+
const key = name.toLowerCase();
|
|
67
|
+
if (listedExtensions(ENV_DISABLED_EXTENSIONS).has(key)) return "disabled";
|
|
68
|
+
if (OPT_IN_EXTENSIONS.has(key) && !listedExtensions(ENV_ENABLED_EXTENSIONS).has(key)) return "opt-in";
|
|
69
|
+
return "on";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether `name` loads this session, i.e. {@link extensionEnablement} says `on`. */
|
|
73
|
+
export function isExtensionEnabled(name: string): boolean {
|
|
74
|
+
return extensionEnablement(name) === "on";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What a report says about `name`: why this session is not loading it, or
|
|
79
|
+
* undefined when it is. One call answers both, so the state and the sentence
|
|
80
|
+
* cannot disagree, and the switch it names is the one that would change it.
|
|
81
|
+
*/
|
|
82
|
+
export function extensionDisabledReason(name: string): string | undefined {
|
|
83
|
+
switch (extensionEnablement(name)) {
|
|
84
|
+
case "on":
|
|
85
|
+
return undefined;
|
|
86
|
+
case "opt-in":
|
|
87
|
+
return `an opt-in extension; load it with \`${ENV_ENABLED_EXTENSIONS}=${name.toLowerCase()}\``;
|
|
88
|
+
case "disabled":
|
|
89
|
+
return `disabled through \`${ENV_DISABLED_EXTENSIONS}\``;
|
|
90
|
+
}
|
|
33
91
|
}
|
|
34
92
|
|
|
35
93
|
/**
|
package/lib/format.ts
CHANGED
|
@@ -38,6 +38,19 @@ export function formatCount(count: number): string {
|
|
|
38
38
|
return sign + out;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A list in a sentence: `a, b, or c`, or the same with `and`.
|
|
43
|
+
*
|
|
44
|
+
* One body, because a refusal that names the lanes a card may move to, one that names the
|
|
45
|
+
* fields a contract needs, and one that names the lanes a tool accepts are the same
|
|
46
|
+
* sentence, and a second copy drifts on the comma.
|
|
47
|
+
*/
|
|
48
|
+
export function listOf(entries: readonly string[], conjunction: "or" | "and" = "or"): string {
|
|
49
|
+
if (entries.length === 0) return "nothing";
|
|
50
|
+
if (entries.length === 1) return entries[0] ?? "nothing";
|
|
51
|
+
return `${entries.slice(0, -1).join(", ")}, ${conjunction} ${entries[entries.length - 1]}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/**
|
|
42
55
|
* Compact duration: `450ms`, `3.2s`, `1m05s`, `2h03m`. Non-finite or
|
|
43
56
|
* negative input reads `0ms`, so a missing timestamp never prints `NaN`.
|
package/lib/http/index.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The loopback HTTP layer
|
|
3
|
-
* response shape, the header policy, the request-envelope
|
|
4
|
-
* guards built on them, the one server entry, the socket seam
|
|
5
|
-
*
|
|
2
|
+
* The served-page kit: the loopback HTTP layer `file-browse`, `plan`, and `board`
|
|
3
|
+
* answer on — the response shape, the header policy, the request-envelope
|
|
4
|
+
* helpers and the guards built on them, the one server entry, the socket seam
|
|
5
|
+
* behind it, and the port band it draws from.
|
|
6
6
|
*
|
|
7
|
-
* Import from here rather than a leaf module, so the layer reads as one kit.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
7
|
+
* Import from here rather than a leaf module, so the layer reads as one kit. This
|
|
8
|
+
* barrel is deliberately *not* on a registration path: it is 978 lines that only
|
|
9
|
+
* a page that is actually served needs, so an extension reaches it from the
|
|
10
|
+
* handler that mounts one, never from its factory. The one exception is the
|
|
11
|
+
* mount contract itself — {@link requestMount} and {@link HTTP_MOUNT_CHANNEL},
|
|
12
|
+
* which live in `./mount.ts` — because asking for a prefix is what a command does
|
|
13
|
+
* before any page exists. `board` and `plan` import that leaf directly; `serve`
|
|
14
|
+
* imports it for the channel and loads this barrel on the first bind; a new
|
|
15
|
+
* `lib/http/` leaf imported from a factory puts the whole kit back on every start,
|
|
16
|
+
* which `test/dependencies.test.ts` fails.
|
|
15
17
|
*
|
|
16
18
|
* Value-only, like the rest of `lib/`: the only state is what a returned closure
|
|
17
19
|
* carries (`createCloser`, `serveHttp`).
|
package/lib/http/port.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The port band the loopback pages share, and the number a project keeps in it.
|
|
3
3
|
*
|
|
4
|
-
* `serve`
|
|
4
|
+
* `serve` takes a session's one port from here, so every page that session serves
|
|
5
5
|
* clusters where a person's bookmarks already point. The band is one product decision,
|
|
6
6
|
* stated here once: a number repeated in each extension's config is several chances to
|
|
7
7
|
* move one of them, and nothing would notice until a bookmark landed outside the range.
|
|
8
8
|
*
|
|
9
|
+
* A project's number is its repository key hashed into the band rather than a fresh
|
|
10
|
+
* draw, so a session that starts again lands on the port its bookmark names; two
|
|
11
|
+
* repositories take two numbers without a setting. `serve.port` overrides it.
|
|
12
|
+
*
|
|
9
13
|
* A pick is a guess, not a reservation. `serveHttp` binds it and falls back to whatever
|
|
10
|
-
* free port the OS gives, so a port another process holds
|
|
11
|
-
* start.
|
|
14
|
+
* free port the OS gives, so a port another process holds — or another project's hash
|
|
15
|
+
* landing in the same slot — costs a retry rather than a start.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
|
-
import {
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
15
19
|
|
|
16
|
-
/** Base of the per-
|
|
20
|
+
/** Base of the per-project port range. */
|
|
17
21
|
export const PORT_BASE = 4780;
|
|
18
22
|
|
|
19
|
-
/** Number of ports in the per-
|
|
23
|
+
/** Number of ports in the per-project range. */
|
|
20
24
|
export const PORT_RANGE = 1000;
|
|
21
25
|
|
|
22
|
-
/**
|
|
23
|
-
export function
|
|
24
|
-
|
|
26
|
+
/** The port a project prefers: its repository key hashed into `PORT_BASE`–`PORT_BASE + PORT_RANGE - 1`. */
|
|
27
|
+
export function projectPort(repoKey: string): number {
|
|
28
|
+
const digest = createHash("sha256").update(repoKey).digest();
|
|
29
|
+
return PORT_BASE + (digest.readUInt32BE(0) % PORT_RANGE);
|
|
25
30
|
}
|
package/lib/lazy.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One async load, run at most once.
|
|
3
|
+
*
|
|
4
|
+
* A surface a command needs but registration does not — a browser page, a
|
|
5
|
+
* review server, a screen — is imported through this: `get()` runs the loader
|
|
6
|
+
* and remembers the value, and `peek()` answers the value already loaded, or
|
|
7
|
+
* `undefined`, so a shutdown path never starts a load the session never used.
|
|
8
|
+
*
|
|
9
|
+
* A rejected load is not remembered. A transient failure can be retried, and a
|
|
10
|
+
* caller that sees the error can ask again; the alternative would make one bad
|
|
11
|
+
* import permanent for the process.
|
|
12
|
+
*
|
|
13
|
+
* State lives in this closure, not at module scope, because `lib/` is loaded
|
|
14
|
+
* with isolated module caches and is value-only.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A value loaded on first use, with a way to ask whether it is already here. */
|
|
18
|
+
export interface Lazy<T> {
|
|
19
|
+
/** Load the value, or return the one already loaded; concurrent calls share one load. */
|
|
20
|
+
get(): Promise<T>;
|
|
21
|
+
/** The loaded value, or `undefined` before a load and after a failed one. */
|
|
22
|
+
peek(): T | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function lazy<T>(load: () => Promise<T>): Lazy<T> {
|
|
26
|
+
let loaded = false;
|
|
27
|
+
let value: T;
|
|
28
|
+
let pending: Promise<T> | undefined;
|
|
29
|
+
const get = (): Promise<T> => {
|
|
30
|
+
if (loaded) return Promise.resolve(value);
|
|
31
|
+
pending ??= load().then(
|
|
32
|
+
(result) => {
|
|
33
|
+
loaded = true;
|
|
34
|
+
value = result;
|
|
35
|
+
return result;
|
|
36
|
+
},
|
|
37
|
+
(error: unknown) => {
|
|
38
|
+
pending = undefined;
|
|
39
|
+
throw error;
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
return pending;
|
|
43
|
+
};
|
|
44
|
+
return { get, peek: () => (loaded ? value : undefined) };
|
|
45
|
+
}
|
package/lib/plan-entry.ts
CHANGED
|
@@ -18,20 +18,24 @@
|
|
|
18
18
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { type BusAnswer, requestOverBus } from "./bus-request.ts";
|
|
20
20
|
import { isPlanModeEnabled } from "./plan-state.ts";
|
|
21
|
-
import {
|
|
21
|
+
import { PLAN_TOOL } from "./tool-names.ts";
|
|
22
22
|
|
|
23
23
|
/** EventBus channel on which a caller asks `plan` to enable plan mode. */
|
|
24
24
|
export const PLAN_ENTER_REQUEST = "my-pi-agent/plan-mode/enter";
|
|
25
25
|
|
|
26
|
+
/** EventBus channel on which a caller asks `plan` to flip the mode. */
|
|
27
|
+
export const PLAN_TOGGLE_REQUEST = "my-pi-agent/plan-mode/toggle";
|
|
28
|
+
|
|
26
29
|
/** One clause describing the read-only guarantee, reused across prompts. */
|
|
27
30
|
export const READ_ONLY_SUMMARY =
|
|
28
31
|
"write and edit are disabled, bash is limited to read-only git commands, and other mutating tools are blocked; " +
|
|
29
|
-
"subagent delegation is forced read-only, and the only file write is
|
|
30
|
-
"directory
|
|
32
|
+
"subagent delegation is forced read-only, and the only file write is the plan file, saved by the `plan` tool's " +
|
|
33
|
+
"`write` action in the agent directory";
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
* The consent question, as the user sees it. Shown by `
|
|
34
|
-
* any caller that offers plan mode itself, so both describe the same
|
|
36
|
+
* The consent question, as the user sees it. Shown by the `plan` tool's `enter`
|
|
37
|
+
* action and by any caller that offers plan mode itself, so both describe the same
|
|
38
|
+
* guarantee.
|
|
35
39
|
*/
|
|
36
40
|
export const PLAN_CONSENT =
|
|
37
41
|
`Plan mode is read-only exploration: ${READ_ONLY_SUMMARY}. ` +
|
|
@@ -53,12 +57,19 @@ export function planModeRefusal(action: string): string {
|
|
|
53
57
|
return `Plan mode is read-only. Approve the plan before ${action}.`;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
/** What a caller sends on {@link PLAN_ENTER_REQUEST}. */
|
|
57
|
-
export interface
|
|
60
|
+
/** What a caller sends on {@link PLAN_ENTER_REQUEST} or {@link PLAN_TOGGLE_REQUEST}. */
|
|
61
|
+
export interface PlanModeRequest {
|
|
58
62
|
/** The caller's context; `plan` uses it for the footer status. */
|
|
59
63
|
ctx: ExtensionContext;
|
|
60
64
|
/** Claimed once, with whether plan mode is on; the caller settles on that claim. */
|
|
61
|
-
respond(
|
|
65
|
+
respond(enabled: BusAnswer<boolean>): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether the `plan` extension registered its tool in this session.
|
|
70
|
+
*/
|
|
71
|
+
export function isPlanExtensionLoaded(pi: Pick<ExtensionAPI, "getAllTools">): boolean {
|
|
72
|
+
return pi.getAllTools().some((tool) => tool.name === PLAN_TOOL);
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
/**
|
|
@@ -74,10 +85,28 @@ export function requestPlanMode(
|
|
|
74
85
|
ctx: ExtensionContext,
|
|
75
86
|
): Promise<boolean> {
|
|
76
87
|
if (isPlanModeEnabled(ctx.sessionManager.getBranch())) return Promise.resolve(true);
|
|
77
|
-
if (!pi
|
|
78
|
-
return requestOverBus<boolean,
|
|
88
|
+
if (!isPlanExtensionLoaded(pi)) return Promise.resolve(false);
|
|
89
|
+
return requestOverBus<boolean, PlanModeRequest>(pi, {
|
|
79
90
|
channel: PLAN_ENTER_REQUEST,
|
|
80
91
|
payload: (respond) => ({ ctx, respond }),
|
|
81
92
|
unclaimed: () => false,
|
|
82
93
|
});
|
|
83
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Ask `plan` to flip the mode, and report the state it left behind.
|
|
98
|
+
*
|
|
99
|
+
* A toggle has no branch precondition: the owner reads the flag it persisted and
|
|
100
|
+
* resolves the opposite. `undefined` means nothing is listening (the `plan`
|
|
101
|
+
* extension is not loaded), so the caller can leave its own key alone.
|
|
102
|
+
*/
|
|
103
|
+
export function requestPlanToggle(
|
|
104
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
105
|
+
ctx: ExtensionContext,
|
|
106
|
+
): Promise<boolean | undefined> {
|
|
107
|
+
return requestOverBus<boolean | undefined, PlanModeRequest>(pi, {
|
|
108
|
+
channel: PLAN_TOGGLE_REQUEST,
|
|
109
|
+
payload: (respond) => ({ ctx, respond }),
|
|
110
|
+
unclaimed: () => undefined,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a plan lives, and which paths may be read or written as one.
|
|
3
|
+
*
|
|
4
|
+
* A plan file is the artifact a card is executed against, so two extensions have to
|
|
5
|
+
* agree about it: `plan` writes and reads the store, and `board` records the file a
|
|
6
|
+
* card promises. The store is `<agent-dir>/plans/<repo-key>` for a repository and
|
|
7
|
+
* `<agent-dir>/plans` outside one — the key is the same one `repo-key.ts` hands
|
|
8
|
+
* every other per-repository store, which is why the keyed branch lives here rather
|
|
9
|
+
* than in either caller.
|
|
10
|
+
*
|
|
11
|
+
* The rules are one body for the same reason: a copy of `isContainedPlan` that
|
|
12
|
+
* dropped the symlink half would look identical at a glance and would be the one
|
|
13
|
+
* that let a link inside the store point outside it. `plan` passes a path here
|
|
14
|
+
* before it writes; `board` passes the path it is about to record, and asks whether
|
|
15
|
+
* it is a plan of this repository.
|
|
16
|
+
*
|
|
17
|
+
* Value-only: no module state, and the only I/O is the `lstat` a symlink check needs.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { lstatSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
23
|
+
import { isInside, isInsideReal } from "../path.ts";
|
|
24
|
+
import type { RepoKey } from "../repo-key.ts";
|
|
25
|
+
|
|
26
|
+
/** The agent-directory entry plans are filed under. */
|
|
27
|
+
export const PLANS_DIR_NAME = "plans";
|
|
28
|
+
|
|
29
|
+
/** The plan store: `<agent-dir>/plans`, or `<agent-dir>/plans/<key>` inside a repository. */
|
|
30
|
+
export function plansDir(agentDir: string, key?: string): string {
|
|
31
|
+
return key === undefined ? join(agentDir, PLANS_DIR_NAME) : join(agentDir, PLANS_DIR_NAME, key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The store for a resolved repository: keyed when git answered, unkeyed outside one.
|
|
36
|
+
*
|
|
37
|
+
* The `fromGit` branch lives here and nowhere else — that is the line a copy gets wrong,
|
|
38
|
+
* and both the plan tool and the board reach it through this one function.
|
|
39
|
+
*/
|
|
40
|
+
export function plansDirFor(agentDir: string, repo: Pick<RepoKey, "key" | "fromGit">): string {
|
|
41
|
+
return plansDir(agentDir, repo.fromGit ? repo.key : undefined);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A plan file name: markdown and not a dotfile, so the self-ignore file is never a target. */
|
|
45
|
+
export function isPlanFileName(path: string): boolean {
|
|
46
|
+
const name = basename(path);
|
|
47
|
+
return name.toLowerCase().endsWith(".md") && !name.startsWith(".");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether `path` may be read, written, or deleted as a plan. Beyond lexical
|
|
52
|
+
* containment (shared with the other path guards through `lib/path.ts`), this
|
|
53
|
+
* rejects dotfiles (including the directory's own `.gitignore`) and resolves
|
|
54
|
+
* symlinks, so a link placed inside the plans directory cannot redirect a read
|
|
55
|
+
* or write outside it.
|
|
56
|
+
*/
|
|
57
|
+
export function isContainedPlan(dir: string, path: string): boolean {
|
|
58
|
+
if (!isInside(dir, path) || !isPlanFileName(path)) return false;
|
|
59
|
+
try {
|
|
60
|
+
// A symlink itself must never be the target; a missing file is a new plan.
|
|
61
|
+
if (lstatSync(path).isSymbolicLink()) return false;
|
|
62
|
+
} catch {
|
|
63
|
+
// Missing (ENOENT) is expected; any other error falls through to the
|
|
64
|
+
// real-path check, which rejects the path if it cannot be resolved.
|
|
65
|
+
}
|
|
66
|
+
return isInsideReal(dir, dirname(path));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a `plan_path` argument against the store.
|
|
71
|
+
*
|
|
72
|
+
* A plan lives outside the project, so a path relative to the working directory
|
|
73
|
+
* can no longer name one. Absolute paths and the `~/…` form the tools display are
|
|
74
|
+
* accepted, and a bare name resolves inside the store — which is what a reader of
|
|
75
|
+
* a tool result has in hand.
|
|
76
|
+
*/
|
|
77
|
+
export function resolvePlanArg(dir: string, planPath: string): string {
|
|
78
|
+
const text = planPath.trim();
|
|
79
|
+
if (text === "~") return homedir();
|
|
80
|
+
if (text.startsWith("~/") || text.startsWith(`~${sep}`)) return resolve(homedir(), text.slice(2));
|
|
81
|
+
return isAbsolute(text) ? resolve(text) : resolve(dir, text);
|
|
82
|
+
}
|
package/lib/policy.ts
CHANGED
|
@@ -10,24 +10,27 @@
|
|
|
10
10
|
* `write`/`edit` being the only mutators.
|
|
11
11
|
*
|
|
12
12
|
* No host dependency beyond the annotation type: the caller supplies the
|
|
13
|
-
* annotation lookup,
|
|
14
|
-
* module stays value-only.
|
|
13
|
+
* annotation lookup, the one reader name comes from `tool-names.ts`, and which board
|
|
14
|
+
* actions read comes from `board/actions.ts`, so this module stays value-only.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ToolAnnotations } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import {
|
|
18
|
+
import { isBoardReadAction } from "./board/actions.ts";
|
|
19
|
+
import { READ_DOC_TOOL } from "./tool-names.ts";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Whether a call to the `board` tool only reads it.
|
|
22
23
|
*
|
|
23
|
-
* The board is one tool over
|
|
24
|
+
* The board is one tool over many actions, so its `readOnlyHint` says "may write" and
|
|
24
25
|
* cannot answer this. The action can, and it travels in the call's input — and in the
|
|
25
26
|
* result's `details`, which is all a `tool_execution_end` event carries. Plan mode
|
|
26
|
-
* asks before the call and the todo nudge after it, so the answer lives here once
|
|
27
|
+
* asks before the call and the todo nudge after it, so the answer lives here once —
|
|
28
|
+
* and the answer is the board's own table, because an action list kept here is a list
|
|
29
|
+
* that drifts.
|
|
27
30
|
*/
|
|
28
31
|
export function isBoardRead(call: unknown): boolean {
|
|
29
32
|
if (typeof call !== "object" || call === null) return false;
|
|
30
|
-
return (call as { action?: unknown }).action
|
|
33
|
+
return isBoardReadAction((call as { action?: unknown }).action);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
package/lib/report.ts
CHANGED
|
@@ -147,8 +147,9 @@ export interface PendingChannelOptions {
|
|
|
147
147
|
* The order inside `session_start` is the contract: the reporter forgets the
|
|
148
148
|
* previous session's busy window, the runtime loads the records, and only then
|
|
149
149
|
* is the report trigger armed — a reload or a replaced session reuses the same
|
|
150
|
-
* runtime
|
|
151
|
-
*
|
|
150
|
+
* runtime, so a runtime that can settle an outcome while loading holds what it
|
|
151
|
+
* settled until this arms. A shutdown clears the trigger before the runtime
|
|
152
|
+
* releases the table it was calling into.
|
|
152
153
|
*/
|
|
153
154
|
export function registerPendingChannel(
|
|
154
155
|
pi: Pick<ExtensionAPI, "on">,
|
package/lib/requirements.ts
CHANGED
|
@@ -322,8 +322,8 @@ export interface ProbeSeams {
|
|
|
322
322
|
readJson: (path: string) => JsonReadResult;
|
|
323
323
|
dirs: DirSeams;
|
|
324
324
|
platform: NodeJS.Platform;
|
|
325
|
-
/**
|
|
326
|
-
|
|
325
|
+
/** Why this session is not loading the extension, or undefined when it loads it. */
|
|
326
|
+
disabledReason: (extension: string) => string | undefined;
|
|
327
327
|
/** Load an optional package; defaults to a real dynamic import. */
|
|
328
328
|
loadModule?: (name: string) => Promise<unknown>;
|
|
329
329
|
/** Resolve the nested-pi command; defaults to {@link getPiInvocation}. */
|
|
@@ -718,11 +718,11 @@ function configFindings(extension: string, spec: ExtensionRequirements, seams: P
|
|
|
718
718
|
/** Probe one extension against its declaration and the current environment. */
|
|
719
719
|
export async function probeExtension(extension: string, seams: ProbeSeams): Promise<ExtensionReport> {
|
|
720
720
|
const spec = REQUIREMENTS[extension];
|
|
721
|
-
const
|
|
721
|
+
const disabledReason = seams.disabledReason(extension);
|
|
722
722
|
if (!spec) {
|
|
723
723
|
return {
|
|
724
724
|
extension,
|
|
725
|
-
enabled,
|
|
725
|
+
enabled: disabledReason === undefined,
|
|
726
726
|
findings: [
|
|
727
727
|
{
|
|
728
728
|
extension,
|
|
@@ -733,16 +733,16 @@ export async function probeExtension(extension: string, seams: ProbeSeams): Prom
|
|
|
733
733
|
],
|
|
734
734
|
};
|
|
735
735
|
}
|
|
736
|
-
if (
|
|
736
|
+
if (disabledReason !== undefined) {
|
|
737
737
|
return {
|
|
738
738
|
extension,
|
|
739
|
-
enabled,
|
|
739
|
+
enabled: false,
|
|
740
740
|
findings: [
|
|
741
741
|
{
|
|
742
742
|
extension,
|
|
743
743
|
check: "state",
|
|
744
744
|
severity: "info",
|
|
745
|
-
detail:
|
|
745
|
+
detail: disabledReason,
|
|
746
746
|
},
|
|
747
747
|
],
|
|
748
748
|
};
|
|
@@ -755,7 +755,7 @@ export async function probeExtension(extension: string, seams: ProbeSeams): Prom
|
|
|
755
755
|
for (const note of spec.notes ?? []) {
|
|
756
756
|
findings.push({ extension, check: "platform", severity: "info", detail: note });
|
|
757
757
|
}
|
|
758
|
-
return { extension, enabled, findings };
|
|
758
|
+
return { extension, enabled: true, findings };
|
|
759
759
|
}
|
|
760
760
|
|
|
761
761
|
/** Probes in flight at once: enough to hide spawn latency, not a burst. */
|
package/lib/session-label.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Two vocabularies share this module, and neither copies the other. `session-bus`
|
|
5
5
|
* settles its own label for a session that did not ask for one — the Pi session name,
|
|
6
6
|
* else the working directory's basename, else the id. The board names a session for
|
|
7
|
-
* the team instead: the page's seat, or
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* session.
|
|
7
|
+
* the team instead: the page's seat, or a preset teammate name drawn from the id, or
|
|
8
|
+
* `team-agent-` and the id's tail when every preset one is taken — one name on every
|
|
9
|
+
* surface, and one that cannot drift on a rename. The rules are here rather than in
|
|
10
|
+
* either extension because a label is a shared vocabulary: it is displayed, it is a
|
|
11
|
+
* send target, and it is stored in a card file that outlives one session.
|
|
12
12
|
*
|
|
13
13
|
* Value-only: no state, no I/O.
|
|
14
14
|
*/
|
|
@@ -64,6 +64,70 @@ export function agentLabel(id: string): string {
|
|
|
64
64
|
return normalizeLabel(`${TEAM_AGENT_PREFIX}${shortId(id)}`, "team-agent");
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The preset teammate names a session with no name of its own draws from.
|
|
69
|
+
*
|
|
70
|
+
* Familiar English names, one or two syllables each, short enough to type as a `to`
|
|
71
|
+
* target: a teammate should read as a person, and `lily` says more than
|
|
72
|
+
* `team-agent-9f21ac` to the lead who hands it a card.
|
|
73
|
+
*/
|
|
74
|
+
export const TEAMMATE_NAMES = [
|
|
75
|
+
"tom",
|
|
76
|
+
"amy",
|
|
77
|
+
"lily",
|
|
78
|
+
"jack",
|
|
79
|
+
"lucy",
|
|
80
|
+
"mike",
|
|
81
|
+
"kate",
|
|
82
|
+
"sam",
|
|
83
|
+
"bob",
|
|
84
|
+
"anna",
|
|
85
|
+
"helen",
|
|
86
|
+
"peter",
|
|
87
|
+
"alice",
|
|
88
|
+
"emma",
|
|
89
|
+
"jenny",
|
|
90
|
+
"mary",
|
|
91
|
+
"linda",
|
|
92
|
+
"susan",
|
|
93
|
+
"tina",
|
|
94
|
+
"ben",
|
|
95
|
+
"may",
|
|
96
|
+
"rose",
|
|
97
|
+
"david",
|
|
98
|
+
"leo",
|
|
99
|
+
] as const;
|
|
100
|
+
|
|
101
|
+
/** A 32-bit FNV-1a hash, so one id always starts the draw at the same name. */
|
|
102
|
+
function hashOf(text: string): number {
|
|
103
|
+
let hash = 0x811c9dc5;
|
|
104
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
105
|
+
hash ^= text.charCodeAt(index);
|
|
106
|
+
hash = Math.imul(hash, 0x01000193);
|
|
107
|
+
}
|
|
108
|
+
return hash >>> 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The preset name a session with none of its own joins under.
|
|
113
|
+
*
|
|
114
|
+
* Its id hashes to a starting point in the list, and a name in `taken` walks on to the
|
|
115
|
+
* next free one; every name taken is `undefined`, so the caller keeps a fallback that
|
|
116
|
+
* cannot collide. Drawn from the id rather than at random, because the board writes a
|
|
117
|
+
* card before it joins: one session has to draw one name on every call and after a
|
|
118
|
+
* restart, so the card, the join, and the roster say the same word. `taken` is
|
|
119
|
+
* compared case-insensitively, the way a label is addressed.
|
|
120
|
+
*/
|
|
121
|
+
export function presetNameFor(id: string, taken: Iterable<string> = []): string | undefined {
|
|
122
|
+
const held = new Set([...taken].map((label) => label.toLowerCase()));
|
|
123
|
+
const start = hashOf(id) % TEAMMATE_NAMES.length;
|
|
124
|
+
for (let step = 0; step < TEAMMATE_NAMES.length; step += 1) {
|
|
125
|
+
const name = TEAMMATE_NAMES[(start + step) % TEAMMATE_NAMES.length];
|
|
126
|
+
if (!held.has(name)) return name;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
67
131
|
/**
|
|
68
132
|
* Pi's session name, which is the first choice for a label.
|
|
69
133
|
*
|
|
@@ -78,3 +142,45 @@ export function sessionNameOf(ctx: Pick<ExtensionContext, "sessionManager">): st
|
|
|
78
142
|
return undefined;
|
|
79
143
|
}
|
|
80
144
|
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The session's id, or undefined when the host does not report one.
|
|
148
|
+
*
|
|
149
|
+
* Read beside the name because they are the same fact from two sides: a name is how a
|
|
150
|
+
* person reads a session, an id is how one is filed — a bus record, a card's owner, a
|
|
151
|
+
* resume. A host that refuses the call is a session with no id, not an error.
|
|
152
|
+
*/
|
|
153
|
+
export function sessionIdOf(ctx: Pick<ExtensionContext, "sessionManager">): string | undefined {
|
|
154
|
+
try {
|
|
155
|
+
const id = ctx.sessionManager.getSessionId();
|
|
156
|
+
return typeof id === "string" && id !== "" ? id : undefined;
|
|
157
|
+
} catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `pi.events` channel: a session settled the label its peers address it by.
|
|
164
|
+
*
|
|
165
|
+
* Here rather than in `lib/events.ts`, the way `lib/plan-entry.ts` and `lib/http/mount.ts`
|
|
166
|
+
* keep their own: a label is this module's vocabulary, and both sides of the channel — the
|
|
167
|
+
* bus that settles one and the footer that draws it — already load this module, so the
|
|
168
|
+
* literal costs a registration graph nothing and cannot be re-typed on one side.
|
|
169
|
+
*/
|
|
170
|
+
export const SESSION_LABEL_CHANNEL = "my-pi-agent/session-label";
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Payload on {@link SESSION_LABEL_CHANNEL}: one session's name on the bus.
|
|
174
|
+
*
|
|
175
|
+
* Emitted by `session-bus` whenever it settles or yields a label, and read by bytefly's
|
|
176
|
+
* footer, which draws it instead of Pi's own session name: the two part company the moment
|
|
177
|
+
* the board seats a session under a preset teammate name, or a collision appends an id. The
|
|
178
|
+
* id rides along because the channel reaches every session in the process, and only the one
|
|
179
|
+
* named here is the reader's own.
|
|
180
|
+
*/
|
|
181
|
+
export interface SessionLabelEvent {
|
|
182
|
+
/** Pi session id the label belongs to. */
|
|
183
|
+
id: string;
|
|
184
|
+
/** The name another session addresses this one by. */
|
|
185
|
+
label: string;
|
|
186
|
+
}
|