@oh-my-pi/pi-tui 18.3.0 → 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 +24 -0
- package/THIRD-PARTY-NOTICES.txt +2 -2
- 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/btw-history-panel.d.ts +4 -1
- package/dist/types/overlays/settings-defs.d.ts +5 -0
- package/dist/types/prompt/composer.d.ts +12 -1
- package/dist/types/prompt/custom-editor.d.ts +5 -26
- package/dist/types/space-hold.d.ts +45 -0
- package/dist/types/status-line/host.d.ts +2 -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/find.d.ts +2 -2
- package/dist/types/tools/output-meta.d.ts +2 -0
- package/dist/types/tools/proc-render.d.ts +1 -8
- package/dist/types/tools/read.d.ts +14 -3
- 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/write.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +9 -1
- package/package.json +9 -9
- package/src/chat/chat-transcript-builder.ts +5 -1
- package/src/chat/read-tool-group.ts +8 -9
- package/src/chat/tool-execution.ts +7 -5
- 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 +18 -6
- package/src/overlays/annotation-overlay.ts +0 -1
- package/src/overlays/btw-history-panel.ts +4 -0
- package/src/overlays/settings-defs.ts +6 -0
- package/src/overlays/settings-selector.ts +4 -3
- package/src/prompt/composer.ts +83 -56
- package/src/prompt/custom-editor.ts +15 -137
- package/src/prompt/tips.txt +1 -0
- package/src/space-hold.ts +143 -0
- package/src/status-line/component.ts +6 -0
- package/src/status-line/host.ts +2 -0
- package/src/status-line/segments.ts +4 -0
- package/src/tools/cfg-render.ts +117 -0
- package/src/tools/cfg-url.ts +30 -0
- package/src/tools/find.ts +7 -6
- package/src/tools/output-meta.ts +2 -0
- package/src/tools/proc-render.ts +2 -26
- package/src/tools/read.ts +109 -68
- package/src/tools/result-card.ts +36 -0
- package/src/tools/url-scheme-host.ts +42 -0
- package/src/tools/write.ts +109 -66
- package/src/tools/xdev.ts +9 -1
- package/dist/types/chat/read-target.d.ts +0 -4
- package/src/chat/read-target.ts +0 -7
package/src/tools/read.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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
9
|
import type { RenderResultOptions, ToolActivityContext, ToolActivitySummary, ToolRenderer } from "./renderer";
|
|
@@ -17,6 +19,9 @@ import { formatBytes, sanitizeDisplayLines, shortenPath, wrapBrackets } from "..
|
|
|
17
19
|
import type { OutputMeta } from "./output-meta";
|
|
18
20
|
import type { TruncationResult } from "./streaming-output";
|
|
19
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";
|
|
20
25
|
|
|
21
26
|
/** Read result metadata retains truncation statistics, not a second copy of the body. */
|
|
22
27
|
export type ReadTruncationStats = Omit<TruncationResult, "content">;
|
|
@@ -25,6 +30,7 @@ export type ReadTruncationStats = Omit<TruncationResult, "content">;
|
|
|
25
30
|
export interface ReadToolDetails {
|
|
26
31
|
kind?: "file" | "url";
|
|
27
32
|
proc?: ProcReadDetails;
|
|
33
|
+
cfg?: CfgReadDetails;
|
|
28
34
|
/** Filesystem hyperlink target resolved by the executing tool. */
|
|
29
35
|
displayTarget?: string;
|
|
30
36
|
truncation?: ReadTruncationStats;
|
|
@@ -77,37 +83,6 @@ const INTERNAL_URL_SELECTOR_PART_RE = new RegExp(
|
|
|
77
83
|
String.raw`^(?:raw|conflicts|img|${RANGE_LIST_SRC}|-\d+(?:[-+]\d+)?)$`,
|
|
78
84
|
"i",
|
|
79
85
|
);
|
|
80
|
-
// Schemes whose host grammar is identifier-shaped, so any trailing
|
|
81
|
-
// `:<selector-chunk>` is unambiguously a read-tool selector. `mcp://` is
|
|
82
|
-
// excluded because mcp resource URIs may legitimately contain colons. `ssh://`
|
|
83
|
-
// is included despite an optional `:port`; `splitInternalUrlSel` skips the peel
|
|
84
|
-
// for an `ssh://host:port` that has no `/path`, so the port colon is never
|
|
85
|
-
// mistaken for a selector (a real ssh selector trails the `/path`, e.g.
|
|
86
|
-
// `ssh://h/f:1-5`).
|
|
87
|
-
const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
88
|
-
agent: true,
|
|
89
|
-
artifact: true,
|
|
90
|
-
issue: true,
|
|
91
|
-
history: true,
|
|
92
|
-
local: true,
|
|
93
|
-
memory: true,
|
|
94
|
-
omp: true,
|
|
95
|
-
pr: true,
|
|
96
|
-
proc: true,
|
|
97
|
-
rule: true,
|
|
98
|
-
security: true,
|
|
99
|
-
skill: true,
|
|
100
|
-
ssh: true,
|
|
101
|
-
vault: true,
|
|
102
|
-
};
|
|
103
|
-
// Schemes whose resource URIs are server-defined and may legitimately end
|
|
104
|
-
// with selector-shaped tails (e.g. `:raw`, `:conflicts`, `:1-50`, `/:raw`).
|
|
105
|
-
// `McpProtocolHandler` resolves by exact URI match (`r.uri === uri`), so
|
|
106
|
-
// peeling syntactically can make valid resources unreachable. Keep these
|
|
107
|
-
// schemes opaque; selector support for them needs a resolver-aware path that
|
|
108
|
-
// tries the exact URI before interpreting any suffix as a read selector.
|
|
109
|
-
const OPAQUE_RESOURCE_SCHEMES: ReadonlySet<string> = new Set(["mcp"]);
|
|
110
|
-
const INTERNAL_URL_SCHEME_RE = /^([a-z][a-z0-9+.-]*):\/\//i;
|
|
111
86
|
/** Split a filesystem path from its trailing read selector. */
|
|
112
87
|
export function splitPathAndSel(rawPath: string): { path: string; sel?: string } {
|
|
113
88
|
const colon = rawPath.lastIndexOf(":");
|
|
@@ -151,30 +126,28 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
|
|
|
151
126
|
* This function iteratively peels selector-shaped chunks (well-formed plus
|
|
152
127
|
* common malformed shapes like `:-N-M`) so the rest of the read tool can pass a
|
|
153
128
|
* clean URL to the protocol handler and surface selector errors via parseSel
|
|
154
|
-
* instead of as misleading "host invalid" errors from the handler.
|
|
155
|
-
*
|
|
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.
|
|
156
135
|
*
|
|
157
136
|
* Falls back to the input unchanged when nothing matches.
|
|
158
137
|
*/
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// authority, a trailing `:NNNN` is the port, not a read selector
|
|
173
|
-
// (e.g. ssh://host:2222). Other schemes' authority-trailing selectors
|
|
174
|
-
// (artifact://5:1-50) still peel, so this guard is ssh-specific.
|
|
175
|
-
if (scheme === "ssh" && rawPath.indexOf("/", schemeEnd) === -1) {
|
|
176
|
-
return { path: rawPath };
|
|
177
|
-
}
|
|
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;
|
|
178
151
|
let path = rawPath;
|
|
179
152
|
const chunks: string[] = [];
|
|
180
153
|
while (true) {
|
|
@@ -218,10 +191,77 @@ export interface ReadRenderArgs {
|
|
|
218
191
|
raw?: boolean;
|
|
219
192
|
}
|
|
220
193
|
|
|
221
|
-
|
|
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
|
+
}
|
|
222
262
|
|
|
223
263
|
function splitReadRenderPath(rawPath: string): { path: string; sel?: string } {
|
|
224
|
-
if (
|
|
264
|
+
if (splitUrlScheme(rawPath)) {
|
|
225
265
|
const internal = splitInternalUrlSel(rawPath);
|
|
226
266
|
if (internal.sel) return internal;
|
|
227
267
|
}
|
|
@@ -259,8 +299,14 @@ function formatReadPathLink(
|
|
|
259
299
|
const plainDisplayPath = options.suffixResolution
|
|
260
300
|
? shortenPath(options.suffixResolution.to)
|
|
261
301
|
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
264
310
|
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
265
311
|
const linkOptions = line !== undefined ? { line } : undefined;
|
|
266
312
|
const linkedPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
@@ -273,15 +319,15 @@ export const readToolRenderer = {
|
|
|
273
319
|
const input = args as ReadRenderArgs | undefined;
|
|
274
320
|
const rawPath =
|
|
275
321
|
typeof input?.file_path === "string" ? input.file_path : typeof input?.path === "string" ? input.path : "";
|
|
276
|
-
|
|
277
|
-
|
|
322
|
+
const routed = readUrlCard(rawPath);
|
|
323
|
+
if (routed) return { label: routed.card.label, detail: routed.target || routed.card.rootDetail };
|
|
278
324
|
return { label: "Read", detail: shortenPath(rawPath) };
|
|
279
325
|
},
|
|
280
326
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
281
327
|
const rawPath =
|
|
282
328
|
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
283
|
-
|
|
284
|
-
|
|
329
|
+
const routed = readUrlCard(rawPath);
|
|
330
|
+
if (routed) return routed.card.render(rawPath, routed.target, undefined, undefined, _options, uiTheme);
|
|
285
331
|
if (isReadableUrlPath(rawPath)) {
|
|
286
332
|
return renderReadUrlCall({ path: rawPath, raw: args.raw }, _options, uiTheme);
|
|
287
333
|
}
|
|
@@ -309,14 +355,9 @@ export const readToolRenderer = {
|
|
|
309
355
|
const urlDetails = result.details as ReadUrlToolDetails | undefined;
|
|
310
356
|
const baseRawPathForKind =
|
|
311
357
|
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
result,
|
|
316
|
-
result.details?.proc,
|
|
317
|
-
options,
|
|
318
|
-
uiTheme,
|
|
319
|
-
);
|
|
358
|
+
const routed = readUrlCard(baseRawPathForKind, result.details);
|
|
359
|
+
if (routed)
|
|
360
|
+
return routed.card.render(baseRawPathForKind, routed.target, result, result.details, options, uiTheme);
|
|
320
361
|
if (urlDetails?.kind === "url" || isReadableUrlPath(baseRawPathForKind)) {
|
|
321
362
|
return renderReadUrlResult(
|
|
322
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
|
+
}
|
package/src/tools/write.ts
CHANGED
|
@@ -23,9 +23,12 @@ import {
|
|
|
23
23
|
} from "../render/render-utils";
|
|
24
24
|
import type { CoordinationDetails } from "./wait";
|
|
25
25
|
import { renderAgentWrite, renderProcWrite, type ProcWriteAction, type ProcWriteDetails } from "./proc-render";
|
|
26
|
+
import { renderCfgWrite, type CfgWriteDetails } from "./cfg-render";
|
|
26
27
|
import type { FileDiagnosticsResult } from "./lsp";
|
|
27
28
|
import type { OutputMeta } from "./output-meta";
|
|
29
|
+
import type { CardToolResult } from "./result-card";
|
|
28
30
|
import type { RenderResultOptions, ToolActivityContext, ToolActivitySummary, ToolRenderer } from "./renderer";
|
|
31
|
+
import { splitUrlScheme } from "./url-scheme-host";
|
|
29
32
|
import { couldBecomeXdUrl, parseXdUrl } from "./xd-url";
|
|
30
33
|
import {
|
|
31
34
|
renderXdevCall,
|
|
@@ -36,6 +39,7 @@ import {
|
|
|
36
39
|
} from "./xdev";
|
|
37
40
|
import { isResolutionDeviceName, renderResolutionDeviceCall } from "./resolve";
|
|
38
41
|
import { REPORT_ISSUE_DEVICE_NAME, renderReportIssueDeviceCall } from "./report-tool-issue";
|
|
42
|
+
import { pendingFileLinkPath } from "./read";
|
|
39
43
|
|
|
40
44
|
/** Details returned by the write tool for transcript rendering. */
|
|
41
45
|
export interface WriteToolDetails {
|
|
@@ -50,6 +54,7 @@ export interface WriteToolDetails {
|
|
|
50
54
|
xdev?: XdevRenderDispatch;
|
|
51
55
|
message?: CoordinationDetails;
|
|
52
56
|
proc?: ProcWriteDetails;
|
|
57
|
+
cfg?: CfgWriteDetails;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
interface WriteRenderArgs {
|
|
@@ -318,13 +323,99 @@ export interface WriteRenderContext {
|
|
|
318
323
|
resolveXdevMounted?: (name: string) => XdevMountedRenderer | undefined;
|
|
319
324
|
}
|
|
320
325
|
|
|
321
|
-
|
|
322
|
-
|
|
326
|
+
/** Process id and operation named by the text after `proc://`. */
|
|
327
|
+
function procWriteTarget(target: string): { id: string; action: ProcWriteAction } {
|
|
323
328
|
if (target.endsWith("/kill")) return { id: target.slice(0, -5), action: "kill" };
|
|
324
329
|
if (target.endsWith("/mode")) return { id: target.slice(0, -5), action: "mode" };
|
|
325
330
|
return { id: target, action: "stdin" };
|
|
326
331
|
}
|
|
327
332
|
|
|
333
|
+
/** Transcript card for writes to a scheme with its own UI (peer message, process control, setting change). */
|
|
334
|
+
interface WriteUrlCard {
|
|
335
|
+
/** Result details field whose presence identifies this card. */
|
|
336
|
+
readonly detailsKey: "message" | "proc" | "cfg";
|
|
337
|
+
/** Compact activity line for the text after `scheme://`. */
|
|
338
|
+
activity(target: string): ToolActivitySummary;
|
|
339
|
+
/** Pending call card when `result` is undefined, else the finished result card. */
|
|
340
|
+
render(
|
|
341
|
+
url: string,
|
|
342
|
+
target: string,
|
|
343
|
+
content: unknown,
|
|
344
|
+
result: CardToolResult | undefined,
|
|
345
|
+
details: WriteToolDetails | undefined,
|
|
346
|
+
options: RenderResultOptions,
|
|
347
|
+
uiTheme: Theme,
|
|
348
|
+
): Component;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Write cards keyed by URL scheme. */
|
|
352
|
+
const WRITE_URL_CARDS: Record<string, WriteUrlCard> = {
|
|
353
|
+
agent: {
|
|
354
|
+
detailsKey: "message",
|
|
355
|
+
activity: target => ({ label: "Message", detail: target === "all" ? "broadcast" : target }),
|
|
356
|
+
render: (_url, target, content, result, details, options, uiTheme) =>
|
|
357
|
+
renderAgentWrite(
|
|
358
|
+
target,
|
|
359
|
+
typeof content === "string" ? content : "",
|
|
360
|
+
result,
|
|
361
|
+
details?.message,
|
|
362
|
+
options,
|
|
363
|
+
uiTheme,
|
|
364
|
+
),
|
|
365
|
+
},
|
|
366
|
+
proc: {
|
|
367
|
+
detailsKey: "proc",
|
|
368
|
+
activity: target => {
|
|
369
|
+
const { id, action } = procWriteTarget(target);
|
|
370
|
+
return { label: "Process", detail: `${action} ${shortenPath(id)}` };
|
|
371
|
+
},
|
|
372
|
+
render: (_url, target, content, result, details, options, uiTheme) => {
|
|
373
|
+
const { id, action } = procWriteTarget(target);
|
|
374
|
+
return renderProcWrite(
|
|
375
|
+
id,
|
|
376
|
+
action,
|
|
377
|
+
typeof content === "string" ? content : undefined,
|
|
378
|
+
result,
|
|
379
|
+
details?.proc,
|
|
380
|
+
options,
|
|
381
|
+
uiTheme,
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
cfg: {
|
|
386
|
+
detailsKey: "cfg",
|
|
387
|
+
activity: target => ({ label: "Config", detail: target }),
|
|
388
|
+
render: (url, _target, content, result, details, options, uiTheme) =>
|
|
389
|
+
renderCfgWrite(url, typeof content === "string" ? content : undefined, result, details?.cfg, options, uiTheme),
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Card that renders a write to `rawPath` with the text after `scheme://` as its target.
|
|
395
|
+
* Result details identify the card before the URL scheme does.
|
|
396
|
+
*/
|
|
397
|
+
function writeUrlCard(rawPath: string, details?: WriteToolDetails): { card: WriteUrlCard; target: string } | undefined {
|
|
398
|
+
const url = splitUrlScheme(rawPath);
|
|
399
|
+
const target = url?.rest ?? "";
|
|
400
|
+
if (details) {
|
|
401
|
+
for (const scheme in WRITE_URL_CARDS) {
|
|
402
|
+
const card = WRITE_URL_CARDS[scheme];
|
|
403
|
+
if (details[card.detailsKey] !== undefined) return { card, target };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!url || !Object.hasOwn(WRITE_URL_CARDS, url.scheme)) return undefined;
|
|
407
|
+
return { card: WRITE_URL_CARDS[url.scheme], target };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Whether a streaming lowercased path is, or could still become, a URL with its own write card. */
|
|
411
|
+
function couldBecomeWriteCardUrl(lowerPath: string): boolean {
|
|
412
|
+
for (const scheme in WRITE_URL_CARDS) {
|
|
413
|
+
const prefix = `${scheme}://`;
|
|
414
|
+
if (prefix.startsWith(lowerPath) || lowerPath.startsWith(prefix)) return true;
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
328
419
|
/** Render file writes and delegated tool-device calls. */
|
|
329
420
|
export const writeToolRenderer = {
|
|
330
421
|
/** Compact one-line activity: device writes read as the mounted tool (`LSP · references foo`), file writes as `Write · <path>`. */
|
|
@@ -337,16 +428,8 @@ export const writeToolRenderer = {
|
|
|
337
428
|
? writeArgs.path
|
|
338
429
|
: "";
|
|
339
430
|
if (!rawPath) return { label: "Write" };
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
label: "Message",
|
|
343
|
-
detail: rawPath.slice("agent://".length) === "all" ? "broadcast" : rawPath.slice("agent://".length),
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
if (/^proc:\/\//i.test(rawPath)) {
|
|
347
|
-
const { id, action } = procWriteTarget(rawPath);
|
|
348
|
-
return { label: "Process", detail: `${action} ${shortenPath(id)}` };
|
|
349
|
-
}
|
|
431
|
+
const routed = writeUrlCard(rawPath);
|
|
432
|
+
if (routed) return routed.card.activity(routed.target);
|
|
350
433
|
const xdev = parseXdUrl(rawPath);
|
|
351
434
|
if (xdev?.name) {
|
|
352
435
|
const resolveMounted = (context.renderContext as WriteRenderContext | undefined)?.resolveXdevMounted;
|
|
@@ -370,36 +453,10 @@ export const writeToolRenderer = {
|
|
|
370
453
|
if (args.path === undefined && args.file_path === undefined) return undefined;
|
|
371
454
|
const pathSettled = args.content !== undefined || options.argsComplete === true;
|
|
372
455
|
const hasStringPath = typeof args.file_path === "string" || typeof args.path === "string";
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
"proc://".startsWith(rawPath.toLowerCase()) ||
|
|
378
|
-
/^(?:agent|proc):\/\//i.test(rawPath))
|
|
379
|
-
)
|
|
380
|
-
return undefined;
|
|
381
|
-
if (/^agent:\/\//i.test(rawPath)) {
|
|
382
|
-
return renderAgentWrite(
|
|
383
|
-
rawPath.slice("agent://".length),
|
|
384
|
-
typeof args.content === "string" ? args.content : "",
|
|
385
|
-
undefined,
|
|
386
|
-
undefined,
|
|
387
|
-
options,
|
|
388
|
-
uiTheme,
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
if (/^proc:\/\//i.test(rawPath)) {
|
|
392
|
-
const { id, action } = procWriteTarget(rawPath);
|
|
393
|
-
return renderProcWrite(
|
|
394
|
-
id,
|
|
395
|
-
action,
|
|
396
|
-
typeof args.content === "string" ? args.content : undefined,
|
|
397
|
-
undefined,
|
|
398
|
-
undefined,
|
|
399
|
-
options,
|
|
400
|
-
uiTheme,
|
|
401
|
-
);
|
|
402
|
-
}
|
|
456
|
+
if (hasStringPath && !pathSettled && couldBecomeWriteCardUrl(rawPath.toLowerCase())) return undefined;
|
|
457
|
+
const routed = writeUrlCard(rawPath);
|
|
458
|
+
if (routed)
|
|
459
|
+
return routed.card.render(rawPath, routed.target, args.content, undefined, undefined, options, uiTheme);
|
|
403
460
|
if (rawPath && couldBecomeXdUrl(rawPath)) {
|
|
404
461
|
const xdev = parseXdUrl(rawPath);
|
|
405
462
|
// The path string is settled once the content field started streaming.
|
|
@@ -412,7 +469,11 @@ export const writeToolRenderer = {
|
|
|
412
469
|
const filePath = shortenPath(rawPath);
|
|
413
470
|
const lang = rawPath ? (getLanguageFromPath(rawPath) ?? "text") : "text";
|
|
414
471
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
415
|
-
const
|
|
472
|
+
const styledPath = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
473
|
+
// The result has not resolved its target yet. Link the containing file
|
|
474
|
+
// rather than an archive member or database row selector.
|
|
475
|
+
const pathDisplay =
|
|
476
|
+
filePath && args.content !== undefined ? fileHyperlink(pendingFileLinkPath(rawPath), styledPath) : styledPath;
|
|
416
477
|
// No status icon on the head row: it's the head of the framed block, and
|
|
417
478
|
// native-scrollback commits are prefix-only — an animated glyph would pin
|
|
418
479
|
// the commit boundary at the top, and the pending hourglass just adds
|
|
@@ -465,29 +526,11 @@ export const writeToolRenderer = {
|
|
|
465
526
|
uiTheme: Theme,
|
|
466
527
|
args?: WriteRenderArgs,
|
|
467
528
|
): Component {
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
result,
|
|
474
|
-
result.details?.message,
|
|
475
|
-
options,
|
|
476
|
-
uiTheme,
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
if (typeof messagePath === "string" && /^proc:\/\//i.test(messagePath)) {
|
|
480
|
-
const { id, action } = procWriteTarget(messagePath);
|
|
481
|
-
return renderProcWrite(
|
|
482
|
-
id,
|
|
483
|
-
action,
|
|
484
|
-
typeof args?.content === "string" ? args.content : undefined,
|
|
485
|
-
result,
|
|
486
|
-
result.details?.proc,
|
|
487
|
-
options,
|
|
488
|
-
uiTheme,
|
|
489
|
-
);
|
|
490
|
-
}
|
|
529
|
+
const cardPath =
|
|
530
|
+
typeof args?.path === "string" ? args.path : typeof args?.file_path === "string" ? args.file_path : "";
|
|
531
|
+
const routed = writeUrlCard(cardPath, result.details);
|
|
532
|
+
if (routed)
|
|
533
|
+
return routed.card.render(cardPath, routed.target, args?.content, result, result.details, options, uiTheme);
|
|
491
534
|
// xd:// dispatch results render as the mounted tool's own result.
|
|
492
535
|
const xdev = result.details?.xdev;
|
|
493
536
|
if (xdev) {
|
package/src/tools/xdev.ts
CHANGED
|
@@ -15,10 +15,18 @@ export interface XdevMountedRenderer {
|
|
|
15
15
|
mergeCallAndResult?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/** Tool presentation state supplied by the host's canonical tool map. */
|
|
19
19
|
export interface XdevMountedState {
|
|
20
20
|
readonly mountedNames: ReadonlySet<string>;
|
|
21
21
|
readonly tools: ReadonlyMap<string, XdevMountedRenderer>;
|
|
22
|
+
/**
|
|
23
|
+
* Canonical renderer for a device name, supplied by the host with the same
|
|
24
|
+
* predicate as dispatch (`resolveXdevTool`: mounted devices plus active
|
|
25
|
+
* top-level tools, which the `write` transport also accepts). The TUI keeps
|
|
26
|
+
* no predicate copy of its own — a second copy is what dropped a top-level
|
|
27
|
+
* dispatch to the generic card.
|
|
28
|
+
*/
|
|
29
|
+
readonly resolve?: (name: string) => XdevMountedRenderer | undefined;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
function isComponent(value: unknown): value is Component {
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** Host-owned internal protocol classifier used by compact read cards. */
|
|
2
|
-
export declare let internalReadTargetPredicate: ((target: string) => boolean) | undefined;
|
|
3
|
-
/** Supply protocol classification without loading the host router in a renderer. */
|
|
4
|
-
export declare function setInternalReadTargetPredicate(predicate: (target: string) => boolean): void;
|
package/src/chat/read-target.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/** Host-owned internal protocol classifier used by compact read cards. */
|
|
2
|
-
export let internalReadTargetPredicate: ((target: string) => boolean) | undefined;
|
|
3
|
-
|
|
4
|
-
/** Supply protocol classification without loading the host router in a renderer. */
|
|
5
|
-
export function setInternalReadTargetPredicate(predicate: (target: string) => boolean): void {
|
|
6
|
-
internalReadTargetPredicate = predicate;
|
|
7
|
-
}
|