@mrclrchtr/supi-cache 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +44 -0
- package/src/config.ts +37 -0
- package/src/fingerprint.ts +187 -0
- package/src/forensics/extract.ts +129 -0
- package/src/forensics/forensics.ts +214 -0
- package/src/forensics/queries.ts +74 -0
- package/src/forensics/redact.ts +61 -0
- package/src/forensics/types.ts +59 -0
- package/src/hash.ts +16 -0
- package/src/index.ts +1 -0
- package/src/monitor/monitor.ts +320 -0
- package/src/monitor/state.ts +308 -0
- package/src/monitor/status.ts +33 -0
- package/src/report/forensics.ts +165 -0
- package/src/report/history.ts +197 -0
- package/src/settings-registration.ts +80 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Forensics report formatting for the /supi-cache-forensics command.
|
|
2
|
+
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { CauseBreakdown, ForensicsFinding } from "../forensics/types.ts";
|
|
5
|
+
|
|
6
|
+
export interface ForensicsReportSnapshot {
|
|
7
|
+
pattern: string;
|
|
8
|
+
findings?: ForensicsFinding[];
|
|
9
|
+
breakdown?: CauseBreakdown;
|
|
10
|
+
sessionsScanned: number;
|
|
11
|
+
turnsAnalyzed: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format forensics results as themed lines for the `/supi-cache-forensics` command.
|
|
16
|
+
*/
|
|
17
|
+
export function formatForensicsReport(snapshot: ForensicsReportSnapshot, theme: Theme): string[] {
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
|
|
20
|
+
lines.push(theme.fg("accent", `Cache forensics — ${snapshot.pattern}`));
|
|
21
|
+
lines.push(
|
|
22
|
+
theme.fg(
|
|
23
|
+
"dim",
|
|
24
|
+
`${snapshot.sessionsScanned} sessions scanned, ${snapshot.turnsAnalyzed} turns analyzed`,
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
lines.push("");
|
|
28
|
+
|
|
29
|
+
if (snapshot.pattern === "breakdown" && snapshot.breakdown) {
|
|
30
|
+
lines.push(
|
|
31
|
+
...formatBreakdown(
|
|
32
|
+
snapshot.breakdown,
|
|
33
|
+
snapshot.findings ?? [],
|
|
34
|
+
theme,
|
|
35
|
+
snapshot.sessionsScanned,
|
|
36
|
+
snapshot.turnsAnalyzed,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
} else if (snapshot.findings && snapshot.findings.length > 0) {
|
|
40
|
+
lines.push(...formatFindings(snapshot.findings, snapshot.pattern, theme));
|
|
41
|
+
} else {
|
|
42
|
+
lines.push(theme.fg("dim", "No regressions found in the queried period."));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// biome-ignore lint/complexity/useMaxParams: breakdown formatter takes all needed context
|
|
49
|
+
function formatBreakdown(
|
|
50
|
+
breakdown: CauseBreakdown,
|
|
51
|
+
findings: ForensicsFinding[],
|
|
52
|
+
theme: Theme,
|
|
53
|
+
sessionsScanned: number,
|
|
54
|
+
turnsAnalyzed: number,
|
|
55
|
+
): string[] {
|
|
56
|
+
const lines: string[] = [];
|
|
57
|
+
const entries = Object.entries(breakdown) as [string, number][];
|
|
58
|
+
|
|
59
|
+
// Sort by count descending
|
|
60
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
61
|
+
|
|
62
|
+
const total = entries.reduce((sum, [, v]) => sum + v, 0);
|
|
63
|
+
if (total === 0) {
|
|
64
|
+
lines.push(theme.fg("dim", "No regressions found in the queried period."));
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
|
|
69
|
+
const maxCount = Math.max(1, ...entries.map(([, v]) => v));
|
|
70
|
+
|
|
71
|
+
// Pre-compute avg drop per cause from findings
|
|
72
|
+
const dropByCause = new Map<string, { sum: number; count: number }>();
|
|
73
|
+
for (const f of findings) {
|
|
74
|
+
const key = f.cause.type;
|
|
75
|
+
const entry = dropByCause.get(key) ?? { sum: 0, count: 0 };
|
|
76
|
+
entry.sum += f.drop;
|
|
77
|
+
entry.count++;
|
|
78
|
+
dropByCause.set(key, entry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const [cause, count] of entries) {
|
|
82
|
+
if (count === 0) continue;
|
|
83
|
+
const label = cause.padEnd(maxKeyLen);
|
|
84
|
+
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
|
85
|
+
const barWidth = Math.max(1, Math.round((count / maxCount) * 20));
|
|
86
|
+
const bar = "█".repeat(barWidth);
|
|
87
|
+
const dropInfo = dropByCause.get(cause);
|
|
88
|
+
const avgDrop = dropInfo ? ` avg ${Math.round(dropInfo.sum / dropInfo.count)}pp drop` : "";
|
|
89
|
+
lines.push(
|
|
90
|
+
`${label} ${String(count).padStart(3)} ${String(pct).padStart(3)}% ${theme.fg("accent", bar)}${avgDrop}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Summary line
|
|
95
|
+
const turnPct = turnsAnalyzed > 0 ? ((total / turnsAnalyzed) * 100).toFixed(1) : "0.0";
|
|
96
|
+
const perSession = sessionsScanned > 0 ? (total / sessionsScanned).toFixed(1) : "0";
|
|
97
|
+
lines.push(theme.fg("dim", "─".repeat(40)));
|
|
98
|
+
lines.push(
|
|
99
|
+
theme.fg(
|
|
100
|
+
"dim",
|
|
101
|
+
`total ${total} (${turnPct}% of ${turnsAnalyzed} turns, ~${perSession}/session)`,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Idle/unknown clarification
|
|
106
|
+
if (breakdown.idle > 0) {
|
|
107
|
+
const unexplained = breakdown.unknown + breakdown.idle;
|
|
108
|
+
lines.push(
|
|
109
|
+
theme.fg(
|
|
110
|
+
"dim",
|
|
111
|
+
`ℹ idle regressions (${breakdown.idle} of ${unexplained} total unexplained drops) are unknown drops with turn gaps > threshold`,
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: renderer branches by pattern
|
|
120
|
+
function formatFindings(findings: ForensicsFinding[], pattern: string, theme: Theme): string[] {
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
|
|
123
|
+
for (const f of findings) {
|
|
124
|
+
const causeStr = formatCause(f.cause);
|
|
125
|
+
const header = ` Session ${f.sessionId.slice(0, 8)} Turn ${f.turnIndex} ${f.drop}pp drop (${causeStr})`;
|
|
126
|
+
lines.push(theme.fg("warning", header));
|
|
127
|
+
|
|
128
|
+
if (f.previousRate !== undefined && f.currentRate !== undefined) {
|
|
129
|
+
lines.push(` ${f.previousRate}% → ${f.currentRate}%`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (pattern === "correlate" || pattern === "idle") {
|
|
133
|
+
if (f.toolsBefore.length > 0) {
|
|
134
|
+
lines.push(theme.fg("dim", " Preceding tools:"));
|
|
135
|
+
for (const tool of f.toolsBefore) {
|
|
136
|
+
lines.push(` • ${tool.toolName} (${tool.paramKeys.join(", ")})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (f._pathsInvolved && f._pathsInvolved.length > 0) {
|
|
142
|
+
lines.push(theme.fg("dim", " Files:"));
|
|
143
|
+
for (const p of f._pathsInvolved.slice(0, 5)) {
|
|
144
|
+
lines.push(` • ${p}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatCause(cause: ForensicsFinding["cause"]): string {
|
|
153
|
+
switch (cause.type) {
|
|
154
|
+
case "compaction":
|
|
155
|
+
return "compaction";
|
|
156
|
+
case "model_change":
|
|
157
|
+
return `model changed${cause.model !== "unknown" ? ` to ${cause.model}` : ""}`;
|
|
158
|
+
case "prompt_change":
|
|
159
|
+
return "prompt changed";
|
|
160
|
+
case "idle":
|
|
161
|
+
return `idle (${cause.idleGapMinutes} min gap)`;
|
|
162
|
+
default:
|
|
163
|
+
return "unknown";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Report formatting for the /supi-cache-history table.
|
|
2
|
+
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { diffFingerprints } from "../fingerprint.ts";
|
|
5
|
+
import { CAUSE_NOTE, type TurnRecord } from "../monitor/state.ts";
|
|
6
|
+
|
|
7
|
+
/** Snapshot payload persisted in message.details for the report renderer. */
|
|
8
|
+
export interface CacheReportSnapshot {
|
|
9
|
+
turns: TurnRecord[];
|
|
10
|
+
cacheSupported: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format the per-turn cache history as themed lines for the `/supi-cache-history` command.
|
|
15
|
+
*
|
|
16
|
+
* Accepts a snapshot of turn records so that rendered messages are stable —
|
|
17
|
+
* they always reflect the data at the time `/supi-cache-history` was invoked, not the
|
|
18
|
+
* current live state.
|
|
19
|
+
*
|
|
20
|
+
* Columns: Turn, Input, CacheR, CacheW, Hit%, Note
|
|
21
|
+
*/
|
|
22
|
+
export function formatCacheReport(snapshot: CacheReportSnapshot, theme: Theme): string[] {
|
|
23
|
+
const { turns } = snapshot;
|
|
24
|
+
|
|
25
|
+
if (turns.length === 0) {
|
|
26
|
+
return [theme.fg("dim", "No cache data yet — send a message to start tracking")];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines: string[] = [];
|
|
30
|
+
|
|
31
|
+
// Header
|
|
32
|
+
const header = formatRow({
|
|
33
|
+
turn: "Turn",
|
|
34
|
+
input: "Input",
|
|
35
|
+
cacheR: "CacheR",
|
|
36
|
+
cacheW: "CacheW",
|
|
37
|
+
hitPct: "Hit%",
|
|
38
|
+
note: "Note",
|
|
39
|
+
});
|
|
40
|
+
lines.push(theme.fg("dim", header));
|
|
41
|
+
lines.push(theme.fg("dim", "─".repeat(header.length)));
|
|
42
|
+
|
|
43
|
+
// Data rows
|
|
44
|
+
for (const turn of turns) {
|
|
45
|
+
const hitStr = turn.hitRate !== undefined ? `${turn.hitRate}%` : "—";
|
|
46
|
+
const noteStr = turn.note ?? "";
|
|
47
|
+
const row = formatRow({
|
|
48
|
+
turn: String(turn.turnIndex),
|
|
49
|
+
input: formatTokenCount(turn.input),
|
|
50
|
+
cacheR: formatTokenCount(turn.cacheRead),
|
|
51
|
+
cacheW: formatTokenCount(turn.cacheWrite),
|
|
52
|
+
hitPct: hitStr,
|
|
53
|
+
note: noteStr,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Color the row based on note
|
|
57
|
+
if (noteStr.startsWith("⚠")) {
|
|
58
|
+
lines.push(theme.fg("warning", row));
|
|
59
|
+
} else if (noteStr === "cold start") {
|
|
60
|
+
lines.push(theme.fg("dim", row));
|
|
61
|
+
} else {
|
|
62
|
+
lines.push(row);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Regression details section ────────────────────────────
|
|
67
|
+
|
|
68
|
+
const detailLines = formatRegressionDetails(turns, theme);
|
|
69
|
+
if (detailLines.length > 0) {
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push(theme.fg("accent", "Regression details:"));
|
|
72
|
+
lines.push(...detailLines);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build a compact regression-detail section for turns with diagnosed causes.
|
|
80
|
+
*
|
|
81
|
+
* Each entry shows the turn index, hit-rate drop (when computable), and
|
|
82
|
+
* fingerprint diff bullet points for prompt_change regressions.
|
|
83
|
+
*/
|
|
84
|
+
function formatRegressionDetails(turns: TurnRecord[], theme: Theme): string[] {
|
|
85
|
+
const lines: string[] = [];
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < turns.length; i++) {
|
|
88
|
+
const turn = turns[i];
|
|
89
|
+
const prevTurn = i > 0 ? turns[i - 1] : undefined;
|
|
90
|
+
addTurnDetail(lines, turn, prevTurn, theme);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addTurnDetail(
|
|
97
|
+
lines: string[],
|
|
98
|
+
turn: TurnRecord,
|
|
99
|
+
prevTurn: TurnRecord | undefined,
|
|
100
|
+
theme: Theme,
|
|
101
|
+
): void {
|
|
102
|
+
const causeLabel = getCauseLabel(turn);
|
|
103
|
+
if (!causeLabel) return;
|
|
104
|
+
|
|
105
|
+
const drop = describeDrop(prevTurn, turn);
|
|
106
|
+
const header = drop
|
|
107
|
+
? ` Turn ${turn.turnIndex}: ${drop} (${causeLabel})`
|
|
108
|
+
: ` Turn ${turn.turnIndex}: (${causeLabel})`;
|
|
109
|
+
lines.push(theme.fg("warning", header));
|
|
110
|
+
|
|
111
|
+
addFingerprintBullets(lines, turn, prevTurn);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function addFingerprintBullets(
|
|
115
|
+
lines: string[],
|
|
116
|
+
turn: TurnRecord,
|
|
117
|
+
prevTurn: TurnRecord | undefined,
|
|
118
|
+
): void {
|
|
119
|
+
if (!isPromptChange(turn)) return;
|
|
120
|
+
|
|
121
|
+
const prevFp = prevTurn?.promptFingerprint;
|
|
122
|
+
const currFp = turn.promptFingerprint;
|
|
123
|
+
if (!prevFp || !currFp) return;
|
|
124
|
+
|
|
125
|
+
for (const d of diffFingerprints(prevFp, currFp)) {
|
|
126
|
+
lines.push(` • ${d}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Determine the cause label for a turn, or undefined if it's a regular turn. */
|
|
131
|
+
function getCauseLabel(turn: TurnRecord): string | undefined {
|
|
132
|
+
if (turn.cause) {
|
|
133
|
+
switch (turn.cause.type) {
|
|
134
|
+
case "compaction":
|
|
135
|
+
return "compaction";
|
|
136
|
+
case "model_change":
|
|
137
|
+
return "model changed";
|
|
138
|
+
case "prompt_change":
|
|
139
|
+
return "prompt changed";
|
|
140
|
+
case "unknown":
|
|
141
|
+
return "unknown";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Fall back to note-based detection for legacy records
|
|
145
|
+
if (turn.note?.startsWith("⚠")) {
|
|
146
|
+
const label = turn.note.replace("⚠ ", "");
|
|
147
|
+
return label;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Check if a turn's cause is prompt_change. */
|
|
153
|
+
function isPromptChange(turn: TurnRecord): boolean {
|
|
154
|
+
if (turn.cause?.type === "prompt_change") return true;
|
|
155
|
+
return turn.note === CAUSE_NOTE.prompt_change;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Describe the hit-rate drop between two adjacent turns.
|
|
160
|
+
* Returns "80% → 5%" or undefined when not computable.
|
|
161
|
+
*/
|
|
162
|
+
function describeDrop(prev: TurnRecord | undefined, curr: TurnRecord): string | undefined {
|
|
163
|
+
if (!prev || prev.hitRate === undefined || curr.hitRate === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return `${prev.hitRate}% → ${curr.hitRate}%`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface RowData {
|
|
170
|
+
turn: string;
|
|
171
|
+
input: string;
|
|
172
|
+
cacheR: string;
|
|
173
|
+
cacheW: string;
|
|
174
|
+
hitPct: string;
|
|
175
|
+
note: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatRow(row: RowData): string {
|
|
179
|
+
return [
|
|
180
|
+
row.turn.padStart(4),
|
|
181
|
+
row.input.padStart(8),
|
|
182
|
+
row.cacheR.padStart(8),
|
|
183
|
+
row.cacheW.padStart(8),
|
|
184
|
+
row.hitPct.padStart(6),
|
|
185
|
+
row.note ? ` ${row.note}` : "",
|
|
186
|
+
].join(" ");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatTokenCount(tokens: number): string {
|
|
190
|
+
if (tokens >= 1_000_000) {
|
|
191
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
192
|
+
}
|
|
193
|
+
if (tokens >= 1_000) {
|
|
194
|
+
return `${(tokens / 1_000).toFixed(1)}k`;
|
|
195
|
+
}
|
|
196
|
+
return String(tokens);
|
|
197
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Cache-monitor settings registration for the supi settings registry.
|
|
2
|
+
|
|
3
|
+
import type { ConfigSettingsHelpers } from "@mrclrchtr/supi-core";
|
|
4
|
+
import { registerConfigSettings } from "@mrclrchtr/supi-core";
|
|
5
|
+
import { CACHE_MONITOR_DEFAULTS } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
const THRESHOLD_VALUES = ["5", "10", "15", "20", "25", "30", "35", "40", "45", "50"];
|
|
8
|
+
const IDLE_THRESHOLD_VALUES = ["1", "2", "3", "5", "10", "15", "20", "30", "45", "60"];
|
|
9
|
+
|
|
10
|
+
/** Register supi-cache settings with the supi settings registry. */
|
|
11
|
+
export function registerCacheMonitorSettings(homeDir?: string): void {
|
|
12
|
+
registerConfigSettings({
|
|
13
|
+
id: "supi-cache",
|
|
14
|
+
label: "Cache",
|
|
15
|
+
section: "supi-cache",
|
|
16
|
+
defaults: CACHE_MONITOR_DEFAULTS,
|
|
17
|
+
buildItems: (settings) => [
|
|
18
|
+
{
|
|
19
|
+
id: "enabled",
|
|
20
|
+
label: "Enabled",
|
|
21
|
+
description: "Enable/disable prompt cache health monitoring",
|
|
22
|
+
currentValue: settings.enabled ? "on" : "off",
|
|
23
|
+
values: ["on", "off"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "notifications",
|
|
27
|
+
label: "Notifications",
|
|
28
|
+
description: "Show warning notifications on cache regressions",
|
|
29
|
+
currentValue: settings.notifications ? "on" : "off",
|
|
30
|
+
values: ["on", "off"],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "regressionThreshold",
|
|
34
|
+
label: "Regression Threshold",
|
|
35
|
+
description: "Percentage-point drop that triggers a regression warning",
|
|
36
|
+
currentValue: String(settings.regressionThreshold),
|
|
37
|
+
values: THRESHOLD_VALUES,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "idleThresholdMinutes",
|
|
41
|
+
label: "Idle Threshold",
|
|
42
|
+
description: "Minutes of inactivity to classify as idle-time regression",
|
|
43
|
+
currentValue: String(settings.idleThresholdMinutes),
|
|
44
|
+
values: IDLE_THRESHOLD_VALUES,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
// biome-ignore lint/complexity/useMaxParams: ConfigSettingsOptions interface callback
|
|
48
|
+
persistChange: (_scope, _cwd, settingId, value, helpers) => {
|
|
49
|
+
handleSettingChange(settingId, value, helpers);
|
|
50
|
+
},
|
|
51
|
+
...(homeDir ? { homeDir } : {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleSettingChange(
|
|
56
|
+
settingId: string,
|
|
57
|
+
value: string,
|
|
58
|
+
helpers: ConfigSettingsHelpers,
|
|
59
|
+
): void {
|
|
60
|
+
switch (settingId) {
|
|
61
|
+
case "enabled": {
|
|
62
|
+
helpers.set("enabled", value === "on");
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "notifications": {
|
|
66
|
+
helpers.set("notifications", value === "on");
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "regressionThreshold": {
|
|
70
|
+
const num = Number.parseInt(value, 10);
|
|
71
|
+
helpers.set("regressionThreshold", Number.isNaN(num) ? 25 : num);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "idleThresholdMinutes": {
|
|
75
|
+
const num = Number.parseInt(value, 10);
|
|
76
|
+
helpers.set("idleThresholdMinutes", Number.isNaN(num) ? 5 : num);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|