@oh-my-pi/pi-tui 18.2.11 → 18.3.1
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/CHANGELOG.md +44 -0
- package/THIRD-PARTY-NOTICES.txt +3 -3
- package/dist/types/apps/ps-data.d.ts +1 -1
- package/dist/types/apps/ps-top.d.ts +1 -1
- package/dist/types/chat/chat-transcript-builder.d.ts +1 -1
- package/dist/types/chat/read-tool-group.d.ts +3 -3
- package/dist/types/chat/user-message.d.ts +4 -1
- package/dist/types/components/editor.d.ts +0 -2
- package/dist/types/components/input.d.ts +16 -0
- package/dist/types/overlays/annotation-overlay.d.ts +30 -0
- package/dist/types/overlays/annotation-types.d.ts +84 -0
- package/dist/types/overlays/btw-history-panel.d.ts +4 -1
- package/dist/types/overlays/copy-selector.d.ts +30 -2
- package/dist/types/overlays/copy-targets.d.ts +2 -0
- package/dist/types/overlays/model-hub.d.ts +2 -2
- package/dist/types/overlays/oauth-selector.d.ts +3 -6
- package/dist/types/overlays/settings-defs.d.ts +5 -0
- package/dist/types/overlays/usage-dashboard.d.ts +2 -0
- package/dist/types/prompt/composer.d.ts +12 -1
- package/dist/types/prompt/custom-editor.d.ts +5 -26
- package/dist/types/prompt/image-source.d.ts +9 -8
- package/dist/types/space-hold.d.ts +45 -0
- package/dist/types/status-line/context-usage.d.ts +3 -1
- package/dist/types/status-line/host.d.ts +2 -0
- package/dist/types/theme/mermaid-cache.d.ts +4 -6
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/cfg-render.d.ts +37 -0
- package/dist/types/tools/cfg-url.d.ts +16 -0
- package/dist/types/tools/daemon.d.ts +46 -0
- package/dist/types/tools/find.d.ts +2 -2
- package/dist/types/tools/irc.d.ts +30 -0
- package/dist/types/tools/line-ranges.d.ts +1 -1
- package/dist/types/tools/output-meta.d.ts +2 -0
- package/dist/types/tools/proc-render.d.ts +27 -0
- package/dist/types/tools/read.d.ts +18 -4
- package/dist/types/tools/result-card.d.ts +20 -0
- package/dist/types/tools/url-scheme-host.d.ts +29 -0
- package/dist/types/tools/wait.d.ts +131 -0
- package/dist/types/tools/write.d.ts +6 -0
- package/dist/types/tools/xd-url.d.ts +8 -0
- package/dist/types/tools/xdev.d.ts +9 -1
- package/dist/types/windows-altgr.d.ts +54 -0
- package/package.json +13 -10
- package/src/apps/debug/protocol-probe.ts +1 -1
- package/src/apps/ps-data.ts +1 -1
- package/src/apps/ps-top.ts +1 -1
- package/src/chat/chat-transcript-builder.ts +9 -5
- package/src/chat/read-tool-group.ts +8 -9
- package/src/chat/tool-execution.ts +15 -13
- package/src/chat/transcript-render-helpers.ts +3 -3
- package/src/chat/user-message.ts +16 -6
- package/src/components/editor.ts +2 -4
- package/src/components/input.ts +103 -30
- package/src/components/markdown.ts +19 -7
- package/src/desktop-notify.ts +1 -1
- package/src/overlays/annotation-overlay.ts +1144 -0
- package/src/overlays/annotation-types.ts +78 -0
- package/src/overlays/btw-history-panel.ts +4 -0
- package/src/overlays/copy-selector.ts +45 -19
- package/src/overlays/copy-targets.ts +3 -3
- package/src/overlays/model-hub.ts +5 -4
- package/src/overlays/oauth-selector.ts +8 -12
- package/src/overlays/plan-review-overlay.ts +213 -40
- package/src/overlays/settings-defs.ts +6 -0
- package/src/overlays/settings-selector.ts +4 -3
- package/src/overlays/usage-dashboard.ts +22 -1
- package/src/prompt/composer.ts +83 -56
- package/src/prompt/custom-editor.ts +15 -137
- package/src/prompt/image-source.ts +9 -8
- package/src/prompt/tips.txt +1 -0
- package/src/setup/scenes/sign-in.ts +1 -1
- package/src/space-hold.ts +143 -0
- package/src/status-line/component.ts +6 -0
- package/src/status-line/context-usage.ts +15 -3
- package/src/status-line/host.ts +2 -0
- package/src/status-line/segments.ts +4 -0
- package/src/terminal-capabilities.ts +2 -2
- package/src/terminal.ts +8 -1
- package/src/theme/mermaid-cache.ts +83 -25
- package/src/tools/bash.ts +14 -1
- package/src/tools/cfg-render.ts +117 -0
- package/src/tools/cfg-url.ts +30 -0
- package/src/tools/daemon.ts +51 -0
- package/src/tools/find.ts +7 -6
- package/src/tools/index.ts +2 -2
- package/src/tools/irc.ts +33 -0
- package/src/tools/line-ranges.ts +13 -3
- package/src/tools/output-meta.ts +2 -0
- package/src/tools/proc-render.ts +270 -0
- package/src/tools/read.ts +118 -56
- package/src/tools/result-card.ts +36 -0
- package/src/tools/url-scheme-host.ts +42 -0
- package/src/tools/wait.ts +595 -0
- package/src/tools/write.ts +120 -1
- package/src/tools/xd-url.ts +9 -0
- package/src/tools/xdev.ts +9 -1
- package/src/windows-altgr.ts +227 -0
- package/dist/types/chat/read-target.d.ts +0 -4
- package/dist/types/tools/hub.d.ts +0 -342
- package/src/chat/read-target.ts +0 -7
- package/src/tools/hub.ts +0 -1492
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { Component } from "../tui";
|
|
2
|
+
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../render";
|
|
3
|
+
import {
|
|
4
|
+
cappedHeadLines,
|
|
5
|
+
formatBadge,
|
|
6
|
+
formatDuration,
|
|
7
|
+
formatErrorDetail,
|
|
8
|
+
formatStatusIcon,
|
|
9
|
+
PREVIEW_LIMITS,
|
|
10
|
+
TRUNCATE_LENGTHS,
|
|
11
|
+
type ToolUIColor,
|
|
12
|
+
} from "../render/render-utils";
|
|
13
|
+
import type { Theme } from "../theme/theme";
|
|
14
|
+
import type { RenderResultOptions } from "./renderer";
|
|
15
|
+
import type { AgentActivitySnapshot, CoordinationDetails, JobSnapshot } from "./wait";
|
|
16
|
+
import type { IrcDeliveryReceipt } from "./irc";
|
|
17
|
+
import type { DaemonSnapshot } from "./daemon";
|
|
18
|
+
import { styleTerminalRow } from "./terminal-output";
|
|
19
|
+
import { card, type CardToolResult as ToolResult, firstText, safe } from "./result-card";
|
|
20
|
+
|
|
21
|
+
export interface ProcReadDetails {
|
|
22
|
+
jobs?: JobSnapshot[];
|
|
23
|
+
agents?: AgentActivitySnapshot[];
|
|
24
|
+
daemons?: DaemonSnapshot[];
|
|
25
|
+
job?: JobSnapshot;
|
|
26
|
+
daemon?: DaemonSnapshot;
|
|
27
|
+
log?: string;
|
|
28
|
+
terminalRows?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ProcWriteDetails =
|
|
32
|
+
| CoordinationDetails
|
|
33
|
+
| { action: "stop" | "stdin" | "mode"; daemon: DaemonSnapshot; input?: string; mode?: string };
|
|
34
|
+
|
|
35
|
+
/** Process operation selected by a write URL, independent of its content. */
|
|
36
|
+
export type ProcWriteAction = "stdin" | "mode" | "kill";
|
|
37
|
+
|
|
38
|
+
function preview(body: string, expanded: boolean, theme: Theme, tone: "dim" | "toolOutput" = "dim"): string[] {
|
|
39
|
+
if (!body.trim()) return [];
|
|
40
|
+
const limit = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
41
|
+
const shown = cappedHeadLines(
|
|
42
|
+
body.split("\n").filter(line => line.trim()),
|
|
43
|
+
limit,
|
|
44
|
+
);
|
|
45
|
+
const quote = theme.fg("dim", theme.md.quoteBorder);
|
|
46
|
+
const lines = shown.lines.map(
|
|
47
|
+
line =>
|
|
48
|
+
` ${quote} ${theme.fg(tone, truncateToWidth(safe(line.trim()), TRUNCATE_LENGTHS.LINE, Ellipsis.Unicode))}`,
|
|
49
|
+
);
|
|
50
|
+
if (shown.hidden) lines.push(` ${quote} ${theme.fg("dim", `… +${shown.hidden} more lines`)}`);
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function receiptColor(outcome: IrcDeliveryReceipt["outcome"]): ToolUIColor {
|
|
55
|
+
return outcome === "failed" ? "error" : outcome === "revived" ? "warning" : "success";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function renderAgentWrite(
|
|
59
|
+
to: string,
|
|
60
|
+
body: string,
|
|
61
|
+
result: ToolResult | undefined,
|
|
62
|
+
details: CoordinationDetails | undefined,
|
|
63
|
+
options: RenderResultOptions,
|
|
64
|
+
theme: Theme,
|
|
65
|
+
): Component {
|
|
66
|
+
return card((width, expanded) => {
|
|
67
|
+
const recipient = safe(details?.to || to || "…");
|
|
68
|
+
const title = `IRC ${theme.nav.selected} ${recipient}`;
|
|
69
|
+
const receipts = details?.receipts ?? [];
|
|
70
|
+
const delivered = receipts.filter(receipt => receipt.outcome !== "failed").length;
|
|
71
|
+
const failed = receipts.length - delivered;
|
|
72
|
+
const error = result?.isError || (failed > 0 && delivered === 0);
|
|
73
|
+
const meta: string[] = [];
|
|
74
|
+
if (recipient === "all") meta.push("broadcast");
|
|
75
|
+
if (receipts.length === 1) meta.push(theme.fg(receiptColor(receipts[0]!.outcome), receipts[0]!.outcome));
|
|
76
|
+
else if (receipts.length > 1) {
|
|
77
|
+
if (delivered) meta.push(theme.fg("success", `${delivered} delivered`));
|
|
78
|
+
if (failed) meta.push(theme.fg("error", `${failed} failed`));
|
|
79
|
+
}
|
|
80
|
+
const header = renderStatusLine(
|
|
81
|
+
result === undefined
|
|
82
|
+
? { icon: "pending", title, meta }
|
|
83
|
+
: error
|
|
84
|
+
? { icon: "error", title, meta }
|
|
85
|
+
: { iconOverride: theme.styledSymbol("tool.irc", "accent"), title, meta },
|
|
86
|
+
theme,
|
|
87
|
+
);
|
|
88
|
+
if (result?.isError && receipts.length === 0)
|
|
89
|
+
return [header, formatErrorDetail(firstText(result) || "Message delivery failed.", theme)];
|
|
90
|
+
const lines = [header, ...preview(body, expanded, theme)];
|
|
91
|
+
if (result && receipts.length === 0)
|
|
92
|
+
lines.push(theme.fg("muted", firstText(result) || "No live peers to broadcast to."));
|
|
93
|
+
if (receipts.length > 1 || failed > 0) {
|
|
94
|
+
lines.push(
|
|
95
|
+
...renderTreeList(
|
|
96
|
+
{
|
|
97
|
+
items: receipts,
|
|
98
|
+
expanded,
|
|
99
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
100
|
+
itemType: "recipient",
|
|
101
|
+
renderItem: receipt =>
|
|
102
|
+
`${theme.fg("toolOutput", safe(receipt.to))} ${formatBadge(receipt.outcome, receiptColor(receipt.outcome), theme)}${receipt.error ? ` ${theme.fg("error", `${theme.format.dash} ${safe(receipt.error)}`)}` : ""}`,
|
|
103
|
+
},
|
|
104
|
+
theme,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return lines.map(line => truncateToWidth(line, width, Ellipsis.Unicode));
|
|
109
|
+
}, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function daemonMeta(daemon: DaemonSnapshot, theme: Theme): string[] {
|
|
113
|
+
const stateColor =
|
|
114
|
+
daemon.state === "ready" || daemon.state === "running"
|
|
115
|
+
? "success"
|
|
116
|
+
: daemon.state === "failed"
|
|
117
|
+
? "error"
|
|
118
|
+
: "warning";
|
|
119
|
+
const meta = [theme.fg(stateColor, daemon.state)];
|
|
120
|
+
if (daemon.pid !== undefined) meta.push(`pid ${daemon.pid}`);
|
|
121
|
+
meta.push(
|
|
122
|
+
`${daemon.exitedAt === undefined ? "up" : "ran"} ${formatDuration(Math.max(0, (daemon.exitedAt ?? Date.now()) - daemon.startedAt))}`,
|
|
123
|
+
);
|
|
124
|
+
if (daemon.detached) meta.push("detached");
|
|
125
|
+
else if (daemon.persist) meta.push("persistent");
|
|
126
|
+
return meta;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function jobRow(job: JobSnapshot, theme: Theme): string {
|
|
130
|
+
const icon = formatStatusIcon(
|
|
131
|
+
job.status === "cancelled"
|
|
132
|
+
? "aborted"
|
|
133
|
+
: job.status === "failed"
|
|
134
|
+
? "error"
|
|
135
|
+
: job.status === "running"
|
|
136
|
+
? "running"
|
|
137
|
+
: "done",
|
|
138
|
+
theme,
|
|
139
|
+
);
|
|
140
|
+
return `${icon} ${formatBadge(job.type, job.status === "failed" ? "error" : job.status === "cancelled" ? "warning" : "accent", theme)} ${theme.fg("toolOutput", safe(job.id))} ${theme.fg("dim", safe(job.label))} ${theme.fg("dim", formatDuration(job.durationMs))}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Render live and completed process writes with the URL-selected operation. */
|
|
144
|
+
export function renderProcWrite(
|
|
145
|
+
id: string,
|
|
146
|
+
action: ProcWriteAction,
|
|
147
|
+
content: string | undefined,
|
|
148
|
+
result: ToolResult | undefined,
|
|
149
|
+
details: ProcWriteDetails | undefined,
|
|
150
|
+
options: RenderResultOptions,
|
|
151
|
+
theme: Theme,
|
|
152
|
+
): Component {
|
|
153
|
+
return card((_width, expanded) => {
|
|
154
|
+
const title = `Proc ${action} ${safe(id || "…")}`;
|
|
155
|
+
const daemon = details && "daemon" in details ? details.daemon : undefined;
|
|
156
|
+
const header = renderStatusLine(
|
|
157
|
+
{
|
|
158
|
+
icon:
|
|
159
|
+
result === undefined ? "pending" : result.isError ? "error" : action === "kill" ? "aborted" : "success",
|
|
160
|
+
title,
|
|
161
|
+
meta: daemon ? daemonMeta(daemon, theme) : action === "mode" && content ? [safe(content)] : [],
|
|
162
|
+
},
|
|
163
|
+
theme,
|
|
164
|
+
);
|
|
165
|
+
if (result?.isError) return [header, formatErrorDetail(firstText(result) || "Process operation failed.", theme)];
|
|
166
|
+
const lines = [header];
|
|
167
|
+
if (content && action === "stdin") lines.push(...preview(content, expanded, theme));
|
|
168
|
+
if (details && "op" in details && details.op === "cancel") {
|
|
169
|
+
const jobs = details.jobs ?? [];
|
|
170
|
+
const outcomes = details.cancelled ?? [];
|
|
171
|
+
lines.push(
|
|
172
|
+
...renderTreeList(
|
|
173
|
+
{
|
|
174
|
+
items: outcomes,
|
|
175
|
+
expanded,
|
|
176
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
177
|
+
itemType: "job",
|
|
178
|
+
renderItem: outcome => {
|
|
179
|
+
const job = jobs.find(item => item.id === outcome.id);
|
|
180
|
+
return `${job ? `${jobRow(job, theme)} ` : `${theme.fg("toolOutput", safe(outcome.id))} `}${formatBadge(outcome.status, outcome.status === "cancelled" ? "warning" : "error", theme)}`;
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
theme,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return lines;
|
|
188
|
+
}, options);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function renderProcRead(
|
|
192
|
+
id: string,
|
|
193
|
+
result: ToolResult | undefined,
|
|
194
|
+
details: ProcReadDetails | undefined,
|
|
195
|
+
options: RenderResultOptions,
|
|
196
|
+
theme: Theme,
|
|
197
|
+
): Component {
|
|
198
|
+
return card((_width, expanded) => {
|
|
199
|
+
const title = id ? `Proc ${safe(id)}` : "Proc jobs & services";
|
|
200
|
+
const daemon = details?.daemon;
|
|
201
|
+
const header = renderStatusLine(
|
|
202
|
+
{
|
|
203
|
+
icon: result === undefined ? "pending" : result.isError ? "error" : "info",
|
|
204
|
+
title,
|
|
205
|
+
meta: daemon ? daemonMeta(daemon, theme) : [],
|
|
206
|
+
},
|
|
207
|
+
theme,
|
|
208
|
+
);
|
|
209
|
+
if (result?.isError) return [header, formatErrorDetail(firstText(result) || "Process read failed.", theme)];
|
|
210
|
+
if (!result) return [header];
|
|
211
|
+
if (details?.job)
|
|
212
|
+
return [
|
|
213
|
+
header,
|
|
214
|
+
jobRow(details.job, theme),
|
|
215
|
+
...preview(
|
|
216
|
+
details.log ?? details.job.errorText ?? details.job.resultText ?? "",
|
|
217
|
+
expanded,
|
|
218
|
+
theme,
|
|
219
|
+
"toolOutput",
|
|
220
|
+
),
|
|
221
|
+
];
|
|
222
|
+
if (daemon) {
|
|
223
|
+
const output = details.terminalRows ?? (details.log ?? "").split("\n").filter(Boolean);
|
|
224
|
+
const limit = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
225
|
+
const visible = output
|
|
226
|
+
.slice(-limit)
|
|
227
|
+
.map(
|
|
228
|
+
line =>
|
|
229
|
+
` ${styleTerminalRow(truncateToWidth(safe(line), TRUNCATE_LENGTHS.LINE, Ellipsis.Unicode), theme.fg("toolOutput", ""))}`,
|
|
230
|
+
);
|
|
231
|
+
if (output.length > limit) visible.unshift(theme.fg("dim", ` … ${output.length - limit} earlier lines`));
|
|
232
|
+
return [header, ...visible];
|
|
233
|
+
}
|
|
234
|
+
if (id && !details?.jobs && !details?.daemons && !details?.agents) {
|
|
235
|
+
return [header, ...preview(firstText(result), expanded, theme, "toolOutput")];
|
|
236
|
+
}
|
|
237
|
+
const jobs = details?.jobs ?? [];
|
|
238
|
+
const services = details?.daemons ?? [];
|
|
239
|
+
const agents = details?.agents ?? [];
|
|
240
|
+
const meta = [
|
|
241
|
+
`${jobs.length} jobs`,
|
|
242
|
+
`${services.length} services`,
|
|
243
|
+
...(agents.length ? [`${agents.length} agents`] : []),
|
|
244
|
+
];
|
|
245
|
+
const listHeader = renderStatusLine({ icon: "info", title, meta }, theme);
|
|
246
|
+
const items = [
|
|
247
|
+
...jobs.map(job => ({ label: jobRow(job, theme) })),
|
|
248
|
+
...services.map(service => ({
|
|
249
|
+
label: `${formatBadge("service", "accent", theme)} ${theme.fg("toolOutput", safe(service.name))} ${formatBadge(service.state, service.state === "failed" ? "error" : service.state === "ready" || service.state === "running" ? "success" : "warning", theme)} ${daemonMeta(service, theme).slice(1).join(theme.sep.dot)}`,
|
|
250
|
+
})),
|
|
251
|
+
...agents.map(agent => ({
|
|
252
|
+
label: `${formatBadge("agent", agent.live ? "accent" : "warning", theme)} ${theme.fg("toolOutput", safe(agent.id))} ${theme.fg("dim", formatDuration(agent.ageMs))}`,
|
|
253
|
+
})),
|
|
254
|
+
];
|
|
255
|
+
return [
|
|
256
|
+
listHeader,
|
|
257
|
+
...renderTreeList(
|
|
258
|
+
{
|
|
259
|
+
items,
|
|
260
|
+
expanded,
|
|
261
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
262
|
+
itemType: "process",
|
|
263
|
+
renderItem: item => item.label,
|
|
264
|
+
},
|
|
265
|
+
theme,
|
|
266
|
+
),
|
|
267
|
+
...(items.length ? [] : [theme.fg("dim", "No background jobs or services.")]),
|
|
268
|
+
];
|
|
269
|
+
}, options);
|
|
270
|
+
}
|
package/src/tools/read.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { SummaryResult } from "@oh-my-pi/pi-natives";
|
|
2
2
|
import { formatNumberedLine } from "./hashline-format";
|
|
3
3
|
import { LINE_RANGE_CHUNK_SOURCE, parseLineRanges } from "./line-ranges";
|
|
4
|
+
import * as os from "node:os";
|
|
4
5
|
import * as path from "node:path";
|
|
6
|
+
import { parseArchivePathCandidates } from "@oh-my-pi/pi-utils/ar";
|
|
5
7
|
import type { Component } from "../tui";
|
|
6
8
|
import { Text } from "../components/text";
|
|
7
|
-
import type { RenderResultOptions, ToolRenderer } from "./renderer";
|
|
9
|
+
import type { RenderResultOptions, ToolActivityContext, ToolActivitySummary, ToolRenderer } from "./renderer";
|
|
8
10
|
import { getLanguageFromPath } from "../lang-from-path";
|
|
9
11
|
import type { Theme } from "../theme/theme";
|
|
10
12
|
import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine } from "../render";
|
|
@@ -16,6 +18,10 @@ import { formatBytes, sanitizeDisplayLines, shortenPath, wrapBrackets } from "..
|
|
|
16
18
|
|
|
17
19
|
import type { OutputMeta } from "./output-meta";
|
|
18
20
|
import type { TruncationResult } from "./streaming-output";
|
|
21
|
+
import { renderProcRead, type ProcReadDetails } from "./proc-render";
|
|
22
|
+
import { renderCfgRead, type CfgReadDetails } from "./cfg-render";
|
|
23
|
+
import type { CardToolResult } from "./result-card";
|
|
24
|
+
import { type InternalUrlSchemeSpec, internalUrlSchemeSpec, splitUrlScheme } from "./url-scheme-host";
|
|
19
25
|
|
|
20
26
|
/** Read result metadata retains truncation statistics, not a second copy of the body. */
|
|
21
27
|
export type ReadTruncationStats = Omit<TruncationResult, "content">;
|
|
@@ -23,6 +29,8 @@ export type ReadTruncationStats = Omit<TruncationResult, "content">;
|
|
|
23
29
|
/** Display metadata for file and URL reads. */
|
|
24
30
|
export interface ReadToolDetails {
|
|
25
31
|
kind?: "file" | "url";
|
|
32
|
+
proc?: ProcReadDetails;
|
|
33
|
+
cfg?: CfgReadDetails;
|
|
26
34
|
/** Filesystem hyperlink target resolved by the executing tool. */
|
|
27
35
|
displayTarget?: string;
|
|
28
36
|
truncation?: ReadTruncationStats;
|
|
@@ -75,36 +83,6 @@ const INTERNAL_URL_SELECTOR_PART_RE = new RegExp(
|
|
|
75
83
|
String.raw`^(?:raw|conflicts|img|${RANGE_LIST_SRC}|-\d+(?:[-+]\d+)?)$`,
|
|
76
84
|
"i",
|
|
77
85
|
);
|
|
78
|
-
// Schemes whose host grammar is identifier-shaped, so any trailing
|
|
79
|
-
// `:<selector-chunk>` is unambiguously a read-tool selector. `mcp://` is
|
|
80
|
-
// excluded because mcp resource URIs may legitimately contain colons. `ssh://`
|
|
81
|
-
// is included despite an optional `:port`; `splitInternalUrlSel` skips the peel
|
|
82
|
-
// for an `ssh://host:port` that has no `/path`, so the port colon is never
|
|
83
|
-
// mistaken for a selector (a real ssh selector trails the `/path`, e.g.
|
|
84
|
-
// `ssh://h/f:1-5`).
|
|
85
|
-
const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
86
|
-
agent: true,
|
|
87
|
-
artifact: true,
|
|
88
|
-
issue: true,
|
|
89
|
-
history: true,
|
|
90
|
-
local: true,
|
|
91
|
-
memory: true,
|
|
92
|
-
omp: true,
|
|
93
|
-
pr: true,
|
|
94
|
-
rule: true,
|
|
95
|
-
security: true,
|
|
96
|
-
skill: true,
|
|
97
|
-
ssh: true,
|
|
98
|
-
vault: true,
|
|
99
|
-
};
|
|
100
|
-
// Schemes whose resource URIs are server-defined and may legitimately end
|
|
101
|
-
// with selector-shaped tails (e.g. `:raw`, `:conflicts`, `:1-50`, `/:raw`).
|
|
102
|
-
// `McpProtocolHandler` resolves by exact URI match (`r.uri === uri`), so
|
|
103
|
-
// peeling syntactically can make valid resources unreachable. Keep these
|
|
104
|
-
// schemes opaque; selector support for them needs a resolver-aware path that
|
|
105
|
-
// tries the exact URI before interpreting any suffix as a read selector.
|
|
106
|
-
const OPAQUE_RESOURCE_SCHEMES: ReadonlySet<string> = new Set(["mcp"]);
|
|
107
|
-
const INTERNAL_URL_SCHEME_RE = /^([a-z][a-z0-9+.-]*):\/\//i;
|
|
108
86
|
/** Split a filesystem path from its trailing read selector. */
|
|
109
87
|
export function splitPathAndSel(rawPath: string): { path: string; sel?: string } {
|
|
110
88
|
const colon = rawPath.lastIndexOf(":");
|
|
@@ -148,30 +126,28 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
|
|
|
148
126
|
* This function iteratively peels selector-shaped chunks (well-formed plus
|
|
149
127
|
* common malformed shapes like `:-N-M`) so the rest of the read tool can pass a
|
|
150
128
|
* clean URL to the protocol handler and surface selector errors via parseSel
|
|
151
|
-
* instead of as misleading "host invalid" errors from the handler.
|
|
152
|
-
*
|
|
129
|
+
* instead of as misleading "host invalid" errors from the handler.
|
|
130
|
+
*
|
|
131
|
+
* Only schemes whose spec declares `lines` selectors peel; server-defined URIs
|
|
132
|
+
* (`opaque`), selector-free schemes, and unregistered schemes pass through
|
|
133
|
+
* verbatim. `specOf` defaults to the installed scheme host, so nothing peels
|
|
134
|
+
* before a host is installed.
|
|
153
135
|
*
|
|
154
136
|
* Falls back to the input unchanged when nothing matches.
|
|
155
137
|
*/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// authority, a trailing `:NNNN` is the port, not a read selector
|
|
170
|
-
// (e.g. ssh://host:2222). Other schemes' authority-trailing selectors
|
|
171
|
-
// (artifact://5:1-50) still peel, so this guard is ssh-specific.
|
|
172
|
-
if (scheme === "ssh" && rawPath.indexOf("/", schemeEnd) === -1) {
|
|
173
|
-
return { path: rawPath };
|
|
174
|
-
}
|
|
138
|
+
export function splitInternalUrlSel(
|
|
139
|
+
rawPath: string,
|
|
140
|
+
specOf: (scheme: string) => InternalUrlSchemeSpec | undefined = internalUrlSchemeSpec,
|
|
141
|
+
): { path: string; sel?: string } {
|
|
142
|
+
const url = splitUrlScheme(rawPath);
|
|
143
|
+
if (!url) return { path: rawPath };
|
|
144
|
+
const spec = specOf(url.scheme);
|
|
145
|
+
if (spec?.selectors !== "lines") return { path: rawPath };
|
|
146
|
+
// With no `/path` after the authority, a trailing `:N` is the port
|
|
147
|
+
// (ssh://host:2222), not a read selector.
|
|
148
|
+
if (spec.portAuthority && !url.rest.includes("/")) return { path: rawPath };
|
|
149
|
+
|
|
150
|
+
const schemeEnd = rawPath.length - url.rest.length;
|
|
175
151
|
let path = rawPath;
|
|
176
152
|
const chunks: string[] = [];
|
|
177
153
|
while (true) {
|
|
@@ -215,10 +191,77 @@ export interface ReadRenderArgs {
|
|
|
215
191
|
raw?: boolean;
|
|
216
192
|
}
|
|
217
193
|
|
|
218
|
-
|
|
194
|
+
/** Transcript card for reads of a scheme with its own UI (process table, settings tree). */
|
|
195
|
+
interface ReadUrlCard {
|
|
196
|
+
/** Activity label, e.g. `Process · web`. */
|
|
197
|
+
readonly label: string;
|
|
198
|
+
/** Activity detail when the URL names no target. */
|
|
199
|
+
readonly rootDetail: string;
|
|
200
|
+
/** Result details field whose presence identifies this card. */
|
|
201
|
+
readonly detailsKey: "proc" | "cfg";
|
|
202
|
+
/** Pending call card when `result` is undefined, else the finished result card. */
|
|
203
|
+
render(
|
|
204
|
+
url: string,
|
|
205
|
+
target: string,
|
|
206
|
+
result: CardToolResult | undefined,
|
|
207
|
+
details: ReadToolDetails | undefined,
|
|
208
|
+
options: RenderResultOptions,
|
|
209
|
+
uiTheme: Theme,
|
|
210
|
+
): Component;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Read cards keyed by URL scheme. */
|
|
214
|
+
const READ_URL_CARDS: Record<string, ReadUrlCard> = {
|
|
215
|
+
proc: {
|
|
216
|
+
label: "Process",
|
|
217
|
+
rootDetail: "jobs & services",
|
|
218
|
+
detailsKey: "proc",
|
|
219
|
+
render: (_url, target, result, details, options, uiTheme) =>
|
|
220
|
+
renderProcRead(target, result, details?.proc, options, uiTheme),
|
|
221
|
+
},
|
|
222
|
+
cfg: {
|
|
223
|
+
label: "Config",
|
|
224
|
+
rootDetail: "all settings",
|
|
225
|
+
detailsKey: "cfg",
|
|
226
|
+
render: (url, _target, result, details, options, uiTheme) =>
|
|
227
|
+
renderCfgRead(splitInternalUrlSel(url).path, result, details?.cfg, options, uiTheme),
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Card that renders a read of `rawPath` with the text after `scheme://` as its target.
|
|
233
|
+
* Result details identify the card before the URL scheme does.
|
|
234
|
+
*/
|
|
235
|
+
function readUrlCard(rawPath: string, details?: ReadToolDetails): { card: ReadUrlCard; target: string } | undefined {
|
|
236
|
+
const url = splitUrlScheme(rawPath);
|
|
237
|
+
const target = url?.rest ?? "";
|
|
238
|
+
if (details) {
|
|
239
|
+
for (const scheme in READ_URL_CARDS) {
|
|
240
|
+
const card = READ_URL_CARDS[scheme];
|
|
241
|
+
if (details[card.detailsKey] !== undefined) return { card, target };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!url || !Object.hasOwn(READ_URL_CARDS, url.scheme)) return undefined;
|
|
245
|
+
return { card: READ_URL_CARDS[url.scheme], target };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A scheme-less host followed by a slash can be a web target. Do not force a
|
|
249
|
+
// file: link onto it; explicit relative paths (./host/path) remain filesystem paths.
|
|
250
|
+
const BARE_WEB_HOST_RE = /^(?:(?:[a-z][a-z0-9-]*|\[[0-9a-f:]+\])(?::\d+)|(?:[a-z0-9-]+\.)+[a-z0-9-]+(?::\d+)?)\//i;
|
|
251
|
+
|
|
252
|
+
/** Local file a pending read/write input points at, before the tool resolves it:
|
|
253
|
+
* expands `~` and drops archive-member / SQLite-row selectors so the link opens
|
|
254
|
+
* the containing file. */
|
|
255
|
+
export function pendingFileLinkPath(inputPath: string): string {
|
|
256
|
+
const expanded = inputPath.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
257
|
+
if (!expanded.includes(":")) return path.resolve(expanded);
|
|
258
|
+
const archive = parseArchivePathCandidates(expanded).find(candidate => candidate.archivePath !== expanded);
|
|
259
|
+
const sqlite = expanded.match(/^(.+\.(?:sqlite3?|db3?))(?=[:?])/i);
|
|
260
|
+
return path.resolve(archive?.archivePath ?? sqlite?.[1] ?? expanded);
|
|
261
|
+
}
|
|
219
262
|
|
|
220
263
|
function splitReadRenderPath(rawPath: string): { path: string; sel?: string } {
|
|
221
|
-
if (
|
|
264
|
+
if (splitUrlScheme(rawPath)) {
|
|
222
265
|
const internal = splitInternalUrlSel(rawPath);
|
|
223
266
|
if (internal.sel) return internal;
|
|
224
267
|
}
|
|
@@ -256,8 +299,14 @@ function formatReadPathLink(
|
|
|
256
299
|
const plainDisplayPath = options.suffixResolution
|
|
257
300
|
? shortenPath(options.suffixResolution.to)
|
|
258
301
|
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
259
|
-
|
|
260
|
-
|
|
302
|
+
// Calls render before the tool has resolved a filesystem target. Preserve
|
|
303
|
+
// protocol resources as plain text, but resolve direct relative file paths
|
|
304
|
+
// so terminals receive an explicit file: link rather than guessing HTTPS.
|
|
305
|
+
const inputPath =
|
|
306
|
+
basePath && !splitUrlScheme(basePath) && !BARE_WEB_HOST_RE.test(basePath)
|
|
307
|
+
? pendingFileLinkPath(basePath)
|
|
308
|
+
: undefined;
|
|
309
|
+
const target = options.resolvedPath ?? options.sourcePath ?? inputPath;
|
|
261
310
|
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
262
311
|
const linkOptions = line !== undefined ? { line } : undefined;
|
|
263
312
|
const linkedPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
@@ -266,9 +315,19 @@ function formatReadPathLink(
|
|
|
266
315
|
|
|
267
316
|
/** Render file, image, and URL reads in the transcript. */
|
|
268
317
|
export const readToolRenderer = {
|
|
318
|
+
activitySummary(args: unknown, _context: ToolActivityContext): ToolActivitySummary {
|
|
319
|
+
const input = args as ReadRenderArgs | undefined;
|
|
320
|
+
const rawPath =
|
|
321
|
+
typeof input?.file_path === "string" ? input.file_path : typeof input?.path === "string" ? input.path : "";
|
|
322
|
+
const routed = readUrlCard(rawPath);
|
|
323
|
+
if (routed) return { label: routed.card.label, detail: routed.target || routed.card.rootDetail };
|
|
324
|
+
return { label: "Read", detail: shortenPath(rawPath) };
|
|
325
|
+
},
|
|
269
326
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
270
327
|
const rawPath =
|
|
271
328
|
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
329
|
+
const routed = readUrlCard(rawPath);
|
|
330
|
+
if (routed) return routed.card.render(rawPath, routed.target, undefined, undefined, _options, uiTheme);
|
|
272
331
|
if (isReadableUrlPath(rawPath)) {
|
|
273
332
|
return renderReadUrlCall({ path: rawPath, raw: args.raw }, _options, uiTheme);
|
|
274
333
|
}
|
|
@@ -296,6 +355,9 @@ export const readToolRenderer = {
|
|
|
296
355
|
const urlDetails = result.details as ReadUrlToolDetails | undefined;
|
|
297
356
|
const baseRawPathForKind =
|
|
298
357
|
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
358
|
+
const routed = readUrlCard(baseRawPathForKind, result.details);
|
|
359
|
+
if (routed)
|
|
360
|
+
return routed.card.render(baseRawPathForKind, routed.target, result, result.details, options, uiTheme);
|
|
299
361
|
if (urlDetails?.kind === "url" || isReadableUrlPath(baseRawPathForKind)) {
|
|
300
362
|
return renderReadUrlResult(
|
|
301
363
|
result as {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-based status cards shared by the internal-URL renderers (`proc://`,
|
|
3
|
+
* `agent://`, `cfg://`): a header plus sanitized, width-truncated body lines.
|
|
4
|
+
*/
|
|
5
|
+
import type { Component } from "../tui";
|
|
6
|
+
import { createCachedComponent, Ellipsis, truncateToWidth } from "../render";
|
|
7
|
+
import { replaceTabs } from "../render/render-utils";
|
|
8
|
+
import type { RenderResultOptions } from "./renderer";
|
|
9
|
+
|
|
10
|
+
/** Tool result fields the card renderers read. */
|
|
11
|
+
export interface CardToolResult {
|
|
12
|
+
content: Array<{ type: string; text?: string }>;
|
|
13
|
+
isError?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** First text block of a tool result, or `""`. */
|
|
17
|
+
export function firstText(result: CardToolResult): string {
|
|
18
|
+
return result.content.find(item => item.type === "text")?.text ?? "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Sanitize one display line: tabs become spaces, carriage returns are dropped. */
|
|
22
|
+
export function safe(value: string): string {
|
|
23
|
+
return replaceTabs(value).replace(/\r/g, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Padded card whose lines are sanitized and truncated, re-rendered when the expanded state flips. */
|
|
27
|
+
export function card(lines: (width: number, expanded: boolean) => string[], options: RenderResultOptions): Component {
|
|
28
|
+
return createCachedComponent(
|
|
29
|
+
() => Boolean(options.expanded),
|
|
30
|
+
(width, expanded) =>
|
|
31
|
+
lines(width, expanded)
|
|
32
|
+
.flatMap(line => line.split("\n"))
|
|
33
|
+
.map(line => truncateToWidth(safe(line), width, Ellipsis.Unicode)),
|
|
34
|
+
{ paddingX: 1 },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheme facts supplied by the internal URL host. pi-tui cannot import the
|
|
3
|
+
* coding-agent router, so the router installs this bridge when it loads;
|
|
4
|
+
* selector peeling and transcript renderers consult it instead of hardcoding
|
|
5
|
+
* scheme names.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Renderer-relevant subset of a registered scheme's declared spec. */
|
|
9
|
+
export interface InternalUrlSchemeSpec {
|
|
10
|
+
/** `lines`: a trailing `:<selector>` chain is a read selector; `none`/`opaque`: never peel. */
|
|
11
|
+
readonly selectors: "lines" | "none" | "opaque";
|
|
12
|
+
/** A trailing `:N` with no path after the authority is a port, not a selector (ssh://host:2222). */
|
|
13
|
+
readonly portAuthority?: boolean;
|
|
14
|
+
/** Read cards collapse into the compact read group like plain files instead of expanding. */
|
|
15
|
+
readonly compactTranscript?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Scheme registry capabilities installed by the internal URL host. */
|
|
19
|
+
export interface InternalUrlSchemeHost {
|
|
20
|
+
/** Declared spec of a registered scheme (case-insensitive); undefined when unregistered. */
|
|
21
|
+
spec(scheme: string): InternalUrlSchemeSpec | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let schemeHost: InternalUrlSchemeHost | undefined;
|
|
25
|
+
|
|
26
|
+
/** Install the scheme registry. Until one is installed, no URL counts as an internal scheme. */
|
|
27
|
+
export function setInternalUrlSchemeHost(host: InternalUrlSchemeHost): void {
|
|
28
|
+
schemeHost = host;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Declared spec of `scheme`; undefined when it is unregistered or no host is installed. */
|
|
32
|
+
export function internalUrlSchemeSpec(scheme: string): InternalUrlSchemeSpec | undefined {
|
|
33
|
+
return schemeHost?.spec(scheme);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const URL_SCHEME_RE = /^([a-z][a-z0-9+.-]*):\/\//i;
|
|
37
|
+
|
|
38
|
+
/** Split a hierarchical `scheme://rest` URL into its lowercased scheme and the text after `://`. */
|
|
39
|
+
export function splitUrlScheme(input: string): { scheme: string; rest: string } | undefined {
|
|
40
|
+
const match = URL_SCHEME_RE.exec(input);
|
|
41
|
+
return match ? { scheme: match[1].toLowerCase(), rest: input.slice(match[0].length) } : undefined;
|
|
42
|
+
}
|