@selesai/code 0.10.1 → 0.11.0
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 +23 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +6 -4
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +1 -0
- package/dist/defaults/settings.json +0 -3
- package/dist/extensions/auto-session-name.test.ts +157 -0
- package/dist/extensions/auto-session-name.ts +96 -0
- package/dist/extensions/cost-reconcile.test.ts +76 -0
- package/dist/extensions/cost-reconcile.ts +278 -0
- package/dist/extensions/enable-readonly-tools.test.ts +11 -0
- package/dist/extensions/enable-readonly-tools.ts +7 -3
- package/dist/extensions/package.json +3 -0
- package/dist/extensions/pi-tool-display/CHANGELOG.md +329 -0
- package/dist/extensions/pi-tool-display/LICENSE +21 -0
- package/dist/extensions/pi-tool-display/README.md +417 -0
- package/dist/extensions/pi-tool-display/config/config.example.json +40 -0
- package/dist/extensions/pi-tool-display/index.ts +3 -0
- package/dist/extensions/pi-tool-display/package-lock.json +2555 -0
- package/dist/extensions/pi-tool-display/package.json +86 -0
- package/dist/extensions/pi-tool-display/src/agent-dir.ts +35 -0
- package/dist/extensions/pi-tool-display/src/ansi-utils.ts +116 -0
- package/dist/extensions/pi-tool-display/src/bash-display.ts +213 -0
- package/dist/extensions/pi-tool-display/src/capabilities.ts +88 -0
- package/dist/extensions/pi-tool-display/src/config-modal.ts +541 -0
- package/dist/extensions/pi-tool-display/src/config-store.ts +304 -0
- package/dist/extensions/pi-tool-display/src/debug-logger.ts +150 -0
- package/dist/extensions/pi-tool-display/src/diff-presentation.ts +72 -0
- package/dist/extensions/pi-tool-display/src/diff-renderer.ts +2517 -0
- package/dist/extensions/pi-tool-display/src/disposable.ts +34 -0
- package/dist/extensions/pi-tool-display/src/extension-lifecycle.ts +14 -0
- package/dist/extensions/pi-tool-display/src/index.ts +109 -0
- package/dist/extensions/pi-tool-display/src/line-width-safety.ts +93 -0
- package/dist/extensions/pi-tool-display/src/modal-icons.ts +57 -0
- package/dist/extensions/pi-tool-display/src/pending-diff-preview.ts +370 -0
- package/dist/extensions/pi-tool-display/src/presets.ts +117 -0
- package/dist/extensions/pi-tool-display/src/render-utils.ts +186 -0
- package/dist/extensions/pi-tool-display/src/settings-inspector-modal.ts +475 -0
- package/dist/extensions/pi-tool-display/src/thinking-label.ts +343 -0
- package/dist/extensions/pi-tool-display/src/tool-call-box.ts +181 -0
- package/dist/extensions/pi-tool-display/src/tool-metadata.ts +139 -0
- package/dist/extensions/pi-tool-display/src/tool-overrides.ts +2168 -0
- package/dist/extensions/pi-tool-display/src/types.ts +109 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-markdown.ts +64 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-native.ts +66 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-patch.ts +72 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-renderer.ts +419 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-utils.ts +106 -0
- package/dist/extensions/pi-tool-display/src/write-display-utils.ts +31 -0
- package/dist/extensions/pi-tool-display/src/zellij-modal.ts +1027 -0
- package/dist/extensions/pi-tool-display/tests/active-backlog-red.test.ts +65 -0
- package/dist/extensions/pi-tool-display/tests/ansi-utils.test.ts +345 -0
- package/dist/extensions/pi-tool-display/tests/bash-display.test.ts +442 -0
- package/dist/extensions/pi-tool-display/tests/capabilities-edge.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +54 -0
- package/dist/extensions/pi-tool-display/tests/config-modal.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/config-store.test.ts +87 -0
- package/dist/extensions/pi-tool-display/tests/custom-tool-overrides.test.ts +415 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger-edge.test.ts +630 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger.test.ts +62 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-ansi.test.ts +55 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-edge.test.ts +886 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-numbering.test.ts +135 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-width.test.ts +454 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-wrap.test.ts +169 -0
- package/dist/extensions/pi-tool-display/tests/index-integration.test.ts +354 -0
- package/dist/extensions/pi-tool-display/tests/pending-diff-preview.test.ts +79 -0
- package/dist/extensions/pi-tool-display/tests/presets-edge.test.ts +229 -0
- package/dist/extensions/pi-tool-display/tests/presets.test.ts +26 -0
- package/dist/extensions/pi-tool-display/tests/reload-behavior.test.ts +1241 -0
- package/dist/extensions/pi-tool-display/tests/render-utils.test.ts +710 -0
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +450 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label-edge.test.ts +904 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label.test.ts +72 -0
- package/dist/extensions/pi-tool-display/tests/tool-call-box.test.ts +105 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-config.test.ts +613 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +499 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-registration.test.ts +315 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-write-meta.test.ts +68 -0
- package/dist/extensions/pi-tool-display/tests/tool-ui-utils.test.ts +413 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box-performance-red.test.ts +176 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box.test.ts +908 -0
- package/dist/extensions/pi-tool-display/tests/zellij-modal.test.ts +848 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.d.ts +30 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.js +40 -0
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +46 -1
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +10 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +3 -3
- package/dist/extensions/pi-zentui/test/format.test.ts +50 -2
- package/dist/modes/interactive/components/startup-box.d.ts +38 -0
- package/dist/modes/interactive/components/startup-box.js +90 -0
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +85 -122
- package/dist/skills/agent-browser/SKILL.md +1 -0
- package/dist/skills/batch-grill-me/SKILL.md +1 -0
- package/dist/skills/brandkit/SKILL.md +1 -0
- package/dist/skills/design-references/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
- package/dist/skills/full-output-enforcement/SKILL.md +1 -0
- package/dist/skills/gpt-taste/SKILL.md +1 -0
- package/dist/skills/grill-me/SKILL.md +1 -0
- package/dist/skills/handoff/SKILL.md +1 -0
- package/dist/skills/handoff-text/SKILL.md +1 -0
- package/dist/skills/high-end-visual-design/SKILL.md +1 -0
- package/dist/skills/image-to-code/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
- package/dist/skills/implanger/SKILL.md +1 -0
- package/dist/skills/improve-codebase/SKILL.md +1 -0
- package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
- package/dist/skills/minimalist-ui/SKILL.md +1 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -0
- package/dist/skills/planger/SKILL.md +1 -0
- package/dist/skills/ponytail/SKILL.md +1 -0
- package/dist/skills/ponytail-audit/SKILL.md +1 -0
- package/dist/skills/ponytail-debt/SKILL.md +1 -0
- package/dist/skills/ponytail-gain/SKILL.md +1 -0
- package/dist/skills/ponytail-help/SKILL.md +1 -0
- package/dist/skills/ponytail-review/SKILL.md +1 -0
- package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
- package/dist/skills/selesai-handoff/SKILL.md +1 -0
- package/dist/skills/stitch-design-taste/SKILL.md +1 -0
- package/dist/skills/unlazy/SKILL.md +1 -0
- package/dist/skills/web-design-guidelines/SKILL.md +1 -0
- package/dist/skills/workflow/SKILL.md +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-tool-display",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Compact tool call rendering, diff visualization, and output truncation extension for Pi coding agent. Hides, collapses, and truncates verbose tool output for cleaner TUI display.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts",
|
|
9
|
+
"./tool-display-api-consumer": {
|
|
10
|
+
"types": "./tool-display-api-consumer.d.ts",
|
|
11
|
+
"default": "./tool-display-api-consumer.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.ts",
|
|
16
|
+
"tool-display-api-consumer.js",
|
|
17
|
+
"tool-display-api-consumer.d.ts",
|
|
18
|
+
"src",
|
|
19
|
+
"config/config.example.json",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"lint": "npm run typecheck",
|
|
27
|
+
"test": "tsx --test tests/*.test.ts",
|
|
28
|
+
"check": "npm run lint && npm run test",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
30
|
+
"postinstall": "node -e \"const fs=require('fs'),cp=require('child_process'),p=require('path');const cwd=process.cwd();const normalized=cwd.split(p.sep).join('/');if(!normalized.includes('/.pi/agent/extensions/'))process.exit(0);const s=p.resolve(cwd,'../../scripts/patch-vulnerable-deps.mjs');if(!fs.existsSync(s))process.exit(0);const r=cp.spawnSync(process.execPath,[s,'--target',cwd,'--quiet'],{stdio:'inherit'});process.exit(r.status||0)\""
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"pi-package",
|
|
34
|
+
"pi",
|
|
35
|
+
"pi-extension",
|
|
36
|
+
"pi-coding-agent",
|
|
37
|
+
"pi-tui",
|
|
38
|
+
"coding-agent",
|
|
39
|
+
"tui",
|
|
40
|
+
"tool-rendering",
|
|
41
|
+
"tool-output",
|
|
42
|
+
"diff-visualization",
|
|
43
|
+
"output-truncation",
|
|
44
|
+
"hide",
|
|
45
|
+
"collapse",
|
|
46
|
+
"truncate",
|
|
47
|
+
"compact",
|
|
48
|
+
"diff",
|
|
49
|
+
"output-mode"
|
|
50
|
+
],
|
|
51
|
+
"author": "MasuRii",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/MasuRii/pi-tool-display.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/MasuRii/pi-tool-display/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/MasuRii/pi-tool-display#readme",
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
},
|
|
67
|
+
"pi": {
|
|
68
|
+
"extensions": [
|
|
69
|
+
"./index.ts"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0",
|
|
74
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0"
|
|
75
|
+
},
|
|
76
|
+
"overrides": {
|
|
77
|
+
"file-type": ">=21.3.1",
|
|
78
|
+
"protobufjs": "7.6.3",
|
|
79
|
+
"ws": "8.21.0"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"tsx": "4.22.4",
|
|
83
|
+
"typescript": "6.0.3",
|
|
84
|
+
"@earendil-works/pi-coding-agent": "^0.80.3"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const PI_AGENT_DIR_ENV_VAR = "PI_CODING_AGENT_DIR";
|
|
6
|
+
// ponytail: keep the upstream default too; SELESAI_CODING_AGENT_DIR is the host fork's override.
|
|
7
|
+
const SELESAI_AGENT_DIR_ENV_VAR = "SELESAI_CODING_AGENT_DIR";
|
|
8
|
+
|
|
9
|
+
interface AgentDirEnvironment {
|
|
10
|
+
[name: string]: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function expandHomeDirectory(configuredDir: string, homeDirectory: string): string {
|
|
14
|
+
if (configuredDir === "~") {
|
|
15
|
+
return homeDirectory;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (configuredDir.startsWith("~/") || configuredDir.startsWith("~\\")) {
|
|
19
|
+
return join(homeDirectory, configuredDir.slice(2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return configuredDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolvePiAgentDir(
|
|
26
|
+
env: AgentDirEnvironment = process.env,
|
|
27
|
+
homeDirectory = homedir(),
|
|
28
|
+
): string {
|
|
29
|
+
const configuredDir = env[PI_AGENT_DIR_ENV_VAR] ?? env[SELESAI_AGENT_DIR_ENV_VAR];
|
|
30
|
+
if (!configuredDir) {
|
|
31
|
+
return join(homeDirectory, CONFIG_DIR_NAME, "agent");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return expandHomeDirectory(configuredDir, homeDirectory);
|
|
35
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const ANSI_SGR_PATTERN = /\x1b\[([0-9;]*)m/g;
|
|
2
|
+
const STYLE_RESET_PARAMS = [39, 22, 23, 24, 25, 27, 28, 29, 59] as const;
|
|
3
|
+
|
|
4
|
+
export { ANSI_SGR_PATTERN, STYLE_RESET_PARAMS };
|
|
5
|
+
|
|
6
|
+
export function expandSgrReset(param: number): readonly number[] | undefined {
|
|
7
|
+
return param === 0 ? STYLE_RESET_PARAMS : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function toSgrParams(rawParams: string): number[] {
|
|
11
|
+
if (!rawParams.trim()) {
|
|
12
|
+
return [0];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parsed = rawParams
|
|
16
|
+
.split(";")
|
|
17
|
+
.map((token) => Number.parseInt(token, 10))
|
|
18
|
+
.filter((value) => Number.isFinite(value));
|
|
19
|
+
|
|
20
|
+
return parsed.length > 0 ? parsed : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isFiniteSgrParam(value: number | undefined): value is number {
|
|
24
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readSgrColorSequence(params: number[], index: number): number[] | undefined {
|
|
28
|
+
const param = params[index];
|
|
29
|
+
if (param !== 38 && param !== 48) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const colorMode = params[index + 1];
|
|
34
|
+
if (colorMode === 5) {
|
|
35
|
+
const colorValue = params[index + 2];
|
|
36
|
+
return isFiniteSgrParam(colorValue) ? [param, colorMode, colorValue] : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (colorMode === 2) {
|
|
40
|
+
const red = params[index + 2];
|
|
41
|
+
const green = params[index + 3];
|
|
42
|
+
const blue = params[index + 4];
|
|
43
|
+
return isFiniteSgrParam(red) && isFiniteSgrParam(green) && isFiniteSgrParam(blue)
|
|
44
|
+
? [param, colorMode, red, green, blue]
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function stripBackgroundSgrParams(params: readonly number[]): number[] {
|
|
52
|
+
const sanitized: number[] = [];
|
|
53
|
+
|
|
54
|
+
for (let index = 0; index < params.length; index++) {
|
|
55
|
+
const param = params[index] ?? 0;
|
|
56
|
+
|
|
57
|
+
if (param === 0) {
|
|
58
|
+
sanitized.push(...expandSgrReset(param)!);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (param === 49 || (param >= 40 && param <= 47) || (param >= 100 && param <= 107)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (param === 38 || param === 48) {
|
|
67
|
+
const sequence = readSgrColorSequence(params as number[], index);
|
|
68
|
+
if (sequence) {
|
|
69
|
+
if (param === 38) {
|
|
70
|
+
sanitized.push(...sequence);
|
|
71
|
+
}
|
|
72
|
+
index += sequence.length - 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const advance = params[index + 1] === 5 ? 2 : params[index + 1] === 2 ? 4 : 0;
|
|
76
|
+
if (advance > 0) {
|
|
77
|
+
index += advance;
|
|
78
|
+
if (param === 38) {
|
|
79
|
+
sanitized.push(param);
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
sanitized.push(param);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return sanitized;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function filterSgrSequences(
|
|
92
|
+
text: string,
|
|
93
|
+
filter: (params: number[]) => number[],
|
|
94
|
+
): string {
|
|
95
|
+
if (!text || !text.includes("\x1b[")) {
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return text.replace(ANSI_SGR_PATTERN, (_sequence, rawParams: string) => {
|
|
100
|
+
const parsed = toSgrParams(rawParams);
|
|
101
|
+
if (parsed.length === 0) {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sanitized = filter(parsed);
|
|
106
|
+
if (sanitized.length === 0) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `\x1b[${sanitized.join(";")}m`;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function sanitizeAnsiForThemedOutput(text: string): string {
|
|
115
|
+
return filterSgrSequences(text, stripBackgroundSgrParams);
|
|
116
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { registerCleanup, registerTimer } from "./disposable.js";
|
|
3
|
+
|
|
4
|
+
const BASH_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
5
|
+
// Slow enough to stay visually alive without making the full transcript re-render five times a second.
|
|
6
|
+
const BASH_SPINNER_INTERVAL_MS = 750;
|
|
7
|
+
const BASH_SPINNER_STATE_KEY = "__piToolDisplayBashSpinner";
|
|
8
|
+
const BASH_SPINNER_TOOL_CALL_ID_KEY = "__piToolDisplayBashSpinnerToolCallId";
|
|
9
|
+
|
|
10
|
+
interface BashCallArgs {
|
|
11
|
+
command?: string;
|
|
12
|
+
commandPrefix?: string;
|
|
13
|
+
shellPath?: string;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BashCallRenderTheme {
|
|
18
|
+
fg(color: string, text: string): string;
|
|
19
|
+
bold(text: string): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BashSpinnerState {
|
|
23
|
+
frameIndex: number;
|
|
24
|
+
startedAt?: number;
|
|
25
|
+
timer?: ReturnType<typeof setInterval>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface BashSpinnerStateCarrier {
|
|
29
|
+
[BASH_SPINNER_STATE_KEY]?: BashSpinnerState;
|
|
30
|
+
[BASH_SPINNER_TOOL_CALL_ID_KEY]?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BashCallRenderContextLike {
|
|
34
|
+
executionStarted: boolean;
|
|
35
|
+
isPartial: boolean;
|
|
36
|
+
invalidate?: () => void;
|
|
37
|
+
lastComponent?: unknown;
|
|
38
|
+
state?: unknown;
|
|
39
|
+
toolCallId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const spinnerStatesByToolCallId = new Map<string, BashSpinnerState>();
|
|
43
|
+
let nextSyntheticToolCallId = 0;
|
|
44
|
+
|
|
45
|
+
function toStateCarrier(value: unknown): BashSpinnerStateCarrier | undefined {
|
|
46
|
+
if (!value || typeof value !== "object") {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return value as BashSpinnerStateCarrier;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSyntheticToolCallId(carrier: BashSpinnerStateCarrier | undefined): string | undefined {
|
|
53
|
+
if (!carrier) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!carrier[BASH_SPINNER_TOOL_CALL_ID_KEY]) {
|
|
58
|
+
carrier[BASH_SPINNER_TOOL_CALL_ID_KEY] = `state:${++nextSyntheticToolCallId}`;
|
|
59
|
+
}
|
|
60
|
+
return carrier[BASH_SPINNER_TOOL_CALL_ID_KEY];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getToolCallId(context: BashCallRenderContextLike): string | undefined {
|
|
64
|
+
if (typeof context.toolCallId === "string" && context.toolCallId.trim().length > 0) {
|
|
65
|
+
return context.toolCallId;
|
|
66
|
+
}
|
|
67
|
+
return getSyntheticToolCallId(toStateCarrier(context.state));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getOrCreateSpinnerState(
|
|
71
|
+
toolCallId: string | undefined,
|
|
72
|
+
carrier: BashSpinnerStateCarrier | undefined,
|
|
73
|
+
): BashSpinnerState | undefined {
|
|
74
|
+
if (!toolCallId) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let state = spinnerStatesByToolCallId.get(toolCallId);
|
|
79
|
+
if (!state) {
|
|
80
|
+
state = { frameIndex: 0 };
|
|
81
|
+
spinnerStatesByToolCallId.set(toolCallId, state);
|
|
82
|
+
}
|
|
83
|
+
if (carrier) {
|
|
84
|
+
carrier[BASH_SPINNER_STATE_KEY] = state;
|
|
85
|
+
}
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stopSpinner(toolCallId: string | undefined, state: BashSpinnerState | undefined): void {
|
|
90
|
+
if (!state) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (state.timer) {
|
|
95
|
+
clearInterval(state.timer);
|
|
96
|
+
state.timer = undefined;
|
|
97
|
+
}
|
|
98
|
+
state.frameIndex = 0;
|
|
99
|
+
state.startedAt = undefined;
|
|
100
|
+
if (toolCallId) {
|
|
101
|
+
spinnerStatesByToolCallId.delete(toolCallId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatElapsed(elapsedMs: number): string {
|
|
106
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
107
|
+
if (totalSeconds < 60) {
|
|
108
|
+
return `${totalSeconds}s`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
112
|
+
const seconds = totalSeconds % 60;
|
|
113
|
+
if (totalMinutes < 60) {
|
|
114
|
+
return `${totalMinutes}m ${seconds}s`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
118
|
+
const minutes = totalMinutes % 60;
|
|
119
|
+
return `${hours}h ${minutes}m`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isDefaultShellPath(shellPath: string): boolean {
|
|
123
|
+
const normalized = shellPath.trim().replace(/\\/g, "/").toLowerCase();
|
|
124
|
+
const basename = normalized.split("/").pop() || normalized;
|
|
125
|
+
return basename === "bash" || basename === "cmd.exe";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildCommandDisplay(args: BashCallArgs): string {
|
|
129
|
+
const command =
|
|
130
|
+
typeof args.command === "string" && args.command.trim().length > 0
|
|
131
|
+
? args.command
|
|
132
|
+
: "...";
|
|
133
|
+
const prefix =
|
|
134
|
+
typeof args.commandPrefix === "string" && args.commandPrefix.trim().length > 0
|
|
135
|
+
? args.commandPrefix.trim()
|
|
136
|
+
: "";
|
|
137
|
+
return prefix ? `${prefix} ${command}` : command;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildBashCallText(
|
|
141
|
+
args: BashCallArgs,
|
|
142
|
+
theme: BashCallRenderTheme,
|
|
143
|
+
spinnerFrame?: string,
|
|
144
|
+
elapsedMs?: number,
|
|
145
|
+
): string {
|
|
146
|
+
const commandDisplay = buildCommandDisplay(args);
|
|
147
|
+
const shellSuffix =
|
|
148
|
+
typeof args.shellPath === "string" &&
|
|
149
|
+
args.shellPath.trim().length > 0 &&
|
|
150
|
+
!isDefaultShellPath(args.shellPath)
|
|
151
|
+
? theme.fg("muted", ` [shell: ${args.shellPath}]`)
|
|
152
|
+
: "";
|
|
153
|
+
const timeoutSuffix = args.timeout
|
|
154
|
+
? theme.fg("muted", ` (timeout ${args.timeout}s)`)
|
|
155
|
+
: "";
|
|
156
|
+
const spinnerPrefix = spinnerFrame ? `${theme.fg("warning", `${spinnerFrame} `)}` : "";
|
|
157
|
+
const elapsedSuffix =
|
|
158
|
+
spinnerFrame && elapsedMs !== undefined
|
|
159
|
+
? theme.fg("muted", ` · ${formatElapsed(elapsedMs)}`)
|
|
160
|
+
: "";
|
|
161
|
+
|
|
162
|
+
return `${spinnerPrefix}${theme.fg("toolTitle", theme.bold("$"))} ${theme.fg("accent", commandDisplay)}${shellSuffix}${timeoutSuffix}${elapsedSuffix}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function renderBashCall(
|
|
166
|
+
args: BashCallArgs,
|
|
167
|
+
theme: BashCallRenderTheme,
|
|
168
|
+
context: BashCallRenderContextLike,
|
|
169
|
+
): Text {
|
|
170
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
171
|
+
const carrier = toStateCarrier(context.state);
|
|
172
|
+
const toolCallId = getToolCallId(context);
|
|
173
|
+
const spinnerState = getOrCreateSpinnerState(toolCallId, carrier);
|
|
174
|
+
const shouldSpin = context.executionStarted && context.isPartial;
|
|
175
|
+
|
|
176
|
+
if (!shouldSpin) {
|
|
177
|
+
stopSpinner(toolCallId, spinnerState);
|
|
178
|
+
text.setText(buildBashCallText(args, theme));
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (spinnerState) {
|
|
183
|
+
spinnerState.startedAt ??= Date.now();
|
|
184
|
+
if (!spinnerState.timer && typeof context.invalidate === "function") {
|
|
185
|
+
const timer = setInterval(() => {
|
|
186
|
+
spinnerState.frameIndex = (spinnerState.frameIndex + 1) % BASH_SPINNER_FRAMES.length;
|
|
187
|
+
text.setText(
|
|
188
|
+
buildBashCallText(
|
|
189
|
+
args,
|
|
190
|
+
theme,
|
|
191
|
+
BASH_SPINNER_FRAMES[spinnerState.frameIndex],
|
|
192
|
+
Date.now() - (spinnerState.startedAt ?? Date.now()),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
context.invalidate?.();
|
|
196
|
+
}, BASH_SPINNER_INTERVAL_MS);
|
|
197
|
+
spinnerState.timer = timer;
|
|
198
|
+
registerTimer(timer);
|
|
199
|
+
registerCleanup(() => {
|
|
200
|
+
if (spinnerStatesByToolCallId.get(toolCallId || "") === spinnerState) {
|
|
201
|
+
stopSpinner(toolCallId, spinnerState);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const spinnerFrame = spinnerState ? BASH_SPINNER_FRAMES[spinnerState.frameIndex] : undefined;
|
|
208
|
+
const elapsedMs = spinnerState?.startedAt !== undefined
|
|
209
|
+
? Date.now() - spinnerState.startedAt
|
|
210
|
+
: undefined;
|
|
211
|
+
text.setText(buildBashCallText(args, theme, spinnerFrame, elapsedMs));
|
|
212
|
+
return text;
|
|
213
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { resolvePiAgentDir } from "./agent-dir.js";
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { logToolDisplayDebug } from "./debug-logger.js";
|
|
6
|
+
import { isMcpToolCandidate } from "./tool-metadata.js";
|
|
7
|
+
import type { ToolDisplayConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export interface ToolDisplayCapabilities {
|
|
10
|
+
hasMcpTooling: boolean;
|
|
11
|
+
hasRtkOptimizer: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hasMcpTooling(pi: ExtensionAPI): boolean {
|
|
15
|
+
try {
|
|
16
|
+
const allTools = pi.getAllTools();
|
|
17
|
+
return allTools.some((tool) => isMcpToolCandidate(tool));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logToolDisplayDebug("MCP capability detection failed.", error);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasRtkCommand(pi: ExtensionAPI): boolean {
|
|
25
|
+
try {
|
|
26
|
+
const commands = pi.getCommands();
|
|
27
|
+
return commands.some((command) => typeof command.name === "string" && (command.name === "rtk" || command.name.startsWith("rtk-")));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logToolDisplayDebug("RTK command capability detection failed.", error);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rtkPathProbeCache = new Map<string, { fingerprint: string; exists: boolean }>();
|
|
35
|
+
|
|
36
|
+
function getPathFingerprint(path: string): string {
|
|
37
|
+
try {
|
|
38
|
+
const stats = statSync(path);
|
|
39
|
+
return `${stats.mtimeMs}:${stats.size}`;
|
|
40
|
+
} catch {
|
|
41
|
+
return "missing";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cachedPathExists(path: string): boolean {
|
|
46
|
+
const fingerprint = getPathFingerprint(path);
|
|
47
|
+
const cached = rtkPathProbeCache.get(path);
|
|
48
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
49
|
+
return cached.exists;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let exists = false;
|
|
53
|
+
try {
|
|
54
|
+
exists = existsSync(path);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logToolDisplayDebug(`RTK capability path probe failed for ${path}.`, error);
|
|
57
|
+
}
|
|
58
|
+
rtkPathProbeCache.set(path, { fingerprint, exists });
|
|
59
|
+
return exists;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasRtkExtensionPath(cwd: string): boolean {
|
|
63
|
+
const candidates = [
|
|
64
|
+
join(resolvePiAgentDir(), "extensions", "pi-rtk-optimizer"),
|
|
65
|
+
join(cwd, CONFIG_DIR_NAME, "extensions", "pi-rtk-optimizer"),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
return candidates.some((candidate) => cachedPathExists(candidate));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function detectToolDisplayCapabilities(pi: ExtensionAPI, cwd: string): ToolDisplayCapabilities {
|
|
72
|
+
return {
|
|
73
|
+
hasMcpTooling: hasMcpTooling(pi),
|
|
74
|
+
hasRtkOptimizer: hasRtkCommand(pi) || hasRtkExtensionPath(cwd),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function applyCapabilityConfigGuards(
|
|
79
|
+
config: ToolDisplayConfig,
|
|
80
|
+
capabilities: ToolDisplayCapabilities,
|
|
81
|
+
): ToolDisplayConfig {
|
|
82
|
+
return {
|
|
83
|
+
...config,
|
|
84
|
+
registerToolOverrides: { ...config.registerToolOverrides },
|
|
85
|
+
mcpOutputMode: config.mcpOutputMode,
|
|
86
|
+
showRtkCompactionHints: capabilities.hasRtkOptimizer ? config.showRtkCompactionHints : false,
|
|
87
|
+
};
|
|
88
|
+
}
|