@gavin-hu/my-pi-agent 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
package/lib/report.ts
CHANGED
|
@@ -2,23 +2,29 @@
|
|
|
2
2
|
* The pending-report coordinator: how a live feature tells the model what
|
|
3
3
|
* happened without interrupting it.
|
|
4
4
|
*
|
|
5
|
-
* A watcher match
|
|
6
|
-
* and raise at most one triggered turn when the work
|
|
7
|
-
*
|
|
5
|
+
* A watcher match, a finished job, and arriving mail follow one rule — report at
|
|
6
|
+
* the next turn, and raise at most one triggered turn per burst when the work
|
|
7
|
+
* opted into `wake`, as soon as the agent is idle. The three traps are subtle
|
|
8
|
+
* enough to have been re-derived once per extension:
|
|
8
9
|
*
|
|
9
10
|
* - a wake is only safe while the agent is idle, so an outcome that arrives
|
|
10
|
-
* mid-turn
|
|
11
|
+
* mid-turn is queued rather than sent, and `agent_settled` retries it;
|
|
12
|
+
* - an outcome outlives the run it arrived in, so the retry asks
|
|
13
|
+
* `wakeRequested()` rather than trusting the one call that queued it;
|
|
11
14
|
* - between the triggered send and the `agent_start` it causes, the agent still
|
|
12
15
|
* looks idle, so a second outcome in that window must not trigger a second
|
|
13
16
|
* turn. `waking` closes that window.
|
|
14
17
|
*
|
|
15
18
|
* Delivery is one shared transcript notice per burst, and a burst is marked
|
|
16
19
|
* reported only after the send succeeded, so a throw leaves it for the next
|
|
17
|
-
* turn.
|
|
18
|
-
*
|
|
20
|
+
* turn. `registerPendingChannel` is the other half: the seven host hooks that
|
|
21
|
+
* drive the machine, registered once for `job` and `watch` rather than copied
|
|
22
|
+
* into each factory. Value-only: every flag lives in the returned closure, never
|
|
23
|
+
* in a module variable, because each extension gets its own module instance.
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
26
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
27
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
28
|
import { pruneStaleNotices, type NoticeMessage } from "./notice.ts";
|
|
23
29
|
|
|
24
30
|
export interface PendingReporterOptions<T> {
|
|
@@ -30,6 +36,11 @@ export interface PendingReporterOptions<T> {
|
|
|
30
36
|
markReported(items: T[]): void;
|
|
31
37
|
/** Build the notice for one burst: model text plus renderer details. */
|
|
32
38
|
buildNotice(items: T[]): NoticeMessage;
|
|
39
|
+
/**
|
|
40
|
+
* Whether any pending outcome asks for a triggered turn. Called by the settle
|
|
41
|
+
* retry, so a mid-turn outcome still wakes once the agent is idle.
|
|
42
|
+
*/
|
|
43
|
+
wakeRequested(): boolean;
|
|
33
44
|
/** The notice's source, so only its own stale entries are pruned. */
|
|
34
45
|
source: string;
|
|
35
46
|
/** Runs after a wake turn was successfully requested, for an extra announcement. */
|
|
@@ -44,8 +55,8 @@ export interface PendingReporter {
|
|
|
44
55
|
sessionStart(): void;
|
|
45
56
|
/** A turn began: outcomes queue up, and none of them triggers a turn. */
|
|
46
57
|
agentStart(): void;
|
|
47
|
-
/** The turn settled: a
|
|
48
|
-
agentSettled(): void;
|
|
58
|
+
/** The turn settled: retry a wake it suppressed, unless it was aborted. */
|
|
59
|
+
agentSettled(event?: { aborted?: boolean }): void;
|
|
49
60
|
/** Report what is queued at the start of a turn, without triggering one. */
|
|
50
61
|
beforeAgentStart(): { message: NoticeMessage } | undefined;
|
|
51
62
|
/** Drop this source's older notices from the outgoing context. */
|
|
@@ -53,26 +64,29 @@ export interface PendingReporter {
|
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
export function createPendingReporter<T>(options: PendingReporterOptions<T>): PendingReporter {
|
|
56
|
-
const { wake, pending, markReported, buildNotice, source, onWoke } = options;
|
|
67
|
+
const { wake, pending, markReported, buildNotice, wakeRequested, source, onWoke } = options;
|
|
57
68
|
let busy = false;
|
|
58
69
|
let waking = false;
|
|
59
70
|
|
|
71
|
+
const report = (): void => {
|
|
72
|
+
if (busy || waking) return;
|
|
73
|
+
if (!wakeRequested()) return;
|
|
74
|
+
const items = pending();
|
|
75
|
+
if (items.length === 0) return;
|
|
76
|
+
try {
|
|
77
|
+
wake(buildNotice(items));
|
|
78
|
+
} catch {
|
|
79
|
+
// Reporting is best-effort; the next turn reports the same items.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Mark only after the send succeeded, so a throw changes nothing.
|
|
83
|
+
markReported(items);
|
|
84
|
+
waking = true;
|
|
85
|
+
onWoke?.(items);
|
|
86
|
+
};
|
|
87
|
+
|
|
60
88
|
return {
|
|
61
|
-
report
|
|
62
|
-
if (busy || waking) return;
|
|
63
|
-
const items = pending();
|
|
64
|
-
if (items.length === 0) return;
|
|
65
|
-
try {
|
|
66
|
-
wake(buildNotice(items));
|
|
67
|
-
} catch {
|
|
68
|
-
// Reporting is best-effort; the next turn reports the same items.
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
// Mark only after the send succeeded, so a throw changes nothing.
|
|
72
|
-
markReported(items);
|
|
73
|
-
waking = true;
|
|
74
|
-
onWoke?.(items);
|
|
75
|
-
},
|
|
89
|
+
report,
|
|
76
90
|
sessionStart: () => {
|
|
77
91
|
busy = false;
|
|
78
92
|
waking = false;
|
|
@@ -80,9 +94,13 @@ export function createPendingReporter<T>(options: PendingReporterOptions<T>): Pe
|
|
|
80
94
|
agentStart: () => {
|
|
81
95
|
busy = true;
|
|
82
96
|
},
|
|
83
|
-
agentSettled: () => {
|
|
97
|
+
agentSettled: (event) => {
|
|
84
98
|
busy = false;
|
|
85
99
|
waking = false;
|
|
100
|
+
// An interrupted run is not a request to interrupt again; its outcomes
|
|
101
|
+
// stay queued for the next turn.
|
|
102
|
+
if (event?.aborted === true) return;
|
|
103
|
+
report();
|
|
86
104
|
},
|
|
87
105
|
beforeAgentStart: () => {
|
|
88
106
|
const items = pending();
|
|
@@ -97,3 +115,77 @@ export function createPendingReporter<T>(options: PendingReporterOptions<T>): Pe
|
|
|
97
115
|
},
|
|
98
116
|
};
|
|
99
117
|
}
|
|
118
|
+
|
|
119
|
+
/** The session-scoped state a channel drives, narrowed to what its hooks need. */
|
|
120
|
+
export interface PendingChannelRuntime {
|
|
121
|
+
/**
|
|
122
|
+
* Load this session's records on every session start. A promise is awaited, so a
|
|
123
|
+
* runtime that resolves a repository or opens a store is fully loaded before the
|
|
124
|
+
* report trigger is armed.
|
|
125
|
+
*/
|
|
126
|
+
load(ctx: ExtensionContext): void | Promise<void>;
|
|
127
|
+
/** Repaint the status chip; a runtime that publishes none omits it. */
|
|
128
|
+
setStatus?(ctx: ExtensionContext): void;
|
|
129
|
+
/** Release the session's resources; idempotent. */
|
|
130
|
+
shutdown(): Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PendingChannelOptions {
|
|
134
|
+
/** The reporter that turns pending outcomes into notices. */
|
|
135
|
+
reporter: PendingReporter;
|
|
136
|
+
/** The runtime whose pending outcomes the notices describe. */
|
|
137
|
+
runtime: PendingChannelRuntime;
|
|
138
|
+
/** Runs on every session start, after the runtime loads: arm the report trigger. */
|
|
139
|
+
onSessionStart?(ctx: ExtensionContext): void;
|
|
140
|
+
/** Runs before the runtime releases: clear what `onSessionStart` armed. */
|
|
141
|
+
onSessionShutdown?(): void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Wire a reporter to the host's session lifecycle.
|
|
146
|
+
*
|
|
147
|
+
* The order inside `session_start` is the contract: the reporter forgets the
|
|
148
|
+
* previous session's busy window, the runtime loads the records, and only then
|
|
149
|
+
* is the report trigger armed — a reload or a replaced session reuses the same
|
|
150
|
+
* runtime. A shutdown clears the trigger before the runtime releases the table it
|
|
151
|
+
* was calling into.
|
|
152
|
+
*/
|
|
153
|
+
export function registerPendingChannel(
|
|
154
|
+
pi: Pick<ExtensionAPI, "on">,
|
|
155
|
+
{ reporter, runtime, onSessionStart, onSessionShutdown }: PendingChannelOptions,
|
|
156
|
+
): void {
|
|
157
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
158
|
+
reporter.sessionStart();
|
|
159
|
+
await runtime.load(ctx);
|
|
160
|
+
onSessionStart?.(ctx);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Tree navigation does not change the pending records; only repaint.
|
|
164
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
165
|
+
runtime.setStatus?.(ctx);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
pi.on("agent_start", () => {
|
|
169
|
+
reporter.agentStart();
|
|
170
|
+
});
|
|
171
|
+
pi.on("agent_settled", (event) => {
|
|
172
|
+
reporter.agentSettled(event);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Report what is queued at the start of the next turn, without triggering one.
|
|
176
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
177
|
+
const reported = reporter.beforeAgentStart();
|
|
178
|
+
if (!reported) return undefined;
|
|
179
|
+
runtime.setStatus?.(ctx);
|
|
180
|
+
return reported;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Keep stale notices out of later turns. Only this source's notices are
|
|
184
|
+
// pruned; another source's are left for its own handler.
|
|
185
|
+
pi.on("context", (event) => reporter.filterContext(event.messages));
|
|
186
|
+
|
|
187
|
+
pi.on("session_shutdown", async () => {
|
|
188
|
+
onSessionShutdown?.();
|
|
189
|
+
await runtime.shutdown();
|
|
190
|
+
});
|
|
191
|
+
}
|
package/lib/requirements.ts
CHANGED
|
@@ -19,7 +19,6 @@ import { configSection, configValueAt, type JsonReadResult, SHARED_CONFIG_FILE }
|
|
|
19
19
|
import type { WebSearchProviderEvent } from "./events.ts";
|
|
20
20
|
import { pdfGuidance } from "./pdf.ts";
|
|
21
21
|
import { getPiInvocation, type PiInvocation } from "./pi-invocation.ts";
|
|
22
|
-
|
|
23
22
|
/** How a check came out. Only `missing` is worth interrupting a session for. */
|
|
24
23
|
export type FindingSeverity = "ok" | "missing" | "degraded" | "info";
|
|
25
24
|
|
|
@@ -216,7 +215,6 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
216
215
|
notes: ["Runs the project's detected command through a shell; a stack with no detector needs `check.command`."],
|
|
217
216
|
},
|
|
218
217
|
plan: {
|
|
219
|
-
optional: [gitCheck("project-local `.pi/plans` (otherwise plans land in the agent directory)")],
|
|
220
218
|
notes: ["Needs an interactive UI to enter plan mode."],
|
|
221
219
|
},
|
|
222
220
|
subagent: {
|
|
@@ -267,6 +265,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
267
265
|
optional: [gitCheck("project-scope memory under the repository root")],
|
|
268
266
|
},
|
|
269
267
|
context: {},
|
|
268
|
+
"session-bus": {},
|
|
270
269
|
wechat: {
|
|
271
270
|
optional: [
|
|
272
271
|
{
|
package/lib/tool-result.ts
CHANGED
|
@@ -1,17 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared shape of a
|
|
2
|
+
* The shared shape of a tool result.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every extension tool returns the same envelope: one text block the model
|
|
5
|
+
* reads, the extension's own `details` for the renderer and the session branch,
|
|
6
|
+
* and — where the tool declares an `outputSchema` — the JSON mirror Pi calls
|
|
7
|
+
* `structuredContent`. A rejected call is that envelope marked `isError`, still
|
|
8
|
+
* carrying the previous state so the model can retry.
|
|
9
|
+
*
|
|
10
|
+
* `textResult` and `failedResult` build the two, so the flag and the absent
|
|
11
|
+
* `structuredContent` are decided once. What stays with the extension is the
|
|
12
|
+
* text and the mirror: `structuredContent` drops `undefined` field by field and
|
|
13
|
+
* projects nested objects, so it is written per `details` type rather than
|
|
14
|
+
* derived. `toolError` / `errorResult` keep the shorthand the
|
|
15
|
+
* action-and-message tools (`job` and `watch`) share.
|
|
7
16
|
*/
|
|
8
17
|
|
|
18
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
19
|
+
|
|
9
20
|
/** One text block in a tool result. */
|
|
10
21
|
export interface ToolErrorContent {
|
|
11
22
|
type: "text";
|
|
12
23
|
text: string;
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
/** A tool result: what the model reads, the extension's details, and its JSON mirror. */
|
|
27
|
+
export interface ToolResult<Details> {
|
|
28
|
+
content: ToolErrorContent[];
|
|
29
|
+
details: Details;
|
|
30
|
+
structuredContent?: Record<string, JsonValue>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A rejected call: the result, marked so the host reports it as an error. */
|
|
34
|
+
export interface ToolFailure<Details> extends ToolResult<Details> {
|
|
35
|
+
isError: true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The content and details both results share. */
|
|
39
|
+
function envelope<Details>(text: string, details: Details): ToolResult<Details> {
|
|
40
|
+
return { content: [{ type: "text", text }], details };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A successful call. */
|
|
44
|
+
export function textResult<Details>(
|
|
45
|
+
text: string,
|
|
46
|
+
details: Details,
|
|
47
|
+
structuredContent?: Record<string, JsonValue>,
|
|
48
|
+
): ToolResult<Details> {
|
|
49
|
+
const result = envelope(text, details);
|
|
50
|
+
return structuredContent === undefined ? result : { ...result, structuredContent };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A rejected call, keeping the state the caller had before it. */
|
|
54
|
+
export function failedResult<Details>(
|
|
55
|
+
text: string,
|
|
56
|
+
details: Details,
|
|
57
|
+
structuredContent?: Record<string, JsonValue>,
|
|
58
|
+
): ToolFailure<Details> {
|
|
59
|
+
const result = envelope(text, details);
|
|
60
|
+
return structuredContent === undefined
|
|
61
|
+
? { ...result, isError: true }
|
|
62
|
+
: { ...result, structuredContent, isError: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
15
65
|
/** A rejected tool call: the message as content, the extension's details, and the error flag. */
|
|
16
66
|
export function toolError<Details extends { action: string; error: string }>(
|
|
17
67
|
message: string,
|
package/lib/tui/README.md
CHANGED
|
@@ -35,17 +35,24 @@ 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,
|
|
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.
|
|
39
40
|
|
|
40
41
|
## Docked list screens
|
|
41
42
|
|
|
42
43
|
Every list screen is `dockListFrame`: the header rule, an optional summary and
|
|
43
|
-
its blank line, the body, an optional range row, an optional detail block,
|
|
44
|
-
the
|
|
45
|
-
it — including the blank line above the detail block, which
|
|
46
|
-
from reading as one more row. A screen that sizes its body
|
|
47
|
-
counts the block with `detailBlockRows`, or the hint row is
|
|
48
|
-
bottom of a short terminal.
|
|
44
|
+
its blank line, the body, an optional range row, an optional detail block, the
|
|
45
|
+
key-hint row, and the closing rule. The frame owns that spacing, so a screen
|
|
46
|
+
never hand-rolls it — including the blank line above the detail block, which
|
|
47
|
+
keeps a footnote from reading as one more row. A screen that sizes its body
|
|
48
|
+
through `fitRows` counts the block with `detailBlockRows`, or the hint row is
|
|
49
|
+
pushed off the bottom of a short terminal.
|
|
50
|
+
|
|
51
|
+
The closing rule is not decoration: Pi's built-in selectors draw one under the
|
|
52
|
+
list and over the footer, and the editor a docked screen replaces draws its own,
|
|
53
|
+
so a screen that ends on a blank removes the dock's bottom edge. A screen that
|
|
54
|
+
lays out its own rows closes its block with `screenFooter(theme, width, hint)`;
|
|
55
|
+
a screen whose row above the hint varies places `plainRule` itself.
|
|
49
56
|
|
|
50
57
|
A list that **opens a pane over one row** — `/jobs`, `/watch`, `/agents`,
|
|
51
58
|
`/check` — subclasses [`dock-screen.ts`](./dock-screen.ts) instead of
|
|
@@ -74,6 +81,20 @@ what a plain list wants — do not hand-roll the frame to get one of them:
|
|
|
74
81
|
(`c` collect, `x` cancel). Return `true` for the keys it claimed; everything
|
|
75
82
|
else falls through to the pane's scroll keys.
|
|
76
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
|
+
|
|
77
98
|
A destructive action asks through [`confirm.ts`](./confirm.ts):
|
|
78
99
|
`ConfirmState.ask(prompt, accept)` holds the question, `resolve(data)` accepts on
|
|
79
100
|
`y`/`Y`/Enter, dismisses on `n`/`N`/Esc/`q`, and returns `undefined` for any
|
|
@@ -92,7 +113,8 @@ Resolve colors through a [`tokens.ts`](./tokens.ts) role
|
|
|
92
113
|
(`paint(theme, role, text)`), never a literal ANSI sequence, hex value, or Pi
|
|
93
114
|
token named inline; a theme swap must re-skin every surface. Use the shared
|
|
94
115
|
`selectionMarker`, `GLYPHS`, `SEPARATORS`, and `STATUS_KEYS` instead of retyping
|
|
95
|
-
their literals,
|
|
116
|
+
their literals, `screenHint` / Pi's `keyHint` for the hint row, and
|
|
117
|
+
`screenFooter` for the hint row with its blank and closing rule.
|
|
96
118
|
|
|
97
119
|
A list accents the focused row's label (`labelRole: selected ? "accent" :
|
|
98
120
|
"label"`); the `❯` marker alone is not enough to tell which row is live. The
|
package/lib/tui/chrome.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* the transcript visible.
|
|
9
9
|
*
|
|
10
10
|
* Screens are full-width, terminal-height-aware components. These helpers build
|
|
11
|
-
* the top rule, clamp a screen's body to the terminal, and
|
|
12
|
-
* footer, so each screen keeps its own layout but not its
|
|
13
|
-
* arithmetic or hint vocabulary.
|
|
11
|
+
* the top rule and the closing rule, clamp a screen's body to the terminal, and
|
|
12
|
+
* render the key-hint footer, so each screen keeps its own layout but not its
|
|
13
|
+
* own copy of the arithmetic or hint vocabulary.
|
|
14
14
|
*
|
|
15
15
|
* No runtime dependencies beyond `@earendil-works/pi-tui`: the theme is passed
|
|
16
16
|
* in, and the viewport options are explicit.
|
|
@@ -20,6 +20,7 @@ import type { Theme, ThemeColor } 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
|
+
import { plainRule } from "./rules.ts";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* `ctx.ui.custom` options that give a screen the whole terminal.
|
|
@@ -88,12 +89,29 @@ export function screenHint(theme: Theme, width: number, hints: ScreenHint[], ind
|
|
|
88
89
|
return truncateToWidth(`${lead}${kept.map(render).join(separator)}`, target);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The bottom of a screen: a blank row, its key-hint row, and the closing rule.
|
|
94
|
+
*
|
|
95
|
+
* {@link dockListFrame} ends a docked list screen with this, and a screen that
|
|
96
|
+
* lays out its own rows — `/context`, the WeChat login and control screens —
|
|
97
|
+
* closes its block with it too, so no screen spells the three rows out alone. A
|
|
98
|
+
* screen whose row above the hint varies (a scroll gauge or a blank) places its
|
|
99
|
+
* own rule with `plainRule` instead.
|
|
100
|
+
*/
|
|
101
|
+
export function screenFooter(theme: Theme, width: number, hint: string): string[] {
|
|
102
|
+
return ["", hint, plainRule(theme, width)];
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* The sections of a docked list screen, assembled by {@link dockListFrame}.
|
|
93
107
|
* `body`, `range`, and `detail` lines are built (and truncated) by the caller;
|
|
94
108
|
* the frame only places them.
|
|
95
109
|
*/
|
|
96
110
|
export interface DockListFrame {
|
|
111
|
+
/** The active theme, used for the closing rule. */
|
|
112
|
+
theme: Theme;
|
|
113
|
+
/** The screen width in columns, used for the closing rule. */
|
|
114
|
+
width: number;
|
|
97
115
|
/** The pre-rendered header line, usually `titleRule(theme, width, title)`. */
|
|
98
116
|
header: string;
|
|
99
117
|
/** A summary line under the header; a blank line follows it. */
|
|
@@ -132,11 +150,14 @@ export function detailBlockRows(detail: readonly string[] | undefined, blank = t
|
|
|
132
150
|
/**
|
|
133
151
|
* Assemble a docked list screen: the caller's header rule, an optional summary
|
|
134
152
|
* and its blank line, the body, an optional range row, optional detail rows after
|
|
135
|
-
* their own blank line,
|
|
153
|
+
* their own blank line, the key-hint row, and the closing rule.
|
|
136
154
|
*
|
|
137
155
|
* This is the convention every docked list screen shares — the bytefly panel,
|
|
138
156
|
* `/jobs`, `/watch`, `/todos`, `/memory`, and `/rewind`. The header line is built
|
|
139
|
-
* by the caller (usually `titleRule`), so the frame never picks
|
|
157
|
+
* by the caller (usually `titleRule`), so the frame never picks the *opening*
|
|
158
|
+
* rule style; the closing rule is always the muted fill, matching the closing
|
|
159
|
+
* rule Pi's built-in selectors draw and the bottom border of the editor slot the
|
|
160
|
+
* screen replaces.
|
|
140
161
|
*/
|
|
141
162
|
export function dockListFrame(frame: DockListFrame): string[] {
|
|
142
163
|
const lines: string[] = [frame.header];
|
|
@@ -147,7 +168,7 @@ export function dockListFrame(frame: DockListFrame): string[] {
|
|
|
147
168
|
if (frame.detailBlank !== false) lines.push("");
|
|
148
169
|
lines.push(...frame.detail);
|
|
149
170
|
}
|
|
150
|
-
lines.push(
|
|
171
|
+
lines.push(...screenFooter(frame.theme, frame.width, frame.hint));
|
|
151
172
|
return lines;
|
|
152
173
|
}
|
|
153
174
|
|
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";
|
|
@@ -33,22 +35,33 @@ import { paint } from "./tokens.ts";
|
|
|
33
35
|
/** Rows the body shows when the terminal height is unknown. */
|
|
34
36
|
const SCREEN_DEFAULT_ROWS = 14;
|
|
35
37
|
|
|
36
|
-
/** Fixed rows around the list body: header, summary, blank, blank, hint,
|
|
38
|
+
/** Fixed rows around the list body: header, summary, blank, blank, hint, closing rule. */
|
|
37
39
|
const LIST_CHROME_ROWS = 6;
|
|
38
40
|
|
|
39
|
-
/** Fixed rows around a detail body: header, subtitle, blank, blank, hint,
|
|
41
|
+
/** Fixed rows around a detail body: header, subtitle, blank, blank, hint, closing rule. */
|
|
40
42
|
const DETAIL_CHROME_ROWS = 6;
|
|
41
43
|
|
|
42
44
|
/** What a detail pane shows: its title, lines, and the one-line subtitle. */
|
|
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));
|
|
@@ -353,12 +407,14 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
353
407
|
}
|
|
354
408
|
|
|
355
409
|
return dockListFrame({
|
|
356
|
-
|
|
410
|
+
theme: this.theme,
|
|
411
|
+
width: w,
|
|
412
|
+
header: titleRule(this.theme, w, this.headerLabel()),
|
|
357
413
|
summary: this.summary(rows, w),
|
|
358
414
|
body,
|
|
359
415
|
range,
|
|
360
416
|
detail: block.map((line) => clip(` ${paint(this.theme, "meta", line)}`, w)),
|
|
361
|
-
hint: this.
|
|
417
|
+
hint: this.footer(w, this.listHints(rows)),
|
|
362
418
|
});
|
|
363
419
|
}
|
|
364
420
|
|
|
@@ -366,6 +422,7 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
366
422
|
this.syncDetail();
|
|
367
423
|
// A live pane whose row is gone has already closed itself: draw the list.
|
|
368
424
|
if (this.mode !== "detail") return this.renderList(w);
|
|
425
|
+
this.syncText(w);
|
|
369
426
|
const facts = this.detailFacts ? this.detailFacts(w) : [];
|
|
370
427
|
const visible = fitRows(
|
|
371
428
|
this.options.viewportRowsSource,
|
|
@@ -389,18 +446,21 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
389
446
|
}
|
|
390
447
|
|
|
391
448
|
return dockListFrame({
|
|
449
|
+
theme: this.theme,
|
|
450
|
+
width: w,
|
|
392
451
|
header: titleRule(this.theme, w, this.pane.title(this.options.detailTitle)),
|
|
393
452
|
summary: clip(` ${paint(this.theme, "faint", this.pane.subtitleLine())}`, w),
|
|
394
453
|
body,
|
|
395
454
|
range,
|
|
396
|
-
hint:
|
|
455
|
+
hint: this.footer(w, this.detailHints(this.detailRow)),
|
|
397
456
|
});
|
|
398
457
|
}
|
|
399
458
|
|
|
400
459
|
/**
|
|
401
460
|
* Re-read a live pane in place. This runs inside a render, so it must not ask
|
|
402
461
|
* for another frame; a source that disappeared closes the pane instead of
|
|
403
|
-
* 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.
|
|
404
464
|
*/
|
|
405
465
|
private syncDetail(): void {
|
|
406
466
|
if (!this.liveDetail()) return;
|
|
@@ -408,11 +468,23 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
408
468
|
if (!id) return;
|
|
409
469
|
const source = this.detailById?.(id);
|
|
410
470
|
if (!source) {
|
|
411
|
-
this.
|
|
412
|
-
this.pane.close();
|
|
413
|
-
this.detailFacts = undefined;
|
|
471
|
+
this.closeDetail();
|
|
414
472
|
return;
|
|
415
473
|
}
|
|
416
|
-
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 });
|
|
417
489
|
}
|
|
418
490
|
}
|