Skip to content

Commit 6421715

Browse files
committed
feat(pi-ui-tweaks): preserve compaction history
1 parent 72dcf42 commit 6421715

8 files changed

Lines changed: 217 additions & 1 deletion

File tree

‎packages/pi-ui-tweaks/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Configure global settings at `~/.pi/agent/pi-ui-tweaks/config.json`.
3333
| `pasteCollapseExpandKey` | `string \| null` | `null` | Extra Pi key id for expanding the marker under cursor. |
3434
| `pasteCollapseLineThreshold` | `number` | `10` | Collapses pastes with more than this many lines. |
3535
| `pasteCollapseUseToolExpandKey` | `boolean` | `true` | Lets Pi's expand key expand paste markers. |
36+
| `preserveCompactionHistory` | `boolean` | `false` | Keeps the live terminal transcript after compaction. |
3637
| `restoreContentAfterAutocompleteClose` | `boolean` | `true` | Redraws content after above-input autocomplete. |
3738
| `selectedOptionPrefix` | `string` | `"→ "` | Sets the selected-row marker in selector UIs. |
3839

@@ -56,6 +57,7 @@ Configure global settings at `~/.pi/agent/pi-ui-tweaks/config.json`.
5657
"pasteCollapseExpandKey": null,
5758
"pasteCollapseLineThreshold": 10,
5859
"pasteCollapseUseToolExpandKey": true,
60+
"preserveCompactionHistory": false,
5961
"restoreContentAfterAutocompleteClose": true,
6062
"selectedOptionPrefix": "→ "
6163
}

‎packages/pi-ui-tweaks/config.schema.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"pasteCollapseLineThreshold": { "type": "integer", "minimum": 0, "default": 10 },
3535
"pasteCollapseUseToolExpandKey": { "type": "boolean", "default": true },
36+
"preserveCompactionHistory": { "type": "boolean", "default": false },
3637
"restoreContentAfterAutocompleteClose": { "type": "boolean", "default": true },
3738
"selectedOptionPrefix": { "type": "string", "minLength": 1, "default": "→ " }
3839
}

‎packages/pi-ui-tweaks/src/index.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ import {
3131
installPasteCollapsePatch,
3232
setPasteCollapseSettings,
3333
} from "./paste-collapse.ts";
34+
import {
35+
installPreserveCompactionHistoryPatch,
36+
setPreserveCompactionHistory,
37+
} from "./preserve-compaction-history.ts";
3438
import { installRenderTracePatch } from "./render-trace.ts";
3539
import {
3640
installSelectedOptionPrefixSelectListPatch,
@@ -75,6 +79,7 @@ function applyUiTweaksConfig(ctx: ExtensionContext): void {
7579
pasteCollapseLineThreshold: loaded.config.pasteCollapseLineThreshold,
7680
pasteCollapseUseToolExpandKey: loaded.config.pasteCollapseUseToolExpandKey,
7781
});
82+
setPreserveCompactionHistory(loaded.config.preserveCompactionHistory);
7883
setRestoreContentAfterAutocompleteClose(loaded.config.restoreContentAfterAutocompleteClose);
7984
setSelectedOptionPrefix(loaded.config.selectedOptionPrefix);
8085
reportConfigErrors(ctx, loaded);
@@ -92,6 +97,7 @@ export default function uiTweaksExtension(pi: ExtensionAPI): void {
9297
void installModelSelectorProviderBadgePatch();
9398
installModelStatusPatch();
9499
installPasteCollapsePatch();
100+
installPreserveCompactionHistoryPatch();
95101
installSelectedOptionPrefixSelectListPatch();
96102
installSlashCommandSourcePatch();
97103
void installNeutralBorderColorPatch();

‎packages/pi-ui-tweaks/src/patch-state.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type UiTweaksPatchState = {
2727
pasteCollapseCharThreshold: number;
2828
pasteCollapseExpandKey: string | null;
2929
pasteCollapseUseToolExpandKey: boolean;
30+
preserveCompactionHistory: boolean;
3031
};
3132

3233
const DEFAULT_UI_TWEAKS_PATCH_STATE: UiTweaksPatchState = {
@@ -48,6 +49,7 @@ const DEFAULT_UI_TWEAKS_PATCH_STATE: UiTweaksPatchState = {
4849
pasteCollapseCharThreshold: DEFAULT_PASTE_COLLAPSE_CHAR_THRESHOLD,
4950
pasteCollapseExpandKey: DEFAULT_PASTE_COLLAPSE_EXPAND_KEY,
5051
pasteCollapseUseToolExpandKey: DEFAULT_PASTE_COLLAPSE_USE_TOOL_EXPAND_KEY,
52+
preserveCompactionHistory: false,
5153
};
5254

5355
function isUiTweaksPatchState(value: unknown): value is UiTweaksPatchState {
@@ -74,7 +76,8 @@ function isUiTweaksPatchState(value: unknown): value is UiTweaksPatchState {
7476
typeof Reflect.get(value, "pasteCollapseCharThreshold") === "number" &&
7577
(typeof Reflect.get(value, "pasteCollapseExpandKey") === "string" ||
7678
Reflect.get(value, "pasteCollapseExpandKey") === null) &&
77-
typeof Reflect.get(value, "pasteCollapseUseToolExpandKey") === "boolean"
79+
typeof Reflect.get(value, "pasteCollapseUseToolExpandKey") === "boolean" &&
80+
typeof Reflect.get(value, "preserveCompactionHistory") === "boolean"
7881
);
7982
}
8083

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { InteractiveMode } from "@earendil-works/pi-coding-agent";
2+
3+
import { getUiTweaksPatchState } from "./patch-state.ts";
4+
5+
const PRESERVE_COMPACTION_HISTORY_PATCH_KEY = Symbol.for(
6+
"zigai.pi-ui-tweaks.preserve-compaction-history-patched",
7+
);
8+
9+
type CompactionEvent = {
10+
readonly type?: unknown;
11+
readonly aborted?: unknown;
12+
readonly result?: unknown;
13+
};
14+
15+
type ChatContainer = {
16+
clear: () => void;
17+
};
18+
19+
type InteractiveModeCompactionTarget = {
20+
[PRESERVE_COMPACTION_HISTORY_PATCH_KEY]?: true;
21+
chatContainer: ChatContainer;
22+
handleEvent: (event: unknown) => Promise<void>;
23+
rebuildChatFromMessages: () => void;
24+
};
25+
26+
function isObject(value: unknown): value is object {
27+
return (typeof value === "object" && value !== null) || typeof value === "function";
28+
}
29+
30+
function isSuccessfulCompaction(event: unknown): event is CompactionEvent {
31+
if (!isObject(event)) {
32+
return false;
33+
}
34+
35+
return (
36+
Reflect.get(event, "type") === "compaction_end" &&
37+
Reflect.get(event, "aborted") === false &&
38+
Reflect.get(event, "result") !== undefined
39+
);
40+
}
41+
42+
function isInteractiveModeCompactionTarget(
43+
value: unknown,
44+
): value is InteractiveModeCompactionTarget {
45+
if (!isObject(value)) {
46+
return false;
47+
}
48+
49+
const chatContainer: unknown = Reflect.get(value, "chatContainer") as unknown;
50+
return (
51+
isObject(chatContainer) &&
52+
typeof Reflect.get(chatContainer, "clear") === "function" &&
53+
typeof Reflect.get(value, "rebuildChatFromMessages") === "function"
54+
);
55+
}
56+
57+
function warnPreserveCompactionHistoryPatchUnavailable(reason?: string): void {
58+
let suffix = "";
59+
if (reason !== undefined && reason.length > 0) {
60+
suffix = `: ${reason}`;
61+
}
62+
63+
console.warn(
64+
`[pi-ui-tweaks] preserve compaction history patch unavailable; Pi internals may have changed${suffix}`,
65+
);
66+
}
67+
68+
/**
69+
* Sets whether successful live compactions retain the existing rendered transcript.
70+
*/
71+
export function setPreserveCompactionHistory(enabled: boolean): void {
72+
getUiTweaksPatchState().preserveCompactionHistory = enabled;
73+
}
74+
75+
/**
76+
* Installs an idempotent patch that preserves rendered history during live compaction.
77+
*/
78+
export function installPreserveCompactionHistoryPatch(prototype?: unknown): void {
79+
const prototypeValue: unknown = prototype ?? InteractiveMode.prototype;
80+
if (!isObject(prototypeValue)) {
81+
warnPreserveCompactionHistoryPatchUnavailable();
82+
return;
83+
}
84+
85+
const originalHandleEventValue: unknown = Reflect.get(prototypeValue, "handleEvent") as unknown;
86+
if (typeof originalHandleEventValue !== "function") {
87+
warnPreserveCompactionHistoryPatchUnavailable("missing handleEvent");
88+
return;
89+
}
90+
91+
// SAFETY: The runtime guard verifies the prototype method before the private
92+
// InteractiveMode adapter is narrowed to the patch target.
93+
const targetPrototype = prototypeValue as InteractiveModeCompactionTarget;
94+
if (targetPrototype[PRESERVE_COMPACTION_HISTORY_PATCH_KEY] === true) {
95+
return;
96+
}
97+
98+
// SAFETY: InteractiveMode.handleEvent is an async runtime method. The wrapper
99+
// forwards its event unchanged and restores temporary method replacements.
100+
const originalHandleEvent =
101+
originalHandleEventValue as InteractiveModeCompactionTarget["handleEvent"];
102+
103+
targetPrototype.handleEvent = async function patchedHandleEvent(
104+
this: InteractiveModeCompactionTarget,
105+
event: unknown,
106+
): Promise<void> {
107+
if (
108+
!getUiTweaksPatchState().preserveCompactionHistory ||
109+
!isSuccessfulCompaction(event) ||
110+
!isInteractiveModeCompactionTarget(this)
111+
) {
112+
return originalHandleEvent.call(this, event);
113+
}
114+
115+
const originalClear = this.chatContainer.clear;
116+
const originalRebuildChatFromMessages = this.rebuildChatFromMessages;
117+
this.chatContainer.clear = () => {};
118+
this.rebuildChatFromMessages = () => {};
119+
120+
try {
121+
await originalHandleEvent.call(this, event);
122+
} finally {
123+
this.chatContainer.clear = originalClear;
124+
this.rebuildChatFromMessages = originalRebuildChatFromMessages;
125+
}
126+
};
127+
128+
targetPrototype[PRESERVE_COMPACTION_HISTORY_PATCH_KEY] = true;
129+
}

‎packages/pi-ui-tweaks/src/settings.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type UiTweaksConfig = {
3333
readonly pasteCollapseExpandKey: string | null;
3434
readonly pasteCollapseLineThreshold: number;
3535
readonly pasteCollapseUseToolExpandKey: boolean;
36+
readonly preserveCompactionHistory: boolean;
3637
readonly restoreContentAfterAutocompleteClose: boolean;
3738
readonly selectedOptionPrefix: string;
3839
};
@@ -66,6 +67,7 @@ type UiTweaksSettings = {
6667
pasteCollapseExpandKey?: string | null;
6768
pasteCollapseLineThreshold?: number;
6869
pasteCollapseUseToolExpandKey?: boolean;
70+
preserveCompactionHistory?: boolean;
6971
restoreContentAfterAutocompleteClose?: boolean;
7072
selectedOptionPrefix?: string;
7173
};
@@ -98,6 +100,7 @@ const UiTweaksConfigSchema = Type.Object(
98100
pasteCollapseExpandKey: Type.Optional(OptionalPasteCollapseExpandKeySchema),
99101
pasteCollapseLineThreshold: Type.Optional(Type.Integer({ minimum: 0 })),
100102
pasteCollapseUseToolExpandKey: Type.Optional(Type.Boolean()),
103+
preserveCompactionHistory: Type.Optional(Type.Boolean()),
101104
restoreContentAfterAutocompleteClose: Type.Optional(Type.Boolean()),
102105
selectedOptionPrefix: Type.Optional(Type.String({ minLength: 1 })),
103106
},
@@ -123,6 +126,7 @@ const DEFAULT_UI_TWEAKS_CONFIG: UiTweaksConfig = {
123126
pasteCollapseExpandKey: DEFAULT_PASTE_COLLAPSE_EXPAND_KEY,
124127
pasteCollapseLineThreshold: DEFAULT_PASTE_COLLAPSE_LINE_THRESHOLD,
125128
pasteCollapseUseToolExpandKey: DEFAULT_PASTE_COLLAPSE_USE_TOOL_EXPAND_KEY,
129+
preserveCompactionHistory: false,
126130
restoreContentAfterAutocompleteClose: true,
127131
selectedOptionPrefix: "→ ",
128132
};
@@ -196,6 +200,7 @@ function buildUiTweaksConfig(settings: UiTweaksSettings): UiTweaksConfig {
196200
pasteCollapseExpandKey: DEFAULT_UI_TWEAKS_CONFIG.pasteCollapseExpandKey,
197201
pasteCollapseLineThreshold: DEFAULT_UI_TWEAKS_CONFIG.pasteCollapseLineThreshold,
198202
pasteCollapseUseToolExpandKey: false,
203+
preserveCompactionHistory: false,
199204
restoreContentAfterAutocompleteClose: false,
200205
selectedOptionPrefix: DEFAULT_UI_TWEAKS_CONFIG.selectedOptionPrefix,
201206
};
@@ -239,6 +244,9 @@ function buildUiTweaksConfig(settings: UiTweaksSettings): UiTweaksConfig {
239244
pasteCollapseUseToolExpandKey:
240245
settings.pasteCollapseUseToolExpandKey ??
241246
DEFAULT_UI_TWEAKS_CONFIG.pasteCollapseUseToolExpandKey,
247+
preserveCompactionHistory:
248+
settings.preserveCompactionHistory ??
249+
DEFAULT_UI_TWEAKS_CONFIG.preserveCompactionHistory,
242250
restoreContentAfterAutocompleteClose:
243251
settings.restoreContentAfterAutocompleteClose ??
244252
DEFAULT_UI_TWEAKS_CONFIG.restoreContentAfterAutocompleteClose,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "vitest";
3+
4+
import {
5+
installPreserveCompactionHistoryPatch,
6+
setPreserveCompactionHistory,
7+
} from "../src/preserve-compaction-history.ts";
8+
9+
class FakeInteractiveMode {
10+
clearCount = 0;
11+
rebuildCount = 0;
12+
summaryCount = 0;
13+
chatContainer = {
14+
clear: () => {
15+
this.clearCount += 1;
16+
},
17+
};
18+
19+
async handleEvent(event: unknown): Promise<void> {
20+
if (
21+
typeof event === "object" &&
22+
event !== null &&
23+
Reflect.get(event, "type") === "compaction_end" &&
24+
Reflect.get(event, "result") !== undefined
25+
) {
26+
this.chatContainer.clear();
27+
this.rebuildChatFromMessages();
28+
this.summaryCount += 1;
29+
}
30+
}
31+
32+
rebuildChatFromMessages(): void {
33+
this.rebuildCount += 1;
34+
}
35+
}
36+
37+
test("preserve compaction history leaves successful live compaction UI intact", async () => {
38+
setPreserveCompactionHistory(true);
39+
installPreserveCompactionHistoryPatch(FakeInteractiveMode.prototype);
40+
41+
const mode = new FakeInteractiveMode();
42+
await mode.handleEvent({ type: "compaction_end", aborted: false, result: {} });
43+
44+
assert.equal(mode.clearCount, 0);
45+
assert.equal(mode.rebuildCount, 0);
46+
assert.equal(mode.summaryCount, 1);
47+
setPreserveCompactionHistory(false);
48+
});
49+
50+
test("preserve compaction history keeps Pi's normal redraw when disabled", async () => {
51+
setPreserveCompactionHistory(false);
52+
installPreserveCompactionHistoryPatch(FakeInteractiveMode.prototype);
53+
54+
const mode = new FakeInteractiveMode();
55+
await mode.handleEvent({ type: "compaction_end", aborted: false, result: {} });
56+
57+
assert.equal(mode.clearCount, 1);
58+
assert.equal(mode.rebuildCount, 1);
59+
assert.equal(mode.summaryCount, 1);
60+
setPreserveCompactionHistory(false);
61+
});

‎packages/pi-ui-tweaks/test/settings.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ test("loadUiTweaksConfig scaffolds missing global config and schema", async () =
3838
pasteCollapseExpandKey: null,
3939
pasteCollapseLineThreshold: 10,
4040
pasteCollapseUseToolExpandKey: true,
41+
preserveCompactionHistory: false,
4142
restoreContentAfterAutocompleteClose: true,
4243
selectedOptionPrefix: "→ ",
4344
});
@@ -81,6 +82,7 @@ test("ui tweaks settings default to enabled tweaks", () => {
8182
assert.equal(loaded.config.pasteCollapseExpandKey, null);
8283
assert.equal(loaded.config.pasteCollapseLineThreshold, 10);
8384
assert.equal(loaded.config.pasteCollapseUseToolExpandKey, true);
85+
assert.equal(loaded.config.preserveCompactionHistory, false);
8486
assert.equal(loaded.config.restoreContentAfterAutocompleteClose, true);
8587
assert.equal(loaded.config.selectedOptionPrefix, "→ ");
8688
assert.deepEqual(loaded.errors, []);
@@ -107,6 +109,7 @@ test("ui tweaks settings merge sources in precedence order", () => {
107109
pasteCollapseExpandKey: "ctrl+shift+o",
108110
pasteCollapseLineThreshold: 20,
109111
pasteCollapseUseToolExpandKey: false,
112+
preserveCompactionHistory: false,
110113
restoreContentAfterAutocompleteClose: false,
111114
selectedOptionPrefix: "❯ ",
112115
},
@@ -128,6 +131,7 @@ test("ui tweaks settings merge sources in precedence order", () => {
128131
pasteCollapseExpandKey: null,
129132
pasteCollapseLineThreshold: 5,
130133
pasteCollapseUseToolExpandKey: true,
134+
preserveCompactionHistory: true,
131135
restoreContentAfterAutocompleteClose: true,
132136
selectedOptionPrefix: "▌",
133137
},
@@ -150,6 +154,7 @@ test("ui tweaks settings merge sources in precedence order", () => {
150154
assert.equal(loaded.config.pasteCollapseExpandKey, null);
151155
assert.equal(loaded.config.pasteCollapseLineThreshold, 5);
152156
assert.equal(loaded.config.pasteCollapseUseToolExpandKey, true);
157+
assert.equal(loaded.config.preserveCompactionHistory, true);
153158
assert.equal(loaded.config.restoreContentAfterAutocompleteClose, true);
154159
assert.equal(loaded.config.selectedOptionPrefix, "▌");
155160
});
@@ -183,6 +188,7 @@ test("ui tweaks enabled false disables every tweak", () => {
183188
assert.equal(loaded.config.pasteCollapseExpandKey, null);
184189
assert.equal(loaded.config.pasteCollapseLineThreshold, 10);
185190
assert.equal(loaded.config.pasteCollapseUseToolExpandKey, false);
191+
assert.equal(loaded.config.preserveCompactionHistory, false);
186192
assert.equal(loaded.config.restoreContentAfterAutocompleteClose, false);
187193
assert.equal(loaded.config.selectedOptionPrefix, "→ ");
188194
});

0 commit comments

Comments
 (0)