@quandev104/pi-style 0.1.2 → 0.1.3
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 +15 -0
- package/README.md +1 -2
- package/dist/extensions/pi-style.js +5510 -4865
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/command-service.ts +2 -2
- package/extension-src/pi-style/app/index.ts +0 -1
- package/extension-src/pi-style/domain/config-authorization.ts +1 -2
- package/extension-src/pi-style/domain/config-normalization.ts +3 -3
- package/extension-src/pi-style/domain/config-types.ts +2 -2
- package/extension-src/pi-style/domain/theme.ts +3 -0
- package/extension-src/pi-style/features/messages/index.ts +2 -8
- package/extension-src/pi-style/features/tools/boxed/bash.ts +384 -0
- package/extension-src/pi-style/features/tools/boxed/batch.ts +459 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +48 -48
- package/extension-src/pi-style/features/tools/boxed/grep.ts +161 -89
- package/extension-src/pi-style/features/tools/boxed/index.ts +4 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +39 -47
- package/extension-src/pi-style/features/tools/boxed/output-tree.ts +368 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +32 -189
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +6 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +91 -49
- package/extension-src/pi-style/features/tools/index.ts +14 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +4 -26
- package/extension-src/pi-style/pi/compatibility-probe.ts +3 -33
- package/extension-src/pi-style/pi/compatibility-registry.ts +0 -1
- package/extension-src/pi-style/pi/config-session.ts +0 -2
- package/extension-src/pi-style/pi/index.ts +35 -1
- package/extension-src/pi-style/pi/session-coordinator.ts +39 -3
- package/extension-src/pi-style/shared/box.ts +41 -7
- package/extension-src/pi-style/shared/theme-extras.ts +0 -2
- package/package.json +1 -1
|
@@ -1,108 +1,180 @@
|
|
|
1
1
|
// Boxed grep/search tool renderer.
|
|
2
|
+
//
|
|
3
|
+
// grep renders a **boxless tree panel**: a summary header
|
|
4
|
+
// (`Grep: <pattern> <N> matches · <M> files · in <path>`) followed by match rows
|
|
5
|
+
// grouped by file (`├─ *line│content`). Like the quiet-tool batch panel, the
|
|
6
|
+
// whole panel lives in the call component and reads a live registry on every
|
|
7
|
+
// render, so the result's match data is picked up without cross-component
|
|
8
|
+
// invalidation. grep does not batch (each call owns its own panel).
|
|
9
|
+
//
|
|
10
|
+
// Lifecycle: panels are keyed by toolCallId and cleared on session reset and new
|
|
11
|
+
// message boundaries (see resetGrepRegistry wiring in session-coordinator.ts and
|
|
12
|
+
// pi/index.ts), mirroring the batch registry.
|
|
2
13
|
|
|
14
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
3
15
|
import { stripAnsi } from "../../../shared/ansi.js";
|
|
16
|
+
import { type BoxTheme, getTextOutput, shortenPath } from "../../../shared/box.js";
|
|
17
|
+
import { safeTruncateToWidth } from "../../../shared/render-budget.js";
|
|
4
18
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "
|
|
19
|
+
type GrepMatch,
|
|
20
|
+
groupMatchesByFile,
|
|
21
|
+
parseGrepOutput,
|
|
22
|
+
pluralForm,
|
|
23
|
+
renderGrepTree,
|
|
24
|
+
SEARCH_ICON,
|
|
25
|
+
TREE_INDENT,
|
|
26
|
+
} from "./output-tree.js";
|
|
13
27
|
import { getToolsRenderConfig } from "./session-config.js";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
import { type BoxedToolDefinition, noteExecutionStart } from "./shared.js";
|
|
29
|
+
|
|
30
|
+
const GREP_HEAD_LIMIT = 6;
|
|
31
|
+
const GREP_ERROR_LINES = 2;
|
|
32
|
+
|
|
33
|
+
interface GrepPanelState {
|
|
34
|
+
pattern: string;
|
|
35
|
+
pathLabel: string;
|
|
36
|
+
/** `undefined` until the result arrives; an empty array means zero matches. */
|
|
37
|
+
matches: GrepMatch[] | undefined;
|
|
38
|
+
isError: boolean;
|
|
39
|
+
errorText: string | undefined;
|
|
40
|
+
isPartial: boolean;
|
|
41
|
+
}
|
|
23
42
|
|
|
24
|
-
const
|
|
43
|
+
const grepPanels = new Map<string, GrepPanelState>();
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
/** Reset all grep panel state (session start/shutdown). */
|
|
46
|
+
export function resetGrepRegistry(): void {
|
|
47
|
+
grepPanels.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pathLabel(rawPath: string): string {
|
|
27
51
|
const displayPath = String(rawPath ?? ".");
|
|
28
|
-
|
|
29
|
-
return pattern ? `/${pattern}/ in ${path}` : path;
|
|
52
|
+
return displayPath === "." || displayPath === "" ? "current directory" : shortenPath(displayPath);
|
|
30
53
|
}
|
|
31
54
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
function registerGrepCall(toolCallId: string, pattern: string, label: string): void {
|
|
56
|
+
const existing = grepPanels.get(toolCallId);
|
|
57
|
+
if (existing) {
|
|
58
|
+
existing.pattern = pattern;
|
|
59
|
+
existing.pathLabel = label;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
grepPanels.set(toolCallId, {
|
|
63
|
+
pattern,
|
|
64
|
+
pathLabel: label,
|
|
65
|
+
matches: undefined,
|
|
66
|
+
isError: false,
|
|
67
|
+
errorText: undefined,
|
|
68
|
+
isPartial: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function registerGrepResult(
|
|
73
|
+
toolCallId: string,
|
|
74
|
+
data: { matches: GrepMatch[]; isError: boolean; errorText: string | undefined; isPartial: boolean },
|
|
75
|
+
): void {
|
|
76
|
+
const state = grepPanels.get(toolCallId);
|
|
77
|
+
if (!state) return;
|
|
78
|
+
state.matches = data.matches;
|
|
79
|
+
state.isError = data.isError;
|
|
80
|
+
state.errorText = data.errorText;
|
|
81
|
+
state.isPartial = data.isPartial;
|
|
82
|
+
}
|
|
48
83
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
(width) => {
|
|
53
|
-
const body = renderLines(theme, stripped || output || "Error", options, {
|
|
54
|
-
maxLines: MAX_GREP_PREVIEW_LINES,
|
|
55
|
-
color: "error",
|
|
56
|
-
width,
|
|
57
|
-
});
|
|
58
|
-
return body ? body.split("\n") : [];
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
widthKey,
|
|
62
|
-
footerLines: resultFooterLines(theme, result, context),
|
|
63
|
-
isError: true,
|
|
64
|
-
},
|
|
65
|
-
);
|
|
66
|
-
}
|
|
84
|
+
function bold(theme: BoxTheme, text: string): string {
|
|
85
|
+
return typeof theme?.bold === "function" ? theme.bold(text) : text;
|
|
86
|
+
}
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
/** `Grep: <pattern> <N> matches · <M> files · in <path>` (done) /
|
|
89
|
+
* `Grep: <pattern> · in <path>` (pending). */
|
|
90
|
+
function formatGrepHeader(theme: BoxTheme, state: GrepPanelState): string {
|
|
91
|
+
const icon = getToolsRenderConfig().nerdFonts ? `${SEARCH_ICON} ` : "";
|
|
92
|
+
const label = bold(theme, "Grep:");
|
|
93
|
+
const patternPart = state.pattern ? ` ${theme.fg("text", state.pattern)}` : "";
|
|
94
|
+
const pathPart = state.pathLabel ? theme.fg("dim", ` · in ${state.pathLabel}`) : "";
|
|
95
|
+
if (state.isError) {
|
|
96
|
+
return `${icon}${theme.fg("error", bold(theme, "✗ Grep:"))}${state.pattern ? ` ${theme.fg("error", state.pattern)}` : ""}${pathPart}`;
|
|
97
|
+
}
|
|
98
|
+
if (state.matches === undefined) {
|
|
99
|
+
return `${icon}${label}${patternPart}${pathPart}`;
|
|
100
|
+
}
|
|
101
|
+
const matchCount = state.matches.length;
|
|
102
|
+
const fileCount = groupMatchesByFile(state.matches).length;
|
|
103
|
+
const matchesPart = theme.fg("accent", `${matchCount} ${pluralForm("match", matchCount)}`);
|
|
104
|
+
const filesPart = theme.fg("dim", ` · ${fileCount} ${pluralForm("file", fileCount)}`);
|
|
105
|
+
return `${icon}${label}${patternPart} ${matchesPart}${filesPart}${pathPart}`;
|
|
106
|
+
}
|
|
69
107
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
function renderErrorLines(theme: BoxTheme, errorText: string, width: number): string[] {
|
|
109
|
+
const raw = stripAnsi(errorText)
|
|
110
|
+
.split("\n")
|
|
111
|
+
.map((line) => line.trim())
|
|
112
|
+
.filter((line) => line.length > 0);
|
|
113
|
+
if (raw.length === 0) return [];
|
|
114
|
+
const prefix = `${TREE_INDENT}${theme.fg("borderMuted", "└─")} `;
|
|
115
|
+
const out = raw
|
|
116
|
+
.slice(0, GREP_ERROR_LINES)
|
|
117
|
+
.map((line) => safeTruncateToWidth(`${prefix}${theme.fg("error", line)}`, Math.max(1, width), "…"));
|
|
118
|
+
if (raw.length > GREP_ERROR_LINES)
|
|
119
|
+
out.push(safeTruncateToWidth(`${prefix}${theme.fg("error", "…")}`, Math.max(1, width), "…"));
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
74
122
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
123
|
+
function renderGrepPanelLines(theme: BoxTheme, state: GrepPanelState, width: number): string[] {
|
|
124
|
+
const safeWidth = Math.max(1, width);
|
|
125
|
+
const header = safeTruncateToWidth(formatGrepHeader(theme, state), safeWidth, "…");
|
|
126
|
+
if (state.isError) {
|
|
127
|
+
return [header, ...(state.errorText ? renderErrorLines(theme, state.errorText, width) : [])];
|
|
128
|
+
}
|
|
129
|
+
if (state.matches === undefined) return [header];
|
|
130
|
+
return renderGrepTree(theme, header, state.matches, safeWidth, {
|
|
131
|
+
headLimit: GREP_HEAD_LIMIT,
|
|
132
|
+
withIcons: getToolsRenderConfig().nerdFonts,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
78
135
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
136
|
+
/** Live panel component reading the registry on every render pass. The state
|
|
137
|
+
* reference is captured at creation (like the batch panel): a registry clear
|
|
138
|
+
* on session reset/resume must not blank already-rendered panels — the result
|
|
139
|
+
* renderer mutates this same object, so live updates still flow. */
|
|
140
|
+
function renderGrepPanel(theme: BoxTheme, toolCallId: string): Component {
|
|
141
|
+
const state = grepPanels.get(toolCallId);
|
|
142
|
+
return {
|
|
143
|
+
invalidate() {},
|
|
144
|
+
render(width: number): string[] {
|
|
145
|
+
if (!state) return [safeTruncateToWidth(bold(theme, "Grep:"), Math.max(1, width), "…")];
|
|
146
|
+
return renderGrepPanelLines(theme, state, width);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
83
150
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
151
|
+
/** Empty result component — the panel lives in the call component, which
|
|
152
|
+
* re-renders when the result arrives (Pi re-renders the tool execution
|
|
153
|
+
* component on tool_execution_end), picking up the stored matches. */
|
|
154
|
+
const EMPTY_GREP_RESULT: Component = {
|
|
155
|
+
invalidate() {},
|
|
156
|
+
render() {
|
|
157
|
+
return [];
|
|
158
|
+
},
|
|
159
|
+
};
|
|
91
160
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
161
|
+
export const grepTool: BoxedToolDefinition = {
|
|
162
|
+
call(args, theme, context) {
|
|
163
|
+
noteExecutionStart(context);
|
|
164
|
+
const pattern = String(args?.pattern ?? "");
|
|
165
|
+
registerGrepCall(context.toolCallId, pattern, pathLabel(String(args?.path ?? ".")));
|
|
166
|
+
return renderGrepPanel(theme, context.toolCallId);
|
|
167
|
+
},
|
|
168
|
+
result(result, options, _theme, context) {
|
|
169
|
+
const output = stripAnsi(getTextOutput(result)).trimEnd();
|
|
170
|
+
const isError = Boolean(context.isError);
|
|
171
|
+
const matches = isError ? [] : parseGrepOutput(output);
|
|
172
|
+
registerGrepResult(context.toolCallId, {
|
|
173
|
+
matches,
|
|
174
|
+
isError,
|
|
175
|
+
errorText: isError ? output || undefined : undefined,
|
|
176
|
+
isPartial: Boolean(options.isPartial),
|
|
177
|
+
});
|
|
178
|
+
return EMPTY_GREP_RESULT;
|
|
107
179
|
},
|
|
108
180
|
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { Component } from "@earendil-works/pi-tui";
|
|
8
8
|
import type { BoxTheme } from "../../../shared/box.js";
|
|
9
9
|
import { bashTool } from "./bash.js";
|
|
10
|
+
import { closeActiveBatch, isBatchableTool } from "./batch.js";
|
|
10
11
|
import { editTool } from "./edit.js";
|
|
11
12
|
import { renderFallbackCall, renderFallbackResult } from "./fallback.js";
|
|
12
13
|
import { findTool } from "./find.js";
|
|
@@ -46,6 +47,9 @@ export function renderBoxedToolCall(
|
|
|
46
47
|
theme: BoxTheme,
|
|
47
48
|
context: BoxedToolContext,
|
|
48
49
|
): Component {
|
|
50
|
+
// Any non-batchable tool call is a batch boundary: the next quiet call starts
|
|
51
|
+
// a fresh batch instead of joining the previous one.
|
|
52
|
+
if (!isBatchableTool(toolName)) closeActiveBatch();
|
|
49
53
|
const tool = typeof toolName === "string" ? REGISTRY[toolName] : undefined;
|
|
50
54
|
if (tool) return tool.call(args, theme, context);
|
|
51
55
|
return renderFallbackCall(toolName, args, theme, context);
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
// Boxed ls tool renderer.
|
|
2
|
+
//
|
|
3
|
+
// ls calls render as a boxless tree panel — a lone ls shows its parsed output as
|
|
4
|
+
// a flat `List: <N> files · in <path>` tree; consecutive ls calls group into one
|
|
5
|
+
// panel with per-member nested subtrees (see batch.ts). Pending/failed calls
|
|
6
|
+
// without output fall back to a path row.
|
|
2
7
|
|
|
3
8
|
import { stripAnsi } from "../../../shared/ansi.js";
|
|
9
|
+
import { getTextOutput, shortenPath } from "../../../shared/box.js";
|
|
4
10
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "./shared.js";
|
|
11
|
+
type BatchToolMeta,
|
|
12
|
+
EMPTY_BATCH_COMPONENT,
|
|
13
|
+
emptyBatchResult,
|
|
14
|
+
registerBatchCall,
|
|
15
|
+
registerBatchResult,
|
|
16
|
+
renderBatchAwareCall,
|
|
17
|
+
} from "./batch.js";
|
|
18
|
+
import { parseLsOutput } from "./output-tree.js";
|
|
19
|
+
import { type BoxedToolDefinition, noteExecutionStart } from "./shared.js";
|
|
20
|
+
|
|
21
|
+
const LIST_META: BatchToolMeta = Object.freeze({
|
|
22
|
+
toolName: "ls",
|
|
23
|
+
label: "List",
|
|
24
|
+
headerLabel: "List",
|
|
25
|
+
});
|
|
21
26
|
|
|
22
27
|
function displayPath(rawPath: string): string {
|
|
23
28
|
const path = String(rawPath ?? ".");
|
|
@@ -28,38 +33,25 @@ function displayPath(rawPath: string): string {
|
|
|
28
33
|
export const lsTool: BoxedToolDefinition = {
|
|
29
34
|
call(args, theme, context) {
|
|
30
35
|
noteExecutionStart(context);
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const rawPath = String(args?.path ?? ".");
|
|
37
|
+
const detail = displayPath(rawPath);
|
|
38
|
+
const { isLeader, batch } = registerBatchCall(LIST_META, detail, context, { pathLabel: detail });
|
|
39
|
+
if (!isLeader) return EMPTY_BATCH_COMPONENT;
|
|
40
|
+
return renderBatchAwareCall(theme, batch);
|
|
36
41
|
},
|
|
37
|
-
result(result, options,
|
|
38
|
-
clearFooterState(context);
|
|
42
|
+
result(result, options, _theme, context) {
|
|
39
43
|
const output = stripAnsi(getTextOutput(result)).trimEnd();
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let itemCount = 0;
|
|
54
|
-
if (output && output !== "(empty directory)") {
|
|
55
|
-
const stripped = stripTrailingNotice(output);
|
|
56
|
-
itemCount = truncationOutputLines(result) ?? countLines(stripped);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const summary = `↳ Listed ${itemCount} ${itemCount === 1 ? "item" : "items"}.`;
|
|
60
|
-
return renderBoxedToolResult(theme, () => [theme.fg("dim", summary)], {
|
|
61
|
-
widthKey,
|
|
62
|
-
footerLines: resultFooterLines(theme, result, context),
|
|
63
|
-
});
|
|
44
|
+
const entries = context.isError ? undefined : parseLsOutput(output);
|
|
45
|
+
registerBatchResult(
|
|
46
|
+
LIST_META,
|
|
47
|
+
{
|
|
48
|
+
isPartial: Boolean(options.isPartial),
|
|
49
|
+
isError: Boolean(context.isError),
|
|
50
|
+
errorText: context.isError ? output || undefined : undefined,
|
|
51
|
+
...(entries !== undefined ? { entries } : {}),
|
|
52
|
+
},
|
|
53
|
+
context,
|
|
54
|
+
);
|
|
55
|
+
return emptyBatchResult();
|
|
64
56
|
},
|
|
65
57
|
};
|