@vanillagreen/pi-qol 2.1.0 → 2.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 CHANGED
@@ -44,6 +44,7 @@ Open `/extensions:settings`; settings appear under the **QOL** tab. Project sett
44
44
  - Permission gate: `permissionGate.enabled`, `permissionGate.commands` (comma-separated literal fragments or `/regex/flags`), `permissionGate.previewLines`, `permissionGate.previewChars`.
45
45
  - Notifications: `notification.*` (triggers, channels, tmux options, protocol, cooldown, title and body).
46
46
  - Compaction and budget guard: `compaction.*` (custom summaries, model, profile, remote endpoint, branch summaries, idle trigger, budget guard thresholds, chunk input cap, handoff artifact, transcript-risk budget).
47
+ - Pi's own `compaction.enabled` (the top-level `compaction` object in the same settings files, not a QOL key) turns off every automatic compaction: when it is `false`, neither the budget guard nor the idle trigger starts one, and `/qol` reports both as disabled by that key. A manual `/compact` still runs. While it is `true` or absent, both keep the defaults and thresholds above: the budget guard on at `compaction.budgetPercent` 85, the idle trigger off until `compaction.idleEnabled` is set.
47
48
  - Thinking: `thinkingLabel.text`, `thinkingTimer.enabled`, `workingIndicator.mode` (`static` if the animated indicator flashes).
48
49
 
49
50
  Maintainer notes are in [DEVELOPMENT.md](DEVELOPMENT.md).
@@ -27,7 +27,7 @@ import {
27
27
  type HandoffWriteResult,
28
28
  type QolBudgetHandoff,
29
29
  } from "./compaction-handoff.js";
30
- import { settingBoolean, settingNumber, settingString } from "./settings.js";
30
+ import { piCompactionEnabled, settingBoolean, settingNumber, settingString } from "./settings.js";
31
31
  import { stringifyError } from "./util.js";
32
32
 
33
33
  export type QolSummaryProfile = "concise" | "balanced" | "exhaustive";
@@ -373,6 +373,22 @@ export function compactionTriggerReason(ctx: ExtensionContext): string | undefin
373
373
  return undefined;
374
374
  }
375
375
 
376
+ /** Whether a QOL automatic compaction trigger may fire, and what turned it
377
+ * off. Pi core's `compaction.enabled` is read first, so that one key stops
378
+ * every automatic compaction in Pi; a manual /compact never passes here. */
379
+ export type AutoCompactionGate = "enabled" | "disabled" | "pi-compaction-disabled";
380
+
381
+ const AUTO_COMPACTION_SWITCHES = {
382
+ budgetGuard: { key: "compaction.budgetGuardEnabled", fallback: true },
383
+ idle: { key: "compaction.idleEnabled", fallback: false },
384
+ } as const;
385
+
386
+ export function autoCompactionGate(trigger: keyof typeof AUTO_COMPACTION_SWITCHES, cwd: string): AutoCompactionGate {
387
+ if (!piCompactionEnabled(cwd)) return "pi-compaction-disabled";
388
+ const { key, fallback } = AUTO_COMPACTION_SWITCHES[trigger];
389
+ return settingBoolean(key, fallback, cwd) ? "enabled" : "disabled";
390
+ }
391
+
376
392
  export type BudgetGuardTrigger = BudgetTrigger;
377
393
  export type TranscriptRiskState = TranscriptRiskResult;
378
394
 
@@ -383,7 +399,7 @@ export type TranscriptRiskState = TranscriptRiskResult;
383
399
  * repeated triggers while usage stays above the threshold.
384
400
  */
385
401
  export function budgetGuardTrigger(ctx: ExtensionContext): BudgetGuardTrigger | undefined {
386
- if (!settingBoolean("compaction.budgetGuardEnabled", true, ctx.cwd)) return undefined;
402
+ if (autoCompactionGate("budgetGuard", ctx.cwd) !== "enabled") return undefined;
387
403
  const usage = contextUsage(ctx);
388
404
  if (!usage) return undefined;
389
405
  return computeBudgetTrigger({
@@ -69,21 +69,49 @@ export function piSettingsPaths(cwd = process.cwd()): string[] {
69
69
  return projectSettingsTrusted(project) ? [user, project] : [user];
70
70
  }
71
71
 
72
- export function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
73
- const merged: Record<string, unknown> = {};
72
+ function isRecord(value: unknown): value is Record<string, unknown> {
73
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
74
+ }
75
+
76
+ /** Each settings file `piSettingsPaths` names that exists and parses, later
77
+ * files overriding earlier ones. A malformed file is skipped, as Pi's own
78
+ * settings load treats one as empty. */
79
+ function readPiSettingsFiles(cwd?: string): Record<string, any>[] {
80
+ const files: Record<string, any>[] = [];
74
81
  for (const settingsPath of piSettingsPaths(cwd)) {
75
82
  if (!existsSync(settingsPath)) continue;
76
83
  try {
77
84
  const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
78
- const config = parsed?.kendex?.extensionManager?.config?.[packageId];
79
- if (config && typeof config === "object" && !Array.isArray(config)) Object.assign(merged, config);
85
+ if (isRecord(parsed)) files.push(parsed);
80
86
  } catch {
81
- // Ignore malformed optional manager config.
87
+ // Ignore a malformed settings file.
82
88
  }
83
89
  }
90
+ return files;
91
+ }
92
+
93
+ export function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
94
+ const merged: Record<string, unknown> = {};
95
+ for (const settings of readPiSettingsFiles(cwd)) {
96
+ const config = settings.kendex?.extensionManager?.config?.[packageId];
97
+ if (isRecord(config)) Object.assign(merged, config);
98
+ }
84
99
  return merged;
85
100
  }
86
101
 
102
+ /** Pi core's `compaction.enabled` over the files QOL reads its own config
103
+ * from, resolved as Pi's `SettingsManager.getCompactionEnabled` resolves it:
104
+ * the last file that sets the key decides, a key no file sets is Pi's default,
105
+ * true, and any falsy value turns compaction off. */
106
+ export function piCompactionEnabled(cwd?: string): boolean {
107
+ let enabled: unknown;
108
+ for (const settings of readPiSettingsFiles(cwd)) {
109
+ const compaction = settings.compaction;
110
+ if (isRecord(compaction) && compaction.enabled !== undefined) enabled = compaction.enabled;
111
+ }
112
+ return Boolean(enabled ?? true);
113
+ }
114
+
87
115
  export function readkendexConfig(cwd?: string): kendexConfig {
88
116
  return readPackageConfig(CONFIG_ID, cwd) as kendexConfig;
89
117
  }
@@ -1,7 +1,9 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { compactionProfile } from "./compaction.js";
2
+ import { autoCompactionGate, compactionProfile, type AutoCompactionGate } from "./compaction.js";
3
3
  import {
4
4
  DEFAULT_AUTO_RENAME_MODEL,
5
+ DEFAULT_BUDGET_GUARD_PERCENT,
6
+ DEFAULT_BUDGET_GUARD_TOKENS,
5
7
  DEFAULT_COMPACTION_MODEL,
6
8
  DEFAULT_IDLE_COMPACTION_SECONDS,
7
9
  DEFAULT_PERMISSION_GATE_PREVIEW_CHARS,
@@ -15,6 +17,21 @@ import { autoRenameEnabled } from "./session-rename.js";
15
17
  import { sessionSearchShortcut } from "./session-search/index.js";
16
18
  import { boundedSettingNumber, newlineFallbackKey, settingBoolean, settingNumber, settingString } from "./settings.js";
17
19
 
20
+ function autoCompactionStatus(gate: AutoCompactionGate, enabled: () => string): string {
21
+ switch (gate) {
22
+ case "enabled":
23
+ return enabled();
24
+ case "disabled":
25
+ return "disabled";
26
+ case "pi-compaction-disabled":
27
+ return "disabled by Pi compaction.enabled=false";
28
+ default: {
29
+ const unknownGate: never = gate;
30
+ throw new Error(`pi-qol: unknown auto-compaction gate ${String(unknownGate)}`);
31
+ }
32
+ }
33
+ }
34
+
18
35
  export function statusMessage(ctx: ExtensionContext): string {
19
36
  const labels = attachmentLabels(currentEditorText(ctx), ctx.cwd);
20
37
  const searchShortcut = sessionSearchShortcut(ctx.cwd);
@@ -37,7 +54,8 @@ export function statusMessage(ctx: ExtensionContext): string {
37
54
  `Handoff prompt review: ${settingBoolean("handoffReviewPrompt", true, ctx.cwd) ? "enabled" : "disabled"}`,
38
55
  `Session search: ${settingBoolean("sessionSearch.enabled", true, ctx.cwd) ? `enabled (/search${searchShortcut ? `, ${searchShortcut}` : ""})` : "disabled"}`,
39
56
  `Custom compaction: ${settingBoolean("compaction.customEnabled", false, ctx.cwd) ? `enabled (${settingString("compaction.model", DEFAULT_COMPACTION_MODEL, ctx.cwd)}, ${compactionProfile(ctx.cwd)})` : "disabled (Pi default)"}`,
40
- `Idle compaction: ${settingBoolean("compaction.idleEnabled", false, ctx.cwd) ? `enabled after ${Math.max(1, Math.floor(settingNumber("compaction.idleTimeoutSeconds", DEFAULT_IDLE_COMPACTION_SECONDS, ctx.cwd)))}s idle` : "disabled"}`,
57
+ `Budget guard: ${autoCompactionStatus(autoCompactionGate("budgetGuard", ctx.cwd), () => `enabled (budgetPercent=${settingNumber("compaction.budgetPercent", DEFAULT_BUDGET_GUARD_PERCENT, ctx.cwd)}, budgetTokens=${settingNumber("compaction.budgetTokens", DEFAULT_BUDGET_GUARD_TOKENS, ctx.cwd)})`)}`,
58
+ `Idle compaction: ${autoCompactionStatus(autoCompactionGate("idle", ctx.cwd), () => `enabled after ${Math.max(1, Math.floor(settingNumber("compaction.idleTimeoutSeconds", DEFAULT_IDLE_COMPACTION_SECONDS, ctx.cwd)))}s idle`)}`,
41
59
  `Branch summary override: ${settingBoolean("compaction.branchSummaryEnabled", false, ctx.cwd) ? "enabled" : "disabled"}`,
42
60
  `Notifications: ${settingBoolean("notification.enabled", true, ctx.cwd) ? `enabled (bell=${settingBoolean("notification.bell", true, ctx.cwd)}, muteBell=${settingBoolean("notification.muteBellSound", false, ctx.cwd)}, native=${settingBoolean("notification.native", true, ctx.cwd)}, tmuxClientTty=${settingBoolean("notification.tmuxNativeClientTty", true, ctx.cwd)}, tmuxMessage=${settingBoolean("notification.tmux", false, ctx.cwd)})` : "disabled"}`,
43
61
  `Permission gate: ${settingBoolean("permissionGate.enabled", false, ctx.cwd) ? `enabled (${permissionGateCommands(ctx.cwd).join(", ") || "none configured"}; preview ${boundedSettingNumber("permissionGate.previewLines", DEFAULT_PERMISSION_GATE_PREVIEW_LINES, 4, 40, ctx.cwd)} lines/${boundedSettingNumber("permissionGate.previewChars", DEFAULT_PERMISSION_GATE_PREVIEW_CHARS, 200, 5000, ctx.cwd)} chars)` : "disabled"}`,
package/extensions/qol.ts CHANGED
@@ -5,6 +5,7 @@ import { getQuestionService, readCavemanBridge, type QuestionOpenedEventLike } f
5
5
  import { BudgetGuardDriver } from "./qol/budget-guard-runtime.js";
6
6
  import { ansiGreen } from "./qol/ansi.js";
7
7
  import {
8
+ autoCompactionGate,
8
9
  budgetGuardTrigger,
9
10
  compactionTriggerReason,
10
11
  handleQolBranchSummary,
@@ -334,7 +335,7 @@ export default function qol(pi: ExtensionAPI): void {
334
335
 
335
336
  const scheduleIdleCompaction = (ctx: ExtensionContext) => {
336
337
  clearIdleCompactionTimer();
337
- if (!settingBoolean("compaction.idleEnabled", false, ctx.cwd)) return;
338
+ if (autoCompactionGate("idle", ctx.cwd) !== "enabled") return;
338
339
  const reason = compactionTriggerReason(ctx);
339
340
  if (!reason) return;
340
341
  const delayMs = Math.max(1, Math.floor(settingNumber("compaction.idleTimeoutSeconds", DEFAULT_IDLE_COMPACTION_SECONDS, ctx.cwd))) * 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Pi quality-of-life extension: compact statusline/π prompt, reliable multiline input, styled pasted-image chips, session naming/search/context import, scheduled prompts, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -839,7 +839,7 @@
839
839
  {
840
840
  "key": "compaction.idleEnabled",
841
841
  "label": "Idle compaction trigger",
842
- "description": "Trigger Pi compaction after the session sits idle above the configured token threshold. Inspired by oh-my-pi idle compaction.",
842
+ "description": "Trigger Pi compaction after the session sits idle above the configured token threshold. Never fires while Pi's own compaction.enabled is false. Inspired by oh-my-pi idle compaction.",
843
843
  "type": "boolean",
844
844
  "default": false,
845
845
  "category": "Compaction Idle",
@@ -884,7 +884,7 @@
884
884
  {
885
885
  "key": "compaction.budgetGuardEnabled",
886
886
  "label": "Long-session budget guard",
887
- "description": "Start bounded compaction when an active session crosses the configured context budget, while giving Pi's built-in compaction priority. Designed for long autonomous runs so transcript growth cannot silently hit provider or buffer limits. Fires once per threshold crossing, not in a loop.",
887
+ "description": "Start bounded compaction when an active session crosses the configured context budget, while giving Pi's built-in compaction priority. Designed for long autonomous runs so transcript growth cannot silently hit provider or buffer limits. Fires once per threshold crossing, not in a loop. Never fires while Pi's own compaction.enabled is false.",
888
888
  "type": "boolean",
889
889
  "default": true,
890
890
  "category": "Compaction Budget Guard",