@pi-unipi/compactor 2.6.1 → 2.6.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.
Files changed (44) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/skills/compactor/SKILL.md +1 -1
  4. package/skills/compactor-detail/SKILL.md +3 -5
  5. package/skills/compactor-doctor/SKILL.md +1 -1
  6. package/skills/compactor-stats/SKILL.md +1 -1
  7. package/src/commands/index.ts +47 -78
  8. package/src/compaction/brief.ts +161 -90
  9. package/src/compaction/build-sections.ts +3 -4
  10. package/src/compaction/compact-args.ts +86 -0
  11. package/src/compaction/cut.ts +270 -28
  12. package/src/compaction/drill-down.ts +261 -0
  13. package/src/compaction/format-recall.ts +96 -0
  14. package/src/compaction/format.ts +8 -3
  15. package/src/compaction/hooks.ts +248 -72
  16. package/src/compaction/merge.ts +34 -4
  17. package/src/compaction/rank.ts +270 -0
  18. package/src/compaction/recall-scope.ts +28 -0
  19. package/src/compaction/search-entries.ts +333 -96
  20. package/src/compaction/skill-collapse.ts +35 -0
  21. package/src/compaction/summarize.ts +37 -6
  22. package/src/compaction/token-estimate.ts +104 -0
  23. package/src/compaction/touched-files.ts +35 -0
  24. package/src/config/manager.ts +2 -27
  25. package/src/config/presets.ts +0 -2
  26. package/src/config/schema.ts +2 -16
  27. package/src/executor/executor.ts +6 -15
  28. package/src/executor/runtime.ts +2 -12
  29. package/src/index.ts +12 -122
  30. package/src/info-screen.ts +3 -10
  31. package/src/security/evaluator.ts +0 -53
  32. package/src/security/policy.ts +7 -8
  33. package/src/session/db.ts +0 -6
  34. package/src/tools/ctx-execute-file.ts +0 -5
  35. package/src/tools/register.ts +27 -50
  36. package/src/tools/vcc-recall.ts +86 -48
  37. package/src/tui/settings-overlay.ts +20 -40
  38. package/src/types.ts +43 -100
  39. package/src/display/diff-renderer.ts +0 -281
  40. package/src/display/line-width-safety.ts +0 -28
  41. package/src/display/render-utils.ts +0 -52
  42. package/src/display/thinking-label.ts +0 -18
  43. package/src/display/tool-overrides.ts +0 -136
  44. package/src/tools/compact.ts +0 -20
package/src/types.ts CHANGED
@@ -8,12 +8,6 @@ import type { Message } from "@earendil-works/pi-ai";
8
8
  // Normalized blocks (from pi-vcc)
9
9
  // ─────────────────────────────────────────────────────────
10
10
 
11
- export interface FileOps {
12
- readFiles?: string[];
13
- modifiedFiles?: string[];
14
- createdFiles?: string[];
15
- }
16
-
17
11
  export type NormalizedBlock =
18
12
  | { kind: "user"; text: string; sourceIndex?: number }
19
13
  | { kind: "assistant"; text: string; sourceIndex?: number }
@@ -21,6 +15,13 @@ export type NormalizedBlock =
21
15
  | { kind: "tool_result"; name: string; text: string; isError: boolean; sourceIndex?: number }
22
16
  | { kind: "thinking"; text: string; redacted: boolean; sourceIndex?: number };
23
17
 
18
+ /** Hook-provided file activity (pi-vcc parity) — structural signal for ranking */
19
+ export interface FileOps {
20
+ readFiles?: string[];
21
+ modifiedFiles?: string[];
22
+ createdFiles?: string[];
23
+ }
24
+
24
25
  // ─────────────────────────────────────────────────────────
25
26
  // Section data (from pi-vcc)
26
27
  // ─────────────────────────────────────────────────────────
@@ -32,16 +33,6 @@ export interface SectionData {
32
33
  outstandingContext: string[];
33
34
  userPreferences: string[];
34
35
  briefTranscript: string;
35
- transcriptEntries: TranscriptEntry[];
36
- }
37
-
38
- export interface TranscriptEntry {
39
- role: "user" | "assistant" | "tool_error";
40
- text?: string;
41
- tool?: string;
42
- cmd?: string;
43
- ref?: string;
44
- count?: number;
45
36
  }
46
37
 
47
38
  export interface BriefLine {
@@ -49,16 +40,6 @@ export interface BriefLine {
49
40
  lines: string[];
50
41
  }
51
42
 
52
- /** Runtime stats tracked during a live session. */
53
- export interface RuntimeStats {
54
- bytesReturned: Record<string, number>;
55
- bytesSandboxed: number;
56
- calls: Record<string, number>;
57
- sessionStart: number;
58
- cacheHits: number;
59
- cacheBytesSaved: number;
60
- }
61
-
62
43
  // ─────────────────────────────────────────────────────────
63
44
  // Compaction input / output
64
45
  // ─────────────────────────────────────────────────────────
@@ -66,26 +47,48 @@ export interface RuntimeStats {
66
47
  export interface CompileInput {
67
48
  messages: Message[];
68
49
  previousSummary?: string;
69
- fileOps?: FileOps;
70
50
  }
71
51
 
72
52
  export interface CompactionStats {
73
53
  summarized: number;
74
54
  kept: number;
75
- totalMessages: number;
55
+ totalMessages?: number;
76
56
  /** Actual token count from Pi's preparation */
77
- tokensBefore: number;
57
+ tokensBefore?: number;
78
58
  /** Estimated tokens after compaction (proportional from kept/total chars) */
79
- tokensAfterEst: number;
59
+ tokensAfterEst?: number;
60
+ keptUserTurns: number;
61
+ totalUserTurns: number;
62
+ requestedKeepUserTurns: number;
63
+ keepUserTurnsExplicit: boolean;
64
+ keepFallbackToCompactAll: boolean;
65
+ /** Set when the tail came from a token-budget cut instead of a user-turn cut. */
66
+ budgetCut?: BudgetCutKind;
67
+ keptTokensEst: number;
68
+ /** True when smart-keep boosted the default keep beyond 1. */
69
+ smartKeepAdjusted?: boolean;
70
+ /** Base keep before smart adjustment (for toast like "1→3"). */
71
+ smartFromKeep?: number;
80
72
  }
81
73
 
74
+ export type BudgetCutKind = "no_anchor" | "oversized_tail";
75
+
82
76
  export type OwnCutCancelReason =
83
77
  | "no_live_messages"
84
- | "too_few_live_messages"
85
- | "no_user_message";
78
+ | "too_few_live_messages";
86
79
 
87
80
  export type OwnCutResult =
88
- | { ok: true; messages: Message[]; firstKeptEntryId: string; compactAll: boolean }
81
+ | {
82
+ ok: true;
83
+ messages: Message[];
84
+ firstKeptEntryId: string;
85
+ compactAll: boolean;
86
+ keptUserTurns: number;
87
+ totalUserTurns: number;
88
+ requestedKeepUserTurns: number;
89
+ keepFallbackToCompactAll: boolean;
90
+ budgetCut?: BudgetCutKind;
91
+ }
89
92
  | { ok: false; reason: OwnCutCancelReason };
90
93
 
91
94
  // ─────────────────────────────────────────────────────────
@@ -125,25 +128,13 @@ export interface CompactorConfig {
125
128
  /** @deprecated Category filtering was never implemented; ignored by runtime. */
126
129
  eventCategories: string[];
127
130
  };
128
- /** @deprecated Project indexing moved to @pi-unipi/cocoindex; retained for config compatibility. */
131
+ /** @deprecated Retained for config compatibility. */
129
132
  fts5Index: CompactorStrategyConfig & { mode: "auto" | "manual" | "off"; chunkSize: number; cacheTtlHours: number };
130
133
  sandboxExecution: CompactorStrategyConfig & { mode: "all" | "safe-only" | "off"; allowedLanguages: Language[]; outputLimit: number };
131
- /** @deprecated Display profiles were never connected to runtime; retained for config compatibility. */
132
- toolDisplay: CompactorStrategyConfig & { mode: "opencode" | "balanced" | "verbose" | "custom"; diffLayout: "auto" | "split" | "unified"; diffIndicator: "bars" | "classic" | "none"; showThinkingLabels: boolean; showUserMessageBox: boolean; showBashSpinner: boolean; showPendingPreviews: boolean };
133
134
 
134
135
  // Pipeline features
135
136
  pipeline: {
136
- /** @deprecated Reserved compatibility field; ignored by runtime. */
137
- ttlCache: boolean;
138
137
  autoInjection: boolean;
139
- /** @deprecated Reserved compatibility field; ignored by runtime. */
140
- proximityReranking: boolean;
141
- /** @deprecated Reserved compatibility field; ignored by runtime. */
142
- timelineSort: boolean;
143
- /** @deprecated Reserved compatibility field; ignored by runtime. */
144
- progressiveThrottling: boolean;
145
- /** @deprecated Reserved compatibility field; ignored by runtime. */
146
- mmapPragma: boolean;
147
138
  customNoisePatterns: string[];
148
139
  };
149
140
 
@@ -152,9 +143,12 @@ export interface CompactorConfig {
152
143
 
153
144
  // Global settings
154
145
  overrideDefaultCompaction: boolean;
146
+ /** Boost default keep:1 to a larger tail when it is small (≤5k tok, capped 25k). Explicit keep:N always respected. */
147
+ smartKeepTail: boolean;
148
+ /** Ask the agent to continue after automatic (threshold/overflow) compaction. */
149
+ continueAfterThresholdCompact: boolean;
150
+ /** Write detailed compaction diagnostics to /tmp/compactor-debug.json. */
155
151
  debug: boolean;
156
- /** @deprecated Truncation hints were never connected to runtime; retained for config compatibility. */
157
- showTruncationHints: boolean;
158
152
  }
159
153
 
160
154
  export type CompactorPreset = "precise" | "balanced" | "thorough" | "lean" | "opencode" | "verbose" | "minimal" | "custom";
@@ -236,61 +230,10 @@ export type Language =
236
230
  | "elixir";
237
231
 
238
232
  // ─────────────────────────────────────────────────────────
239
- // Content store — REMOVED (moved to @pi-unipi/cocoindex)
233
+ // Content store — REMOVED
240
234
  // SearchResult, IndexResult, StoreStats types no longer needed.
241
235
  // ─────────────────────────────────────────────────────────
242
236
 
243
- // ─────────────────────────────────────────────────────────
244
- // Security (from context-mode)
245
- // ─────────────────────────────────────────────────────────
246
-
247
- export type PermissionDecision = "allow" | "deny" | "ask";
248
-
249
- export interface SecurityPolicy {
250
- allow: string[];
251
- deny: string[];
252
- ask: string[];
253
- }
254
-
255
- // ─────────────────────────────────────────────────────────
256
- // Display (from pi-tool-display)
257
- // ─────────────────────────────────────────────────────────
258
-
259
- export type DiffLayout = "auto" | "split" | "unified";
260
- export type DiffIndicator = "bars" | "classic" | "none";
261
- export type OutputMode = "hidden" | "summary" | "preview" | "count";
262
-
263
- export interface ToolDisplayConfig {
264
- registerToolOverrides: {
265
- read: boolean;
266
- grep: boolean;
267
- find: boolean;
268
- ls: boolean;
269
- bash: boolean;
270
- edit: boolean;
271
- write: boolean;
272
- };
273
- enableNativeUserMessageBox: boolean;
274
- readOutputMode: OutputMode;
275
- searchOutputMode: OutputMode;
276
- mcpOutputMode: OutputMode;
277
- previewLines: number;
278
- expandedPreviewMaxLines: number;
279
- bashOutputMode: OutputMode;
280
- bashCollapsedLines: number;
281
- diffViewMode: DiffLayout;
282
- diffIndicatorMode: DiffIndicator;
283
- diffSplitMinWidth: number;
284
- diffCollapsedLines: number;
285
- diffWordWrap: boolean;
286
- showTruncationHints: boolean;
287
- showRtkCompactionHints: boolean;
288
- }
289
-
290
- // ─────────────────────────────────────────────────────────
291
- // Runtime counters (live session stats)
292
- // ─────────────────────────────────────────────────────────
293
-
294
237
  export interface RuntimeCounters {
295
238
  sandboxRuns: number;
296
239
  searchQueries: number;
@@ -1,281 +0,0 @@
1
- /**
2
- * Diff rendering engine — LCS-based diff with 3 layouts, 3 indicators,
3
- * syntax highlighting, and Nerd Font detection
4
- */
5
-
6
- import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
7
-
8
- export type DiffLayout = "auto" | "split" | "unified";
9
- export type DiffIndicator = "bars" | "classic" | "nerd" | "none";
10
-
11
- // --- Nerd Font Detection ---
12
-
13
- let nerdFontDetected: boolean | null = null;
14
-
15
- /** Detect if terminal supports Nerd Font icons */
16
- export function detectNerdFont(): boolean {
17
- if (nerdFontDetected !== null) return nerdFontDetected;
18
- // Check common Nerd Font env vars or terminal emulators
19
- const term = process.env.TERM_PROGRAM ?? "";
20
- const termFont = process.env.TERM_FONT ?? "";
21
- nerdFontDetected =
22
- termFont.toLowerCase().includes("nerd") ||
23
- process.env.NERD_FONT === "1" ||
24
- term === "WezTerm" ||
25
- term === "iTerm.app" ||
26
- (process.env.TERMINAL_EMULATOR ?? "").includes("JetBrains") ||
27
- false;
28
- return nerdFontDetected;
29
- }
30
-
31
- /** Nerd Font indicator chars */
32
- const NERD_INDICATORS = {
33
- add: "\uf055 ", // nf-fa-plus_circle
34
- remove: "\uf056 ", // nf-fa-minus_circle
35
- same: " ",
36
- } as const;
37
-
38
- // --- Syntax Highlighting ---
39
-
40
- const KEYWORDS: Record<string, Set<string>> = {
41
- 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"]),
42
- 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"]),
43
- 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"]),
44
- 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"]),
45
- };
46
-
47
- /** Apply basic syntax highlighting to a line */
48
- function highlightLine(line: string, lang?: string): string {
49
- if (!lang) return line;
50
- const keywords = KEYWORDS[lang] ?? KEYWORDS.js;
51
- // Highlight keywords (simple word boundary match)
52
- return line.replace(/\b([a-zA-Z_]\w*)\b/g, (match) => {
53
- if (keywords.has(match)) return `\x1b[34m${match}\x1b[0m`; // blue
54
- return match;
55
- });
56
- }
57
-
58
- /** Detect language from file extension or content */
59
- function detectLanguage(filePath?: string, content?: string): string | undefined {
60
- if (filePath) {
61
- const ext = filePath.split(".").pop()?.toLowerCase();
62
- const extMap: Record<string, string> = {
63
- js: "js", mjs: "js", cjs: "js", jsx: "js",
64
- ts: "ts", mts: "ts", cts: "ts", tsx: "ts",
65
- py: "py", pyw: "py",
66
- sh: "sh", bash: "sh", zsh: "sh",
67
- };
68
- if (ext && extMap[ext]) return extMap[ext];
69
- }
70
- if (content) {
71
- if (content.includes("def ") || content.includes("import ")) return "py";
72
- if (content.includes("function ") || content.includes("const ")) return "js";
73
- if (content.includes("#!/bin/")) return "sh";
74
- }
75
- return undefined;
76
- }
77
-
78
- interface DiffLine {
79
- type: "same" | "add" | "remove";
80
- text: string;
81
- }
82
-
83
- function lcs<T>(a: T[], b: T[]): T[] {
84
- const m = a.length;
85
- const n = b.length;
86
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
87
-
88
- for (let i = 1; i <= m; i++) {
89
- for (let j = 1; j <= n; j++) {
90
- if (a[i - 1] === b[j - 1]) {
91
- dp[i][j] = dp[i - 1][j - 1] + 1;
92
- } else {
93
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
94
- }
95
- }
96
- }
97
-
98
- const result: T[] = [];
99
- let i = m, j = n;
100
- while (i > 0 && j > 0) {
101
- if (a[i - 1] === b[j - 1]) {
102
- result.unshift(a[i - 1]);
103
- i--;
104
- j--;
105
- } else if (dp[i - 1][j] > dp[i][j - 1]) {
106
- i--;
107
- } else {
108
- j--;
109
- }
110
- }
111
-
112
- return result;
113
- }
114
-
115
- function computeDiff(before: string[], after: string[]): DiffLine[] {
116
- const common = lcs(before, after);
117
- const result: DiffLine[] = [];
118
- let i = 0, j = 0, k = 0;
119
-
120
- while (i < before.length || j < after.length) {
121
- if (k < common.length && before[i] === common[k] && after[j] === common[k]) {
122
- result.push({ type: "same", text: after[j] });
123
- i++;
124
- j++;
125
- k++;
126
- } else if (i < before.length && (k >= common.length || before[i] !== common[k])) {
127
- result.push({ type: "remove", text: before[i] });
128
- i++;
129
- } else if (j < after.length && (k >= common.length || after[j] !== common[k])) {
130
- result.push({ type: "add", text: after[j] });
131
- j++;
132
- } else {
133
- break;
134
- }
135
- }
136
-
137
- return result;
138
- }
139
-
140
- function indicatorChar(type: DiffLine["type"], style: DiffIndicator): string {
141
- if (style === "none") return " ";
142
- if (style === "nerd") {
143
- if (type === "add") return `\x1b[32m${NERD_INDICATORS.add}\x1b[0m`;
144
- if (type === "remove") return `\x1b[31m${NERD_INDICATORS.remove}\x1b[0m`;
145
- return NERD_INDICATORS.same;
146
- }
147
- if (style === "bars") {
148
- if (type === "add") return `\x1b[32m│ \x1b[0m`;
149
- if (type === "remove") return `\x1b[31m│ \x1b[0m`;
150
- return " ";
151
- }
152
- return type === "add" ? "+ " : type === "remove" ? "- " : " ";
153
- }
154
-
155
- function renderUnified(
156
- diff: DiffLine[],
157
- indicator: DiffIndicator,
158
- maxWidth?: number,
159
- ): string {
160
- return diff.map((line) => {
161
- const prefix = indicator === "bars"
162
- ? (line.type === "add" ? "│ " : line.type === "remove" ? "│ " : " ")
163
- : indicatorChar(line.type, indicator);
164
- const rendered = prefix + line.text;
165
- if (maxWidth && visibleWidth(rendered) > maxWidth) {
166
- return truncateToWidth(rendered, maxWidth, "…");
167
- }
168
- return rendered;
169
- }).join("\n");
170
- }
171
-
172
- function renderSplit(
173
- diff: DiffLine[],
174
- indicator: DiffIndicator,
175
- maxW?: number,
176
- ): string {
177
- const left: string[] = [];
178
- const right: string[] = [];
179
-
180
- for (const line of diff) {
181
- if (line.type === "same") {
182
- left.push(" " + line.text);
183
- right.push(" " + line.text);
184
- } else if (line.type === "remove") {
185
- left.push(indicatorChar("remove", indicator) + line.text);
186
- right.push("");
187
- } else if (line.type === "add") {
188
- left.push("");
189
- right.push(indicatorChar("add", indicator) + line.text);
190
- }
191
- }
192
-
193
- const halfW = maxW ? Math.floor(maxW / 2) - 2 : 40;
194
- const colW = Math.max(
195
- ...left.map((l) => visibleWidth(l)),
196
- ...right.map((l) => visibleWidth(l)),
197
- Math.min(halfW, 40),
198
- );
199
- const result: string[] = [];
200
- for (let i = 0; i < left.length; i++) {
201
- const lTrunc = visibleWidth(left[i]) > colW
202
- ? truncateToWidth(left[i], colW, "…")
203
- : left[i].padEnd(colW);
204
- const sep = left[i] && right[i] ? " │ " : " ";
205
- let rLine = right[i];
206
- if (maxW && visibleWidth(lTrunc + sep + rLine) > maxW) {
207
- const rBudget = maxW - visibleWidth(lTrunc + sep);
208
- rLine = truncateToWidth(rLine, Math.max(1, rBudget), "…");
209
- }
210
- result.push(lTrunc + sep + rLine);
211
- }
212
-
213
- return result.join("\n");
214
- }
215
-
216
- export function renderDiff(
217
- before: string,
218
- after: string,
219
- opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number; filePath?: string; highlight?: boolean },
220
- ): string {
221
- const layout = opts?.layout ?? "auto";
222
- let indicator = opts?.indicator ?? "bars";
223
- const maxWidth = opts?.maxWidth ?? 120;
224
- const doHighlight = opts?.highlight ?? true;
225
-
226
- // Auto-detect Nerd Font for indicator selection
227
- if (indicator === "bars" && detectNerdFont()) {
228
- indicator = "nerd";
229
- }
230
-
231
- const beforeLines = before.split("\n");
232
- const afterLines = after.split("\n");
233
- const diff = computeDiff(beforeLines, afterLines);
234
-
235
- const effectiveLayout = layout === "auto"
236
- ? (maxWidth >= 100 ? "split" : "unified")
237
- : layout;
238
-
239
- let effectiveIndicator = indicator;
240
- // Auto mode: use classic indicator for unified to keep output clean
241
- if (layout === "auto" && effectiveLayout === "unified" && indicator !== "nerd") {
242
- effectiveIndicator = "classic";
243
- }
244
-
245
- // Apply syntax highlighting if enabled
246
- let highlightedDiff = diff;
247
- if (doHighlight) {
248
- const lang = detectLanguage(opts?.filePath, after);
249
- if (lang) {
250
- highlightedDiff = diff.map((line) => ({
251
- ...line,
252
- text: highlightLine(line.text, lang),
253
- }));
254
- }
255
- }
256
-
257
- if (effectiveLayout === "split") {
258
- return renderSplit(highlightedDiff, effectiveIndicator, maxWidth);
259
- }
260
-
261
- return renderUnified(highlightedDiff, effectiveIndicator, maxWidth);
262
- }
263
-
264
- export function renderEditDiffResult(
265
- previousContent: string,
266
- newContent: string,
267
- opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number },
268
- ): string {
269
- return renderDiff(previousContent, newContent, opts);
270
- }
271
-
272
- export function renderWriteDiffResult(
273
- previousContent: string | undefined,
274
- newContent: string,
275
- opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number },
276
- ): string {
277
- if (!previousContent) {
278
- return "[New file created]\n" + newContent.split("\n").map((l) => "+ " + l).join("\n");
279
- }
280
- return renderDiff(previousContent, newContent, opts);
281
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Line width safety — width clamping with collapsed hints
3
- *
4
- * Uses ANSI-aware visibleWidth measurement from pi-tui to properly
5
- * handle lines containing escape codes. Falls back to raw-length
6
- * measurement when pi-tui is unavailable.
7
- */
8
-
9
- import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
10
-
11
- /**
12
- * Clamp each line to maxWidth visible columns.
13
- * Uses pi-tui's visibleWidth() for ANSI-aware measurement and
14
- * truncateToWidth() for ANSI-safe truncation.
15
- */
16
- export function clampLineWidth(lines: string[], maxWidth: number): string[] {
17
- return lines.map((line) => {
18
- const vw = visibleWidth(line);
19
- if (vw <= maxWidth) return line;
20
- return truncateToWidth(line, maxWidth, "…");
21
- });
22
- }
23
-
24
- export function collapseHint(originalCount: number, shownCount: number): string {
25
- const omitted = originalCount - shownCount;
26
- if (omitted <= 0) return "";
27
- return `...(${omitted} more lines)...`;
28
- }
@@ -1,52 +0,0 @@
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
- }
@@ -1,18 +0,0 @@
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
- }