@howaboua/pi-codex-conversion 1.5.10 → 1.5.11-dev.46.3146d2f

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.11
4
+
5
+ - Aligned the custom OpenAI Codex provider and Pi development dependencies with Pi 0.75.4.
6
+ - Added Codex context-budget alignment so Pi auto-compaction for OpenAI Codex subscription models triggers near Codex's native 90% compacting threshold.
7
+ - Improved native Responses compaction fallback: failed native compactions now fall back to Pi compaction, and reuse the previous native compacted window when available.
8
+ - Pruned low-value compatibility tests while keeping focused coverage for adapter activation, native tools, compaction fallback, and Codex context budgeting.
9
+
3
10
  ## 1.5.10
4
11
 
5
12
  - Added `/codex usage` and a Usage tab for OpenAI Codex subscription limits, with automatic refresh and aligned 5-hour/weekly usage columns.
package/README.md CHANGED
@@ -9,7 +9,7 @@ GPT/Codex models are strongest when the tool surface looks like the Codex CLI th
9
9
 
10
10
  The point is to give the model tools it already knows how to use well: shell-first inspection, resumable command sessions, and large one-shot patch edits instead of piecemeal read/edit/write steps.
11
11
 
12
- You can also opt into using the adapter on every provider/model. YMMV: Codex-tuned models are still the best fit, but the shell/patch workflow can help elsewhere too. The extension also has a small `/codex` settings UI for toggling adapter behavior, web search, image generation, fast mode, and verbosity. See [Settings](#settings).
12
+ You can also opt into using the adapter on every provider/model. YMMV: Codex-tuned models are still the best fit, but the shell/patch workflow can help elsewhere too. The extension also has a small `/codex` settings UI for toggling adapter behavior, web search, image generation, fast mode, native Responses compaction, and verbosity. See [Settings](#settings).
13
13
 
14
14
  ## Install
15
15
 
@@ -55,6 +55,10 @@ The settings UI also has **Usage**, **Overrides**, and **About** tabs. **Usage**
55
55
 
56
56
  - add only the Pi `apply_patch` tool for GPT/Codex models while keeping Pi's default toolkit, prompt, provider behavior, and compaction flow
57
57
 
58
+ The **Compaction** tab can enable native OpenAI Responses compaction and choose the compaction model/reasoning. If native compaction fails, the extension falls back to Pi's normal compaction flow; when an older native compacted window exists, it is included in that Pi fallback summarization request so OpenAI can still use the prior opaque context server-side.
59
+
60
+ For OpenAI Codex subscription models, the extension also adjusts Pi's registered model context windows so Pi's fixed reserve-token compaction heuristic trips at roughly Codex's native auto-compact budget: 90% of Pi's resolved model window. This is calculated from Pi's current model metadata instead of hardcoded per-model limits.
61
+
58
62
  When `all` is on, non-Codex providers get the shell, patch, skill, and prompt-adapter behavior, but keep their normal Pi provider path. Native web search, native image generation, and priority service tier stay limited to the OpenAI Codex provider. Verbosity is applied to Responses API providers.
59
63
 
60
64
  The footer shows the active state, for example:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.5.10",
3
+ "version": "1.5.11-dev.46.3146d2f",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -60,9 +60,10 @@
60
60
  "typebox": "*"
61
61
  },
62
62
  "devDependencies": {
63
- "@earendil-works/pi-ai": "^0.75.3",
64
- "@earendil-works/pi-coding-agent": "^0.75.3",
65
- "@earendil-works/pi-tui": "^0.75.3",
63
+ "@earendil-works/pi-agent-core": "^0.75.4",
64
+ "@earendil-works/pi-ai": "^0.75.4",
65
+ "@earendil-works/pi-coding-agent": "^0.75.4",
66
+ "@earendil-works/pi-tui": "^0.75.4",
66
67
  "tsx": "^4.20.5",
67
68
  "typebox": "^1.1.24",
68
69
  "typescript": "^5.9.3"
@@ -0,0 +1,93 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import type { AdapterState } from "./state.ts";
6
+
7
+ const OPENAI_CODEX_PROVIDER = "openai-codex";
8
+ const OPENAI_CODEX_API = "openai-codex-responses";
9
+ const CODEX_AUTO_COMPACT_NUMERATOR = 9;
10
+ const CODEX_AUTO_COMPACT_DENOMINATOR = 10;
11
+ const PI_DEFAULT_COMPACTION_RESERVE_TOKENS = 16_384;
12
+
13
+ export function getCodexAutoCompactBudget(contextWindow: number): number {
14
+ return Math.floor((contextWindow * CODEX_AUTO_COMPACT_NUMERATOR) / CODEX_AUTO_COMPACT_DENOMINATOR);
15
+ }
16
+
17
+ export function getPiContextWindowForCodexAutoCompact(contextWindow: number, reserveTokens = PI_DEFAULT_COMPACTION_RESERVE_TOKENS): number {
18
+ return Math.min(contextWindow, getCodexAutoCompactBudget(contextWindow) + reserveTokens);
19
+ }
20
+
21
+ function readSettings(path: string): Record<string, unknown> | undefined {
22
+ if (!existsSync(path)) return undefined;
23
+ try {
24
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
25
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ function getReserveTokens(settings: Record<string, unknown> | undefined): number | undefined {
32
+ const compaction = settings?.compaction;
33
+ if (!compaction || typeof compaction !== "object" || Array.isArray(compaction)) return undefined;
34
+ const reserveTokens = (compaction as { reserveTokens?: unknown }).reserveTokens;
35
+ return typeof reserveTokens === "number" && Number.isFinite(reserveTokens) && reserveTokens >= 0 ? reserveTokens : undefined;
36
+ }
37
+
38
+ export function readPiCompactionReserveTokens(cwd: string): number {
39
+ return (
40
+ getReserveTokens(readSettings(join(cwd, ".pi", "settings.json"))) ??
41
+ getReserveTokens(readSettings(join(getAgentDir(), "settings.json"))) ??
42
+ PI_DEFAULT_COMPACTION_RESERVE_TOKENS
43
+ );
44
+ }
45
+
46
+ export function shouldUseCodexContextBudgetModel(model: Model<Api> | undefined): model is Model<Api> {
47
+ return model?.provider === OPENAI_CODEX_PROVIDER && model.api === OPENAI_CODEX_API;
48
+ }
49
+
50
+ function getModelKey(model: Model<Api>): string {
51
+ return `${model.provider}:${model.api}:${model.id}`;
52
+ }
53
+
54
+ function resolveRawContextWindow<TApi extends Api>(model: Model<TApi>, state: AdapterState | undefined): number {
55
+ if (!state) return model.contextWindow;
56
+ const key = getModelKey(model);
57
+ const cachedRaw = state.codexContextBudgetRawWindows?.[key];
58
+ if (cachedRaw === undefined) {
59
+ state.codexContextBudgetRawWindows ??= {};
60
+ state.codexContextBudgetRawWindows[key] = model.contextWindow;
61
+ return model.contextWindow;
62
+ }
63
+
64
+ const cachedAdjusted = getPiContextWindowForCodexAutoCompact(cachedRaw, state.codexContextBudgetReserveTokens);
65
+ const previousAdjusted = state.codexContextBudgetAdjustedWindows?.[key];
66
+ if (model.contextWindow !== cachedRaw && model.contextWindow !== cachedAdjusted && model.contextWindow !== previousAdjusted) {
67
+ state.codexContextBudgetRawWindows ??= {};
68
+ state.codexContextBudgetRawWindows[key] = model.contextWindow;
69
+ delete state.codexContextBudgetAdjustedWindows?.[key];
70
+ return model.contextWindow;
71
+ }
72
+ return cachedRaw;
73
+ }
74
+
75
+ export function getCodexContextBudgetAdjustedModel<TApi extends Api>(model: Model<TApi>, state?: AdapterState): Model<TApi> {
76
+ if (!shouldUseCodexContextBudgetModel(model)) return model;
77
+ const rawContextWindow = resolveRawContextWindow(model, state);
78
+ const contextWindow = getPiContextWindowForCodexAutoCompact(rawContextWindow, state?.codexContextBudgetReserveTokens);
79
+ if (state) {
80
+ state.codexContextBudgetAdjustedWindows ??= {};
81
+ state.codexContextBudgetAdjustedWindows[getModelKey(model)] = contextWindow;
82
+ }
83
+ return contextWindow === model.contextWindow ? model : { ...model, contextWindow };
84
+ }
85
+
86
+ export function applyCodexContextBudgetToModel<TApi extends Api>(model: Model<TApi> | undefined, state: AdapterState): void {
87
+ if (!model) return;
88
+ state.codexContextBudgetReserveTokens ??= readPiCompactionReserveTokens(state.cwd);
89
+ const adjustedModel = getCodexContextBudgetAdjustedModel(model, state);
90
+ if (adjustedModel !== model) {
91
+ model.contextWindow = adjustedModel.contextWindow;
92
+ }
93
+ }
@@ -2,9 +2,9 @@ import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent } from "
2
2
  import { clampThinkingLevel, type ModelThinkingLevel, type Tool } from "@earendil-works/pi-ai";
3
3
  import { executeNativeCompaction } from "./compact-client.ts";
4
4
  import { extractCompactionSummaryText, hasCompactionOutputItem, sanitizeCompactedWindow, summarizeCompactionOutputForDiagnostics } from "./compaction-output.ts";
5
- import { resolveLatestNativeCompactionEntry } from "./details-store.ts";
5
+ import { findLatestNativeCompactionEntry, findLatestNativeCompactionEntryIndex, resolveLatestNativeCompactionEntry } from "./details-store.ts";
6
6
  import { rewriteResponsesPayloadWithNativeReplay, serializeLiveTailToResponsesInput } from "./payload-rewrite.ts";
7
- import { resolveNativeCompactionEnvironment } from "./compaction-runtime.ts";
7
+ import { isResponsesCompatiblePayload, resolveNativeCompactionEnvironment, type ResponsesCompatibleRequestPayload } from "./compaction-runtime.ts";
8
8
  import { convertResponsesTools } from "../providers/openai-responses-shared.ts";
9
9
  import {
10
10
  serializeCompactionPreparationToRequest,
@@ -12,7 +12,7 @@ import {
12
12
  type NativeCompactionRequestOptions,
13
13
  type ResponsesInputItem,
14
14
  } from "./serializer.ts";
15
- import { createNativeCompactionDetails, createNativeCompactionShimResult, isNativeCompactionDetails, NATIVE_COMPACTION_SHIM_SUMMARY } from "./types.ts";
15
+ import { createNativeCompactionDetails, createNativeCompactionShimResult, isNativeCompactionDetails, NATIVE_COMPACTION_SHIM_SUMMARY, type NativeCompactionEntry } from "./types.ts";
16
16
  import { isOpenAICodexContext, isResponsesContext } from "./codex-model.ts";
17
17
  import { shouldUseCodexAdapter } from "./activation.ts";
18
18
  import type { AdapterState } from "./state.ts";
@@ -23,6 +23,31 @@ function isRecord(value: unknown): value is Record<string, unknown> {
23
23
  return !!value && typeof value === "object" && !Array.isArray(value);
24
24
  }
25
25
 
26
+ function stashLatestNativeWindowForPiCompactionFallback(
27
+ ctx: ExtensionContext,
28
+ branchEntries: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>,
29
+ runtime: { provider: string; api: string; baseUrl: string },
30
+ state: AdapterState,
31
+ ): boolean {
32
+ state.pendingPiCompactionNativeWindow = undefined;
33
+ const nativeEntry = findLatestNativeCompactionEntry(branchEntries, {
34
+ provider: runtime.provider,
35
+ api: runtime.api,
36
+ baseUrl: runtime.baseUrl,
37
+ });
38
+ const compactedWindow = cloneCompactedWindow(nativeEntry?.details?.compactedWindow ?? []);
39
+ if (!compactedWindow || compactedWindow.length === 0) return false;
40
+ state.pendingPiCompactionNativeWindow = {
41
+ window: compactedWindow,
42
+ provider: runtime.provider,
43
+ api: runtime.api,
44
+ baseUrl: runtime.baseUrl,
45
+ sessionId: ctx.sessionManager.getSessionId(),
46
+ sourceCompactionEntryId: nativeEntry?.id,
47
+ };
48
+ return true;
49
+ }
50
+
26
51
  function cloneCompactedWindow(window: readonly unknown[]): ResponsesInputItem[] | undefined {
27
52
  if (!window.every(isRecord)) return undefined;
28
53
  return window.map((item) => structuredClone(item));
@@ -70,12 +95,20 @@ function clampCodexReasoningEffort(modelId: string, effort: string): string {
70
95
  return effort;
71
96
  }
72
97
 
98
+ const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
99
+
100
+ function clampOpenAIPromptCacheKey(key: string): string {
101
+ const chars = Array.from(key);
102
+ if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return key;
103
+ return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
104
+ }
105
+
73
106
  function buildCompactionRequestOptions(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState, compactionModel: string): NativeCompactionRequestOptions {
74
107
  const tools = buildCompactionTools(pi, ctx, state);
75
108
  const reasoning = buildCompactionReasoning(pi, ctx, state, compactionModel);
76
109
  return {
77
110
  parallel_tool_calls: true,
78
- prompt_cache_key: ctx.sessionManager.getSessionId(),
111
+ prompt_cache_key: clampOpenAIPromptCacheKey(ctx.sessionManager.getSessionId()),
79
112
  ...(isOpenAICodexContext(ctx) && state.config.fast ? { service_tier: "priority" } : {}),
80
113
  text: { verbosity: state.config.verbosity },
81
114
  ...(tools ? { tools } : {}),
@@ -94,7 +127,7 @@ function formatCompactFailureMessage(compactResult: Awaited<ReturnType<typeof ex
94
127
  const status = compactResult.status ? ` HTTP ${compactResult.status}` : "";
95
128
  const response = compactResult.responseText?.trim();
96
129
  const detail = response ? `: ${response.slice(0, 500)}` : compactResult.errorMessage ? `: ${compactResult.errorMessage}` : "";
97
- return `OpenAI native compaction failed (${compactResult.reason}${status})${detail}; Pi compaction was not run.`;
130
+ return `OpenAI native compaction failed (${compactResult.reason}${status})${detail}`;
98
131
  }
99
132
 
100
133
  function formatCompactRequestDiagnostics(request: NativeCompactionRequestBody): string {
@@ -104,6 +137,33 @@ function formatCompactRequestDiagnostics(request: NativeCompactionRequestBody):
104
137
  return `model=${request.model}, input=${request.input.length}, tools=${tools}, reasoning=${reasoning}, service_tier=${serviceTier}`;
105
138
  }
106
139
 
140
+ function notifyNativeCompactionFallback(ctx: ExtensionContext, state: AdapterState, branchEntries: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>, runtime: { provider: string; api: string; baseUrl: string }, message: string): void {
141
+ const stashed = stashLatestNativeWindowForPiCompactionFallback(ctx, branchEntries, runtime, state);
142
+ ctx.ui.notify(`${message}; Pi compaction will run.${stashed ? " Previous native compacted window will be included in Pi compaction fallback." : ""}`, "error");
143
+ }
144
+
145
+ function textFromResponsesContent(content: unknown): string {
146
+ if (typeof content === "string") return content;
147
+ if (!Array.isArray(content)) return "";
148
+ return content
149
+ .map((item) => isRecord(item) && item.type === "input_text" && typeof item.text === "string" ? item.text : "")
150
+ .join("\n");
151
+ }
152
+
153
+ function isPiCompactionSummarizationPayload(payload: ResponsesCompatibleRequestPayload): boolean {
154
+ const instructions = typeof payload.instructions === "string" ? payload.instructions : "";
155
+ if (/compact|summar/i.test(instructions)) return true;
156
+
157
+ return payload.input.some((item) => {
158
+ if (!isRecord(item)) return false;
159
+ const role = item.role;
160
+ const text = textFromResponsesContent(item.content);
161
+ if ((role === "system" || role === "developer") && /compact|summar/i.test(text)) return true;
162
+ if (role === "user" && /<conversation>|previous compaction summary|summary/i.test(text)) return true;
163
+ return false;
164
+ });
165
+ }
166
+
107
167
  export async function handleCodexSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx: ExtensionContext, state: AdapterState, pi: ExtensionAPI) {
108
168
  if (!state.config.responsesCompaction || !shouldUseCodexAdapter(ctx, state.config)) {
109
169
  return undefined;
@@ -127,6 +187,9 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
127
187
 
128
188
  const resolution = await resolveNativeCompactionEnvironment(ctx, { enabled: true });
129
189
  if (!resolution.ok) {
190
+ if (resolution.reason === "unsupported-provider" || resolution.reason === "unsupported-api") {
191
+ return undefined;
192
+ }
130
193
  ctx.ui.notify(`OpenAI native compaction is enabled but unavailable (${resolution.reason}); Pi compaction was not run.`, "error");
131
194
  return { cancel: true };
132
195
  }
@@ -203,23 +266,23 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
203
266
  const compactResult = await executeNativeCompaction({ runtime, request, signal: event.signal });
204
267
  if (!compactResult.ok) {
205
268
  if (compactResult.reason !== "aborted") {
206
- ctx.ui.notify(formatCompactFailureMessage(compactResult), "error");
269
+ notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, formatCompactFailureMessage(compactResult));
207
270
  }
208
- return { cancel: true };
271
+ return compactResult.reason === "aborted" ? { cancel: true } : undefined;
209
272
  }
210
273
  const compactedWindow = sanitizeCompactedWindow(compactResult.compactedWindow);
211
274
  if (compactedWindow.length === 0) {
212
- ctx.ui.notify(`OpenAI native compaction returned no installable compacted context; Pi compaction was not run. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`, "error");
213
- return { cancel: true };
275
+ notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, `OpenAI native compaction returned no installable compacted context. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`);
276
+ return undefined;
214
277
  }
215
278
  if (!hasCompactionOutputItem(compactedWindow)) {
216
- ctx.ui.notify(`OpenAI native compaction did not return a compaction item; Pi compaction was not run. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`, "error");
217
- return { cancel: true };
279
+ notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, `OpenAI native compaction did not return a compaction item. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`);
280
+ return undefined;
218
281
  }
219
282
  const encryptedSummary = extractCompactionSummaryText(compactedWindow);
220
283
  if (!encryptedSummary) {
221
- ctx.ui.notify(`OpenAI native compaction returned compacted context without a displayable summary; Pi compaction was not run. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`, "error");
222
- return { cancel: true };
284
+ notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, `OpenAI native compaction returned compacted context without a displayable summary. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`);
285
+ return undefined;
223
286
  }
224
287
  try {
225
288
  const details = createNativeCompactionDetails({
@@ -234,8 +297,8 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
234
297
  });
235
298
  return { compaction: createNativeCompactionShimResult({ summary: NATIVE_COMPACTION_SHIM_SUMMARY, firstKeptEntryId: event.preparation.firstKeptEntryId, tokensBefore: event.preparation.tokensBefore, details }) };
236
299
  } catch {
237
- ctx.ui.notify("OpenAI native compaction produced details Pi could not store; Pi compaction was not run.", "error");
238
- return { cancel: true };
300
+ notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, "OpenAI native compaction produced details Pi could not store");
301
+ return undefined;
239
302
  }
240
303
  }
241
304
 
@@ -245,17 +308,54 @@ export async function rewriteCodexCompactedProviderRequest(payload: unknown, ctx
245
308
  if (!resolution.ok) return undefined;
246
309
  const runtime = resolution.runtime;
247
310
  const branchEntries = ctx.sessionManager.getBranch();
248
- const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
311
+ const latestNativeCompactionIndex = findLatestNativeCompactionEntryIndex(branchEntries, {
249
312
  provider: runtime.provider,
250
313
  api: runtime.api,
251
314
  baseUrl: runtime.baseUrl,
252
315
  });
253
- if (!latestNativeCompaction.ok) return undefined;
316
+ if (latestNativeCompactionIndex === undefined) return undefined;
254
317
  if (!runtime.payload) return undefined;
255
- const rewrite = rewriteResponsesPayloadWithNativeReplay({ model: runtime.currentModel, payload: runtime.payload, branchEntries, compactionEntry: latestNativeCompaction.entry });
318
+ const rewrite = rewriteResponsesPayloadWithNativeReplay({ model: runtime.currentModel, payload: runtime.payload, branchEntries, compactionEntry: branchEntries[latestNativeCompactionIndex] as NativeCompactionEntry });
256
319
  if (rewrite.ok) return rewrite.rewrittenPayload;
257
320
  const detail = rewrite.parity?.mismatches.slice(0, 3).join("; ");
258
321
  const message = `OpenAI native compaction replay failed (${rewrite.reason})${detail ? `: ${detail}` : ""}; request was not sent with placeholder compaction context.`;
259
322
  ctx.ui.notify(message, "error");
260
323
  throw new Error(message);
261
324
  }
325
+
326
+ export async function injectPendingNativeWindowIntoPiCompactionRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): Promise<unknown | undefined> {
327
+ const pending = state.pendingPiCompactionNativeWindow;
328
+ if (!pending || pending.window.length === 0) return undefined;
329
+ if (!isResponsesCompatiblePayload(payload)) return undefined;
330
+ if (pending.sessionId !== ctx.sessionManager.getSessionId()) {
331
+ state.pendingPiCompactionNativeWindow = undefined;
332
+ return undefined;
333
+ }
334
+ if (!isPiCompactionSummarizationPayload(payload)) return undefined;
335
+
336
+ const resolution = await resolveNativeCompactionEnvironment(ctx, { enabled: true }, payload);
337
+ if (!resolution.ok) return undefined;
338
+ const runtime = resolution.runtime;
339
+ if (pending.provider !== runtime.provider || pending.api !== runtime.api || pending.baseUrl !== runtime.baseUrl) {
340
+ state.pendingPiCompactionNativeWindow = undefined;
341
+ return undefined;
342
+ }
343
+
344
+ const input = [...payload.input];
345
+ let insertAt = 0;
346
+ while (insertAt < input.length) {
347
+ const item = input[insertAt];
348
+ if (!isRecord(item) || (item.role !== "system" && item.role !== "developer")) break;
349
+ insertAt++;
350
+ }
351
+
352
+ state.pendingPiCompactionNativeWindow = undefined;
353
+ return {
354
+ ...payload,
355
+ input: [
356
+ ...input.slice(0, insertAt),
357
+ ...pending.window.map((item) => structuredClone(item)),
358
+ ...input.slice(insertAt),
359
+ ],
360
+ };
361
+ }
@@ -390,6 +390,15 @@ function createReplayVariants<TApi extends Api>(args: {
390
390
  return [contextSet, createReplayMessageSet(args.model, piMessages)];
391
391
  }
392
392
 
393
+ function clonePayloadConversationInput(args: {
394
+ payloadInput: readonly unknown[];
395
+ freshPreamble: FreshAuthoritativePreamble;
396
+ }): ResponsesInputItem[] | undefined {
397
+ const tailEndIndex = args.payloadInput.length - args.freshPreamble.trailingInput.length;
398
+ if (tailEndIndex < args.freshPreamble.leadingInput.length) return undefined;
399
+ return cloneResponsesInputSlice(args.payloadInput.slice(args.freshPreamble.leadingInput.length, tailEndIndex));
400
+ }
401
+
393
402
  function findReplayMatch<TApi extends Api>(args: {
394
403
  model: Model<TApi>;
395
404
  payloadInput: readonly unknown[];
@@ -506,21 +515,57 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
506
515
  };
507
516
  }
508
517
 
509
- const newerCompactionEntry = args.branchEntries
510
- .slice(boundaryIndex + 1)
511
- .some((entry) => entry.type === "compaction");
512
- if (newerCompactionEntry) {
518
+ const compactedWindow = cloneOpaqueCompactedWindow(args.compactionEntry.details?.compactedWindow ?? []);
519
+ if (!compactedWindow) {
513
520
  return {
514
521
  ok: false,
515
- reason: "unexpected-compaction-after-boundary",
522
+ reason: "invalid-compacted-window",
516
523
  };
517
524
  }
518
525
 
519
- const compactedWindow = cloneOpaqueCompactedWindow(args.compactionEntry.details?.compactedWindow ?? []);
520
- if (!compactedWindow) {
526
+ const newerCompactionEntry = args.branchEntries
527
+ .slice(boundaryIndex + 1)
528
+ .some((entry) => entry.type === "compaction");
529
+ if (newerCompactionEntry) {
530
+ const conversationInput = clonePayloadConversationInput({ payloadInput: args.payload.input, freshPreamble });
531
+ const originalPiReplayInput = cloneResponsesInputSlice(args.payload.input);
532
+ if (!conversationInput || !originalPiReplayInput) {
533
+ return {
534
+ ok: false,
535
+ reason: "unexpected-compaction-after-boundary",
536
+ };
537
+ }
538
+
521
539
  return {
522
- ok: false,
523
- reason: "invalid-compacted-window",
540
+ ok: true,
541
+ segments: {
542
+ boundaryIndex,
543
+ firstKeptEntryIndex,
544
+ instructions: freshPreamble.instructions,
545
+ freshPreamble: freshPreamble.leadingInput,
546
+ trailingPreamble: freshPreamble.trailingInput,
547
+ compactionSummary: [],
548
+ preCompactionKeptWindow: createReplaySlice([], [], []),
549
+ compactedWindow,
550
+ postCompactionTail: createReplaySlice(args.branchEntries.slice(boundaryIndex + 1), [], conversationInput),
551
+ originalPiReplayInput,
552
+ replayInput: [
553
+ ...freshPreamble.leadingInput,
554
+ ...compactedWindow,
555
+ ...conversationInput,
556
+ ...freshPreamble.trailingInput,
557
+ ],
558
+ },
559
+ rewrittenPayload: {
560
+ ...args.payload,
561
+ ...(freshPreamble.instructions !== undefined ? { instructions: freshPreamble.instructions } : {}),
562
+ input: [
563
+ ...freshPreamble.leadingInput,
564
+ ...compactedWindow,
565
+ ...conversationInput,
566
+ ...freshPreamble.trailingInput,
567
+ ],
568
+ },
524
569
  };
525
570
  }
526
571
 
@@ -5,7 +5,7 @@ import type { AdapterState } from "./state.ts";
5
5
  import { rewriteNativeImageGenerationTool } from "../tools/image-generation-tool.ts";
6
6
  import { rewriteNativeWebSearchTool } from "../tools/web-search-tool.ts";
7
7
  import { shouldUseCodexAdapter } from "./activation.ts";
8
- import { rewriteCodexCompactedProviderRequest } from "./compaction.ts";
8
+ import { injectPendingNativeWindowIntoPiCompactionRequest, rewriteCodexCompactedProviderRequest } from "./compaction.ts";
9
9
 
10
10
  export async function rewriteCodexProviderRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): Promise<unknown | undefined> {
11
11
  if (!shouldUseCodexAdapter(ctx, state.config) || (!isOpenAICodexContext(ctx) && !isResponsesContext(ctx))) {
@@ -21,5 +21,7 @@ export async function rewriteCodexProviderRequest(payload: unknown, ctx: Extensi
21
21
  serviceTier: isOpenAICodex,
22
22
  verbosity: true,
23
23
  });
24
+ const piCompactionPayload = await injectPendingNativeWindowIntoPiCompactionRequest(configuredPayload, ctx, state);
25
+ if (piCompactionPayload !== undefined) return piCompactionPayload;
24
26
  return (await rewriteCodexCompactedProviderRequest(configuredPayload, ctx, state)) ?? configuredPayload;
25
27
  }
@@ -1,5 +1,15 @@
1
1
  import type { PromptSkill } from "../prompt/build-system-prompt.ts";
2
2
  import type { CodexConversionConfig } from "./config.ts";
3
+ import type { ResponsesInputItem } from "./serializer.ts";
4
+
5
+ export interface PendingPiCompactionNativeWindow {
6
+ window: ResponsesInputItem[];
7
+ provider: string;
8
+ api: string;
9
+ baseUrl: string;
10
+ sessionId: string;
11
+ sourceCompactionEntryId?: string;
12
+ }
3
13
 
4
14
  export interface AdapterState {
5
15
  enabled: boolean;
@@ -8,4 +18,8 @@ export interface AdapterState {
8
18
  previousToolNames?: string[];
9
19
  promptSkills: PromptSkill[];
10
20
  config: CodexConversionConfig;
21
+ pendingPiCompactionNativeWindow?: PendingPiCompactionNativeWindow;
22
+ codexContextBudgetRawWindows?: Record<string, number>;
23
+ codexContextBudgetAdjustedWindows?: Record<string, number>;
24
+ codexContextBudgetReserveTokens?: number;
11
25
  }
@@ -9,7 +9,7 @@ export const NATIVE_COMPACTION_DISPLAY_TEXT = [
9
9
  "",
10
10
  "The compaction result is encrypted by OpenAI and is not human-readable in Pi.",
11
11
  "",
12
- "Warning: do not turn Responses compaction off mid-session; old context may be much less reliable.",
12
+ "Warning: do not turn Responses compaction off or switch providers mid-session; old context may be much less reliable.",
13
13
  ].join("\n");
14
14
 
15
15
  export type NativeCompactionStrategy = typeof NATIVE_COMPACTION_STRATEGY;
@@ -92,7 +92,8 @@ export async function openCodexSettingsScreen(ctx: ExtensionContext, options: Co
92
92
  function formatCompactionNotes(theme: Theme): string[] {
93
93
  return [
94
94
  theme.fg("dim", " Beta: native OpenAI Responses compaction is experimental. Please report any issues."),
95
- theme.fg("error", " Warning: do not turn this off mid-session; old context may be much less reliable."),
95
+ theme.fg("error", " Warning: do not turn this off or switch providers mid-session; old context may be much less reliable."),
96
+ theme.fg("warning", " If native compaction recovery fails, go back below 90% context and compact from there."),
96
97
  ];
97
98
  }
98
99
 
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { Model } from "@earendil-works/pi-ai";
2
3
  import { Box, Text, truncateToWidth } from "@earendil-works/pi-tui";
3
4
  import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
4
5
  import { clearApplyPatchRenderState, registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
@@ -22,6 +23,7 @@ import { getCodexSkillPaths, hasNoSkillsFlag } from "./adapter/skills.ts";
22
23
  import type { AdapterState } from "./adapter/state.ts";
23
24
  import { registerCodexCommand } from "./codex-settings/command.ts";
24
25
  import { WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
26
+ import { applyCodexContextBudgetToModel, readPiCompactionReserveTokens } from "./adapter/codex-context-budget.ts";
25
27
 
26
28
  function getCommandArg(args: unknown): string | undefined {
27
29
  if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
@@ -62,6 +64,10 @@ export default function codexConversion(pi: ExtensionAPI) {
62
64
  }
63
65
  }
64
66
 
67
+ function ensureCodexContextBudgetModel(ctx: { model: Model<any> | undefined }): void {
68
+ applyCodexContextBudgetToModel(ctx.model, state);
69
+ }
70
+
65
71
  registerOpenAICodexCustomProvider(pi, {
66
72
  getCurrentCwd: () => state.cwd,
67
73
  getNativeToolRewriteConfig: () => ({
@@ -92,6 +98,8 @@ export default function codexConversion(pi: ExtensionAPI) {
92
98
  pi.on("session_start", async (_event, ctx) => {
93
99
  state.cwd = ctx.cwd;
94
100
  state.config = readCodexConversionConfig();
101
+ state.codexContextBudgetReserveTokens = readPiCompactionReserveTokens(ctx.cwd);
102
+ ensureCodexContextBudgetModel(ctx);
95
103
  ensureOptionalNativeToolsRegistered();
96
104
  state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
97
105
  registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
@@ -108,6 +116,8 @@ export default function codexConversion(pi: ExtensionAPI) {
108
116
 
109
117
  pi.on("model_select", async (_event, ctx) => {
110
118
  state.cwd = ctx.cwd;
119
+ state.codexContextBudgetReserveTokens = readPiCompactionReserveTokens(ctx.cwd);
120
+ ensureCodexContextBudgetModel(ctx);
111
121
  state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
112
122
  registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
113
123
  syncAdapter(pi, ctx, state);
@@ -163,6 +173,7 @@ export default function codexConversion(pi: ExtensionAPI) {
163
173
  });
164
174
 
165
175
  pi.on("session_compact", async (event) => {
176
+ state.pendingPiCompactionNativeWindow = undefined;
166
177
  if (!event.fromExtension || !isNativeCompactionDetails(event.compactionEntry.details)) return;
167
178
  pi.sendMessage(
168
179
  {
@@ -37,6 +37,14 @@ const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
37
37
  const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
38
38
  const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
39
39
  const dynamicImport = (specifier: string) => import(specifier);
40
+ const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
41
+
42
+ function clampOpenAIPromptCacheKey(key: string | undefined): string | undefined {
43
+ if (key === undefined) return undefined;
44
+ const chars = Array.from(key);
45
+ if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return key;
46
+ return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
47
+ }
40
48
  let _os: { platform(): string; release(): string; arch(): string } | null = null;
41
49
 
42
50
  if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
@@ -87,6 +95,7 @@ interface QueuedWebSearchActivity {
87
95
  }
88
96
 
89
97
  type PendingActivity = QueuedImageActivity | QueuedWebSearchActivity;
98
+ type SendActivityMessage = ExtensionAPI["sendMessage"];
90
99
 
91
100
  interface CachedImagePreview {
92
101
  data: string;
@@ -577,7 +586,7 @@ export function buildRequestBody<TApi extends Api>(model: Model<TApi>, context:
577
586
  input: messages,
578
587
  text: { verbosity: ((options as { textVerbosity?: string } | undefined)?.textVerbosity ?? "low") as string },
579
588
  include: ["reasoning.encrypted_content"],
580
- prompt_cache_key: options?.sessionId,
589
+ prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),
581
590
  tool_choice: "auto",
582
591
  parallel_tool_calls: true,
583
592
  };
@@ -1449,6 +1458,87 @@ export function buildWebSearchSummaryText(searches: SurfacedWebSearch[]): string
1449
1458
  return searches.length === 1 ? "Searched the web once" : `Searched the web ${searches.length} times`;
1450
1459
  }
1451
1460
 
1461
+ function sendActivityMessages(
1462
+ sendMessage: SendActivityMessage,
1463
+ imagePreviewCache: Map<string, CachedImagePreview>,
1464
+ activities: PendingActivity[],
1465
+ ): void {
1466
+ for (let index = 0; index < activities.length; index++) {
1467
+ const activity = activities[index];
1468
+ if (activity.kind === "image") {
1469
+ imagePreviewCache.set(activity.savedImage.absolutePath, activity.imageData);
1470
+ sendMessage(
1471
+ {
1472
+ customType: IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
1473
+ content: [{ type: "text", text: buildGeneratedImageDisplayText(activity.savedImage, { expanded: false }) }],
1474
+ display: true,
1475
+ details: { savedImages: [activity.savedImage] } satisfies ImageDisplayMessageDetails,
1476
+ },
1477
+ { triggerTurn: false },
1478
+ );
1479
+ continue;
1480
+ }
1481
+
1482
+ const searches = [activity.search];
1483
+ while (index + 1 < activities.length && activities[index + 1]?.kind === "web-search") {
1484
+ searches.push((activities[++index] as QueuedWebSearchActivity).search);
1485
+ }
1486
+ sendMessage(
1487
+ {
1488
+ customType: WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
1489
+ content: buildWebSearchActivityMessage(searches),
1490
+ display: true,
1491
+ details: { searches },
1492
+ },
1493
+ { triggerTurn: false },
1494
+ );
1495
+ }
1496
+ }
1497
+
1498
+ export function createActivityMessageDispatcher(sendMessage: SendActivityMessage): {
1499
+ imagePreviewCache: Map<string, CachedImagePreview>;
1500
+ enqueueSettledActivities(activities: PendingActivity[]): void;
1501
+ flushNow(): void;
1502
+ scheduleFlush(): void;
1503
+ clear(): void;
1504
+ } {
1505
+ const completedActivities: PendingActivity[] = [];
1506
+ const imagePreviewCache = new Map<string, CachedImagePreview>();
1507
+ let pendingFlushTimer: ReturnType<typeof setTimeout> | undefined;
1508
+
1509
+ const flush = () => {
1510
+ pendingFlushTimer = undefined;
1511
+ const activities = completedActivities.splice(0, completedActivities.length);
1512
+ if (activities.length > 0) sendActivityMessages(sendMessage, imagePreviewCache, activities);
1513
+ };
1514
+
1515
+ return {
1516
+ imagePreviewCache,
1517
+ enqueueSettledActivities(activities) {
1518
+ completedActivities.push(...activities);
1519
+ },
1520
+ flushNow() {
1521
+ if (pendingFlushTimer) {
1522
+ clearTimeout(pendingFlushTimer);
1523
+ pendingFlushTimer = undefined;
1524
+ }
1525
+ flush();
1526
+ },
1527
+ scheduleFlush() {
1528
+ if (pendingFlushTimer || completedActivities.length === 0) return;
1529
+ pendingFlushTimer = setTimeout(flush, 0);
1530
+ },
1531
+ clear() {
1532
+ if (pendingFlushTimer) {
1533
+ clearTimeout(pendingFlushTimer);
1534
+ pendingFlushTimer = undefined;
1535
+ }
1536
+ completedActivities.length = 0;
1537
+ imagePreviewCache.clear();
1538
+ },
1539
+ };
1540
+ }
1541
+
1452
1542
  function loadCachedImagePreview(savedImage: SavedGeneratedImage, imagePreviewCache: Map<string, CachedImagePreview>): CachedImagePreview | undefined {
1453
1543
  const cached = imagePreviewCache.get(savedImage.absolutePath);
1454
1544
  if (cached) return cached;
@@ -1543,6 +1633,7 @@ function createCodexStream<TApi extends Api>(
1543
1633
  getNativeToolRewriteConfig?: () => { webSearch: boolean; imageGeneration: boolean };
1544
1634
  onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
1545
1635
  onWebSearchCaptured?: (search: SurfacedWebSearch) => void;
1636
+ onStreamSettled?: () => void;
1546
1637
  },
1547
1638
  ): AssistantMessageEventStream {
1548
1639
  const stream = createAssistantMessageEventStream();
@@ -1696,6 +1787,8 @@ function createCodexStream<TApi extends Api>(
1696
1787
  error: createErrorMessage(output, error, !!options?.signal?.aborted),
1697
1788
  });
1698
1789
  stream.end();
1790
+ } finally {
1791
+ deps.onStreamSettled?.();
1699
1792
  }
1700
1793
  })();
1701
1794
 
@@ -1703,75 +1796,31 @@ function createCodexStream<TApi extends Api>(
1703
1796
  }
1704
1797
 
1705
1798
  export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { getCurrentCwd: () => string; getNativeToolRewriteConfig?: () => { webSearch: boolean; imageGeneration: boolean } }): void {
1706
- const pendingActivities: PendingActivity[] = [];
1707
- const imagePreviewCache = new Map<string, CachedImagePreview>();
1708
- let pendingFlushTimer: ReturnType<typeof setTimeout> | undefined;
1709
-
1710
- const flushPendingMessages = () => {
1711
- pendingFlushTimer = undefined;
1712
- const activities = pendingActivities.splice(0, pendingActivities.length);
1713
-
1714
- for (let index = 0; index < activities.length; index++) {
1715
- const activity = activities[index];
1716
- if (activity.kind === "image") {
1717
- imagePreviewCache.set(activity.savedImage.absolutePath, activity.imageData);
1718
- pi.sendMessage(
1719
- {
1720
- customType: IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
1721
- content: [{ type: "text", text: buildGeneratedImageDisplayText(activity.savedImage, { expanded: false }) }],
1722
- display: true,
1723
- details: { savedImages: [activity.savedImage] } satisfies ImageDisplayMessageDetails,
1724
- },
1725
- { triggerTurn: false },
1726
- );
1727
- continue;
1728
- }
1729
-
1730
- const searches = [activity.search];
1731
- while (index + 1 < activities.length && activities[index + 1]?.kind === "web-search") {
1732
- searches.push((activities[++index] as QueuedWebSearchActivity).search);
1733
- }
1734
- pi.sendMessage(
1735
- {
1736
- customType: WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
1737
- content: buildWebSearchActivityMessage(searches),
1738
- display: true,
1739
- details: { searches },
1740
- },
1741
- { triggerTurn: false },
1742
- );
1743
- }
1744
- };
1745
-
1746
- const schedulePendingMessageFlush = () => {
1747
- if (pendingFlushTimer || pendingActivities.length === 0) {
1748
- return;
1749
- }
1750
- pendingFlushTimer = setTimeout(flushPendingMessages, 0);
1751
- };
1799
+ const activityDispatcher = createActivityMessageDispatcher(pi.sendMessage.bind(pi));
1752
1800
 
1753
1801
  const clearPendingMessages = () => {
1754
- if (pendingFlushTimer) {
1755
- clearTimeout(pendingFlushTimer);
1756
- pendingFlushTimer = undefined;
1757
- }
1758
- pendingActivities.length = 0;
1759
- imagePreviewCache.clear();
1802
+ activityDispatcher.clear();
1760
1803
  };
1761
1804
 
1762
1805
  pi.registerProvider("openai-codex", {
1763
1806
  api: "openai-codex-responses",
1764
- streamSimple: (model, context, streamOptions) =>
1765
- createCodexStream(model, context, streamOptions, {
1807
+ streamSimple: (model, context, streamOptions) => {
1808
+ const turnActivities: PendingActivity[] = [];
1809
+ return createCodexStream(model, context, streamOptions, {
1766
1810
  getCurrentCwd: options.getCurrentCwd,
1767
1811
  getNativeToolRewriteConfig: options.getNativeToolRewriteConfig,
1768
1812
  onImageSaved: (savedImage, imageData) => {
1769
- pendingActivities.push({ kind: "image", savedImage, imageData });
1813
+ turnActivities.push({ kind: "image", savedImage, imageData });
1770
1814
  },
1771
1815
  onWebSearchCaptured: (search) => {
1772
- pendingActivities.push({ kind: "web-search", search });
1816
+ turnActivities.push({ kind: "web-search", search });
1817
+ },
1818
+ onStreamSettled: () => {
1819
+ const activities = turnActivities.splice(0, turnActivities.length);
1820
+ if (activities.length > 0) activityDispatcher.enqueueSettledActivities(activities);
1773
1821
  },
1774
- }),
1822
+ });
1823
+ },
1775
1824
  });
1776
1825
 
1777
1826
  pi.on("session_start", async () => {
@@ -1779,15 +1828,13 @@ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { g
1779
1828
  });
1780
1829
 
1781
1830
  pi.on("session_shutdown", async () => {
1782
- if (pendingActivities.length > 0) {
1783
- flushPendingMessages();
1784
- }
1831
+ activityDispatcher.flushNow();
1785
1832
  clearPendingMessages();
1786
1833
  closeOpenAICodexWebSocketSessions();
1787
1834
  });
1788
1835
 
1789
1836
  pi.on("agent_end", async () => {
1790
- schedulePendingMessageFlush();
1837
+ activityDispatcher.scheduleFlush();
1791
1838
  });
1792
1839
 
1793
1840
  pi.registerMessageRenderer<ImageDisplayMessageDetails>(IMAGE_SAVE_DISPLAY_MESSAGE_TYPE, (message, options, theme) => {
@@ -1804,7 +1851,7 @@ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { g
1804
1851
  .join("\n");
1805
1852
  box.addChild(new Text(`\n${theme.fg("customMessageText", textContent)}`, 0, 0));
1806
1853
  if (savedImage) {
1807
- const preview = loadCachedImagePreview(savedImage, imagePreviewCache);
1854
+ const preview = loadCachedImagePreview(savedImage, activityDispatcher.imagePreviewCache);
1808
1855
  if (preview) {
1809
1856
  box.addChild(new Spacer(1));
1810
1857
  box.addChild(