@pi-unipi/compactor 0.1.7 → 0.2.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.
- package/README.md +50 -24
- package/index.ts +7 -0
- package/package.json +4 -2
- package/skills/compactor/SKILL.md +21 -65
- package/skills/compactor-detail/SKILL.md +133 -0
- package/src/commands/index.ts +186 -109
- package/src/compaction/filter-noise.ts +4 -3
- package/src/compaction/hooks.ts +25 -6
- package/src/compaction/search-entries.ts +51 -4
- package/src/config/manager.ts +55 -6
- package/src/config/presets.ts +69 -5
- package/src/config/schema.ts +10 -1
- package/src/display/diff-presentation.ts +6 -1
- package/src/display/diff-renderer.ts +34 -8
- package/src/display/diff-width-safety.ts +83 -0
- package/src/display/line-width-safety.ts +14 -2
- package/src/index.ts +297 -16
- package/src/info-screen.ts +137 -46
- package/src/security/policy.ts +23 -0
- package/src/session/analytics.ts +198 -0
- package/src/session/auto-inject.ts +60 -0
- package/src/session/db.ts +68 -8
- package/src/session/resume-inject.ts +13 -1
- package/src/store/db-base.ts +11 -0
- package/src/store/index.ts +150 -4
- package/src/store/unified.ts +109 -0
- package/src/tools/context-budget.ts +50 -0
- package/src/tools/ctx-batch-execute.ts +2 -5
- package/src/tools/ctx-fetch-and-index.ts +3 -8
- package/src/tools/ctx-index.ts +3 -9
- package/src/tools/ctx-search.ts +3 -7
- package/src/tools/ctx-stats.ts +6 -4
- package/src/tools/register.ts +251 -216
- package/src/tui/settings-overlay.ts +359 -149
- package/src/types.ts +30 -7
- package/skills/compactor-ops/SKILL.md +0 -65
- package/skills/compactor-tools/SKILL.md +0 -120
package/src/config/presets.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Preset definitions + detection for compactor config
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import type { CompactorConfig, CompactorPreset } from "../types.js";
|
|
6
7
|
import { DEFAULT_COMPACTOR_CONFIG } from "./schema.js";
|
|
7
8
|
|
|
@@ -22,15 +23,43 @@ const preset = (
|
|
|
22
23
|
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, ...(overrides.toolDisplay as any) },
|
|
23
24
|
});
|
|
24
25
|
|
|
26
|
+
// Pipeline feature defaults per preset:
|
|
27
|
+
// precise: ttlCache+mmap on, rest off
|
|
28
|
+
// balanced: all on
|
|
29
|
+
// thorough: all on
|
|
30
|
+
// lean: all off
|
|
31
|
+
|
|
25
32
|
export const PRESET_CONFIGS: Record<CompactorPreset, CompactorConfig> = {
|
|
26
|
-
|
|
33
|
+
// New preset names
|
|
34
|
+
precise: preset({
|
|
27
35
|
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "opencode" },
|
|
28
36
|
}),
|
|
37
|
+
thorough: preset({
|
|
38
|
+
briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "full" },
|
|
39
|
+
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "verbose" },
|
|
40
|
+
}),
|
|
41
|
+
lean: preset({
|
|
42
|
+
sessionGoals: { ...DEFAULT_COMPACTOR_CONFIG.sessionGoals, enabled: true, mode: "brief" },
|
|
43
|
+
filesAndChanges: { ...DEFAULT_COMPACTOR_CONFIG.filesAndChanges, enabled: true, mode: "modified-only" },
|
|
44
|
+
commits: { ...DEFAULT_COMPACTOR_CONFIG.commits, enabled: false, mode: "off" },
|
|
45
|
+
outstandingContext: { ...DEFAULT_COMPACTOR_CONFIG.outstandingContext, enabled: true, mode: "critical-only" },
|
|
46
|
+
userPreferences: { ...DEFAULT_COMPACTOR_CONFIG.userPreferences, enabled: false, mode: "off" },
|
|
47
|
+
briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, enabled: true, mode: "minimal" },
|
|
48
|
+
sessionContinuity: { ...DEFAULT_COMPACTOR_CONFIG.sessionContinuity, enabled: false, mode: "off" },
|
|
49
|
+
fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, enabled: false, mode: "off" },
|
|
50
|
+
sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, enabled: false, mode: "off" },
|
|
51
|
+
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, enabled: true, mode: "opencode" },
|
|
52
|
+
}),
|
|
29
53
|
balanced: preset({
|
|
30
54
|
briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "compact" },
|
|
31
55
|
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "balanced" },
|
|
32
56
|
fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, mode: "auto" },
|
|
33
57
|
}),
|
|
58
|
+
|
|
59
|
+
// Backward-compat aliases — map old names to new
|
|
60
|
+
opencode: preset({
|
|
61
|
+
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "opencode" },
|
|
62
|
+
}),
|
|
34
63
|
verbose: preset({
|
|
35
64
|
briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "full" },
|
|
36
65
|
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "verbose" },
|
|
@@ -50,7 +79,24 @@ export const PRESET_CONFIGS: Record<CompactorPreset, CompactorConfig> = {
|
|
|
50
79
|
custom: structuredClone(DEFAULT_COMPACTOR_CONFIG),
|
|
51
80
|
};
|
|
52
81
|
|
|
82
|
+
// Pre-computed identity hashes for fast preset detection
|
|
83
|
+
const presetHashes = new Map<string, string>();
|
|
84
|
+
|
|
85
|
+
function presetHash(config: CompactorConfig): string {
|
|
86
|
+
return createHash("sha256").update(JSON.stringify(config)).digest("hex");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Compute hashes once at module load
|
|
90
|
+
for (const name of ["precise", "balanced", "thorough", "lean"] as const) {
|
|
91
|
+
presetHashes.set(name, presetHash(PRESET_CONFIGS[name]));
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
function configsEqual(a: CompactorConfig, b: CompactorConfig): boolean {
|
|
95
|
+
// Fast path: hash comparison
|
|
96
|
+
const aHash = presetHash(a);
|
|
97
|
+
const bHash = presetHash(b);
|
|
98
|
+
if (aHash !== bHash) return false;
|
|
99
|
+
// Defensive: confirm with full comparison
|
|
54
100
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
55
101
|
}
|
|
56
102
|
|
|
@@ -58,8 +104,11 @@ function configsEqual(a: CompactorConfig, b: CompactorConfig): boolean {
|
|
|
58
104
|
* Detect which preset a config matches, or "custom".
|
|
59
105
|
*/
|
|
60
106
|
export function detectPreset(config: CompactorConfig): CompactorPreset {
|
|
61
|
-
|
|
62
|
-
|
|
107
|
+
const configHash = presetHash(config);
|
|
108
|
+
for (const name of ["precise", "balanced", "thorough", "lean"] as const) {
|
|
109
|
+
if (presetHashes.get(name) === configHash && configsEqual(config, PRESET_CONFIGS[name])) {
|
|
110
|
+
return name;
|
|
111
|
+
}
|
|
63
112
|
}
|
|
64
113
|
return "custom";
|
|
65
114
|
}
|
|
@@ -71,13 +120,28 @@ export function applyPreset(name: CompactorPreset): CompactorConfig {
|
|
|
71
120
|
return structuredClone(PRESET_CONFIGS[name]);
|
|
72
121
|
}
|
|
73
122
|
|
|
123
|
+
// Old → new preset name mapping for backward compatibility
|
|
124
|
+
const OLD_TO_NEW: Record<string, CompactorPreset> = {
|
|
125
|
+
opencode: "precise",
|
|
126
|
+
verbose: "thorough",
|
|
127
|
+
minimal: "lean",
|
|
128
|
+
};
|
|
129
|
+
|
|
74
130
|
/**
|
|
75
|
-
* Parse a preset name (case-insensitive).
|
|
131
|
+
* Parse a preset name (case-insensitive). Old names are mapped to new with deprecation.
|
|
76
132
|
*/
|
|
77
133
|
export function parsePreset(raw: string): CompactorPreset | undefined {
|
|
78
134
|
const normalized = raw.trim().toLowerCase();
|
|
79
|
-
|
|
135
|
+
|
|
136
|
+
// Check new names first
|
|
137
|
+
if (normalized === "precise" || normalized === "balanced" || normalized === "thorough" || normalized === "lean" || normalized === "custom") {
|
|
80
138
|
return normalized;
|
|
81
139
|
}
|
|
140
|
+
|
|
141
|
+
// Map old names to new (backward compat)
|
|
142
|
+
if (OLD_TO_NEW[normalized]) {
|
|
143
|
+
return OLD_TO_NEW[normalized];
|
|
144
|
+
}
|
|
145
|
+
|
|
82
146
|
return undefined;
|
|
83
147
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -49,7 +49,16 @@ export const DEFAULT_COMPACTOR_CONFIG: CompactorConfig = {
|
|
|
49
49
|
showBashSpinner: true,
|
|
50
50
|
showPendingPreviews: true,
|
|
51
51
|
},
|
|
52
|
-
|
|
52
|
+
pipeline: {
|
|
53
|
+
ttlCache: false,
|
|
54
|
+
autoInjection: false,
|
|
55
|
+
proximityReranking: false,
|
|
56
|
+
timelineSort: false,
|
|
57
|
+
progressiveThrottling: false,
|
|
58
|
+
mmapPragma: false,
|
|
59
|
+
customNoisePatterns: [],
|
|
60
|
+
},
|
|
61
|
+
overrideDefaultCompaction: true,
|
|
53
62
|
debug: false,
|
|
54
63
|
showTruncationHints: true,
|
|
55
64
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { DiffLayout } from "../types.js";
|
|
6
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
6
7
|
|
|
7
8
|
export function selectDiffLayout(
|
|
8
9
|
terminalWidth: number,
|
|
@@ -12,9 +13,13 @@ export function selectDiffLayout(
|
|
|
12
13
|
return terminalWidth >= 100 ? "split" : "unified";
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** Clamp text to maxWidth visible columns, ANSI-aware */
|
|
15
17
|
export function clampWidth(text: string, maxWidth: number): string {
|
|
16
18
|
return text
|
|
17
19
|
.split("\n")
|
|
18
|
-
.map((line) =>
|
|
20
|
+
.map((line) => {
|
|
21
|
+
if (visibleWidth(line) <= maxWidth) return line;
|
|
22
|
+
return truncateToWidth(line, maxWidth, "…");
|
|
23
|
+
})
|
|
19
24
|
.join("\n");
|
|
20
25
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* syntax highlighting, and Nerd Font detection
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
7
|
+
|
|
6
8
|
export type DiffLayout = "auto" | "split" | "unified";
|
|
7
9
|
export type DiffIndicator = "bars" | "classic" | "nerd" | "none";
|
|
8
10
|
|
|
@@ -150,16 +152,28 @@ function indicatorChar(type: DiffLine["type"], style: DiffIndicator): string {
|
|
|
150
152
|
return type === "add" ? "+ " : type === "remove" ? "- " : " ";
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
function renderUnified(
|
|
155
|
+
function renderUnified(
|
|
156
|
+
diff: DiffLine[],
|
|
157
|
+
indicator: DiffIndicator,
|
|
158
|
+
maxWidth?: number,
|
|
159
|
+
): string {
|
|
154
160
|
return diff.map((line) => {
|
|
155
161
|
const prefix = indicator === "bars"
|
|
156
162
|
? (line.type === "add" ? "│ " : line.type === "remove" ? "│ " : " ")
|
|
157
163
|
: indicatorChar(line.type, indicator);
|
|
158
|
-
|
|
164
|
+
const rendered = prefix + line.text;
|
|
165
|
+
if (maxWidth && visibleWidth(rendered) > maxWidth) {
|
|
166
|
+
return truncateToWidth(rendered, maxWidth, "…");
|
|
167
|
+
}
|
|
168
|
+
return rendered;
|
|
159
169
|
}).join("\n");
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
function renderSplit(
|
|
172
|
+
function renderSplit(
|
|
173
|
+
diff: DiffLine[],
|
|
174
|
+
indicator: DiffIndicator,
|
|
175
|
+
maxW?: number,
|
|
176
|
+
): string {
|
|
163
177
|
const left: string[] = [];
|
|
164
178
|
const right: string[] = [];
|
|
165
179
|
|
|
@@ -176,12 +190,24 @@ function renderSplit(diff: DiffLine[], indicator: DiffIndicator): string {
|
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
192
|
|
|
179
|
-
const
|
|
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
|
+
);
|
|
180
199
|
const result: string[] = [];
|
|
181
200
|
for (let i = 0; i < left.length; i++) {
|
|
182
|
-
const
|
|
201
|
+
const lTrunc = visibleWidth(left[i]) > colW
|
|
202
|
+
? truncateToWidth(left[i], colW, "…")
|
|
203
|
+
: left[i].padEnd(colW);
|
|
183
204
|
const sep = left[i] && right[i] ? " │ " : " ";
|
|
184
|
-
|
|
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);
|
|
185
211
|
}
|
|
186
212
|
|
|
187
213
|
return result.join("\n");
|
|
@@ -229,10 +255,10 @@ export function renderDiff(
|
|
|
229
255
|
}
|
|
230
256
|
|
|
231
257
|
if (effectiveLayout === "split") {
|
|
232
|
-
return renderSplit(highlightedDiff, effectiveIndicator);
|
|
258
|
+
return renderSplit(highlightedDiff, effectiveIndicator, maxWidth);
|
|
233
259
|
}
|
|
234
260
|
|
|
235
|
-
return renderUnified(highlightedDiff, effectiveIndicator);
|
|
261
|
+
return renderUnified(highlightedDiff, effectiveIndicator, maxWidth);
|
|
236
262
|
}
|
|
237
263
|
|
|
238
264
|
export function renderEditDiffResult(
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff width safety — truncate diff lines to terminal width
|
|
3
|
+
*
|
|
4
|
+
* Pi's renderDiff() in diff.js produces lines without width
|
|
5
|
+
* truncation. When a diff line's visible content exceeds the
|
|
6
|
+
* terminal width, the TUI crashes with:
|
|
7
|
+
* "Rendered line N exceeds terminal width (X > Y)"
|
|
8
|
+
*
|
|
9
|
+
* This module provides clampDiffToWidth() which truncates
|
|
10
|
+
* each diff line to a safe width, preventing TUI crashes.
|
|
11
|
+
*
|
|
12
|
+
* The diff format from pi's generateDiffString() is:
|
|
13
|
+
* [+/-/ ]LINE_NUM CONTENT
|
|
14
|
+
* e.g.: "+ 38 │ The root cause is a **compound failure**..."
|
|
15
|
+
*
|
|
16
|
+
* We detect the terminal width from process.stdout and clamp
|
|
17
|
+
* each line, accounting for the rendering overhead of the
|
|
18
|
+
* edit tool's Box nesting (approx 4-6 chars of padding).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
22
|
+
|
|
23
|
+
/** Rendering overhead from Box nesting in edit tool components */
|
|
24
|
+
const RENDER_OVERHEAD = 6;
|
|
25
|
+
|
|
26
|
+
/** Minimum useful line width (don't clamp below this) */
|
|
27
|
+
const MIN_LINE_WIDTH = 20;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the terminal width with fallback.
|
|
31
|
+
* Uses process.stdout.columns (updated on resize).
|
|
32
|
+
*/
|
|
33
|
+
function getTerminalWidth(): number {
|
|
34
|
+
return process.stdout?.columns ?? 80;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clamp a diff string so every line fits within terminal width.
|
|
39
|
+
* Preserves the diff prefix (+/-/ ) and line number while
|
|
40
|
+
* truncating the content portion.
|
|
41
|
+
*
|
|
42
|
+
* @param diff - The diff string from generateDiffString()
|
|
43
|
+
* @param maxWidth - Override for terminal width (for testing)
|
|
44
|
+
* @returns The clamped diff string (may be same reference if no clamping needed)
|
|
45
|
+
*/
|
|
46
|
+
export function clampDiffToWidth(
|
|
47
|
+
diff: string,
|
|
48
|
+
maxWidth?: number,
|
|
49
|
+
): string {
|
|
50
|
+
const termW = maxWidth ?? getTerminalWidth();
|
|
51
|
+
const safeW = Math.max(MIN_LINE_WIDTH, termW - RENDER_OVERHEAD);
|
|
52
|
+
|
|
53
|
+
const lines = diff.split("\n");
|
|
54
|
+
let changed = false;
|
|
55
|
+
|
|
56
|
+
const result = lines.map((line) => {
|
|
57
|
+
const vw = visibleWidth(line);
|
|
58
|
+
if (vw <= safeW) return line;
|
|
59
|
+
|
|
60
|
+
changed = true;
|
|
61
|
+
|
|
62
|
+
// Try to preserve the diff prefix (+/-/ ) and line number
|
|
63
|
+
// Format: [+/-/ ]LINE_NUM CONTENT
|
|
64
|
+
// The prefix and line number are critical for readability
|
|
65
|
+
const prefixMatch = line.match(/^([+\- ])\s*(\d*)\s/);
|
|
66
|
+
if (prefixMatch) {
|
|
67
|
+
const prefixLen = prefixMatch[0].length;
|
|
68
|
+
// Calculate how much content we can keep
|
|
69
|
+
const contentBudget = safeW - prefixLen;
|
|
70
|
+
if (contentBudget >= MIN_LINE_WIDTH) {
|
|
71
|
+
const prefix = line.slice(0, prefixLen);
|
|
72
|
+
const content = line.slice(prefixLen);
|
|
73
|
+
const truncated = truncateToWidth(content, contentBudget, "…");
|
|
74
|
+
return prefix + truncated;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fallback: truncate the entire line
|
|
79
|
+
return truncateToWidth(line, safeW, "…");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return changed ? result.join("\n") : diff;
|
|
83
|
+
}
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
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.
|
|
3
7
|
*/
|
|
4
8
|
|
|
9
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/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
|
+
*/
|
|
5
16
|
export function clampLineWidth(lines: string[], maxWidth: number): string[] {
|
|
6
17
|
return lines.map((line) => {
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
const vw = visibleWidth(line);
|
|
19
|
+
if (vw <= maxWidth) return line;
|
|
20
|
+
return truncateToWidth(line, maxWidth, "…");
|
|
9
21
|
});
|
|
10
22
|
}
|
|
11
23
|
|