@gavin-hu/my-pi-agent 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -45
- package/README.zh-CN.md +54 -42
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +554 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +66 -53
- package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
- package/docs/extensions/plan.md +976 -0
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
- package/docs/extensions/team.md +226 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +11 -21
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +54 -82
- package/extensions/file-browse/live/rev.ts +42 -0
- package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/view/chrome.ts +83 -0
- package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
- package/extensions/file-browse/view/diff-html.ts +94 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/job/index.ts +15 -7
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +77 -21
- package/extensions/plan/feedback.ts +409 -63
- package/extensions/plan/index.ts +7 -5
- package/extensions/plan/plan-index.ts +44 -10
- package/extensions/plan/plan-text.ts +51 -24
- package/extensions/plan/plans.ts +162 -57
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +51 -95
- package/extensions/plan/runtime.ts +22 -10
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +63 -95
- package/extensions/plan/tools/enter.ts +8 -3
- package/extensions/plan/tools/exit.ts +13 -19
- package/extensions/plan/tools/write.ts +8 -3
- package/extensions/plan/tools.ts +1 -1
- package/extensions/plan/tui.ts +29 -15
- package/extensions/plan/types.ts +29 -3
- package/extensions/plan/web-chrome.ts +2 -1
- package/extensions/plan/web-client.ts +122 -178
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-page.ts +96 -17
- package/extensions/plan/web-router.ts +109 -47
- package/extensions/plan/web-share-session.ts +191 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +245 -151
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +113 -0
- package/extensions/session-bus/format.ts +101 -0
- package/extensions/session-bus/index.ts +75 -0
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +86 -0
- package/extensions/session-bus/overlap.ts +36 -0
- package/extensions/session-bus/protocol.ts +104 -0
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +208 -0
- package/extensions/session-bus/rows.ts +171 -0
- package/extensions/session-bus/runtime.ts +466 -0
- package/extensions/session-bus/store.ts +365 -0
- package/extensions/session-bus/tools.ts +192 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- package/extensions/watch/index.ts +15 -9
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +76 -92
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +66 -51
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +119 -56
- package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +15 -7
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +104 -40
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/guard.ts +3 -5
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/status.ts +1 -3
- package/lib/README.md +12 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/router.ts +40 -13
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +29 -13
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/interaction.ts +10 -0
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/policy.ts +12 -2
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +52 -33
- package/lib/requirements.ts +7 -9
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/README.md +16 -2
- package/lib/tui/dock-screen.ts +85 -17
- package/lib/tui/glyphs.ts +2 -0
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +6 -1
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +27 -14
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +77 -51
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/file-browse/README.md +0 -467
- package/extensions/file-browse/server.ts +0 -134
- package/extensions/plan/README.md +0 -709
- package/extensions/plan/config.ts +0 -27
- package/extensions/wechat/outbound.ts +0 -100
- package/extensions/wechat/qrcode.ts +0 -46
- package/skills/changelog/SKILL.md +0 -148
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `session` tool, as the extensions that are not `session-bus` call it.
|
|
3
|
+
*
|
|
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/structure.test.ts`). So the one place
|
|
6
|
+
* that turns a call into a typed answer lives here, next to the tool's name in
|
|
7
|
+
* `lib/tool-names.ts` — two copies would be two copies of the details-shape
|
|
8
|
+
* knowledge, and a bus that renamed a field would be broken in one of them.
|
|
9
|
+
*
|
|
10
|
+
* There are two transports, and the difference is the context, not the caller's
|
|
11
|
+
* choice: `join` travels as an in-process request on `pi.events` because a *command*
|
|
12
|
+
* context has no `ctx.executeTool` — the same constraint `lib/plan-entry.ts` exists
|
|
13
|
+
* for — while `peers` and `send` are nested tool calls, which only ever run inside a
|
|
14
|
+
* tool. One function either way, so a caller never has to know which it is holding.
|
|
15
|
+
*
|
|
16
|
+
* Every function returns a result and none throws: "the bus is not loaded", "a plan
|
|
17
|
+
* is open", and "that label is taken" are all answers a caller reports, not reasons
|
|
18
|
+
* to abandon a board write or a card operation that has nothing to do with the bus.
|
|
19
|
+
*
|
|
20
|
+
* This module is the *caller* side only. Reading the bus's records from disk is
|
|
21
|
+
* `session-roster.ts`, which is what works when the bus is not loaded at all.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { messageOf } from "./errors.ts";
|
|
26
|
+
import { watchingOf } from "./session-roster.ts";
|
|
27
|
+
import { SESSION_TOOL } from "./tool-names.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The `session send` text limit, which is also the longest handoff or offer.
|
|
31
|
+
*
|
|
32
|
+
* It is copied from the bus's protocol rather than imported — the dependency runs one
|
|
33
|
+
* way — and it lives here so `team` and `board` clip to one number instead of two.
|
|
34
|
+
*/
|
|
35
|
+
export const MAX_MESSAGE_TEXT = 4_000;
|
|
36
|
+
|
|
37
|
+
/** Whether a session is listening for mail, and whether it raises a turn. */
|
|
38
|
+
export interface Watching {
|
|
39
|
+
armed: boolean;
|
|
40
|
+
wake: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One session the bus lists. */
|
|
44
|
+
export interface Peer {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
/** Absent when the record does not say, which is not the same as "not listening". */
|
|
48
|
+
listening?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What a join did: the label it landed on, and whether mail will reach the session. */
|
|
52
|
+
export type JoinResult = { ok: true; label: string; watching: Watching } | { ok: false; error: string };
|
|
53
|
+
|
|
54
|
+
/** EventBus channel on which a caller asks `session-bus` to join the team for it. */
|
|
55
|
+
export const TEAM_JOIN_REQUEST = "my-pi-agent/team-join/request";
|
|
56
|
+
|
|
57
|
+
/** What a caller sends on {@link TEAM_JOIN_REQUEST}. */
|
|
58
|
+
export interface TeamJoinRequest {
|
|
59
|
+
/** The caller's context; the bus starts and names through it. */
|
|
60
|
+
ctx: ExtensionContext;
|
|
61
|
+
/** What to join as and how to listen. */
|
|
62
|
+
input: { label?: string; wake?: boolean; listen?: boolean };
|
|
63
|
+
/** Called once, when the bus has answered. */
|
|
64
|
+
respond(result: JoinResult): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a send did. */
|
|
68
|
+
export type SendResult = { ok: true; delivered: string[] } | { ok: false; error: string };
|
|
69
|
+
|
|
70
|
+
/** One nested call's answer: the details on success, the sentence on failure. */
|
|
71
|
+
type Answer = { details: Record<string, unknown> } | { error: string };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Call the `session` tool once.
|
|
75
|
+
*
|
|
76
|
+
* A nested call reports failure in its result rather than by rejecting; a rejection
|
|
77
|
+
* is still caught, because a bus that is not registered at all throws before the
|
|
78
|
+
* tool runs and the caller deserves the sentence rather than an exception.
|
|
79
|
+
*/
|
|
80
|
+
async function call(ctx: ExtensionToolContext, args: Record<string, unknown>): Promise<Answer> {
|
|
81
|
+
const outcome = await ctx.executeTool(SESSION_TOOL, args).catch((error: unknown) => ({
|
|
82
|
+
isError: true as const,
|
|
83
|
+
result: { content: [{ type: "text", text: messageOf(error) }], details: undefined },
|
|
84
|
+
}));
|
|
85
|
+
if (outcome.isError) return { error: nestedMessage(outcome.result) };
|
|
86
|
+
const details = outcome.result.details;
|
|
87
|
+
return { details: typeof details === "object" && details !== null ? (details as Record<string, unknown>) : {} };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The message a failed nested call carries, or a sentence that says only that it failed. */
|
|
91
|
+
function nestedMessage(result: { content?: Array<{ type: string; text?: string }> } | undefined): string {
|
|
92
|
+
const text = result?.content?.find((block) => block.type === "text")?.text;
|
|
93
|
+
return text !== undefined && text !== "" ? text.replace(/^Error:\s*/, "") : "the session bus did not answer.";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* How an extension joins the team, whatever kind of context it is holding.
|
|
98
|
+
*
|
|
99
|
+
* Two extensions ask — `board` when the page opens or a card is worked, `team` for
|
|
100
|
+
* `/team join` — and both hold a command or a tool context, so the request is the one
|
|
101
|
+
* transport that works from either.
|
|
102
|
+
*/
|
|
103
|
+
export interface TeamJoin {
|
|
104
|
+
/** Join the team, naming this session when `label` is given. Never throws. */
|
|
105
|
+
join(ctx: ExtensionContext, label?: string): Promise<JoinResult>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The production join, bound to one extension's `pi`. */
|
|
109
|
+
export function createTeamJoin(pi: Pick<ExtensionAPI, "events" | "getAllTools">): TeamJoin {
|
|
110
|
+
return {
|
|
111
|
+
join: (ctx, label) => join(pi, ctx, label === undefined ? {} : { label }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Join the team: on the bus, named when a label is given, and listening for mail.
|
|
117
|
+
*
|
|
118
|
+
* One call for all three, because that is one act — see `join` in
|
|
119
|
+
* `extensions/session-bus` — and one transport for both kinds of caller: the request
|
|
120
|
+
* travels on `pi.events` and the bus answers it.
|
|
121
|
+
*
|
|
122
|
+
* With no bus loaded there is no listener, so the answer is the sentence the caller
|
|
123
|
+
* reports rather than a promise that never settles: the `session` tool's presence is
|
|
124
|
+
* the same check `lib/plan-entry.ts` makes, and the listener always answers.
|
|
125
|
+
*/
|
|
126
|
+
export async function join(
|
|
127
|
+
pi: Pick<ExtensionAPI, "events" | "getAllTools">,
|
|
128
|
+
ctx: ExtensionContext,
|
|
129
|
+
input: { label?: string; wake?: boolean; listen?: boolean } = {},
|
|
130
|
+
): Promise<JoinResult> {
|
|
131
|
+
if (!pi.getAllTools().some((tool) => tool.name === SESSION_TOOL)) {
|
|
132
|
+
return { ok: false, error: "the session bus is not loaded" };
|
|
133
|
+
}
|
|
134
|
+
return new Promise<JoinResult>((resolve) => {
|
|
135
|
+
let settled = false;
|
|
136
|
+
const respond = (result: JoinResult): void => {
|
|
137
|
+
if (settled) return;
|
|
138
|
+
settled = true;
|
|
139
|
+
resolve(result);
|
|
140
|
+
};
|
|
141
|
+
pi.events.emit(TEAM_JOIN_REQUEST, { ctx, input, respond } satisfies TeamJoinRequest);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Every session the bus lists, or `undefined` when it cannot answer.
|
|
147
|
+
*
|
|
148
|
+
* Named `listPeers` rather than `peers` so it cannot be mistaken for a caller's own
|
|
149
|
+
* formatting helper — `wechat`'s doctor has one — and a caller reads the verb.
|
|
150
|
+
*
|
|
151
|
+
* A caller renders "the record does not say" differently from "nobody is here", so a
|
|
152
|
+
* failed read is not an empty list.
|
|
153
|
+
*/
|
|
154
|
+
export async function listPeers(ctx: ExtensionToolContext): Promise<Peer[] | undefined> {
|
|
155
|
+
const answer = await call(ctx, { action: "status" });
|
|
156
|
+
if ("error" in answer) return undefined;
|
|
157
|
+
const status = answer.details.status;
|
|
158
|
+
if (typeof status !== "object" || status === null) return undefined;
|
|
159
|
+
const sessions = (status as { sessions?: unknown }).sessions;
|
|
160
|
+
if (!Array.isArray(sessions)) return undefined;
|
|
161
|
+
const peers: Peer[] = [];
|
|
162
|
+
for (const entry of sessions) {
|
|
163
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
164
|
+
const { id, label, watching } = entry as Record<string, unknown>;
|
|
165
|
+
if (typeof id !== "string" || typeof label !== "string") continue;
|
|
166
|
+
const state = watchingOf(watching);
|
|
167
|
+
peers.push({ id, label, ...(state === undefined ? {} : { listening: state.armed }) });
|
|
168
|
+
}
|
|
169
|
+
return peers;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Send one message, and report the labels a message file was written for. */
|
|
173
|
+
export async function send(
|
|
174
|
+
ctx: ExtensionToolContext,
|
|
175
|
+
input: { kind: "handoff" | "event"; to: string; text: string },
|
|
176
|
+
): Promise<SendResult> {
|
|
177
|
+
const answer = await call(ctx, { action: "send", kind: input.kind, to: input.to, text: input.text });
|
|
178
|
+
if ("error" in answer) return { ok: false, error: answer.error };
|
|
179
|
+
const delivered = answer.details.delivered;
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
delivered: Array.isArray(delivered) ? delivered.filter((label): label is string => typeof label === "string") : [],
|
|
183
|
+
};
|
|
184
|
+
}
|
package/lib/tool-names.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Tool names are otherwise owned by the extension that registers the tool, and
|
|
5
5
|
* no extension imports another's modules (see this directory's README). When an
|
|
6
6
|
* orchestrating tool calls another extension's tool through `ctx.executeTool`,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* reads another extension's registered parameter schema, lists the name in a
|
|
8
|
+
* read-only policy, or passes it on a child's `--tools` allowlist, the shared
|
|
9
|
+
* name lives here so a rename breaks the build on both sides instead of silently
|
|
9
10
|
* disabling the integration.
|
|
10
11
|
*/
|
|
11
12
|
|
|
@@ -31,9 +32,31 @@ export const SUBAGENT_TOOL = "subagent";
|
|
|
31
32
|
*/
|
|
32
33
|
export const ASK_USER_QUESTION_TOOL = "ask_user_question";
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The `session` tool. `session-bus` registers it, and `team` calls it through
|
|
37
|
+
* `ctx.executeTool` to send a handoff, so a rename breaks both sides.
|
|
38
|
+
*/
|
|
39
|
+
export const SESSION_TOOL = "session";
|
|
40
|
+
|
|
34
41
|
/**
|
|
35
42
|
* Parameter on the `subagent` tool that forces every spawned agent to a
|
|
36
43
|
* read-only tool set. Plan mode sets it on every call while planning, and
|
|
37
44
|
* refuses a `subagent` whose schema does not declare it.
|
|
38
45
|
*/
|
|
39
46
|
export const SUBAGENT_READ_ONLY_PARAM = "readOnly";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The `read_doc` tool. `file-read` registers it, and the plan, rewind, and
|
|
50
|
+
* subagent reader lists carry the name, so the document reader is one reader
|
|
51
|
+
* everywhere instead of three spellings.
|
|
52
|
+
*/
|
|
53
|
+
export const READ_DOC_TOOL = "read_doc";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The `web_search` tool. `web-access` registers it; plan's and subagent's reader
|
|
57
|
+
* lists carry the name, so a rename cannot silently drop the web reader.
|
|
58
|
+
*/
|
|
59
|
+
export const WEB_SEARCH_TOOL = "web_search";
|
|
60
|
+
|
|
61
|
+
/** The `web_fetch` tool, shared the same way as {@link WEB_SEARCH_TOOL}. */
|
|
62
|
+
export const WEB_FETCH_TOOL = "web_fetch";
|
package/lib/tui/README.md
CHANGED
|
@@ -35,8 +35,8 @@ Reach for a `lib/tui/` primitive before composing your own: `plainRule` /
|
|
|
35
35
|
`fitCells` for two columns, `LinePane` for a scrollable log, `ListCursor` /
|
|
36
36
|
`IdCursor` for selection, `byRunningFirst` for a live list's order,
|
|
37
37
|
`DockListScreen` for a list that opens a pane, `TailResult` for a tool result
|
|
38
|
-
that lists rows or tails output, `screenFooter` for a
|
|
39
|
-
rows, and `ConfirmState` before a destructive action.
|
|
38
|
+
that lists rows, heads several row sections, or tails output, `screenFooter` for a
|
|
39
|
+
screen's own last three rows, and `ConfirmState` before a destructive action.
|
|
40
40
|
|
|
41
41
|
## Docked list screens
|
|
42
42
|
|
|
@@ -81,6 +81,20 @@ what a plain list wants — do not hand-roll the frame to get one of them:
|
|
|
81
81
|
(`c` collect, `x` cancel). Return `true` for the keys it claimed; everything
|
|
82
82
|
else falls through to the pane's scroll keys.
|
|
83
83
|
|
|
84
|
+
A pane is a log by default: it opens at the tail and its subtitle says
|
|
85
|
+
`following` or `paused`. Two more fields cover the other body:
|
|
86
|
+
|
|
87
|
+
- `DetailContents.text` — prose instead of `lines`, wrapped to the width the pane
|
|
88
|
+
renders at and re-wrapped on a resize. One source line is one paragraph; a blank
|
|
89
|
+
line stays blank. `lines` remains the right shape for output that is already
|
|
90
|
+
split.
|
|
91
|
+
- `DetailContents.tail: false` — a prose or list pane opens at the top and never
|
|
92
|
+
claims to follow anything. Leave it out for a log.
|
|
93
|
+
|
|
94
|
+
A screen whose list is one of several scopes overrides `headerLabel()` (the
|
|
95
|
+
header rule's name) and `emptyLabel()` (the empty-list line), so `Tab` can change
|
|
96
|
+
what is listed without a second renderer; both default to the screen's options.
|
|
97
|
+
|
|
84
98
|
A destructive action asks through [`confirm.ts`](./confirm.ts):
|
|
85
99
|
`ConfirmState.ask(prompt, accept)` holds the question, `resolve(data)` accepts on
|
|
86
100
|
`y`/`Y`/Enter, dismisses on `n`/`N`/Esc/`q`, and returns `undefined` for any
|
package/lib/tui/dock-screen.ts
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
23
|
+
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
23
24
|
import { detailBlockRows, dockListFrame, screenHint, type ScreenHint, type ViewportRowsSource } from "./chrome.ts";
|
|
24
25
|
import { clip, fitCells } from "./columns.ts";
|
|
25
26
|
import { ConfirmState } from "./confirm.ts";
|
|
26
27
|
import { fitRows, formatRange, IdCursor, isCloseKey, navIntent, wheelDelta } from "./cursor.ts";
|
|
27
28
|
import { SEPARATORS } from "./glyphs.ts";
|
|
29
|
+
import { BODY_INDENT } from "./indent.ts";
|
|
28
30
|
import { LinePane } from "./line-pane.ts";
|
|
29
31
|
import { emptyState } from "./rows.ts";
|
|
30
32
|
import { titleRule } from "./rules.ts";
|
|
@@ -43,12 +45,23 @@ const DETAIL_CHROME_ROWS = 6;
|
|
|
43
45
|
export interface DetailContents {
|
|
44
46
|
/** Pane title, e.g. `j1 build`. */
|
|
45
47
|
title: string;
|
|
46
|
-
/** The body lines, already sanitized by the caller. */
|
|
47
|
-
lines
|
|
48
|
+
/** The body lines, already sanitized by the caller. Omit for a `text` pane. */
|
|
49
|
+
lines?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Prose body, built once at open and wrapped to the render width on every
|
|
52
|
+
* frame. Takes precedence over `lines`. A resize re-wraps; a blank line in the
|
|
53
|
+
* source stays a blank line in the pane.
|
|
54
|
+
*/
|
|
55
|
+
text?: string;
|
|
48
56
|
/** More lines exist above the ones supplied. */
|
|
49
57
|
more?: boolean;
|
|
50
58
|
/** Subtitle under the title, e.g. the log file path. */
|
|
51
59
|
subtitle?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the pane follows its tail. Default true, which is a log's behavior;
|
|
62
|
+
* false opens a prose or list pane at the top and never says `following`.
|
|
63
|
+
*/
|
|
64
|
+
tail?: boolean;
|
|
52
65
|
/**
|
|
53
66
|
* Rows above the scrollable body that depend on the width: key/value facts,
|
|
54
67
|
* a legend, a divider. Read fresh every frame with the live width and already
|
|
@@ -101,6 +114,9 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
101
114
|
private readonly confirmState = new ConfirmState();
|
|
102
115
|
private feedback: string | undefined;
|
|
103
116
|
private detailFacts: ((width: number) => string[]) | undefined;
|
|
117
|
+
/** Prose body for the open pane, wrapped to the width it is rendered at. */
|
|
118
|
+
private detailText: string | undefined;
|
|
119
|
+
private detailWrap: { width: number; lines: string[] } | undefined;
|
|
104
120
|
|
|
105
121
|
constructor(protected readonly options: DockListScreenOptions) {}
|
|
106
122
|
|
|
@@ -163,6 +179,19 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
163
179
|
this.options.requestRender();
|
|
164
180
|
}
|
|
165
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The header rule's label. A screen whose list is one of several scopes
|
|
184
|
+
* overrides it, so the rule names what is on screen.
|
|
185
|
+
*/
|
|
186
|
+
protected headerLabel(): string {
|
|
187
|
+
return this.options.title;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The empty-list line; a screen whose scopes differ overrides it. */
|
|
191
|
+
protected emptyLabel(): string {
|
|
192
|
+
return this.options.emptyList;
|
|
193
|
+
}
|
|
194
|
+
|
|
166
195
|
/** Show one line of feedback in place of the hints. */
|
|
167
196
|
protected notify(text: string): void {
|
|
168
197
|
this.feedback = text;
|
|
@@ -182,6 +211,12 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
182
211
|
// Facts stay with the pane unless this update supplies its own, so a poll that
|
|
183
212
|
// only refreshes the body cannot drop the facts the row opened with.
|
|
184
213
|
if (contents.facts) this.detailFacts = contents.facts;
|
|
214
|
+
// `text` and `tail` belong to the row that opened the pane; an update either
|
|
215
|
+
// replaces the prose or settles the pane back onto a line buffer.
|
|
216
|
+
if (contents.text !== undefined || contents.lines !== undefined) {
|
|
217
|
+
this.detailText = contents.text;
|
|
218
|
+
this.detailWrap = undefined;
|
|
219
|
+
}
|
|
185
220
|
const changed =
|
|
186
221
|
contents.subtitle === undefined
|
|
187
222
|
? this.pane.update({ title: contents.title, lines: contents.lines, more: contents.more })
|
|
@@ -201,6 +236,7 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
201
236
|
|
|
202
237
|
/** Re-read the open detail pane, so a poll cannot show stale lines. */
|
|
203
238
|
refreshDetail(id: string): void {
|
|
239
|
+
if (this.detailText !== undefined) return;
|
|
204
240
|
const source = this.detailById?.(id);
|
|
205
241
|
if (source && this.pane.update({ lines: source.lines, more: source.more })) this.options.requestRender();
|
|
206
242
|
}
|
|
@@ -242,10 +278,30 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
242
278
|
const contents = this.detailFor(row);
|
|
243
279
|
this.mode = "detail";
|
|
244
280
|
this.detailFacts = contents.facts;
|
|
245
|
-
this.
|
|
281
|
+
this.detailText = contents.text;
|
|
282
|
+
this.detailWrap = undefined;
|
|
283
|
+
this.pane.open(
|
|
284
|
+
this.idOf(row),
|
|
285
|
+
{
|
|
286
|
+
title: contents.title,
|
|
287
|
+
lines: contents.text === undefined ? (contents.lines ?? []) : [],
|
|
288
|
+
more: contents.more,
|
|
289
|
+
subtitle: contents.subtitle,
|
|
290
|
+
},
|
|
291
|
+
contents.tail ?? true,
|
|
292
|
+
);
|
|
246
293
|
this.options.requestRender();
|
|
247
294
|
}
|
|
248
295
|
|
|
296
|
+
/** Leave detail mode and drop everything that belonged to the open row. */
|
|
297
|
+
private closeDetail(): void {
|
|
298
|
+
this.mode = "list";
|
|
299
|
+
this.pane.close();
|
|
300
|
+
this.detailFacts = undefined;
|
|
301
|
+
this.detailText = undefined;
|
|
302
|
+
this.detailWrap = undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
249
305
|
private handleListKey(data: string): void {
|
|
250
306
|
if (isCloseKey(data) || data === "q") {
|
|
251
307
|
this.options.onClose();
|
|
@@ -294,9 +350,7 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
294
350
|
|
|
295
351
|
private handleDetailKey(data: string): void {
|
|
296
352
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.backspace) || data === "q") {
|
|
297
|
-
this.
|
|
298
|
-
this.pane.close();
|
|
299
|
-
this.detailFacts = undefined;
|
|
353
|
+
this.closeDetail();
|
|
300
354
|
this.options.requestRender();
|
|
301
355
|
return;
|
|
302
356
|
}
|
|
@@ -307,11 +361,11 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
307
361
|
}
|
|
308
362
|
|
|
309
363
|
/** The footer: a pending confirmation, one line of feedback, or the hints. */
|
|
310
|
-
private
|
|
364
|
+
private footer(width: number, hints: ScreenHint[]): string {
|
|
311
365
|
const pending = this.confirmState.pending;
|
|
312
366
|
if (pending) return clip(` ${paint(this.theme, "warn", pending.prompt)}`, width);
|
|
313
367
|
if (this.feedback) return clip(` ${paint(this.theme, "meta", this.feedback)}`, width);
|
|
314
|
-
return screenHint(this.theme, width,
|
|
368
|
+
return screenHint(this.theme, width, hints);
|
|
315
369
|
}
|
|
316
370
|
|
|
317
371
|
private summary(rows: readonly T[], width: number): string {
|
|
@@ -343,7 +397,7 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
343
397
|
const body: string[] = [];
|
|
344
398
|
let range: string | undefined;
|
|
345
399
|
if (rows.length === 0) {
|
|
346
|
-
body.push(...emptyState(this.theme, w, { label: this.
|
|
400
|
+
body.push(...emptyState(this.theme, w, { label: this.emptyLabel() }));
|
|
347
401
|
} else {
|
|
348
402
|
const end = Math.min(rows.length, this.cursor.scroll + this.cursor.visible);
|
|
349
403
|
for (let i = this.cursor.scroll; i < end; i++) body.push(this.rowLine(rows[i], w, i === index));
|
|
@@ -355,12 +409,12 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
355
409
|
return dockListFrame({
|
|
356
410
|
theme: this.theme,
|
|
357
411
|
width: w,
|
|
358
|
-
header: titleRule(this.theme, w, this.
|
|
412
|
+
header: titleRule(this.theme, w, this.headerLabel()),
|
|
359
413
|
summary: this.summary(rows, w),
|
|
360
414
|
body,
|
|
361
415
|
range,
|
|
362
416
|
detail: block.map((line) => clip(` ${paint(this.theme, "meta", line)}`, w)),
|
|
363
|
-
hint: this.
|
|
417
|
+
hint: this.footer(w, this.listHints(rows)),
|
|
364
418
|
});
|
|
365
419
|
}
|
|
366
420
|
|
|
@@ -368,6 +422,7 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
368
422
|
this.syncDetail();
|
|
369
423
|
// A live pane whose row is gone has already closed itself: draw the list.
|
|
370
424
|
if (this.mode !== "detail") return this.renderList(w);
|
|
425
|
+
this.syncText(w);
|
|
371
426
|
const facts = this.detailFacts ? this.detailFacts(w) : [];
|
|
372
427
|
const visible = fitRows(
|
|
373
428
|
this.options.viewportRowsSource,
|
|
@@ -397,14 +452,15 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
397
452
|
summary: clip(` ${paint(this.theme, "faint", this.pane.subtitleLine())}`, w),
|
|
398
453
|
body,
|
|
399
454
|
range,
|
|
400
|
-
hint:
|
|
455
|
+
hint: this.footer(w, this.detailHints(this.detailRow)),
|
|
401
456
|
});
|
|
402
457
|
}
|
|
403
458
|
|
|
404
459
|
/**
|
|
405
460
|
* Re-read a live pane in place. This runs inside a render, so it must not ask
|
|
406
461
|
* for another frame; a source that disappeared closes the pane instead of
|
|
407
|
-
* showing lines the list no longer has a row for.
|
|
462
|
+
* showing lines the list no longer has a row for. A prose pane is skipped here:
|
|
463
|
+
* `syncText` owns its lines, and this still closes it when the row is gone.
|
|
408
464
|
*/
|
|
409
465
|
private syncDetail(): void {
|
|
410
466
|
if (!this.liveDetail()) return;
|
|
@@ -412,11 +468,23 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
412
468
|
if (!id) return;
|
|
413
469
|
const source = this.detailById?.(id);
|
|
414
470
|
if (!source) {
|
|
415
|
-
this.
|
|
416
|
-
this.pane.close();
|
|
417
|
-
this.detailFacts = undefined;
|
|
471
|
+
this.closeDetail();
|
|
418
472
|
return;
|
|
419
473
|
}
|
|
420
|
-
this.pane.update({ lines: source.lines, more: source.more });
|
|
474
|
+
if (this.detailText === undefined) this.pane.update({ lines: source.lines, more: source.more });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Wrap a prose body to the width the pane actually renders at, once per width.
|
|
479
|
+
* The pane's body lines are built at open, so this is the one source that can
|
|
480
|
+
* follow a resize.
|
|
481
|
+
*/
|
|
482
|
+
private syncText(width: number): void {
|
|
483
|
+
if (this.detailText === undefined) return;
|
|
484
|
+
const inner = Math.max(1, width - BODY_INDENT);
|
|
485
|
+
if (this.detailWrap?.width === inner) return;
|
|
486
|
+
const lines = this.detailText.split("\n").flatMap((line) => (line === "" ? [""] : wrapTextWithAnsi(line, inner)));
|
|
487
|
+
this.detailWrap = { width: inner, lines };
|
|
488
|
+
this.pane.update({ lines });
|
|
421
489
|
}
|
|
422
490
|
}
|
package/lib/tui/glyphs.ts
CHANGED
|
@@ -30,6 +30,7 @@ export const GLYPHS = {
|
|
|
30
30
|
jobsFailure: "✗",
|
|
31
31
|
stopped: "⊘",
|
|
32
32
|
browse: "◉",
|
|
33
|
+
board: "▤",
|
|
33
34
|
delegation: "↳",
|
|
34
35
|
wechat: "✆",
|
|
35
36
|
gaugeFull: "▰",
|
|
@@ -71,6 +72,7 @@ export const STATUS_KEYS = {
|
|
|
71
72
|
watch: "watch",
|
|
72
73
|
watchMatches: "watch-matches",
|
|
73
74
|
browse: "browse",
|
|
75
|
+
board: "board",
|
|
74
76
|
rewind: "rewind",
|
|
75
77
|
agents: "agents",
|
|
76
78
|
wechat: "wechat",
|
package/lib/tui/line-pane.ts
CHANGED
|
@@ -45,6 +45,11 @@ export class LinePane {
|
|
|
45
45
|
visible = 1;
|
|
46
46
|
/** Whether the view is parked at the tail; set false once the user scrolls up. */
|
|
47
47
|
follow = true;
|
|
48
|
+
/**
|
|
49
|
+
* Whether this pane follows its tail. A log does; a prose or list pane does not:
|
|
50
|
+
* it opens at the top and never reports `following`/`paused`.
|
|
51
|
+
*/
|
|
52
|
+
tail = true;
|
|
48
53
|
|
|
49
54
|
private buffer: string[] = [];
|
|
50
55
|
private label = "";
|
|
@@ -66,16 +71,21 @@ export class LinePane {
|
|
|
66
71
|
return Math.max(0, this.buffer.length - this.visible);
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
/** Open the pane on a row, reset to the tail. */
|
|
70
|
-
open(id: string, source: PaneSource): void {
|
|
74
|
+
/** Open the pane on a row, reset to the tail for a log, or to the top for prose. */
|
|
75
|
+
open(id: string, source: PaneSource, tail = true): void {
|
|
71
76
|
this.id = id;
|
|
72
77
|
this.buffer = [];
|
|
73
78
|
this.label = "";
|
|
74
79
|
this.subtitle = undefined;
|
|
75
80
|
this.more = false;
|
|
76
81
|
this.scroll = 0;
|
|
77
|
-
this.
|
|
82
|
+
this.tail = tail;
|
|
83
|
+
this.follow = tail;
|
|
78
84
|
this.update(source);
|
|
85
|
+
if (!tail) {
|
|
86
|
+
this.follow = false;
|
|
87
|
+
this.scroll = 0;
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/** Close the pane and drop its buffer. */
|
|
@@ -87,6 +97,7 @@ export class LinePane {
|
|
|
87
97
|
this.more = false;
|
|
88
98
|
this.scroll = 0;
|
|
89
99
|
this.follow = true;
|
|
100
|
+
this.tail = true;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
/**
|
|
@@ -127,7 +138,7 @@ export class LinePane {
|
|
|
127
138
|
const parts: string[] = [];
|
|
128
139
|
if (this.subtitle) parts.push(this.subtitle);
|
|
129
140
|
if (this.more && this.buffer.length > 0) parts.push(`showing last ${this.buffer.length} lines`);
|
|
130
|
-
if (this.buffer.length > 0) parts.push(this.follow ? "following" : "paused");
|
|
141
|
+
if (this.tail && this.buffer.length > 0) parts.push(this.follow ? "following" : "paused");
|
|
131
142
|
return parts.join(" · ");
|
|
132
143
|
}
|
|
133
144
|
|
package/lib/tui/tool.ts
CHANGED
|
@@ -200,18 +200,29 @@ export function earlierHint(theme: Theme, hidden: number, label: string): string
|
|
|
200
200
|
/** The row renderer a {@link TailResult} list uses: the extension's row grammar at a width. */
|
|
201
201
|
export type TailRowLine<Row> = (row: Row, width: number, theme: Theme) => string;
|
|
202
202
|
|
|
203
|
-
/**
|
|
203
|
+
/** One group of rows inside a {@link TailResult}, under an optional faint heading. */
|
|
204
|
+
export interface TailSection<Row> {
|
|
205
|
+
/** A faint heading above the rows, e.g. `Claims`; omit for the block's primary list. */
|
|
206
|
+
label?: string;
|
|
207
|
+
rows: readonly Row[];
|
|
208
|
+
more: number;
|
|
209
|
+
rowLine: TailRowLine<Row>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** What a {@link TailResult} draws: the caller's rows plus a hidden count, a headed row list, or a bounded tail. */
|
|
204
213
|
export type TailResultInput<Row> =
|
|
205
214
|
| { kind: "list"; rows: readonly Row[]; more: number; rowLine: TailRowLine<Row> }
|
|
215
|
+
| { kind: "sections"; sections: readonly TailSection<Row>[] }
|
|
206
216
|
| { kind: "lines"; lines: readonly string[]; earlier: number; earlierLabel: string };
|
|
207
217
|
|
|
208
218
|
/**
|
|
209
219
|
* The transcript result block a tool that lists rows or tails output shares.
|
|
210
220
|
*
|
|
211
221
|
* One leading blank line per the block grammar, then either the caller's rows
|
|
212
|
-
* (already ordered and capped) with a "more" note,
|
|
213
|
-
* "earlier" note. The caller owns the row
|
|
214
|
-
* grammar — a peer-session tag, a status
|
|
222
|
+
* (already ordered and capped) with a "more" note, one headed group of rows per
|
|
223
|
+
* section, or the themed tail with an "earlier" note. The caller owns the row
|
|
224
|
+
* renderer, so the extension's own row grammar — a peer-session tag, a status
|
|
225
|
+
* glyph — stays with its vocabulary.
|
|
215
226
|
*/
|
|
216
227
|
export class TailResult<Row> implements Component {
|
|
217
228
|
private input: TailResultInput<Row>;
|
|
@@ -239,6 +250,14 @@ export class TailResult<Row> implements Component {
|
|
|
239
250
|
if (input.more > 0) lines.push(clip(` ${overflowHint(this.theme, input.more)}`, w));
|
|
240
251
|
return lines;
|
|
241
252
|
}
|
|
253
|
+
if (input.kind === "sections") {
|
|
254
|
+
for (const section of input.sections) {
|
|
255
|
+
if (section.label) lines.push(clip(paint(this.theme, "faint", section.label), w));
|
|
256
|
+
for (const row of section.rows) lines.push(section.rowLine(row, w, this.theme));
|
|
257
|
+
if (section.more > 0) lines.push(clip(` ${overflowHint(this.theme, section.more)}`, w));
|
|
258
|
+
}
|
|
259
|
+
return lines;
|
|
260
|
+
}
|
|
242
261
|
if (input.earlier > 0) lines.push(clip(earlierHint(this.theme, input.earlier, input.earlierLabel), w));
|
|
243
262
|
for (const line of input.lines) lines.push(this.theme.fg("toolOutput", clip(line, w)));
|
|
244
263
|
return lines;
|
package/lib/web/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The tokens, base rules, app shell, `bf-*` components, HTML atoms, and client
|
|
4
4
|
runtime the two loopback pages draw with: the plan review
|
|
5
5
|
([`../../extensions/plan/web-page.ts`](../../extensions/plan/web-page.ts)) and
|
|
6
|
-
file browse ([`../../extensions/file-browse/styles.ts`](../../extensions/file-browse/styles.ts)).
|
|
6
|
+
file browse ([`../../extensions/file-browse/view/styles.ts`](../../extensions/file-browse/view/styles.ts)).
|
|
7
7
|
The module-by-module inventory is the `web/` row of [`../README.md`](../README.md);
|
|
8
8
|
this file is the conventions those modules exist to make easy.
|
|
9
9
|
|
|
@@ -34,7 +34,10 @@ are computed rather than mapped:
|
|
|
34
34
|
- `--scrim` and `--shadow` — per appearance, because a dimming layer and a lifted
|
|
35
35
|
panel need their own alpha rather than a palette entry.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
`--raised` is the one derived neutral: `--surface` mixed 6% (dark) or 3% (light)
|
|
38
|
+
toward `--text`, for a block that has to read as a plane above the page rather than
|
|
39
|
+
as an outline — the light step is smaller because a darker accent on a lighter step
|
|
40
|
+
falls below AA. Components read tokens only: no literal hex or `rgba()` in `components.ts`, and a
|
|
38
41
|
test fails one. Contrast is asserted, not assumed — text, muted, and accent at
|
|
39
42
|
4.5:1 on both surfaces, the primary label at 4.5:1 on the accent fill, `--dim` at
|
|
40
43
|
3:1 because it carries the line-number gutter, and the status colours at 3:1.
|
package/lib/web/components.ts
CHANGED
|
@@ -18,6 +18,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
18
18
|
.bf-btn:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
|
|
19
19
|
.bf-btn[aria-pressed="true"]{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
|
|
20
20
|
.bf-btn--primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
|
|
21
|
+
.bf-btn--danger{border-color:var(--danger);color:var(--danger)}
|
|
22
|
+
.bf-btn--danger:hover{border-color:var(--danger);background:var(--danger);color:var(--on-accent)}
|
|
21
23
|
.bf-btn--primary:hover{filter:brightness(1.06);color:var(--on-accent)}
|
|
22
24
|
.bf-btn--ghost{border-color:transparent;background:none;padding:0;font:inherit;color:inherit}
|
|
23
25
|
.bf-btn--ghost:hover{color:var(--accent)}
|
|
@@ -36,6 +38,9 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
36
38
|
.bf-segmented__item[aria-current="page"],.bf-segmented__item[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent)}
|
|
37
39
|
.bf-input{border:0;background:transparent;color:var(--text);font:var(--fs-sm) var(--font-mono);width:100%;outline:none}
|
|
38
40
|
.bf-field{display:flex;align-items:center;gap:var(--space-1);background:var(--canvas);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-1) var(--space-2);color:var(--muted);font-size:var(--fs-sm)}
|
|
41
|
+
/* The field is the control: ringing the input inside it drew a border within a border. */
|
|
42
|
+
.bf-field:focus-within{outline:var(--focus-ring) solid var(--accent);outline-offset:2px}
|
|
43
|
+
.bf-field .bf-input:focus-visible{outline:none}
|
|
39
44
|
.bf-textarea{width:100%;min-height:72px;padding:var(--space-2);font:inherit;color:var(--text);background:var(--canvas);border:1px solid var(--border);border-radius:var(--radius-md);resize:vertical}
|
|
40
45
|
.bf-panel{min-width:0;overflow:auto;overscroll-behavior:contain}
|
|
41
46
|
.bf-panel__summary{display:none}
|
|
@@ -60,7 +65,7 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
60
65
|
.bf-dialog mark{background:transparent;color:var(--warn);font-weight:var(--fw-semibold)}
|
|
61
66
|
.bf-dialog__head{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border)}
|
|
62
67
|
.bf-dialog__head h2{margin:0;font-size:var(--fs-lg)}
|
|
63
|
-
.bf-dialog__body{padding:var(--space-3) var(--space-4);font-size:var(--fs-sm);line-height:var(--lh-relaxed)}
|
|
68
|
+
.bf-dialog__body{padding:var(--space-3) var(--space-4);font-size:var(--fs-sm);line-height:var(--lh-relaxed);overflow:auto;min-height:0}
|
|
64
69
|
.bf-dialog__body p{margin:0 0 var(--space-2)}
|
|
65
70
|
.bf-dialog__body p:last-child{margin:0}
|
|
66
71
|
.bf-dialog__hint{color:var(--muted)}
|
package/lib/web/runtime.ts
CHANGED
|
@@ -123,9 +123,15 @@ export function dialogScript(options: DialogOptions): string {
|
|
|
123
123
|
// A button inside a closed dialog has no boxes, so the rects are the test that
|
|
124
124
|
// survives a hidden attribute, a collapsed panel, and a stylesheet swap alike.
|
|
125
125
|
function focusables() {
|
|
126
|
-
|
|
126
|
+
var nodes = Array.prototype.filter.call(dialog.querySelectorAll('${FOCUSABLE}'), function (node) {
|
|
127
127
|
return node.getClientRects && node.getClientRects().length > 0;
|
|
128
128
|
});
|
|
129
|
+
// A panel that names itself focusable is where focus lands on open, so the close
|
|
130
|
+
// button is not the first thing a dialog says with its ring. It stays first in the
|
|
131
|
+
// list, which is what keeps Shift+Tab wrapping inside instead of escaping the scrim.
|
|
132
|
+
var panel = dialog.querySelector('[role="dialog"][tabindex]');
|
|
133
|
+
if (panel && panel.getClientRects && panel.getClientRects().length > 0) nodes.unshift(panel);
|
|
134
|
+
return nodes;
|
|
129
135
|
}
|
|
130
136
|
function toggle(force) {
|
|
131
137
|
var open = force === undefined ? dialog.hidden : force;
|