@pi-unipi/compactor 0.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 +86 -0
- package/package.json +54 -0
- package/skills/compactor/SKILL.md +74 -0
- package/skills/compactor-doctor/SKILL.md +74 -0
- package/skills/compactor-ops/SKILL.md +65 -0
- package/skills/compactor-stats/SKILL.md +49 -0
- package/skills/compactor-tools/SKILL.md +120 -0
- package/src/commands/index.ts +248 -0
- package/src/compaction/brief.ts +334 -0
- package/src/compaction/build-sections.ts +77 -0
- package/src/compaction/content.ts +47 -0
- package/src/compaction/cut.ts +80 -0
- package/src/compaction/extract/commits.ts +52 -0
- package/src/compaction/extract/files.ts +58 -0
- package/src/compaction/extract/goals.ts +36 -0
- package/src/compaction/extract/preferences.ts +40 -0
- package/src/compaction/filter-noise.ts +46 -0
- package/src/compaction/format.ts +48 -0
- package/src/compaction/hooks.ts +145 -0
- package/src/compaction/merge.ts +113 -0
- package/src/compaction/normalize.ts +68 -0
- package/src/compaction/recall-scope.ts +32 -0
- package/src/compaction/sanitize.ts +12 -0
- package/src/compaction/search-entries.ts +101 -0
- package/src/compaction/sections.ts +15 -0
- package/src/compaction/summarize.ts +29 -0
- package/src/config/manager.ts +89 -0
- package/src/config/presets.ts +83 -0
- package/src/config/schema.ts +55 -0
- package/src/display/bash-display.ts +28 -0
- package/src/display/diff-presentation.ts +20 -0
- package/src/display/diff-renderer.ts +255 -0
- package/src/display/line-width-safety.ts +16 -0
- package/src/display/pending-diff-preview.ts +51 -0
- package/src/display/render-utils.ts +52 -0
- package/src/display/thinking-label.ts +18 -0
- package/src/display/tool-overrides.ts +136 -0
- package/src/display/user-message-box.ts +16 -0
- package/src/executor/executor.ts +242 -0
- package/src/executor/runtime.ts +125 -0
- package/src/index.ts +211 -0
- package/src/info-screen.ts +60 -0
- package/src/security/evaluator.ts +142 -0
- package/src/security/policy.ts +74 -0
- package/src/security/scanner.ts +65 -0
- package/src/session/db.ts +237 -0
- package/src/session/extract.ts +107 -0
- package/src/session/resume-inject.ts +25 -0
- package/src/session/snapshot.ts +326 -0
- package/src/store/chunking.ts +126 -0
- package/src/store/db-base.ts +79 -0
- package/src/store/index.ts +364 -0
- package/src/tools/compact.ts +20 -0
- package/src/tools/ctx-batch-execute.ts +53 -0
- package/src/tools/ctx-doctor.ts +78 -0
- package/src/tools/ctx-execute-file.ts +26 -0
- package/src/tools/ctx-execute.ts +21 -0
- package/src/tools/ctx-fetch-and-index.ts +37 -0
- package/src/tools/ctx-index.ts +42 -0
- package/src/tools/ctx-search.ts +23 -0
- package/src/tools/ctx-stats.ts +37 -0
- package/src/tools/register.ts +360 -0
- package/src/tools/vcc-recall.ts +64 -0
- package/src/tui/settings-overlay.ts +290 -0
- package/src/types.ts +269 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff rendering engine — LCS-based diff with 3 layouts, 3 indicators,
|
|
3
|
+
* syntax highlighting, and Nerd Font detection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type DiffLayout = "auto" | "split" | "unified";
|
|
7
|
+
export type DiffIndicator = "bars" | "classic" | "nerd" | "none";
|
|
8
|
+
|
|
9
|
+
// --- Nerd Font Detection ---
|
|
10
|
+
|
|
11
|
+
let nerdFontDetected: boolean | null = null;
|
|
12
|
+
|
|
13
|
+
/** Detect if terminal supports Nerd Font icons */
|
|
14
|
+
export function detectNerdFont(): boolean {
|
|
15
|
+
if (nerdFontDetected !== null) return nerdFontDetected;
|
|
16
|
+
// Check common Nerd Font env vars or terminal emulators
|
|
17
|
+
const term = process.env.TERM_PROGRAM ?? "";
|
|
18
|
+
const termFont = process.env.TERM_FONT ?? "";
|
|
19
|
+
nerdFontDetected =
|
|
20
|
+
termFont.toLowerCase().includes("nerd") ||
|
|
21
|
+
process.env.NERD_FONT === "1" ||
|
|
22
|
+
term === "WezTerm" ||
|
|
23
|
+
term === "iTerm.app" ||
|
|
24
|
+
(process.env.TERMINAL_EMULATOR ?? "").includes("JetBrains") ||
|
|
25
|
+
false;
|
|
26
|
+
return nerdFontDetected;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Nerd Font indicator chars */
|
|
30
|
+
const NERD_INDICATORS = {
|
|
31
|
+
add: "\uf055 ", // nf-fa-plus_circle
|
|
32
|
+
remove: "\uf056 ", // nf-fa-minus_circle
|
|
33
|
+
same: " ",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
// --- Syntax Highlighting ---
|
|
37
|
+
|
|
38
|
+
const KEYWORDS: Record<string, Set<string>> = {
|
|
39
|
+
js: new Set(["const", "let", "var", "function", "return", "if", "else", "for", "while", "class", "import", "export", "from", "async", "await", "try", "catch", "throw", "new", "this", "typeof", "instanceof", "in", "of", "switch", "case", "break", "continue", "default", "yield", "void", "delete", "null", "undefined", "true", "false"]),
|
|
40
|
+
py: new Set(["def", "class", "import", "from", "return", "if", "elif", "else", "for", "while", "try", "except", "finally", "raise", "with", "as", "yield", "lambda", "pass", "break", "continue", "and", "or", "not", "in", "is", "None", "True", "False", "self", "async", "await"]),
|
|
41
|
+
ts: new Set(["const", "let", "var", "function", "return", "if", "else", "for", "while", "class", "import", "export", "from", "async", "await", "try", "catch", "throw", "new", "this", "typeof", "instanceof", "interface", "type", "enum", "namespace", "module", "declare", "implements", "extends", "public", "private", "protected", "readonly", "static", "abstract", "override", "keyof", "infer", "never", "unknown", "any", "void", "string", "number", "boolean", "null", "undefined", "true", "false"]),
|
|
42
|
+
sh: new Set(["if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac", "function", "return", "local", "export", "readonly", "declare", "unset", "shift", "source", "exit", "echo", "printf", "read", "test", "true", "false"]),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Apply basic syntax highlighting to a line */
|
|
46
|
+
function highlightLine(line: string, lang?: string): string {
|
|
47
|
+
if (!lang) return line;
|
|
48
|
+
const keywords = KEYWORDS[lang] ?? KEYWORDS.js;
|
|
49
|
+
// Highlight keywords (simple word boundary match)
|
|
50
|
+
return line.replace(/\b([a-zA-Z_]\w*)\b/g, (match) => {
|
|
51
|
+
if (keywords.has(match)) return `\x1b[34m${match}\x1b[0m`; // blue
|
|
52
|
+
return match;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Detect language from file extension or content */
|
|
57
|
+
function detectLanguage(filePath?: string, content?: string): string | undefined {
|
|
58
|
+
if (filePath) {
|
|
59
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
60
|
+
const extMap: Record<string, string> = {
|
|
61
|
+
js: "js", mjs: "js", cjs: "js", jsx: "js",
|
|
62
|
+
ts: "ts", mts: "ts", cts: "ts", tsx: "ts",
|
|
63
|
+
py: "py", pyw: "py",
|
|
64
|
+
sh: "sh", bash: "sh", zsh: "sh",
|
|
65
|
+
};
|
|
66
|
+
if (ext && extMap[ext]) return extMap[ext];
|
|
67
|
+
}
|
|
68
|
+
if (content) {
|
|
69
|
+
if (content.includes("def ") || content.includes("import ")) return "py";
|
|
70
|
+
if (content.includes("function ") || content.includes("const ")) return "js";
|
|
71
|
+
if (content.includes("#!/bin/")) return "sh";
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface DiffLine {
|
|
77
|
+
type: "same" | "add" | "remove";
|
|
78
|
+
text: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function lcs<T>(a: T[], b: T[]): T[] {
|
|
82
|
+
const m = a.length;
|
|
83
|
+
const n = b.length;
|
|
84
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
85
|
+
|
|
86
|
+
for (let i = 1; i <= m; i++) {
|
|
87
|
+
for (let j = 1; j <= n; j++) {
|
|
88
|
+
if (a[i - 1] === b[j - 1]) {
|
|
89
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
90
|
+
} else {
|
|
91
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result: T[] = [];
|
|
97
|
+
let i = m, j = n;
|
|
98
|
+
while (i > 0 && j > 0) {
|
|
99
|
+
if (a[i - 1] === b[j - 1]) {
|
|
100
|
+
result.unshift(a[i - 1]);
|
|
101
|
+
i--;
|
|
102
|
+
j--;
|
|
103
|
+
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
104
|
+
i--;
|
|
105
|
+
} else {
|
|
106
|
+
j--;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function computeDiff(before: string[], after: string[]): DiffLine[] {
|
|
114
|
+
const common = lcs(before, after);
|
|
115
|
+
const result: DiffLine[] = [];
|
|
116
|
+
let i = 0, j = 0, k = 0;
|
|
117
|
+
|
|
118
|
+
while (i < before.length || j < after.length) {
|
|
119
|
+
if (k < common.length && before[i] === common[k] && after[j] === common[k]) {
|
|
120
|
+
result.push({ type: "same", text: after[j] });
|
|
121
|
+
i++;
|
|
122
|
+
j++;
|
|
123
|
+
k++;
|
|
124
|
+
} else if (i < before.length && (k >= common.length || before[i] !== common[k])) {
|
|
125
|
+
result.push({ type: "remove", text: before[i] });
|
|
126
|
+
i++;
|
|
127
|
+
} else if (j < after.length && (k >= common.length || after[j] !== common[k])) {
|
|
128
|
+
result.push({ type: "add", text: after[j] });
|
|
129
|
+
j++;
|
|
130
|
+
} else {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function indicatorChar(type: DiffLine["type"], style: DiffIndicator): string {
|
|
139
|
+
if (style === "none") return " ";
|
|
140
|
+
if (style === "nerd") {
|
|
141
|
+
if (type === "add") return `\x1b[32m${NERD_INDICATORS.add}\x1b[0m`;
|
|
142
|
+
if (type === "remove") return `\x1b[31m${NERD_INDICATORS.remove}\x1b[0m`;
|
|
143
|
+
return NERD_INDICATORS.same;
|
|
144
|
+
}
|
|
145
|
+
if (style === "bars") {
|
|
146
|
+
if (type === "add") return `\x1b[32m│ \x1b[0m`;
|
|
147
|
+
if (type === "remove") return `\x1b[31m│ \x1b[0m`;
|
|
148
|
+
return " ";
|
|
149
|
+
}
|
|
150
|
+
return type === "add" ? "+ " : type === "remove" ? "- " : " ";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderUnified(diff: DiffLine[], indicator: DiffIndicator): string {
|
|
154
|
+
return diff.map((line) => {
|
|
155
|
+
const prefix = indicator === "bars"
|
|
156
|
+
? (line.type === "add" ? "│ " : line.type === "remove" ? "│ " : " ")
|
|
157
|
+
: indicatorChar(line.type, indicator);
|
|
158
|
+
return prefix + line.text;
|
|
159
|
+
}).join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderSplit(diff: DiffLine[], indicator: DiffIndicator): string {
|
|
163
|
+
const left: string[] = [];
|
|
164
|
+
const right: string[] = [];
|
|
165
|
+
|
|
166
|
+
for (const line of diff) {
|
|
167
|
+
if (line.type === "same") {
|
|
168
|
+
left.push(" " + line.text);
|
|
169
|
+
right.push(" " + line.text);
|
|
170
|
+
} else if (line.type === "remove") {
|
|
171
|
+
left.push(indicatorChar("remove", indicator) + line.text);
|
|
172
|
+
right.push("");
|
|
173
|
+
} else if (line.type === "add") {
|
|
174
|
+
left.push("");
|
|
175
|
+
right.push(indicatorChar("add", indicator) + line.text);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const maxWidth = Math.max(...left.map((l) => l.length), 40);
|
|
180
|
+
const result: string[] = [];
|
|
181
|
+
for (let i = 0; i < left.length; i++) {
|
|
182
|
+
const l = left[i].padEnd(maxWidth);
|
|
183
|
+
const sep = left[i] && right[i] ? " │ " : " ";
|
|
184
|
+
result.push(l + sep + right[i]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function renderDiff(
|
|
191
|
+
before: string,
|
|
192
|
+
after: string,
|
|
193
|
+
opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number; filePath?: string; highlight?: boolean },
|
|
194
|
+
): string {
|
|
195
|
+
const layout = opts?.layout ?? "auto";
|
|
196
|
+
let indicator = opts?.indicator ?? "bars";
|
|
197
|
+
const maxWidth = opts?.maxWidth ?? 120;
|
|
198
|
+
const doHighlight = opts?.highlight ?? true;
|
|
199
|
+
|
|
200
|
+
// Auto-detect Nerd Font for indicator selection
|
|
201
|
+
if (indicator === "bars" && detectNerdFont()) {
|
|
202
|
+
indicator = "nerd";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const beforeLines = before.split("\n");
|
|
206
|
+
const afterLines = after.split("\n");
|
|
207
|
+
const diff = computeDiff(beforeLines, afterLines);
|
|
208
|
+
|
|
209
|
+
const effectiveLayout = layout === "auto"
|
|
210
|
+
? (maxWidth >= 100 ? "split" : "unified")
|
|
211
|
+
: layout;
|
|
212
|
+
|
|
213
|
+
let effectiveIndicator = indicator;
|
|
214
|
+
// Auto mode: use classic indicator for unified to keep output clean
|
|
215
|
+
if (layout === "auto" && effectiveLayout === "unified" && indicator !== "nerd") {
|
|
216
|
+
effectiveIndicator = "classic";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Apply syntax highlighting if enabled
|
|
220
|
+
let highlightedDiff = diff;
|
|
221
|
+
if (doHighlight) {
|
|
222
|
+
const lang = detectLanguage(opts?.filePath, after);
|
|
223
|
+
if (lang) {
|
|
224
|
+
highlightedDiff = diff.map((line) => ({
|
|
225
|
+
...line,
|
|
226
|
+
text: highlightLine(line.text, lang),
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (effectiveLayout === "split") {
|
|
232
|
+
return renderSplit(highlightedDiff, effectiveIndicator);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return renderUnified(highlightedDiff, effectiveIndicator);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function renderEditDiffResult(
|
|
239
|
+
previousContent: string,
|
|
240
|
+
newContent: string,
|
|
241
|
+
opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number },
|
|
242
|
+
): string {
|
|
243
|
+
return renderDiff(previousContent, newContent, opts);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function renderWriteDiffResult(
|
|
247
|
+
previousContent: string | undefined,
|
|
248
|
+
newContent: string,
|
|
249
|
+
opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number },
|
|
250
|
+
): string {
|
|
251
|
+
if (!previousContent) {
|
|
252
|
+
return "[New file created]\n" + newContent.split("\n").map((l) => "+ " + l).join("\n");
|
|
253
|
+
}
|
|
254
|
+
return renderDiff(previousContent, newContent, opts);
|
|
255
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line width safety — width clamping with collapsed hints
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function clampLineWidth(lines: string[], maxWidth: number): string[] {
|
|
6
|
+
return lines.map((line) => {
|
|
7
|
+
if (line.length <= maxWidth) return line;
|
|
8
|
+
return line.slice(0, maxWidth - 3) + "...";
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function collapseHint(originalCount: number, shownCount: number): string {
|
|
13
|
+
const omitted = originalCount - shownCount;
|
|
14
|
+
if (omitted <= 0) return "";
|
|
15
|
+
return `...(${omitted} more lines)...`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending diff previews during streaming
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PendingDiffPreviewData {
|
|
6
|
+
type: "edit" | "write" | "create";
|
|
7
|
+
filePath: string;
|
|
8
|
+
previousContent?: string;
|
|
9
|
+
newContent: string;
|
|
10
|
+
confidence: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildPendingEditPreviewData(
|
|
14
|
+
filePath: string,
|
|
15
|
+
oldText: string,
|
|
16
|
+
newText: string,
|
|
17
|
+
): PendingDiffPreviewData {
|
|
18
|
+
return {
|
|
19
|
+
type: "edit",
|
|
20
|
+
filePath,
|
|
21
|
+
previousContent: oldText,
|
|
22
|
+
newContent: newText,
|
|
23
|
+
confidence: 1.0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildPendingWritePreviewData(
|
|
28
|
+
filePath: string,
|
|
29
|
+
content: string,
|
|
30
|
+
fileExisted: boolean,
|
|
31
|
+
): PendingDiffPreviewData {
|
|
32
|
+
return {
|
|
33
|
+
type: fileExisted ? "write" : "create",
|
|
34
|
+
filePath,
|
|
35
|
+
newContent: content,
|
|
36
|
+
confidence: 1.0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderPendingDiffPreview(data: PendingDiffPreviewData, opts?: { maxLines?: number }): string {
|
|
41
|
+
const maxLines = opts?.maxLines ?? 20;
|
|
42
|
+
const lines = data.newContent.split("\n").slice(0, maxLines);
|
|
43
|
+
const header = data.type === "edit" ? `✏️ Edit: ${data.filePath}`
|
|
44
|
+
: data.type === "write" ? `📝 Write: ${data.filePath}`
|
|
45
|
+
: `📄 Create: ${data.filePath}`;
|
|
46
|
+
|
|
47
|
+
const omitted = data.newContent.split("\n").length - lines.length;
|
|
48
|
+
const hint = omitted > 0 ? `\n...(${omitted} more lines)...` : "";
|
|
49
|
+
|
|
50
|
+
return `${header}\n${"─".repeat(header.length)}\n${lines.join("\n")}${hint}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared display rendering utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function splitLines(text: string): string[] {
|
|
6
|
+
return text.split(/\r?\n/);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function countNonEmptyLines(text: string): number {
|
|
10
|
+
return splitLines(text).filter((l) => l.trim()).length;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function compactOutputLines(text: string, maxLines: number): string {
|
|
14
|
+
const lines = splitLines(text);
|
|
15
|
+
if (lines.length <= maxLines) return text;
|
|
16
|
+
const omitted = lines.length - maxLines;
|
|
17
|
+
return `...(${omitted} lines omitted)...\n${lines.slice(-maxLines).join("\n")}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function previewLines(text: string, lines: number): string {
|
|
21
|
+
const all = splitLines(text);
|
|
22
|
+
if (all.length <= lines) return text;
|
|
23
|
+
return all.slice(0, lines).join("\n") + `\n...(${all.length - lines} more lines)`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function pluralize(count: number, singular: string, plural?: string): string {
|
|
27
|
+
return count === 1 ? `${count} ${singular}` : `${count} ${plural ?? singular + "s"}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shortenPath(path: string, maxLen: number = 60): string {
|
|
31
|
+
if (path.length <= maxLen) return path;
|
|
32
|
+
const parts = path.split("/");
|
|
33
|
+
if (parts.length <= 2) return "..." + path.slice(-(maxLen - 3));
|
|
34
|
+
return parts[0] + "/.../" + parts.slice(-2).join("/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractTextOutput(result: any): string {
|
|
38
|
+
if (typeof result === "string") return result;
|
|
39
|
+
if (result?.output) return String(result.output);
|
|
40
|
+
if (result?.stdout) return String(result.stdout);
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isLikelyQuietCommand(command: string): boolean {
|
|
45
|
+
const quietPatterns = [/^\s*cd\s/, /^\s*mkdir\s+-p/, /^\s*touch\s/, /^\s*rm\s+/];
|
|
46
|
+
return quietPatterns.some((re) => re.test(command));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function sanitizeAnsiForThemedOutput(text: string): string {
|
|
50
|
+
// Strip ANSI escape sequences for clean themed rendering
|
|
51
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking labels during streaming
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function formatThinkingLabel(text: string, opts?: { prefix?: string }): string {
|
|
6
|
+
const prefix = opts?.prefix ?? "🤔";
|
|
7
|
+
const lines = text.split("\n");
|
|
8
|
+
if (lines.length === 1) return `${prefix} ${lines[0]}`;
|
|
9
|
+
return `${prefix} Thinking...\n${text}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function sanitizeThinkingArtifacts(text: string): string {
|
|
13
|
+
// Remove thinking blocks from context before LLM turn
|
|
14
|
+
return text
|
|
15
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/g, "")
|
|
16
|
+
.replace(/\[thinking\][\s\S]*?\[\/thinking\]/g, "")
|
|
17
|
+
.trim();
|
|
18
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool override renderers — mode-aware output for built-in tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { OutputMode } from "../types.js";
|
|
6
|
+
import { previewLines, countNonEmptyLines, shortenPath } from "./render-utils.js";
|
|
7
|
+
|
|
8
|
+
export interface ToolOverrideConfig {
|
|
9
|
+
readOutputMode?: OutputMode;
|
|
10
|
+
searchOutputMode?: OutputMode;
|
|
11
|
+
bashOutputMode?: OutputMode;
|
|
12
|
+
previewLines?: number;
|
|
13
|
+
bashCollapsedLines?: number;
|
|
14
|
+
showTruncationHints?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderReadResult(
|
|
18
|
+
content: string,
|
|
19
|
+
filePath: string,
|
|
20
|
+
config: ToolOverrideConfig,
|
|
21
|
+
): string {
|
|
22
|
+
switch (config.readOutputMode) {
|
|
23
|
+
case "hidden":
|
|
24
|
+
return `[Read: ${shortenPath(filePath)}]`;
|
|
25
|
+
case "summary":
|
|
26
|
+
return `[Read: ${shortenPath(filePath)} — ${countNonEmptyLines(content)} lines]`;
|
|
27
|
+
case "preview":
|
|
28
|
+
return previewLines(content, config.previewLines ?? 20);
|
|
29
|
+
default:
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderSearchResult(
|
|
35
|
+
results: string,
|
|
36
|
+
config: ToolOverrideConfig,
|
|
37
|
+
): string {
|
|
38
|
+
switch (config.searchOutputMode) {
|
|
39
|
+
case "hidden":
|
|
40
|
+
return `[Search results hidden]`;
|
|
41
|
+
case "count":
|
|
42
|
+
return `[Search: ${countNonEmptyLines(results)} matches]`;
|
|
43
|
+
case "preview":
|
|
44
|
+
return previewLines(results, config.previewLines ?? 20);
|
|
45
|
+
default:
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function renderBashResult(
|
|
51
|
+
output: string,
|
|
52
|
+
command: string,
|
|
53
|
+
config: ToolOverrideConfig,
|
|
54
|
+
): string {
|
|
55
|
+
switch (config.bashOutputMode) {
|
|
56
|
+
case "hidden":
|
|
57
|
+
return `[Bash: ${command.slice(0, 60)}]`;
|
|
58
|
+
case "summary":
|
|
59
|
+
return `[Bash: ${command.slice(0, 60)} — ${countNonEmptyLines(output)} lines]`;
|
|
60
|
+
case "preview":
|
|
61
|
+
return previewLines(output, config.bashCollapsedLines ?? 5);
|
|
62
|
+
default:
|
|
63
|
+
return output;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply display overrides for built-in tool results.
|
|
69
|
+
* Returns modified event result if override applies, undefined otherwise.
|
|
70
|
+
*
|
|
71
|
+
* Call this from the `tool_result` event handler to intercept and transform
|
|
72
|
+
* tool output before it enters the LLM context.
|
|
73
|
+
*/
|
|
74
|
+
export function applyToolDisplayOverride(
|
|
75
|
+
toolName: string,
|
|
76
|
+
event: Record<string, unknown>,
|
|
77
|
+
config: ToolOverrideConfig,
|
|
78
|
+
): { content?: Array<{ type: string; text: string }> } | undefined {
|
|
79
|
+
const content = (event as any).content as Array<{ type: string; text: string }> | undefined;
|
|
80
|
+
if (!content || !Array.isArray(content) || content.length === 0) return undefined;
|
|
81
|
+
|
|
82
|
+
const textContent = content[0]?.text ?? "";
|
|
83
|
+
const previewLinesCount = config.previewLines ?? 20;
|
|
84
|
+
const collapsedLines = config.bashCollapsedLines ?? 5;
|
|
85
|
+
|
|
86
|
+
let overridden: string | undefined;
|
|
87
|
+
|
|
88
|
+
switch (toolName) {
|
|
89
|
+
case "read": {
|
|
90
|
+
const filePath = String((event as any).args?.path ?? "");
|
|
91
|
+
const mode = config.readOutputMode ?? "full";
|
|
92
|
+
if (mode === "hidden") {
|
|
93
|
+
overridden = `[Read: ${shortenPath(filePath)}]`;
|
|
94
|
+
} else if (mode === "summary") {
|
|
95
|
+
overridden = `[Read: ${shortenPath(filePath)} — ${countNonEmptyLines(textContent)} lines]`;
|
|
96
|
+
} else if (mode === "preview") {
|
|
97
|
+
overridden = previewLines(textContent, previewLinesCount);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "grep":
|
|
102
|
+
case "find":
|
|
103
|
+
case "ls": {
|
|
104
|
+
const mode = config.searchOutputMode ?? "full";
|
|
105
|
+
if (mode === "hidden") {
|
|
106
|
+
overridden = `[${toolName} results hidden]`;
|
|
107
|
+
} else if (mode === "count") {
|
|
108
|
+
overridden = `[${toolName}: ${countNonEmptyLines(textContent)} matches]`;
|
|
109
|
+
} else if (mode === "preview") {
|
|
110
|
+
overridden = previewLines(textContent, previewLinesCount);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "bash": {
|
|
115
|
+
const command = String((event as any).args?.command ?? "");
|
|
116
|
+
const mode = config.bashOutputMode ?? "full";
|
|
117
|
+
if (mode === "hidden") {
|
|
118
|
+
overridden = `[Bash: ${command.slice(0, 60)}]`;
|
|
119
|
+
} else if (mode === "summary") {
|
|
120
|
+
overridden = `[Bash: ${command.slice(0, 60)} — ${countNonEmptyLines(textContent)} lines]`;
|
|
121
|
+
} else if (mode === "preview") {
|
|
122
|
+
overridden = previewLines(textContent, collapsedLines);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (overridden !== undefined) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: overridden }],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User message box — bordered display for user messages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function renderUserMessageBox(text: string, opts?: { maxWidth?: number }): string {
|
|
6
|
+
const maxWidth = opts?.maxWidth ?? 80;
|
|
7
|
+
const lines = text.split("\n");
|
|
8
|
+
const clamped = lines.map((l) => (l.length > maxWidth - 4 ? l.slice(0, maxWidth - 7) + "..." : l));
|
|
9
|
+
const width = Math.min(maxWidth, Math.max(...clamped.map((l) => l.length), 10) + 4);
|
|
10
|
+
|
|
11
|
+
const top = `╭${"─".repeat(width - 2)}╮`;
|
|
12
|
+
const bottom = `╰${"─".repeat(width - 2)}╯`;
|
|
13
|
+
const middle = clamped.map((l) => `│ ${l.padEnd(width - 4)} │`);
|
|
14
|
+
|
|
15
|
+
return [top, ...middle, bottom].join("\n");
|
|
16
|
+
}
|