@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
package/lib/requirements.ts
CHANGED
|
@@ -208,6 +208,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
208
208
|
},
|
|
209
209
|
todo: {},
|
|
210
210
|
goal: {},
|
|
211
|
+
serve: { notes: ["Without it `/board`, `/browse`, and `/plan review` refuse, naming it."] },
|
|
211
212
|
board: {},
|
|
212
213
|
team: { notes: ["`assign` needs `session-bus` loaded to deliver a handoff; without it the card is untouched."] },
|
|
213
214
|
check: {
|
|
@@ -256,8 +257,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
256
257
|
"file-open": {
|
|
257
258
|
notes: [
|
|
258
259
|
"The `/open` command and its notification need an interactive UI; the `open_path` tool works without one.",
|
|
259
|
-
"Launching the OS handler is best-effort
|
|
260
|
-
"The tool result names the target even when the launch fails.",
|
|
260
|
+
"Launching the OS handler is best-effort — a launcher that fails to start, or exits non-zero on macOS and Linux, is reported, Windows `explorer` can exit non-zero after a successful open, and the tool result names the target either way.",
|
|
261
261
|
],
|
|
262
262
|
},
|
|
263
263
|
memory: {
|
|
@@ -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
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session label: the one name another session addresses this one by.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Two vocabularies share this module, and neither copies the other. `session-bus`
|
|
5
|
+
* settles its own label for a session that did not ask for one — the Pi session name,
|
|
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 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.
|
|
10
12
|
*
|
|
11
13
|
* Value-only: no state, no I/O.
|
|
12
14
|
*/
|
|
@@ -42,12 +44,90 @@ export function shortId(id: string): string {
|
|
|
42
44
|
* The bus appends a suffix when two live sessions share one; that decision belongs
|
|
43
45
|
* to the registry, so this is only the name a session would pick for itself.
|
|
44
46
|
*/
|
|
45
|
-
export function defaultLabel(sessionName: string | undefined, cwd: string, id
|
|
47
|
+
export function defaultLabel(sessionName: string | undefined, cwd: string, id = "session"): string {
|
|
46
48
|
const directory = cwd.split(/[\\/]/).filter(Boolean).at(-1);
|
|
47
49
|
const base = normalizeLabel(sessionName, normalizeLabel(directory, "session"));
|
|
48
50
|
return normalizeLabel(base, `session-${shortId(id)}`);
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/** The prefix of the team's name for a session that is not the page's seat. */
|
|
54
|
+
export const TEAM_AGENT_PREFIX = "team-agent-";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The team's name for a session: the prefix and the id's tail.
|
|
58
|
+
*
|
|
59
|
+
* A team is addressed by id, not by a name a user chose: an id is unique, so two
|
|
60
|
+
* sessions cannot settle on one name, and a rename cannot strand the card that
|
|
61
|
+
* recorded it. The seat is the one name that is a role instead.
|
|
62
|
+
*/
|
|
63
|
+
export function agentLabel(id: string): string {
|
|
64
|
+
return normalizeLabel(`${TEAM_AGENT_PREFIX}${shortId(id)}`, "team-agent");
|
|
65
|
+
}
|
|
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
|
+
|
|
51
131
|
/**
|
|
52
132
|
* Pi's session name, which is the first choice for a label.
|
|
53
133
|
*
|
|
@@ -62,3 +142,45 @@ export function sessionNameOf(ctx: Pick<ExtensionContext, "sessionManager">): st
|
|
|
62
142
|
return undefined;
|
|
63
143
|
}
|
|
64
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
|
+
}
|
package/lib/session-roster.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The Team tab lists teammates, and `session-bus` owns those records. Neither
|
|
5
5
|
* extension may import the other, and the board must keep working when the bus is
|
|
6
6
|
* not loaded — so the records are read where the bus already put them,
|
|
7
|
-
* `<
|
|
7
|
+
* `<agent-dir>/session-bus/<repo-key>/`, as files rather than through its code. The
|
|
8
8
|
* directory names, the record fields this reader trusts, and the liveness rule live
|
|
9
9
|
* here once, and `session-bus` imports them, so a layout change cannot land in one
|
|
10
10
|
* copy and not the other.
|
|
@@ -158,6 +158,48 @@ async function mailCount(dir: string, stem: string): Promise<number> {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* One session's own label, found by the id it belongs to.
|
|
163
|
+
*
|
|
164
|
+
* The record is filed under the repository's key, which a caller without a board store
|
|
165
|
+
* would have to resolve with git first — and a footer asking for a name that may not
|
|
166
|
+
* exist should not cost two subprocesses at every session start. So the bus root is
|
|
167
|
+
* walked instead and the record is matched on its id: a session id belongs to one
|
|
168
|
+
* session, so the first record that names it is the answer wherever the bus filed it.
|
|
169
|
+
*
|
|
170
|
+
* Every miss is ordinary: a machine with no bus, or with records for other sessions
|
|
171
|
+
* only, answers `undefined` rather than an error.
|
|
172
|
+
*/
|
|
173
|
+
export async function readSessionLabel(agentDir: string, id: string): Promise<string | undefined> {
|
|
174
|
+
const root = join(agentDir, BUS_DIR_NAME);
|
|
175
|
+
let keys: string[];
|
|
176
|
+
try {
|
|
177
|
+
keys = await readdir(root);
|
|
178
|
+
} catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
for (const key of keys) {
|
|
182
|
+
const dir = join(root, key, SESSIONS_DIR_NAME);
|
|
183
|
+
let names: string[];
|
|
184
|
+
try {
|
|
185
|
+
names = await readdir(dir);
|
|
186
|
+
} catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
if (!name.endsWith(".json")) continue;
|
|
191
|
+
let parsed: unknown;
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(await readFile(join(dir, name), "utf-8"));
|
|
194
|
+
} catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (isSessionPayload(parsed) && parsed.id === id) return parsed.label;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
161
203
|
/**
|
|
162
204
|
* Every session the bus has recorded for one repository, or `undefined` when the bus
|
|
163
205
|
* has never been used here.
|
package/lib/session-tool.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The `session` tool, as the extensions that are not `session-bus` call it.
|
|
3
3
|
*
|
|
4
4
|
* `board` and `team` are the user's surface for the team; the bus is the transport
|
|
5
|
-
* underneath, and neither may import it (`test/
|
|
5
|
+
* underneath, and neither may import it (`test/boundaries.test.ts`). So the one place
|
|
6
6
|
* that turns a call into a typed answer lives here, next to the tool's name in
|
|
7
7
|
* `lib/tool-names.ts` — two copies would be two copies of the details-shape
|
|
8
8
|
* knowledge, and a bus that renamed a field would be broken in one of them.
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { type BusAnswer, requestOverBus } from "./bus-request.ts";
|
|
25
26
|
import { messageOf } from "./errors.ts";
|
|
26
27
|
import { watchingOf } from "./session-roster.ts";
|
|
27
28
|
import { SESSION_TOOL } from "./tool-names.ts";
|
|
@@ -54,14 +55,30 @@ export type JoinResult = { ok: true; label: string; watching: Watching } | { ok:
|
|
|
54
55
|
/** EventBus channel on which a caller asks `session-bus` to join the team for it. */
|
|
55
56
|
export const TEAM_JOIN_REQUEST = "my-pi-agent/team-join/request";
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* What a join asks for.
|
|
60
|
+
*
|
|
61
|
+
* `label` is the name to be on the team as; without one the bus settles its own. `name`
|
|
62
|
+
* is the separate question of whether that label also becomes Pi's session name — what
|
|
63
|
+
* `/resume` lists — and it is opt-in, because a join is not a rename: only
|
|
64
|
+
* `/team join <label>` means "call me this from now on". The footer needs no opt-in: it
|
|
65
|
+
* draws the label itself, so a plain join moves what the bar names this session.
|
|
66
|
+
*/
|
|
67
|
+
export interface TeamJoinInput {
|
|
68
|
+
label?: string;
|
|
69
|
+
wake?: boolean;
|
|
70
|
+
listen?: boolean;
|
|
71
|
+
name?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
/** What a caller sends on {@link TEAM_JOIN_REQUEST}. */
|
|
58
75
|
export interface TeamJoinRequest {
|
|
59
76
|
/** The caller's context; the bus starts and names through it. */
|
|
60
77
|
ctx: ExtensionContext;
|
|
61
78
|
/** What to join as and how to listen. */
|
|
62
|
-
input:
|
|
63
|
-
/**
|
|
64
|
-
respond(result: JoinResult): void;
|
|
79
|
+
input: TeamJoinInput;
|
|
80
|
+
/** Claimed once, with what the bus answered; the caller settles on that claim. */
|
|
81
|
+
respond(result: BusAnswer<JoinResult>): void;
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
/** What a send did. */
|
|
@@ -96,49 +113,49 @@ function nestedMessage(result: { content?: Array<{ type: string; text?: string }
|
|
|
96
113
|
/**
|
|
97
114
|
* How an extension joins the team, whatever kind of context it is holding.
|
|
98
115
|
*
|
|
99
|
-
* Two extensions ask — `board` when the page opens or a card is
|
|
116
|
+
* Two extensions ask — `board` when the page opens or a card is claimed, `team` for
|
|
100
117
|
* `/team join` — and both hold a command or a tool context, so the request is the one
|
|
101
118
|
* transport that works from either.
|
|
102
119
|
*/
|
|
103
120
|
export interface TeamJoin {
|
|
104
|
-
/** Join the team
|
|
105
|
-
join(ctx: ExtensionContext,
|
|
121
|
+
/** Join the team as `input.label` when given. Never throws. */
|
|
122
|
+
join(ctx: ExtensionContext, input?: TeamJoinInput): Promise<JoinResult>;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
/** The production join, bound to one extension's `pi`. */
|
|
109
126
|
export function createTeamJoin(pi: Pick<ExtensionAPI, "events" | "getAllTools">): TeamJoin {
|
|
110
127
|
return {
|
|
111
|
-
join: (ctx,
|
|
128
|
+
join: (ctx, input) => join(pi, ctx, input ?? {}),
|
|
112
129
|
};
|
|
113
130
|
}
|
|
114
131
|
|
|
132
|
+
/** What a caller is told when there is no bus to join: one sentence on both paths. */
|
|
133
|
+
const NO_BUS = "the session bus is not loaded";
|
|
134
|
+
|
|
115
135
|
/**
|
|
116
136
|
* Join the team: on the bus, named when a label is given, and listening for mail.
|
|
117
137
|
*
|
|
118
138
|
* One call for all three, because that is one act — see `join` in
|
|
119
139
|
* `extensions/session-bus` — and one transport for both kinds of caller: the request
|
|
120
|
-
* travels on `pi.events` and the bus answers it.
|
|
140
|
+
* travels on `pi.events` and the bus answers it. Pi's session name is not one of the
|
|
141
|
+
* three: that is `input.name`, which only a caller meaning to rename asks for.
|
|
121
142
|
*
|
|
122
143
|
* With no bus loaded there is no listener, so the answer is the sentence the caller
|
|
123
144
|
* reports rather than a promise that never settles: the `session` tool's presence is
|
|
124
|
-
* the same
|
|
145
|
+
* checked here, and the shared handshake refuses the same way if a bus is loaded but
|
|
146
|
+
* listening for nothing.
|
|
125
147
|
*/
|
|
126
148
|
export async function join(
|
|
127
149
|
pi: Pick<ExtensionAPI, "events" | "getAllTools">,
|
|
128
150
|
ctx: ExtensionContext,
|
|
129
|
-
input:
|
|
151
|
+
input: TeamJoinInput = {},
|
|
130
152
|
): Promise<JoinResult> {
|
|
131
|
-
if (!pi.getAllTools().some((tool) => tool.name === SESSION_TOOL)) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (settled) return;
|
|
138
|
-
settled = true;
|
|
139
|
-
resolve(result);
|
|
140
|
-
};
|
|
141
|
-
pi.events.emit(TEAM_JOIN_REQUEST, { ctx, input, respond } satisfies TeamJoinRequest);
|
|
153
|
+
if (!pi.getAllTools().some((tool) => tool.name === SESSION_TOOL)) return { ok: false, error: NO_BUS };
|
|
154
|
+
return requestOverBus<JoinResult, TeamJoinRequest>(pi, {
|
|
155
|
+
channel: TEAM_JOIN_REQUEST,
|
|
156
|
+
payload: (respond) => ({ ctx, input, respond }),
|
|
157
|
+
unclaimed: () => ({ ok: false, error: NO_BUS }),
|
|
158
|
+
failed: (error) => ({ ok: false, error: messageOf(error) }),
|
|
142
159
|
});
|
|
143
160
|
}
|
|
144
161
|
|
package/lib/stray-fields.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* Value-only, as `lib/` must be: the tables are the callers'.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { listOf } from "./format.ts";
|
|
22
|
+
|
|
21
23
|
/** The fields each action reads, keyed in the order the actions are documented. */
|
|
22
24
|
export type ActionFields = Readonly<Record<string, readonly string[]>>;
|
|
23
25
|
|
|
@@ -38,8 +40,11 @@ export function strayField(action: string, args: Record<string, unknown>, fields
|
|
|
38
40
|
const named = (field: string): string => {
|
|
39
41
|
const taking = Object.keys(fields).filter((candidate) => fields[candidate]?.includes(field));
|
|
40
42
|
if (taking.length === 0) return `${field} belongs to no action`;
|
|
41
|
-
const verbs = taking.map((one) => `\`${one}\``)
|
|
42
|
-
|
|
43
|
+
const verbs = taking.map((one) => `\`${one}\``);
|
|
44
|
+
// Two read as "a and b"; three or more take the serial comma, or the list stops saying
|
|
45
|
+
// where one verb ends and the next begins.
|
|
46
|
+
const list = verbs.length === 2 ? verbs.join(" and ") : listOf(verbs, "and");
|
|
47
|
+
return `${list} ${taking.length === 1 ? "takes" : "take"} ${stray.length === 1 ? "it" : field}`;
|
|
43
48
|
};
|
|
44
49
|
return `${action} does not act on ${stray.join(", ")}: ${stray.map(named).join("; ")}.`;
|
|
45
50
|
}
|
package/lib/tool-names.ts
CHANGED
|
@@ -17,11 +17,12 @@ export const TODO_TOOL = "todo";
|
|
|
17
17
|
export const GOAL_TOOL = "goal";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* The `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* The `plan` tool. `plan` registers it and `goal` offers the same phase to the user,
|
|
21
|
+
* so the name is shared: the request channel and the registration probe both look
|
|
22
|
+
* it up. Its actions are the planning verbs, so one name replaces the five tools
|
|
23
|
+
* that used to spell them.
|
|
23
24
|
*/
|
|
24
|
-
export const
|
|
25
|
+
export const PLAN_TOOL = "plan";
|
|
25
26
|
|
|
26
27
|
/** The `subagent` tool, which plan mode pins to read-only delegation. */
|
|
27
28
|
export const SUBAGENT_TOOL = "subagent";
|
|
@@ -52,12 +53,6 @@ export const SUBAGENT_READ_ONLY_PARAM = "readOnly";
|
|
|
52
53
|
*/
|
|
53
54
|
export const BOARD_TOOL = "board";
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
-
* The one `board` action that reads. Plan mode admits it and refuses the rest, which
|
|
57
|
-
* is the distinction the tool's `readOnlyHint` cannot carry: the same tool writes.
|
|
58
|
-
*/
|
|
59
|
-
export const BOARD_READ_ACTION = "list";
|
|
60
|
-
|
|
61
56
|
/**
|
|
62
57
|
* The `read_doc` tool. `file-read` registers it, and the plan, rewind, and
|
|
63
58
|
* subagent reader lists carry the name, so the document reader is one reader
|
package/lib/tui/README.md
CHANGED
|
@@ -111,17 +111,32 @@ clipped mid-word at the widths the screen runs at.
|
|
|
111
111
|
|
|
112
112
|
Resolve colors through a [`tokens.ts`](./tokens.ts) role
|
|
113
113
|
(`paint(theme, role, text)`), never a literal ANSI sequence, hex value, or Pi
|
|
114
|
-
token named inline; a theme swap must re-skin every surface.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
token named inline; a theme swap must re-skin every surface. The role table is
|
|
115
|
+
the one place a Pi token is named, its tool vocabulary (`toolTitle`,
|
|
116
|
+
`toolOutput`) included, so a renderer draws a tool block the way it draws a
|
|
117
|
+
list. Use the shared `selectionMarker`, `GLYPHS`, `SEPARATORS`, and `STATUS_KEYS`
|
|
118
|
+
instead of retyping their literals, `screenHint` / Pi's `keyHint` for the hint
|
|
119
|
+
row, and `screenFooter` for the hint row with its blank and closing rule.
|
|
118
120
|
|
|
119
121
|
A list accents the focused row's label (`labelRole: selected ? "accent" :
|
|
120
122
|
"label"`); the `❯` marker alone is not enough to tell which row is live. The
|
|
121
|
-
hint row colors by role, not by taste
|
|
122
|
-
`
|
|
123
|
-
forget), and `
|
|
124
|
-
or select stay
|
|
123
|
+
hint row colors by role, not by taste — `ScreenHint.color` is a `TuiRole`:
|
|
124
|
+
`accent` for the screen's primary action, `warn` for one that discards state (a
|
|
125
|
+
revert, a kill, a stop, a clear, a forget), and `bad` for one that ends the
|
|
126
|
+
session. Keys that navigate, scroll, or select stay `faint`, so a colored key is
|
|
127
|
+
one worth a second thought.
|
|
128
|
+
|
|
129
|
+
## A hue identifies, a verdict judges
|
|
130
|
+
|
|
131
|
+
`ok` / `warn` / `bad` say how something *is*, and a surface that shows them is
|
|
132
|
+
making a judgement. `hue1`…`hue5` — blue, purple, pink, green, amber — say only
|
|
133
|
+
which category a row belongs to, which is what a table or a bar needs to be
|
|
134
|
+
told apart at a glance. They are the same tokens in places (green is `success`,
|
|
135
|
+
amber is `warning`), so the two vocabularies do not mix on one surface: a screen
|
|
136
|
+
that paints hues keeps its verdicts to the gauge, a status word, or a feedback
|
|
137
|
+
line, and a hue never lands in a slot that also carries a judgement. Color is
|
|
138
|
+
redundant there — the label, the order, and the printed number already say what
|
|
139
|
+
the hue repeats — so nothing may be encoded by hue alone.
|
|
125
140
|
|
|
126
141
|
## Width is columns
|
|
127
142
|
|
|
@@ -160,9 +175,12 @@ hold themed strings in state unless `invalidate()` rebuilds them. A widget is
|
|
|
160
175
|
derived from state, never authoritative.
|
|
161
176
|
|
|
162
177
|
Pi renders the whole tree every frame, so a component that derives its lines from
|
|
163
|
-
another one memoizes them with `RenderCache` (keyed on width and the
|
|
164
|
-
lines' identity
|
|
165
|
-
|
|
178
|
+
another one memoizes them with `RenderCache` — `lines()` keyed on width and the
|
|
179
|
+
source lines' identity, or `keyed()` for a component with no inner component (a
|
|
180
|
+
header's view, a footer's snapshot), keyed on the parts it read: primitives by
|
|
181
|
+
value, an object such as the theme by identity — and drops the memo in
|
|
182
|
+
`invalidate()`. Re-deriving every line per frame is what makes a long transcript
|
|
183
|
+
feel slow.
|
|
166
184
|
|
|
167
185
|
## Focus
|
|
168
186
|
|
package/lib/tui/chrome.ts
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* in, and the viewport options are explicit.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { Theme
|
|
19
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Text, truncateToWidth, visibleWidth, type EditorTheme, type OverlayOptions } from "@earendil-works/pi-tui";
|
|
21
21
|
import { SEPARATORS } from "./glyphs.ts";
|
|
22
22
|
import { BODY_INDENT } from "./indent.ts";
|
|
23
23
|
import { plainRule } from "./rules.ts";
|
|
24
|
+
import { paint, type TuiRole } from "./tokens.ts";
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* `ctx.ui.custom` options that give a screen the whole terminal.
|
|
@@ -61,15 +62,15 @@ export function viewportRows(source: ViewportRowsSource | undefined, options: Vi
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
* One key-hint entry: plain
|
|
65
|
-
*
|
|
65
|
+
* One key-hint entry: plain faint text, or text in a chosen role so a primary
|
|
66
|
+
* action (approve, submit) stands out from the scroll hints.
|
|
66
67
|
*/
|
|
67
|
-
export type ScreenHint = string | { text: string; color?:
|
|
68
|
+
export type ScreenHint = string | { text: string; color?: TuiRole };
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
71
|
* A key-hint footer indented by `indent` columns (default `BODY_INDENT`, which
|
|
71
|
-
* hangs it under a screen's title rule), joined with ` · `. Plain hints are
|
|
72
|
-
* an object hint
|
|
72
|
+
* hangs it under a screen's title rule), joined with ` · `. Plain hints are
|
|
73
|
+
* `faint`; an object hint names a role (`faint` when omitted). Hints are added while they
|
|
73
74
|
* fit, so a narrow terminal drops whole keys instead of truncating one in half.
|
|
74
75
|
* The first hint is always kept; the result is clipped to `width`.
|
|
75
76
|
*/
|
|
@@ -78,8 +79,8 @@ export function screenHint(theme: Theme, width: number, hints: ScreenHint[], ind
|
|
|
78
79
|
// Keep a column for the hint itself, however deep the caller indents.
|
|
79
80
|
const lead = " ".repeat(Math.max(0, Math.min(Math.floor(indent), target - 1)));
|
|
80
81
|
const render = (hint: ScreenHint): string =>
|
|
81
|
-
typeof hint === "string" ? theme
|
|
82
|
-
const separator = theme
|
|
82
|
+
typeof hint === "string" ? paint(theme, "faint", hint) : paint(theme, hint.color ?? "faint", hint.text);
|
|
83
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
83
84
|
const kept: ScreenHint[] = [];
|
|
84
85
|
for (const hint of hints) {
|
|
85
86
|
const candidate = [...kept, hint].map(render).join(separator);
|
|
@@ -196,16 +197,16 @@ export function toolDetails<T extends object>(details: unknown): T | undefined {
|
|
|
196
197
|
return Object.keys(details).length > 0 ? (details as T) : undefined;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
/** Minimal editor theme for an extension-owned inline editor, on semantic
|
|
200
|
+
/** Minimal editor theme for an extension-owned inline editor, on semantic roles. */
|
|
200
201
|
export function editorTheme(theme: Theme): EditorTheme {
|
|
201
202
|
return {
|
|
202
|
-
borderColor: (text) => theme
|
|
203
|
+
borderColor: (text) => paint(theme, "rule", text),
|
|
203
204
|
selectList: {
|
|
204
|
-
selectedPrefix: (text) => theme
|
|
205
|
-
selectedText: (text) => theme
|
|
206
|
-
description: (text) => theme
|
|
207
|
-
scrollInfo: (text) => theme
|
|
208
|
-
noMatch: (text) => theme
|
|
205
|
+
selectedPrefix: (text) => paint(theme, "accent", text),
|
|
206
|
+
selectedText: (text) => paint(theme, "accent", text),
|
|
207
|
+
description: (text) => paint(theme, "meta", text),
|
|
208
|
+
scrollInfo: (text) => paint(theme, "faint", text),
|
|
209
|
+
noMatch: (text) => paint(theme, "warn", text),
|
|
209
210
|
},
|
|
210
211
|
};
|
|
211
212
|
}
|
package/lib/tui/cursor.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
20
20
|
import { Key, matchesKey, truncateToWidth, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
21
21
|
import { viewportRows, type ViewportRowsSource } from "./chrome.ts";
|
|
22
22
|
import { GLYPHS } from "./glyphs.ts";
|
|
23
|
+
import { paint } from "./tokens.ts";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Rows that fit for a list of `itemCount`, reserving one extra line when the
|
|
@@ -76,11 +77,11 @@ export function wheelDelta(event: TuiMouseEvent): number | undefined {
|
|
|
76
77
|
|
|
77
78
|
/** The `❯ ` selection marker (accent) or a same-width blank. */
|
|
78
79
|
export function selectionMarker(theme: Theme, selected: boolean): string {
|
|
79
|
-
return selected ? theme
|
|
80
|
+
return selected ? paint(theme, "accent", `${GLYPHS.prompt} `) : " ";
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
|
-
* One
|
|
84
|
+
* One faint, two-space-indented range row: `showing A–B of N` by default, or
|
|
84
85
|
* `line A–B of N` for a log pane. Clipped to `width`.
|
|
85
86
|
*/
|
|
86
87
|
export function formatRange(
|
|
@@ -90,7 +91,7 @@ export function formatRange(
|
|
|
90
91
|
): string {
|
|
91
92
|
const label = range.label ?? "showing";
|
|
92
93
|
const text = ` ${label} ${range.start + 1}–${range.end} of ${range.total}`;
|
|
93
|
-
return truncateToWidth(theme
|
|
94
|
+
return truncateToWidth(paint(theme, "faint", text), Math.max(1, width));
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
|
@@ -247,6 +248,20 @@ export function isCloseKey(data: string): boolean {
|
|
|
247
248
|
return matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"));
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/** Shift+Tab arrives as this sequence on terminals without the Kitty protocol. */
|
|
252
|
+
export const SHIFT_TAB = "\u001b[Z";
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether `data` is Shift+Tab, in either encoding.
|
|
256
|
+
*
|
|
257
|
+
* `matchesKey` decodes the Kitty and modifyOtherKeys forms but not the legacy
|
|
258
|
+
* `\u001b[Z` one, so every screen or editor that binds Shift+Tab needs both tests;
|
|
259
|
+
* this is that one test.
|
|
260
|
+
*/
|
|
261
|
+
export function isShiftTab(data: string): boolean {
|
|
262
|
+
return data === SHIFT_TAB || matchesKey(data, Key.shift("tab"));
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
/**
|
|
251
266
|
* The next scroll offset for a standard navigation key, or `undefined` when
|
|
252
267
|
* `data` is not a navigation key.
|