From bf862656aee5fb46d1398ec7298124c6eb35a147 Mon Sep 17 00:00:00 2001 From: GRayHook Date: Wed, 17 Jun 2026 11:47:27 +0700 Subject: [PATCH] =?UTF-8?q?Phase=206:=20add=20compaction=20coexistence=20h?= =?UTF-8?q?int=20=E2=80=94=20one-time=20notify=20when=20both=20compactors?= =?UTF-8?q?=20run.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows ui.notify once when pi-auto-compact is detected while Pi compaction.enabled stays true. Co-authored-by: Cursor --- src/auto-compact.ts | 57 ++++++++++++++++++++++++++++++++++++++- src/state.ts | 3 +++ test/auto-compact.test.ts | 53 ++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/auto-compact.ts b/src/auto-compact.ts index a80df6b..5981d02 100644 --- a/src/auto-compact.ts +++ b/src/auto-compact.ts @@ -1,4 +1,5 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent"; import { effectiveIntegration, type ReinjectDeliveryMode, @@ -33,3 +34,57 @@ export const PI_AUTO_COMPACT_FOLLOW_UP_PREFIXES = [ "Emergency auto-compact ran.", "Auto-compact ran on session resume.", ] as const; + +const PI_DEFAULT_COMPACTION_ENABLED = true; + +function readCompactionEnabled(settings: object): boolean | undefined { + const compaction = (settings as Record).compaction; + if (!compaction || typeof compaction !== "object" || Array.isArray(compaction)) { + return undefined; + } + const enabled = (compaction as Record).enabled; + return typeof enabled === "boolean" ? enabled : undefined; +} + +/** Merged Pi compaction.enabled with project-over-global semantics (SPEC §16.7). */ +export function resolvePiDefaultCompactionEnabled( + globalSettings: object, + projectSettings: object, +): boolean { + return ( + readCompactionEnabled(projectSettings) ?? + readCompactionEnabled(globalSettings) ?? + PI_DEFAULT_COMPACTION_ENABLED + ); +} + +export function isPiDefaultCompactionEnabled(ctx: ExtensionContext): boolean { + const manager = SettingsManager.create(ctx.cwd, getAgentDir(), { + projectTrusted: ctx.isProjectTrusted(), + }); + return resolvePiDefaultCompactionEnabled( + manager.getGlobalSettings(), + manager.getProjectSettings(), + ); +} + +/** One-time ui.notify when Pi default compaction and pi-auto-compact are both active (SPEC §16.7). */ +export function maybeNotifyCompactionCoexistenceHint( + ctx: ExtensionContext, + runtime: RuntimeFlags, + piDefaultCompactionEnabled = isPiDefaultCompactionEnabled(ctx), +): void { + if (runtime.compactionCoexistenceHintShown || !runtime.autoCompactDetected) { + return; + } + if (!piDefaultCompactionEnabled) { + return; + } + if (ctx.hasUI) { + ctx.ui.notify( + "Pi built-in auto-compaction and pi-auto-compact are both enabled. Consider setting compaction.enabled to false in settings.json.", + "info", + ); + } + runtime.compactionCoexistenceHintShown = true; +} diff --git a/src/state.ts b/src/state.ts index 89d7bb0..e38fd26 100644 --- a/src/state.ts +++ b/src/state.ts @@ -31,6 +31,8 @@ export interface ExtensionState { export interface RuntimeFlags { autoCompactDetected: boolean; autoCompactIntegration: AutoCompactIntegration; + /** One-time hint when Pi default compaction coexists with pi-auto-compact (SPEC §16.7). */ + compactionCoexistenceHintShown: boolean; } export const STATE_ENTRY_TYPE = "skill-reinject:state"; @@ -84,6 +86,7 @@ export function createRuntimeFlags(): RuntimeFlags { return { autoCompactDetected: false, autoCompactIntegration: "auto", + compactionCoexistenceHintShown: false, }; } diff --git a/test/auto-compact.test.ts b/test/auto-compact.test.ts index a106621..0a0d1ad 100644 --- a/test/auto-compact.test.ts +++ b/test/auto-compact.test.ts @@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest"; import { detectAndCachePiAutoCompact, detectPiAutoCompact, + maybeNotifyCompactionCoexistenceHint, PI_AUTO_COMPACT_FOLLOW_UP_PREFIXES, resolveDeliveryMode, + resolvePiDefaultCompactionEnabled, } from "../src/auto-compact"; import { createDefaultSettings } from "../src/settings"; import { createRuntimeFlags } from "../src/state"; @@ -53,3 +55,54 @@ describe("PI_AUTO_COMPACT_FOLLOW_UP_PREFIXES", () => { ]); }); }); + +describe("resolvePiDefaultCompactionEnabled", () => { + it("defaults to enabled when compaction settings are absent", () => { + expect(resolvePiDefaultCompactionEnabled({}, {})).toBe(true); + }); + + it("lets project override global compaction.enabled", () => { + expect( + resolvePiDefaultCompactionEnabled( + { compaction: { enabled: true } }, + { compaction: { enabled: false } }, + ), + ).toBe(false); + }); +}); + +describe("maybeNotifyCompactionCoexistenceHint", () => { + it("notifies once when pi-auto-compact and Pi compaction are both active", () => { + const runtime = createRuntimeFlags(); + runtime.autoCompactDetected = true; + const notifications: string[] = []; + const ctx = { + hasUI: true, + ui: { + notify: (message: string) => { + notifications.push(message); + }, + }, + }; + + maybeNotifyCompactionCoexistenceHint(ctx as never, runtime, true); + maybeNotifyCompactionCoexistenceHint(ctx as never, runtime, true); + + expect(notifications).toHaveLength(1); + expect(runtime.compactionCoexistenceHintShown).toBe(true); + }); + + it("skips notify when pi-auto-compact is not detected", () => { + const runtime = createRuntimeFlags(); + const notifications: string[] = []; + const ctx = { + hasUI: true, + ui: { notify: (message: string) => notifications.push(message) }, + }; + + maybeNotifyCompactionCoexistenceHint(ctx as never, runtime, true); + + expect(notifications).toHaveLength(0); + expect(runtime.compactionCoexistenceHintShown).toBe(false); + }); +});