pi-minimalist 0.2.0 → 0.2.2
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 +3 -3
- package/index.ts +2 -9
- package/package.json +1 -1
- package/src/bridge.ts +76 -55
- package/src/components.ts +97 -32
- package/src/config-file.ts +7 -83
- package/src/config-ui.ts +0 -6
- package/src/config.ts +11 -33
- package/src/core-patch.ts +167 -178
- package/src/row.ts +44 -28
- package/src/run-grouping.ts +98 -52
- package/src/thinking.ts +5 -8
- package/src/tool-renderer.ts +21 -10
- package/src/tools.ts +57 -42
package/src/tools.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool vocabulary: how each tool describes itself
|
|
2
|
+
* Tool vocabulary: how each tool describes itself (a label and its details).
|
|
3
3
|
*
|
|
4
4
|
* Pure and deterministic — no components, no theme, no globals. Which tools get
|
|
5
5
|
* compacted is `Config.compacts()`, not this file.
|
|
6
6
|
*
|
|
7
7
|
* `describeTool` returns the label and details SEPARATELY and must never join
|
|
8
|
-
* them: the painter colors each part, and
|
|
9
|
-
*
|
|
10
|
-
* assuming no label ever contains one.
|
|
8
|
+
* them: the painter colors each part, and one joined string would
|
|
9
|
+
* force it to split on the first space, assuming no label contains one.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
import type { Config } from "./config.ts";
|
|
14
|
-
|
|
15
12
|
/** Native tools with a per-tool details extractor below. */
|
|
16
13
|
// `as const` preserves literal names instead of widening every item to string.
|
|
17
14
|
export const BUILT_INS = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
|
|
@@ -19,7 +16,7 @@ export const BUILT_INS = ["read", "bash", "edit", "write", "grep", "find", "ls"]
|
|
|
19
16
|
// `(typeof BUILT_INS)[number]` turns the tuple values into a union type.
|
|
20
17
|
export type ToolName = (typeof BUILT_INS)[number];
|
|
21
18
|
|
|
22
|
-
/** True for native tools with a hand-written
|
|
19
|
+
/** True for native tools with a hand-written description. */
|
|
23
20
|
export function isBuiltIn(name: string): name is ToolName {
|
|
24
21
|
return BUILT_INS.includes(name as ToolName);
|
|
25
22
|
}
|
|
@@ -30,8 +27,8 @@ const mcpTools = new Set<string>();
|
|
|
30
27
|
/**
|
|
31
28
|
* Refresh after MCP adapter registration.
|
|
32
29
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* Decides what gets LABELED `mcp` (not what gets compacted; the blacklist does
|
|
31
|
+
* that). It is real information: a bare
|
|
35
32
|
* `atlassian_getConfluencePage` row does not tell you it crossed an MCP server.
|
|
36
33
|
*/
|
|
37
34
|
export function refreshMcpTools(tools: { name: string; sourceInfo: { path: string } }[]): void {
|
|
@@ -48,60 +45,80 @@ export function isMcpTool(name: string): boolean {
|
|
|
48
45
|
/**
|
|
49
46
|
* Collapse whitespace and truncate for one-line display.
|
|
50
47
|
*
|
|
51
|
-
* `max` is a DISPLAY budget in characters
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `CompactLine` truncates at the real viewport column count.
|
|
48
|
+
* `max` is a DISPLAY budget in characters. It is not the final word on width —
|
|
49
|
+
* `CompactLine` truncates at the real viewport column count. There is no other
|
|
50
|
+
* cap: with no budget the whole value is kept.
|
|
55
51
|
*/
|
|
56
|
-
export function compact(value: unknown, max = Number.POSITIVE_INFINITY
|
|
57
|
-
const limit = Math.min(max, hardCap);
|
|
58
|
-
// Slice BEFORE the regex so a huge string is never fully scanned. The extra
|
|
59
|
-
// headroom leaves room for whitespace runs that collapse away.
|
|
52
|
+
export function compact(value: unknown, max = Number.POSITIVE_INFINITY): string {
|
|
60
53
|
const raw = String(value ?? "");
|
|
61
|
-
|
|
54
|
+
// With a finite budget, slice BEFORE the regex so a huge string is never fully
|
|
55
|
+
// scanned for a collapsed row. The extra headroom leaves room for whitespace
|
|
56
|
+
// runs that collapse away.
|
|
57
|
+
const sliced = Number.isFinite(max) ? raw.slice(0, max * 2 + 16) : raw;
|
|
62
58
|
// Replacing every whitespace run prevents multi-line call rows.
|
|
63
59
|
const text = sliced.replace(/\s+/g, " ").trim();
|
|
64
|
-
return text.length >
|
|
60
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The value for an EXPANDED row: complete, with its line structure kept.
|
|
65
|
+
* `\r\n` becomes `\n`, tabs become two spaces, trailing whitespace goes, and
|
|
66
|
+
* blank lines at the start and end of the whole value go. Each line keeps its
|
|
67
|
+
* own leading indentation; `CompactLine` wraps every line under the details.
|
|
68
|
+
*/
|
|
69
|
+
export function block(value: unknown): string {
|
|
70
|
+
const lines = String(value ?? "")
|
|
71
|
+
// \v, \f, \u2028, \u2029 and \u0085 would reach the terminal raw (the
|
|
72
|
+
// collapsed path folds them away with \s+).
|
|
73
|
+
.replace(/[\v\f\u2028\u2029\u0085]/g, " ")
|
|
74
|
+
.replace(/\r\n?/g, "\n")
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line) => line.replace(/\t/g, " ").trimEnd());
|
|
77
|
+
while (lines.length > 0 && lines[0] === "") lines.shift();
|
|
78
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A budget means a collapsed one-line row; no budget means an expanded row. */
|
|
83
|
+
function field(value: unknown, budget: number): string {
|
|
84
|
+
return Number.isFinite(budget) ? compact(value, budget) : block(value);
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
/** Minimum columns a composed field keeps, so it never vanishes entirely. */
|
|
68
88
|
const MIN_FIELD = 12;
|
|
69
89
|
|
|
70
90
|
export type DescribeOptions = {
|
|
71
|
-
/** Expanded rows keep full-length details (still on ONE physical line). */
|
|
72
|
-
expanded?: boolean;
|
|
73
91
|
/**
|
|
74
92
|
* Columns available for the details segment.
|
|
75
93
|
*
|
|
76
94
|
* Approximate on purpose: `CompactLine` performs the exact, ANSI-aware,
|
|
77
95
|
* wide-character-aware truncation. This budget only decides WHICH field is
|
|
78
96
|
* sacrificed when a composed detail (`/pattern/ in path`) cannot fit.
|
|
97
|
+
* Omitted (expanded rows): full-length details, wrapped by `CompactLine`.
|
|
79
98
|
*/
|
|
80
99
|
budget?: number;
|
|
81
|
-
config?: Config;
|
|
82
100
|
};
|
|
83
101
|
|
|
84
102
|
/**
|
|
85
|
-
* The
|
|
103
|
+
* The description of a call: `read` + `src/a.ts:1-50`, `bash` +
|
|
86
104
|
* `go test ./...`, or a third-party tool's name plus its most identifying
|
|
87
105
|
* argument.
|
|
88
106
|
*/
|
|
89
107
|
export function describeTool(name: string, args: any, options: DescribeOptions = {}): { label: string; details: string } {
|
|
90
|
-
const hardCap = options.config?.get("maxDetailChars") ?? 4000;
|
|
91
108
|
const budget = options.budget ?? Number.POSITIVE_INFINITY;
|
|
92
109
|
const a = args ?? {};
|
|
93
110
|
|
|
94
|
-
if (name === "mcp") return { label: "mcp", details: mcpDetails(a, budget
|
|
95
|
-
if (name === "mcpScript") return { label: "mcpScript", details:
|
|
111
|
+
if (name === "mcp") return { label: "mcp", details: mcpDetails(a, budget) };
|
|
112
|
+
if (name === "mcpScript") return { label: "mcpScript", details: field(a.code, budget) };
|
|
96
113
|
if (name.startsWith("mcp__")) {
|
|
97
114
|
const server = name.slice("mcp__".length);
|
|
98
|
-
const tool =
|
|
115
|
+
const tool = field(a.tool, budget);
|
|
99
116
|
return { label: "mcp", details: tool ? `${tool} @ ${server}` : `@ ${server}` };
|
|
100
117
|
}
|
|
101
118
|
if (isMcpTool(name)) return { label: "mcp", details: name };
|
|
102
|
-
if (isBuiltIn(name)) return { label: name, details: builtInDetails(name, a, options
|
|
119
|
+
if (isBuiltIn(name)) return { label: name, details: builtInDetails(name, a, options) };
|
|
103
120
|
// Any other tool: show the name plus whichever argument identifies the call.
|
|
104
|
-
return { label: name, details: genericDetails(a, budget
|
|
121
|
+
return { label: name, details: genericDetails(a, budget) };
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
/**
|
|
@@ -120,10 +137,10 @@ export function summaryName(name: string, args: any): string {
|
|
|
120
137
|
return name;
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
function mcpDetails(args: any, budget: number
|
|
124
|
-
if (args.tool) return
|
|
140
|
+
function mcpDetails(args: any, budget: number): string {
|
|
141
|
+
if (args.tool) return field(args.tool, budget);
|
|
125
142
|
for (const key of ["search", "describe", "connect", "action"] as const) {
|
|
126
|
-
if (args[key]) return `${key} ${
|
|
143
|
+
if (args[key]) return `${key} ${field(args[key], budget)}`;
|
|
127
144
|
}
|
|
128
145
|
return "";
|
|
129
146
|
}
|
|
@@ -139,20 +156,19 @@ const GENERIC_KEYS = [
|
|
|
139
156
|
"tool", "action", "id", "target", "message", "text", "prompt",
|
|
140
157
|
] as const;
|
|
141
158
|
|
|
142
|
-
function genericDetails(args: any, budget: number
|
|
159
|
+
function genericDetails(args: any, budget: number): string {
|
|
143
160
|
for (const key of GENERIC_KEYS) {
|
|
144
161
|
const value = args[key];
|
|
145
|
-
// Only scalars: an object would serialize into noise on a
|
|
162
|
+
// Only scalars: an object would serialize into noise on a row.
|
|
146
163
|
if (value !== undefined && value !== null && typeof value !== "object") {
|
|
147
|
-
const text =
|
|
164
|
+
const text = field(value, budget);
|
|
148
165
|
if (text) return text;
|
|
149
166
|
}
|
|
150
167
|
}
|
|
151
168
|
return "";
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
function builtInDetails(name: ToolName, args: any, options: DescribeOptions
|
|
155
|
-
const expanded = options.expanded === true;
|
|
171
|
+
function builtInDetails(name: ToolName, args: any, options: DescribeOptions): string {
|
|
156
172
|
const budget = options.budget ?? Number.POSITIVE_INFINITY;
|
|
157
173
|
// Paths are shown verbatim (no ~/ home abbreviation — it was decorative and
|
|
158
174
|
// cost a homedir() call per render).
|
|
@@ -161,10 +177,9 @@ function builtInDetails(name: ToolName, args: any, options: DescribeOptions, har
|
|
|
161
177
|
switch (name) {
|
|
162
178
|
case "bash":
|
|
163
179
|
// ONE field, so no budget split is needed: CompactLine truncates at the
|
|
164
|
-
// real viewport width.
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
return compact(args.command, expanded ? Number.POSITIVE_INFINITY : budget, hardCap);
|
|
180
|
+
// real viewport width (no character cap). A collapsed row folds newlines into spaces; an expanded one
|
|
181
|
+
// keeps its lines (see `field`).
|
|
182
|
+
return field(args.command, budget);
|
|
168
183
|
case "read": {
|
|
169
184
|
// Show the line range when offset/limit were used, mirroring the built-in
|
|
170
185
|
// read tool's "path:start-end" notation.
|
|
@@ -186,7 +201,7 @@ function builtInDetails(name: ToolName, args: any, options: DescribeOptions, har
|
|
|
186
201
|
const wrapper = name === "grep" ? 2 : 0; // the two slashes in /pattern/
|
|
187
202
|
const overhead = where.length + " in ".length + wrapper;
|
|
188
203
|
const room = Number.isFinite(budget) ? Math.max(MIN_FIELD, budget - overhead) : Number.POSITIVE_INFINITY;
|
|
189
|
-
const pattern =
|
|
204
|
+
const pattern = field(args.pattern, room);
|
|
190
205
|
return name === "grep" ? `/${pattern}/ in ${where}` : `${pattern} in ${where}`;
|
|
191
206
|
}
|
|
192
207
|
case "ls":
|