@owlburtoe/pi-cc-tools 1.1.0 → 1.1.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/README.md +4 -1
- package/config/config.example.json +1 -0
- package/extensions/index.ts +204 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots,
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Compact built-in tool rendering** for `read`, `bash`, `grep`, `find`, `ls`, `edit`, and `write`
|
|
8
|
+
- **Semantic bash display** that renders common read-only shell one-liners like `nl -ba file | sed -n '1,200p'` as Claude Code-style `Read file (lines 1-200)` rows
|
|
8
9
|
- **Claude-style OpenAI tool rendering** for `apply_patch` plus common Pi/OpenAI-style tools like `webfetch`, `web_search`, `fetch_content`, task tools, and context tools
|
|
9
10
|
- **`apply_patch` diff previews** that render parsed file patches in the call phase, similar to `edit`/`write`
|
|
10
11
|
- **Adaptive edit/write diffs** with split or unified layouts, syntax highlighting, and inline word-level emphasis
|
|
@@ -33,6 +34,7 @@ Set in `.pi/settings.json` or `~/.pi/settings.json`:
|
|
|
33
34
|
"bashOutputMode": "opencode",
|
|
34
35
|
"bashCollapsedLines": 10,
|
|
35
36
|
"bashStackConsecutive": true,
|
|
37
|
+
"bashSemanticDisplay": true,
|
|
36
38
|
"diffCollapsedLines": 24,
|
|
37
39
|
"themeAdaptive": true,
|
|
38
40
|
"diffTheme": "github-dark",
|
|
@@ -140,7 +142,7 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
|
|
|
140
142
|
|
|
141
143
|
| Value | Behavior |
|
|
142
144
|
|-------|----------|
|
|
143
|
-
| `opencode` | Compact status while collapsed, with `Ctrl+O` hint for output preview |
|
|
145
|
+
| `opencode` | Compact status while collapsed, with `Ctrl+O` hint for output preview on raw shell commands. Semantic read-only bash rows omit the repeated hint to stay closer to Claude Code. |
|
|
144
146
|
| `summary` | Status only; no output preview or expansion hint |
|
|
145
147
|
| `preview` | Show a small output preview even while collapsed |
|
|
146
148
|
|
|
@@ -149,6 +151,7 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
|
|
|
149
151
|
| Setting | Default | Description |
|
|
150
152
|
|---------|---------|-------------|
|
|
151
153
|
| `bashStackConsecutive` | `true` | Remove the extra synthetic spacer between adjacent bash tool rows so command bursts render as a tight stack |
|
|
154
|
+
| `bashSemanticDisplay` | `true` | Render common read-only shell file-inspection commands as semantic `Read` rows instead of raw `Bash` rows |
|
|
152
155
|
|
|
153
156
|
### Numeric settings
|
|
154
157
|
|
package/extensions/index.ts
CHANGED
|
@@ -86,6 +86,8 @@ interface SettingsFile {
|
|
|
86
86
|
bashCollapsedLines?: number;
|
|
87
87
|
/** When true (default), consecutive bash tool rows render without the extra inter-tool spacer. */
|
|
88
88
|
bashStackConsecutive?: boolean;
|
|
89
|
+
/** When true (default), read-only shell file-inspection one-liners render as semantic Read rows instead of raw Bash rows. */
|
|
90
|
+
bashSemanticDisplay?: boolean;
|
|
89
91
|
showTruncationHints?: boolean;
|
|
90
92
|
diffCollapsedLines?: number;
|
|
91
93
|
diffTheme?: string;
|
|
@@ -1373,6 +1375,191 @@ function bashOutputMode(): "opencode" | "summary" | "preview" {
|
|
|
1373
1375
|
return getMode(readSettings().bashOutputMode, ["opencode", "summary", "preview"] as const, "opencode");
|
|
1374
1376
|
}
|
|
1375
1377
|
|
|
1378
|
+
function bashSemanticDisplayEnabled(): boolean {
|
|
1379
|
+
return readSettings().bashSemanticDisplay !== false;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
export interface BashDisplayInfo {
|
|
1383
|
+
kind: "read";
|
|
1384
|
+
label: "Read";
|
|
1385
|
+
path: string;
|
|
1386
|
+
rangeLabel?: string;
|
|
1387
|
+
suppressCollapsedHint: boolean;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function tokenizeShellCommand(command: string): string[] | null {
|
|
1391
|
+
const tokens: string[] = [];
|
|
1392
|
+
let current = "";
|
|
1393
|
+
let quote: "'" | '"' | null = null;
|
|
1394
|
+
let escaping = false;
|
|
1395
|
+
|
|
1396
|
+
const push = () => {
|
|
1397
|
+
if (current.length > 0) {
|
|
1398
|
+
tokens.push(current);
|
|
1399
|
+
current = "";
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
for (let i = 0; i < command.length; i++) {
|
|
1404
|
+
const char = command[i];
|
|
1405
|
+
if (escaping) {
|
|
1406
|
+
current += char;
|
|
1407
|
+
escaping = false;
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (quote) {
|
|
1411
|
+
if (char === quote) {
|
|
1412
|
+
quote = null;
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (quote === '"' && char === "\\") {
|
|
1416
|
+
escaping = true;
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
current += char;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (char === "\\") {
|
|
1423
|
+
escaping = true;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (char === "'" || char === '"') {
|
|
1427
|
+
quote = char;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (/\s/.test(char)) {
|
|
1431
|
+
push();
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (char === "|" || char === ";") {
|
|
1435
|
+
push();
|
|
1436
|
+
if (char === "|" && command[i + 1] === "|") {
|
|
1437
|
+
tokens.push("||");
|
|
1438
|
+
i++;
|
|
1439
|
+
} else {
|
|
1440
|
+
tokens.push(char);
|
|
1441
|
+
}
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (char === "&" && command[i + 1] === "&") {
|
|
1445
|
+
push();
|
|
1446
|
+
tokens.push("&&");
|
|
1447
|
+
i++;
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
current += char;
|
|
1451
|
+
}
|
|
1452
|
+
if (escaping) current += "\\";
|
|
1453
|
+
if (quote) return null;
|
|
1454
|
+
push();
|
|
1455
|
+
return tokens;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function isControlToken(token: string): boolean {
|
|
1459
|
+
return token === "&&" || token === "||" || token === ";";
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function isOptionToken(token: string): boolean {
|
|
1463
|
+
return token.startsWith("-") && token !== "-" && token !== "--";
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function singlePathArg(tokens: readonly string[]): string | null {
|
|
1467
|
+
const paths = tokens.filter((token) => token !== "--" && token !== "-" && !isOptionToken(token));
|
|
1468
|
+
return paths.length === 1 ? paths[0] : null;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function formatSedRangeLabel(script: string | undefined): string | undefined {
|
|
1472
|
+
if (!script) return undefined;
|
|
1473
|
+
const match = script.match(/^(\d+|\$)(?:,(\d+|\$))?p$/);
|
|
1474
|
+
if (!match) return undefined;
|
|
1475
|
+
const start = match[1];
|
|
1476
|
+
const end = match[2];
|
|
1477
|
+
if (!end || end === start) return `line ${start}`;
|
|
1478
|
+
return `lines ${start}-${end}`;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function parseSedPrintRange(tokens: readonly string[]): string | undefined {
|
|
1482
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1483
|
+
if (tokens[i] === "-n" && tokens[i + 1]) {
|
|
1484
|
+
const label = formatSedRangeLabel(tokens[i + 1]);
|
|
1485
|
+
if (label) return label;
|
|
1486
|
+
}
|
|
1487
|
+
const direct = formatSedRangeLabel(tokens[i]);
|
|
1488
|
+
if (direct) return direct;
|
|
1489
|
+
}
|
|
1490
|
+
return undefined;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function classifyNlRead(tokens: readonly string[]): BashDisplayInfo | null {
|
|
1494
|
+
const pipeIndex = tokens.indexOf("|");
|
|
1495
|
+
const left = pipeIndex === -1 ? tokens : tokens.slice(0, pipeIndex);
|
|
1496
|
+
if (left[0] !== "nl") return null;
|
|
1497
|
+
const path = singlePathArg(left.slice(1));
|
|
1498
|
+
if (!path) return null;
|
|
1499
|
+
let rangeLabel: string | undefined;
|
|
1500
|
+
if (pipeIndex !== -1) {
|
|
1501
|
+
const right = tokens.slice(pipeIndex + 1);
|
|
1502
|
+
if (right[0] !== "sed" || right.includes("|")) return null;
|
|
1503
|
+
rangeLabel = parseSedPrintRange(right);
|
|
1504
|
+
}
|
|
1505
|
+
return { kind: "read", label: "Read", path, rangeLabel, suppressCollapsedHint: true };
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function classifySedRead(tokens: readonly string[]): BashDisplayInfo | null {
|
|
1509
|
+
if (tokens[0] !== "sed") return null;
|
|
1510
|
+
const rangeLabel = parseSedPrintRange(tokens);
|
|
1511
|
+
if (!rangeLabel) return null;
|
|
1512
|
+
const scriptIndex = tokens.findIndex((token) => formatSedRangeLabel(token) !== undefined);
|
|
1513
|
+
const path = singlePathArg(tokens.slice(scriptIndex + 1));
|
|
1514
|
+
if (!path) return null;
|
|
1515
|
+
return { kind: "read", label: "Read", path, rangeLabel, suppressCollapsedHint: true };
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function classifyCatRead(tokens: readonly string[]): BashDisplayInfo | null {
|
|
1519
|
+
if (tokens[0] !== "cat") return null;
|
|
1520
|
+
const path = singlePathArg(tokens.slice(1));
|
|
1521
|
+
if (!path) return null;
|
|
1522
|
+
return { kind: "read", label: "Read", path, suppressCollapsedHint: true };
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function classifyHeadTailRead(tokens: readonly string[]): BashDisplayInfo | null {
|
|
1526
|
+
const command = tokens[0];
|
|
1527
|
+
if (command !== "head" && command !== "tail") return null;
|
|
1528
|
+
const args = tokens.slice(1);
|
|
1529
|
+
let count: string | undefined;
|
|
1530
|
+
const pathCandidates: string[] = [];
|
|
1531
|
+
for (let i = 0; i < args.length; i++) {
|
|
1532
|
+
const token = args[i];
|
|
1533
|
+
if (token === "--") continue;
|
|
1534
|
+
if (token === "-n" && args[i + 1]) {
|
|
1535
|
+
count = args[i + 1];
|
|
1536
|
+
i++;
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
const compactCount = token.match(/^-([0-9]+)$/)?.[1];
|
|
1540
|
+
if (compactCount) {
|
|
1541
|
+
count = compactCount;
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (isOptionToken(token)) continue;
|
|
1545
|
+
pathCandidates.push(token);
|
|
1546
|
+
}
|
|
1547
|
+
if (pathCandidates.length !== 1) return null;
|
|
1548
|
+
const amount = count && /^\d+$/.test(count) ? count : "10";
|
|
1549
|
+
const rangeLabel = `${command === "head" ? "first" : "last"} ${amount} lines`;
|
|
1550
|
+
return { kind: "read", label: "Read", path: pathCandidates[0], rangeLabel, suppressCollapsedHint: true };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
export function classifyBashCommandForDisplay(command: string): BashDisplayInfo | null {
|
|
1554
|
+
const tokens = tokenizeShellCommand(command.trim());
|
|
1555
|
+
if (!tokens || tokens.length === 0) return null;
|
|
1556
|
+
if (tokens.some(isControlToken)) return null;
|
|
1557
|
+
return classifyNlRead(tokens)
|
|
1558
|
+
?? classifySedRead(tokens)
|
|
1559
|
+
?? classifyCatRead(tokens)
|
|
1560
|
+
?? classifyHeadTailRead(tokens);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1376
1563
|
function diffCollapsedLimit(): number {
|
|
1377
1564
|
const value = readSettings().diffCollapsedLines;
|
|
1378
1565
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 24;
|
|
@@ -4281,16 +4468,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
4281
4468
|
},
|
|
4282
4469
|
renderCall(args, theme, ctx) {
|
|
4283
4470
|
syncToolCallStatus(ctx);
|
|
4284
|
-
const
|
|
4285
|
-
|
|
4471
|
+
const semantic = bashSemanticDisplayEnabled() ? classifyBashCommandForDisplay(args.command ?? "") : null;
|
|
4472
|
+
const summary = stableCallSummary(ctx, "_callSummary", () => {
|
|
4473
|
+
if (!semantic) return summarizeText(args.command, 72);
|
|
4474
|
+
const path = sp(semantic.path);
|
|
4475
|
+
return semantic.rangeLabel ? `${path} ${theme.fg("muted", `(${semantic.rangeLabel})`)}` : path;
|
|
4476
|
+
});
|
|
4477
|
+
return makeText(ctx.lastComponent, toolHeader(semantic?.label ?? "Bash", summary, theme, toolStatusDot(ctx, theme)));
|
|
4286
4478
|
},
|
|
4287
4479
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
4288
4480
|
const details = result.details as BashToolDetails | undefined;
|
|
4289
4481
|
const output = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
4290
4482
|
const nonEmpty = output.split("\n").filter((line) => line.trim().length > 0);
|
|
4483
|
+
const semantic = bashSemanticDisplayEnabled() ? classifyBashCommandForDisplay(ctx.args?.command ?? "") : null;
|
|
4291
4484
|
if (isPartial) {
|
|
4292
4485
|
setupBlinkTimer(ctx);
|
|
4293
|
-
|
|
4486
|
+
const running = semantic?.kind === "read" ? "Reading" : "Running";
|
|
4487
|
+
return makeText(ctx.lastComponent, withBranch(theme.fg("warning", `${running}... (${nonEmpty.length} lines)`), theme));
|
|
4294
4488
|
}
|
|
4295
4489
|
clearBlinkTimer(ctx);
|
|
4296
4490
|
setToolStatus(ctx, ctx.isError ? "error" : "success");
|
|
@@ -4299,8 +4493,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
4299
4493
|
const isError = ctx.isError || (exitCode !== null && exitCode !== 0);
|
|
4300
4494
|
let text = isError
|
|
4301
4495
|
? theme.fg("error", exitCode !== null ? `Exit ${exitCode}` : "Failed")
|
|
4302
|
-
:
|
|
4303
|
-
|
|
4496
|
+
: semantic?.kind === "read"
|
|
4497
|
+
? `${theme.fg("success", "Read")} ${theme.fg("muted", `${nonEmpty.length} line${nonEmpty.length === 1 ? "" : "s"}`)}`
|
|
4498
|
+
: `${theme.fg("success", "Done")}${theme.fg("muted", ` (${nonEmpty.length} lines)`)}`;
|
|
4304
4499
|
if (details?.truncation?.truncated) text += theme.fg("warning", " [truncated]");
|
|
4305
4500
|
|
|
4306
4501
|
const mode = bashOutputMode();
|
|
@@ -4311,7 +4506,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
4311
4506
|
return makeText(ctx.lastComponent, withBranch(`${text}\n${preview}`, theme));
|
|
4312
4507
|
}
|
|
4313
4508
|
|
|
4314
|
-
if (!expanded && nonEmpty.length > 0)
|
|
4509
|
+
if (!expanded && nonEmpty.length > 0) {
|
|
4510
|
+
const hint = semantic?.suppressCollapsedHint ? "" : theme.fg("muted", " • Ctrl+O to expand");
|
|
4511
|
+
return makeText(ctx.lastComponent, withBranch(`${text}${hint}`, theme));
|
|
4512
|
+
}
|
|
4315
4513
|
if (!expanded) return makeText(ctx.lastComponent, withBranch(text, theme));
|
|
4316
4514
|
text += `\n${buildPreviewText(nonEmpty.map((line) => theme.fg(isError ? "error" : "dim", line)), true, theme, bashCollapsedLimit())}`;
|
|
4317
4515
|
return makeText(ctx.lastComponent, withBranch(text, theme));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlburtoe/pi-cc-tools",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Claude Code-style tool rows for pi with Ctrl+O image previews and consistent built-in, MCP, and custom tool rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"bench:tools": "bun scripts/benchmark-tools.ts",
|
|
35
|
+
"test:bash-display": "bun scripts/test-bash-display.ts",
|
|
35
36
|
"test:message-chrome": "bun scripts/test-message-chrome.ts"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|