@pi-unipi/compactor 0.1.7 → 0.2.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 +50 -24
- package/index.ts +7 -0
- package/package.json +2 -1
- 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 +22 -1
- 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 +9 -0
- package/src/index.ts +183 -10
- package/src/info-screen.ts +10 -4
- package/src/security/policy.ts +23 -0
- package/src/session/auto-inject.ts +60 -0
- package/src/session/db.ts +65 -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 +25 -1
- package/skills/compactor-ops/SKILL.md +0 -65
- package/skills/compactor-tools/SKILL.md +0 -120
|
@@ -2,34 +2,27 @@
|
|
|
2
2
|
* @pi-unipi/compactor — TUI Settings Overlay
|
|
3
3
|
*
|
|
4
4
|
* Interactive settings editor for compactor configuration.
|
|
5
|
-
*
|
|
5
|
+
* Uses pi-tui SettingsList for proper keybinding support.
|
|
6
|
+
* Tabbed sections (Presets / Strategies / Pipeline), search,
|
|
7
|
+
* preset preview, per-project override.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { Component } from "@mariozechner/pi-tui";
|
|
9
|
-
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
10
|
-
import { loadConfig, saveConfig } from "../config/manager.js";
|
|
11
|
+
import { truncateToWidth, visibleWidth, SettingsList, type SettingItem, type SettingsListTheme } from "@mariozechner/pi-tui";
|
|
12
|
+
import { loadConfig, saveConfig, projectConfigPath } from "../config/manager.js";
|
|
11
13
|
import { applyPreset, detectPreset } from "../config/presets.js";
|
|
12
14
|
import type { CompactorPreset } from "../types.js";
|
|
13
15
|
import type { CompactorConfig } from "../types.js";
|
|
16
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
dim: "\x1b[2m",
|
|
20
|
-
cyan: "\x1b[36m",
|
|
21
|
-
green: "\x1b[32m",
|
|
22
|
-
yellow: "\x1b[33m",
|
|
23
|
-
red: "\x1b[31m",
|
|
24
|
-
gray: "\x1b[90m",
|
|
25
|
-
magenta: "\x1b[35m",
|
|
26
|
-
};
|
|
18
|
+
// ─── Section types ─────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
type Section = "presets" | "strategies" | "pipeline";
|
|
21
|
+
const SECTIONS: Section[] = ["presets", "strategies", "pipeline"];
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
const TOGGLE_OFF = `${ansi.dim}○${ansi.reset}`;
|
|
23
|
+
// ─── Strategy item definition ──────────────────────────────────────────
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
interface StrategyItem {
|
|
25
|
+
interface StrategyDef {
|
|
33
26
|
key: string;
|
|
34
27
|
label: string;
|
|
35
28
|
description: string;
|
|
@@ -40,25 +33,34 @@ interface StrategyItem {
|
|
|
40
33
|
setMode: (c: CompactorConfig, v: string) => void;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
key:
|
|
46
|
-
label:
|
|
47
|
-
description:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
36
|
+
/** Pipeline feature item */
|
|
37
|
+
interface PipelineDef {
|
|
38
|
+
key: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description: string;
|
|
41
|
+
group: string;
|
|
42
|
+
getValue: (c: CompactorConfig) => boolean;
|
|
43
|
+
setValue: (c: CompactorConfig, v: boolean) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Static definitions ────────────────────────────────────────────────
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
const STRATEGIES: StrategyDef[] = [
|
|
49
|
+
{
|
|
50
|
+
key: "debug",
|
|
51
|
+
label: "Verbose Debug",
|
|
52
|
+
description: "Log ALL compaction events to console",
|
|
53
|
+
modes: ["on", "off"],
|
|
54
|
+
getEnabled: (c) => c.debug,
|
|
55
|
+
setEnabled: (c, v) => (c.debug = v),
|
|
56
|
+
getMode: (c) => (c.debug ? "on" : "off"),
|
|
57
|
+
setMode: (c, v) => (c.debug = v === "on"),
|
|
58
|
+
},
|
|
57
59
|
{
|
|
58
60
|
key: "sessionGoals",
|
|
59
61
|
label: "Session Goals",
|
|
60
62
|
description: "Extract goals from conversation",
|
|
61
|
-
modes: ["full", "
|
|
63
|
+
modes: ["full", "brief", "off"],
|
|
62
64
|
getEnabled: (c) => c.sessionGoals.enabled,
|
|
63
65
|
setEnabled: (c, v) => (c.sessionGoals.enabled = v),
|
|
64
66
|
getMode: (c) => c.sessionGoals.mode,
|
|
@@ -68,7 +70,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
68
70
|
key: "filesAndChanges",
|
|
69
71
|
label: "Files & Changes",
|
|
70
72
|
description: "Track file activity",
|
|
71
|
-
modes: ["all", "modified", "
|
|
73
|
+
modes: ["all", "modified-only", "off"],
|
|
72
74
|
getEnabled: (c) => c.filesAndChanges.enabled,
|
|
73
75
|
setEnabled: (c, v) => (c.filesAndChanges.enabled = v),
|
|
74
76
|
getMode: (c) => c.filesAndChanges.mode,
|
|
@@ -78,7 +80,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
78
80
|
key: "commits",
|
|
79
81
|
label: "Commits",
|
|
80
82
|
description: "Extract git commits",
|
|
81
|
-
modes: ["full", "
|
|
83
|
+
modes: ["full", "brief", "off"],
|
|
82
84
|
getEnabled: (c) => c.commits.enabled,
|
|
83
85
|
setEnabled: (c, v) => (c.commits.enabled = v),
|
|
84
86
|
getMode: (c) => c.commits.mode,
|
|
@@ -88,7 +90,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
88
90
|
key: "outstandingContext",
|
|
89
91
|
label: "Outstanding Context",
|
|
90
92
|
description: "Track blockers and pending items",
|
|
91
|
-
modes: ["full", "
|
|
93
|
+
modes: ["full", "critical-only", "off"],
|
|
92
94
|
getEnabled: (c) => c.outstandingContext.enabled,
|
|
93
95
|
setEnabled: (c, v) => (c.outstandingContext.enabled = v),
|
|
94
96
|
getMode: (c) => c.outstandingContext.mode,
|
|
@@ -98,7 +100,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
98
100
|
key: "userPreferences",
|
|
99
101
|
label: "User Preferences",
|
|
100
102
|
description: "Track learned preferences",
|
|
101
|
-
modes: ["all", "
|
|
103
|
+
modes: ["all", "recent-only", "off"],
|
|
102
104
|
getEnabled: (c) => c.userPreferences.enabled,
|
|
103
105
|
setEnabled: (c, v) => (c.userPreferences.enabled = v),
|
|
104
106
|
getMode: (c) => c.userPreferences.mode,
|
|
@@ -108,7 +110,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
108
110
|
key: "briefTranscript",
|
|
109
111
|
label: "Brief Transcript",
|
|
110
112
|
description: "Rolling window of recent messages",
|
|
111
|
-
modes: ["full", "compact", "minimal"],
|
|
113
|
+
modes: ["full", "compact", "minimal", "off"],
|
|
112
114
|
getEnabled: (c) => c.briefTranscript.enabled,
|
|
113
115
|
setEnabled: (c, v) => (c.briefTranscript.enabled = v),
|
|
114
116
|
getMode: (c) => c.briefTranscript.mode,
|
|
@@ -118,7 +120,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
118
120
|
key: "sessionContinuity",
|
|
119
121
|
label: "Session Continuity",
|
|
120
122
|
description: "XML resume snapshot for compaction survival",
|
|
121
|
-
modes: ["full", "
|
|
123
|
+
modes: ["full", "essential-only", "off"],
|
|
122
124
|
getEnabled: (c) => c.sessionContinuity.enabled,
|
|
123
125
|
setEnabled: (c, v) => (c.sessionContinuity.enabled = v),
|
|
124
126
|
getMode: (c) => c.sessionContinuity.mode,
|
|
@@ -128,7 +130,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
128
130
|
key: "fts5Index",
|
|
129
131
|
label: "FTS5 Index",
|
|
130
132
|
description: "Full-text search index",
|
|
131
|
-
modes: ["auto", "manual", "
|
|
133
|
+
modes: ["auto", "manual", "off"],
|
|
132
134
|
getEnabled: (c) => c.fts5Index.enabled,
|
|
133
135
|
setEnabled: (c, v) => (c.fts5Index.enabled = v),
|
|
134
136
|
getMode: (c) => c.fts5Index.mode,
|
|
@@ -138,7 +140,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
138
140
|
key: "sandboxExecution",
|
|
139
141
|
label: "Sandbox Execution",
|
|
140
142
|
description: "Polyglot code execution",
|
|
141
|
-
modes: ["all", "safe", "
|
|
143
|
+
modes: ["all", "safe-only", "off"],
|
|
142
144
|
getEnabled: (c) => c.sandboxExecution.enabled,
|
|
143
145
|
setEnabled: (c, v) => (c.sandboxExecution.enabled = v),
|
|
144
146
|
getMode: (c) => c.sandboxExecution.mode,
|
|
@@ -148,7 +150,7 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
148
150
|
key: "toolDisplay",
|
|
149
151
|
label: "Tool Display",
|
|
150
152
|
description: "Override tool output rendering",
|
|
151
|
-
modes: ["opencode", "
|
|
153
|
+
modes: ["opencode", "balanced", "verbose", "custom"],
|
|
152
154
|
getEnabled: (c) => c.toolDisplay.enabled,
|
|
153
155
|
setEnabled: (c, v) => (c.toolDisplay.enabled = v),
|
|
154
156
|
getMode: (c) => c.toolDisplay.mode,
|
|
@@ -156,149 +158,357 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
156
158
|
},
|
|
157
159
|
];
|
|
158
160
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
const PIPELINE_ITEMS: PipelineDef[] = [
|
|
162
|
+
{ key: "ttlCache", label: "TTL Cache", description: "Cache with time-based expiry", group: "On Compaction", getValue: (c) => c.pipeline.ttlCache, setValue: (c, v) => (c.pipeline.ttlCache = v) },
|
|
163
|
+
{ key: "autoInjection", label: "Auto Injection", description: "Inject behavioral state after compaction", group: "On Compaction", getValue: (c) => c.pipeline.autoInjection, setValue: (c, v) => (c.pipeline.autoInjection = v) },
|
|
164
|
+
{ key: "mmapPragma", label: "MMap Pragma", description: "Use mmap for SQLite I/O", group: "On Compaction", getValue: (c) => c.pipeline.mmapPragma, setValue: (c, v) => (c.pipeline.mmapPragma = v) },
|
|
165
|
+
{ key: "proximityReranking", label: "Proximity Reranking", description: "Rerank search results by proximity", group: "On Search", getValue: (c) => c.pipeline.proximityReranking, setValue: (c, v) => (c.pipeline.proximityReranking = v) },
|
|
166
|
+
{ key: "timelineSort", label: "Timeline Sort", description: "Sort session events chronologically", group: "On Search", getValue: (c) => c.pipeline.timelineSort, setValue: (c, v) => (c.pipeline.timelineSort = v) },
|
|
167
|
+
{ key: "progressiveThrottling", label: "Progressive Throttling", description: "Slow down indexing for large projects", group: "On Index", getValue: (c) => c.pipeline.progressiveThrottling, setValue: (c, v) => (c.pipeline.progressiveThrottling = v) },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const PRESETS: CompactorPreset[] = ["precise", "balanced", "thorough", "lean"];
|
|
171
|
+
|
|
172
|
+
const PRESET_DESCRIPTIONS: Record<string, { summary: string; detail: string }> = {
|
|
173
|
+
precise: {
|
|
174
|
+
summary: "Code-heavy, minimal waste — compaction: full, FTS5: manual, pipeline: 2/6 on",
|
|
175
|
+
detail: "Max token savings. Compaction: full. Display: minimal.\nFTS5: manual. Sandbox: safe-only. Pipeline: ttlCache+mmap on.",
|
|
176
|
+
},
|
|
177
|
+
balanced: {
|
|
178
|
+
summary: "Daily use (default) — all strategies moderate, pipeline: all on",
|
|
179
|
+
detail: "Moderate all strategies. Display: balanced.\nFTS5: auto. Sandbox: all. Pipeline: all 6 on.",
|
|
180
|
+
},
|
|
181
|
+
thorough: {
|
|
182
|
+
summary: "Debug/audit — everything on, full transcript, pipeline: all on",
|
|
183
|
+
detail: "Everything enabled. Display: verbose.\nFTS5: auto. Sandbox: all. Pipeline: all 6 on.",
|
|
184
|
+
},
|
|
185
|
+
lean: {
|
|
186
|
+
summary: "Quick fixes, short sessions — compaction only, pipeline: all off",
|
|
187
|
+
detail: "Compaction only. Display: opencode.\nFTS5: off. Sandbox: off. Pipeline: all 6 off.",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ─── Theme for SettingsList ────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
const THEME: SettingsListTheme = {
|
|
194
|
+
label: (text, selected) => selected ? `\x1b[1m${text}\x1b[0m` : `\x1b[2m${text}\x1b[0m`,
|
|
195
|
+
value: (text, selected) => selected ? `\x1b[35m${text}\x1b[0m` : `\x1b[35m${text}\x1b[0m`,
|
|
196
|
+
description: (text) => `\x1b[90m${text}\x1b[0m`,
|
|
197
|
+
cursor: `\x1b[36m▸\x1b[0m`,
|
|
198
|
+
hint: (text) => `\x1b[2m${text}\x1b[0m`,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ─── Helper: frame a line inside box drawing ───────────────────────────
|
|
202
|
+
|
|
203
|
+
function frameLine(content: string, innerWidth: number): string {
|
|
204
|
+
const truncated = truncateToWidth(content, innerWidth, "");
|
|
205
|
+
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
206
|
+
return `\x1b[90m│\x1b[0m${truncated}${" ".repeat(padding)}\x1b[90m│\x1b[0m`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ruleLine(innerWidth: number): string {
|
|
210
|
+
return `\x1b[90m├${"─".repeat(innerWidth)}┤\x1b[0m`;
|
|
211
|
+
}
|
|
161
212
|
|
|
162
|
-
|
|
213
|
+
function borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
214
|
+
const left = edge === "top" ? "┌" : "└";
|
|
215
|
+
const right = edge === "top" ? "┐" : "┘";
|
|
216
|
+
return `\x1b[90m${left}${"─".repeat(innerWidth)}${right}\x1b[0m`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Main component ────────────────────────────────────────────────────
|
|
163
220
|
|
|
164
221
|
/**
|
|
165
222
|
* Settings overlay component for compactor configuration.
|
|
223
|
+
* Uses SettingsList from pi-tui for proper vim/arrow keybinding support.
|
|
166
224
|
*/
|
|
167
225
|
export class CompactorSettingsOverlay implements Component {
|
|
168
226
|
private config: CompactorConfig;
|
|
169
|
-
private
|
|
170
|
-
private
|
|
171
|
-
private
|
|
227
|
+
private section: Section = "presets";
|
|
228
|
+
private perProjectOverride = false;
|
|
229
|
+
private projectDir: string;
|
|
230
|
+
private saved = false;
|
|
172
231
|
onClose?: () => void;
|
|
173
232
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
233
|
+
// Per-section SettingsList instances
|
|
234
|
+
private presetList!: SettingsList;
|
|
235
|
+
private strategyList!: SettingsList;
|
|
236
|
+
private pipelineList!: SettingsList;
|
|
237
|
+
|
|
238
|
+
constructor(opts?: { cwd?: string }) {
|
|
239
|
+
this.projectDir = opts?.cwd ?? process.cwd();
|
|
240
|
+
this.config = loadConfig(this.projectDir);
|
|
241
|
+
|
|
242
|
+
// Detect per-project override
|
|
243
|
+
const projPath = projectConfigPath(this.projectDir);
|
|
244
|
+
this.perProjectOverride = existsSync(projPath);
|
|
245
|
+
|
|
246
|
+
this.buildLists();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
invalidate(): void {
|
|
250
|
+
this.currentList?.invalidate();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Build SettingsList instances ──────────────────────────────────
|
|
254
|
+
|
|
255
|
+
private buildLists(): void {
|
|
256
|
+
// ── Presets list ──────────────────────────────────────────────────
|
|
257
|
+
const presetItems: SettingItem[] = PRESETS.map((name) => {
|
|
258
|
+
const desc = PRESET_DESCRIPTIONS[name]!;
|
|
259
|
+
return {
|
|
260
|
+
id: `preset:${name}`,
|
|
261
|
+
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
262
|
+
description: desc.summary,
|
|
263
|
+
currentValue: detectPreset(this.config) === name ? "✓ active" : "",
|
|
264
|
+
values: ["apply"],
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
// Add per-project override as a setting item
|
|
268
|
+
presetItems.push({
|
|
269
|
+
id: "projectOverride",
|
|
270
|
+
label: "Project Override",
|
|
271
|
+
description: "Override global config for this project only",
|
|
272
|
+
currentValue: this.perProjectOverride ? "enabled" : "disabled",
|
|
273
|
+
values: ["enabled", "disabled"],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
this.presetList = new SettingsList(
|
|
277
|
+
presetItems,
|
|
278
|
+
8,
|
|
279
|
+
THEME,
|
|
280
|
+
(id, newValue) => this.onPresetChange(id, newValue),
|
|
281
|
+
() => this.onCancel(),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// ── Strategies list ───────────────────────────────────────────────
|
|
285
|
+
const strategyItems: SettingItem[] = STRATEGIES.map((s) => ({
|
|
286
|
+
id: `strategy:${s.key}`,
|
|
287
|
+
label: s.label,
|
|
288
|
+
description: s.description,
|
|
289
|
+
currentValue: this.formatStrategyValue(s),
|
|
290
|
+
values: s.modes,
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
this.strategyList = new SettingsList(
|
|
294
|
+
strategyItems,
|
|
295
|
+
12,
|
|
296
|
+
THEME,
|
|
297
|
+
(id, newValue) => this.onStrategyChange(id, newValue),
|
|
298
|
+
() => this.onCancel(),
|
|
299
|
+
{ enableSearch: true },
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// ── Pipeline list ─────────────────────────────────────────────────
|
|
303
|
+
const pipelineItems: SettingItem[] = PIPELINE_ITEMS.map((p) => ({
|
|
304
|
+
id: `pipeline:${p.key}`,
|
|
305
|
+
label: `${p.group}: ${p.label}`,
|
|
306
|
+
description: p.description,
|
|
307
|
+
currentValue: p.getValue(this.config) ? "on" : "off",
|
|
308
|
+
values: ["on", "off"],
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
this.pipelineList = new SettingsList(
|
|
312
|
+
pipelineItems,
|
|
313
|
+
8,
|
|
314
|
+
THEME,
|
|
315
|
+
(id, newValue) => this.onPipelineChange(id, newValue),
|
|
316
|
+
() => this.onCancel(),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── Current section's list ────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
private get currentList(): SettingsList {
|
|
323
|
+
if (this.section === "strategies") return this.strategyList;
|
|
324
|
+
if (this.section === "pipeline") return this.pipelineList;
|
|
325
|
+
return this.presetList;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── Change handlers ───────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
private onPresetChange(id: string, _newValue: string): void {
|
|
331
|
+
if (id === "projectOverride") {
|
|
332
|
+
this.perProjectOverride = _newValue === "enabled";
|
|
333
|
+
if (!this.perProjectOverride) {
|
|
334
|
+
const projPath = projectConfigPath(this.projectDir);
|
|
335
|
+
try { unlinkSync(projPath); } catch { /* ignore */ }
|
|
336
|
+
}
|
|
337
|
+
this.presetList.updateValue("projectOverride", this.perProjectOverride ? "enabled" : "disabled");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Apply the preset
|
|
341
|
+
const presetName = id.replace("preset:", "") as CompactorPreset;
|
|
342
|
+
if (PRESETS.includes(presetName)) {
|
|
343
|
+
this.config = applyPreset(presetName);
|
|
344
|
+
// Update all strategy/pipeline items to reflect new config
|
|
345
|
+
this.refreshStrategyValues();
|
|
346
|
+
this.refreshPipelineValues();
|
|
347
|
+
// Update preset indicators
|
|
348
|
+
for (const name of PRESETS) {
|
|
349
|
+
this.presetList.updateValue(
|
|
350
|
+
`preset:${name}`,
|
|
351
|
+
detectPreset(this.config) === name ? "✓ active" : "",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private onStrategyChange(id: string, newValue: string): void {
|
|
358
|
+
const key = id.replace("strategy:", "");
|
|
359
|
+
const strat = STRATEGIES.find((s) => s.key === key);
|
|
360
|
+
if (!strat) return;
|
|
361
|
+
|
|
362
|
+
// Map the cycled value to enabled + mode
|
|
363
|
+
strat.setMode(this.config, newValue);
|
|
364
|
+
// If mode is "off", disable; otherwise enable
|
|
365
|
+
strat.setEnabled(this.config, newValue !== "off");
|
|
366
|
+
|
|
367
|
+
this.strategyList.updateValue(id, this.formatStrategyValue(strat));
|
|
368
|
+
|
|
369
|
+
// Update preset indicators since config may no longer match a preset
|
|
370
|
+
for (const name of PRESETS) {
|
|
371
|
+
this.presetList.updateValue(
|
|
372
|
+
`preset:${name}`,
|
|
373
|
+
detectPreset(this.config) === name ? "✓ active" : "",
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private onPipelineChange(id: string, newValue: string): void {
|
|
379
|
+
const key = id.replace("pipeline:", "");
|
|
380
|
+
const item = PIPELINE_ITEMS.find((p) => p.key === key);
|
|
381
|
+
if (!item) return;
|
|
382
|
+
item.setValue(this.config, newValue === "on");
|
|
383
|
+
this.pipelineList.updateValue(id, newValue);
|
|
384
|
+
|
|
385
|
+
// Update preset indicators
|
|
386
|
+
for (const name of PRESETS) {
|
|
387
|
+
this.presetList.updateValue(
|
|
388
|
+
`preset:${name}`,
|
|
389
|
+
detectPreset(this.config) === name ? "✓ active" : "",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private onCancel(): void {
|
|
395
|
+
this.onClose?.();
|
|
179
396
|
}
|
|
180
397
|
|
|
181
|
-
|
|
398
|
+
// ─── Helpers ───────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
private formatStrategyValue(s: StrategyDef): string {
|
|
401
|
+
const enabled = s.getEnabled(this.config);
|
|
402
|
+
const mode = s.getMode(this.config);
|
|
403
|
+
if (!enabled) return "off";
|
|
404
|
+
return mode;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private refreshStrategyValues(): void {
|
|
408
|
+
for (const s of STRATEGIES) {
|
|
409
|
+
this.strategyList.updateValue(`strategy:${s.key}`, this.formatStrategyValue(s));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private refreshPipelineValues(): void {
|
|
414
|
+
for (const p of PIPELINE_ITEMS) {
|
|
415
|
+
this.pipelineList.updateValue(`pipeline:${p.key}`, p.getValue(this.config) ? "on" : "off");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ─── Input handling ────────────────────────────────────────────────
|
|
182
420
|
|
|
183
421
|
handleInput(data: string): void {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
} else {
|
|
198
|
-
this.presetIndex = (this.presetIndex + 1) % PRESETS.length;
|
|
199
|
-
}
|
|
200
|
-
break;
|
|
201
|
-
case " ": // Space - toggle enabled
|
|
202
|
-
if (this.mode === "strategy") {
|
|
203
|
-
const item = ALL_ITEMS[this.selectedIndex];
|
|
204
|
-
item.setEnabled(this.config, !item.getEnabled(this.config));
|
|
205
|
-
}
|
|
206
|
-
break;
|
|
207
|
-
case "\x1b[C": // Right - cycle mode forward
|
|
208
|
-
case "\r": // Enter
|
|
209
|
-
if (this.mode === "strategy") {
|
|
210
|
-
const strat = ALL_ITEMS[this.selectedIndex];
|
|
211
|
-
const modes = strat.modes;
|
|
212
|
-
const currentIdx = modes.indexOf(strat.getMode(this.config));
|
|
213
|
-
const nextIdx = (currentIdx + 1) % modes.length;
|
|
214
|
-
strat.setMode(this.config, modes[nextIdx]);
|
|
215
|
-
} else {
|
|
216
|
-
// Apply preset
|
|
217
|
-
this.config = applyPreset(PRESETS[this.presetIndex]);
|
|
218
|
-
this.mode = "strategy";
|
|
219
|
-
}
|
|
220
|
-
break;
|
|
221
|
-
case "\x1b[D": // Left - cycle mode backward
|
|
222
|
-
if (this.mode === "strategy") {
|
|
223
|
-
const strat2 = ALL_ITEMS[this.selectedIndex];
|
|
224
|
-
const modes2 = strat2.modes;
|
|
225
|
-
const curIdx = modes2.indexOf(strat2.getMode(this.config));
|
|
226
|
-
const prevIdx = (curIdx - 1 + modes2.length) % modes2.length;
|
|
227
|
-
strat2.setMode(this.config, modes2[prevIdx]);
|
|
228
|
-
}
|
|
229
|
-
break;
|
|
230
|
-
case "p": // Toggle preset mode
|
|
231
|
-
this.mode = this.mode === "preset" ? "strategy" : "preset";
|
|
232
|
-
break;
|
|
233
|
-
case "s": // Save
|
|
234
|
-
saveConfig(this.config);
|
|
235
|
-
this.onClose?.();
|
|
236
|
-
break;
|
|
237
|
-
case "\x1b": // Escape - cancel
|
|
238
|
-
this.onClose?.();
|
|
239
|
-
break;
|
|
422
|
+
// Tab switches section
|
|
423
|
+
if (data === "\t" || data === "\x1b[Z") {
|
|
424
|
+
const idx = SECTIONS.indexOf(this.section);
|
|
425
|
+
this.section = SECTIONS[(idx + 1) % SECTIONS.length];
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Enter saves and closes
|
|
430
|
+
if (data === "\r") {
|
|
431
|
+
saveConfig(this.config, { perProject: this.perProjectOverride, cwd: this.projectDir });
|
|
432
|
+
this.saved = true;
|
|
433
|
+
setTimeout(() => this.onClose?.(), 400);
|
|
434
|
+
return;
|
|
240
435
|
}
|
|
436
|
+
|
|
437
|
+
// Escape cancels (but SettingsList also handles it, calling onCancel)
|
|
438
|
+
if (data === "\x1b") {
|
|
439
|
+
this.onClose?.();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Delegate all other input to the current section's SettingsList
|
|
444
|
+
this.currentList.handleInput(data);
|
|
241
445
|
}
|
|
242
446
|
|
|
447
|
+
// ─── Render ────────────────────────────────────────────────────────
|
|
448
|
+
|
|
243
449
|
render(width: number): string[] {
|
|
450
|
+
const innerWidth = Math.max(22, width - 2);
|
|
244
451
|
const lines: string[] = [];
|
|
245
|
-
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
246
452
|
|
|
247
453
|
// Header
|
|
248
|
-
|
|
454
|
+
lines.push(borderLine(innerWidth, "top"));
|
|
455
|
+
lines.push(frameLine(`\x1b[1m\x1b[36m🗜️ Compactor Settings\x1b[0m`, innerWidth));
|
|
456
|
+
|
|
457
|
+
// Current preset indicator
|
|
249
458
|
const presetName = detectPreset(this.config);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
add("");
|
|
264
|
-
add(`${ansi.dim}↑↓ navigate • Enter apply • p back to strategies • s save • Esc cancel${ansi.reset}`);
|
|
265
|
-
} else {
|
|
266
|
-
// Strategy list (GLOBAL_DEBUG at top, then all strategies)
|
|
267
|
-
for (let i = 0; i < ALL_ITEMS.length; i++) {
|
|
268
|
-
const item = ALL_ITEMS[i];
|
|
269
|
-
const isSelected = i === this.selectedIndex;
|
|
270
|
-
const enabled = item.getEnabled(this.config);
|
|
271
|
-
const mode = item.getMode(this.config);
|
|
272
|
-
const toggle = enabled ? TOGGLE_ON : TOGGLE_OFF;
|
|
273
|
-
const labelColor = isSelected ? ansi.bold : "";
|
|
274
|
-
const modeColor = ansi.magenta;
|
|
275
|
-
const descColor = ansi.gray;
|
|
276
|
-
|
|
277
|
-
const cursor = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
|
|
278
|
-
add(`${cursor} ${toggle} ${labelColor}${item.label}${ansi.reset} ${modeColor}[${mode}]${ansi.reset}`);
|
|
279
|
-
add(` ${descColor}${item.description}${ansi.reset}`);
|
|
459
|
+
const presetLabel = presetName === "custom" ? "custom (modified)" : presetName;
|
|
460
|
+
const overrideLabel = this.perProjectOverride
|
|
461
|
+
? `\x1b[33mProject override\x1b[0m`
|
|
462
|
+
: `\x1b[2mGlobal config\x1b[0m`;
|
|
463
|
+
lines.push(frameLine(`\x1b[2mPreset: ${presetLabel} · ${overrideLabel}\x1b[0m`, innerWidth));
|
|
464
|
+
lines.push(ruleLine(innerWidth));
|
|
465
|
+
|
|
466
|
+
// Section tabs
|
|
467
|
+
const tabParts = SECTIONS.map((s) => {
|
|
468
|
+
const label = s.charAt(0).toUpperCase() + s.slice(1);
|
|
469
|
+
if (s === this.section) {
|
|
470
|
+
return `\x1b[1m\x1b[36m[${label}]\x1b[0m`;
|
|
280
471
|
}
|
|
472
|
+
return `\x1b[2m${label}\x1b[0m`;
|
|
473
|
+
});
|
|
474
|
+
lines.push(frameLine(` ${tabParts.join(" ")}`, innerWidth));
|
|
475
|
+
lines.push(ruleLine(innerWidth));
|
|
281
476
|
|
|
282
|
-
|
|
283
|
-
|
|
477
|
+
// Section content (rendered by SettingsList)
|
|
478
|
+
const contentLines = this.currentList.render(innerWidth - 2);
|
|
479
|
+
for (const line of contentLines) {
|
|
480
|
+
lines.push(frameLine(` ${line}`, innerWidth));
|
|
284
481
|
}
|
|
285
482
|
|
|
483
|
+
// Saved indicator
|
|
484
|
+
if (this.saved) {
|
|
485
|
+
lines.push(ruleLine(innerWidth));
|
|
486
|
+
lines.push(frameLine(` \x1b[32m✓ Settings saved\x1b[0m`, innerWidth));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Footer hints
|
|
490
|
+
lines.push(ruleLine(innerWidth));
|
|
491
|
+
const hints = this.section === "strategies"
|
|
492
|
+
? "↑↓ navigate · Space change · Tab switch · / search · Enter save · Esc cancel"
|
|
493
|
+
: "↑↓ navigate · Space change · Tab switch · Enter save · Esc cancel";
|
|
494
|
+
lines.push(frameLine(`\x1b[2m${hints}\x1b[0m`, innerWidth));
|
|
495
|
+
lines.push(borderLine(innerWidth, "bottom"));
|
|
496
|
+
|
|
286
497
|
return lines;
|
|
287
498
|
}
|
|
288
499
|
}
|
|
289
500
|
|
|
290
501
|
/**
|
|
291
502
|
* Factory function for ctx.ui.custom() integration.
|
|
292
|
-
* Returns a render function compatible with pi-tui's custom overlay API.
|
|
293
503
|
*/
|
|
294
|
-
export function renderSettingsOverlay() {
|
|
295
|
-
return (
|
|
296
|
-
const overlay = new CompactorSettingsOverlay();
|
|
504
|
+
export function renderSettingsOverlay(cwd?: string) {
|
|
505
|
+
return (_tui: any, _theme: any, _kb: any, done: (result: any) => void) => {
|
|
506
|
+
const overlay = new CompactorSettingsOverlay({ cwd });
|
|
297
507
|
overlay.onClose = () => done(overlay);
|
|
298
508
|
|
|
299
509
|
return {
|
|
300
510
|
render: (width: number) => overlay.render(width),
|
|
301
|
-
invalidate: () =>
|
|
511
|
+
invalidate: () => overlay.invalidate(),
|
|
302
512
|
handleInput: (data: string) => overlay.handleInput(data),
|
|
303
513
|
};
|
|
304
514
|
};
|