@howaboua/pi-codex-conversion 1.5.10-dev.39.e39a154 → 1.5.10-dev.44.ec03884
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 +5 -1
- package/package.json +5 -4
- package/src/adapter/codex-context-budget.ts +93 -0
- package/src/adapter/compaction.ts +88 -18
- package/src/adapter/provider-request.ts +3 -1
- package/src/adapter/state.ts +14 -0
- package/src/index.ts +11 -0
- package/src/providers/openai-codex-custom-provider.ts +110 -63
- package/vendor/apply-patch/win32-arm64/apply_patch.exe +0 -0
- package/vendor/apply-patch/win32-x64/apply_patch.exe +0 -0
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-dev.
|
|
3
|
+
"version": "1.5.10-dev.44.ec03884",
|
|
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-
|
|
64
|
-
"@earendil-works/pi-
|
|
65
|
-
"@earendil-works/pi-
|
|
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}
|
|
130
|
+
return `OpenAI native compaction failed (${compactResult.reason}${status})${detail}`;
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
function formatCompactRequestDiagnostics(request: NativeCompactionRequestBody): string {
|
|
@@ -104,6 +137,11 @@ 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
|
+
|
|
107
145
|
export async function handleCodexSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx: ExtensionContext, state: AdapterState, pi: ExtensionAPI) {
|
|
108
146
|
if (!state.config.responsesCompaction || !shouldUseCodexAdapter(ctx, state.config)) {
|
|
109
147
|
return undefined;
|
|
@@ -203,23 +241,23 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
|
|
|
203
241
|
const compactResult = await executeNativeCompaction({ runtime, request, signal: event.signal });
|
|
204
242
|
if (!compactResult.ok) {
|
|
205
243
|
if (compactResult.reason !== "aborted") {
|
|
206
|
-
ctx
|
|
244
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, formatCompactFailureMessage(compactResult));
|
|
207
245
|
}
|
|
208
|
-
return { cancel: true };
|
|
246
|
+
return compactResult.reason === "aborted" ? { cancel: true } : undefined;
|
|
209
247
|
}
|
|
210
248
|
const compactedWindow = sanitizeCompactedWindow(compactResult.compactedWindow);
|
|
211
249
|
if (compactedWindow.length === 0) {
|
|
212
|
-
ctx
|
|
213
|
-
return
|
|
250
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, `OpenAI native compaction returned no installable compacted context. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`);
|
|
251
|
+
return undefined;
|
|
214
252
|
}
|
|
215
253
|
if (!hasCompactionOutputItem(compactedWindow)) {
|
|
216
|
-
ctx
|
|
217
|
-
return
|
|
254
|
+
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)}`);
|
|
255
|
+
return undefined;
|
|
218
256
|
}
|
|
219
257
|
const encryptedSummary = extractCompactionSummaryText(compactedWindow);
|
|
220
258
|
if (!encryptedSummary) {
|
|
221
|
-
ctx
|
|
222
|
-
return
|
|
259
|
+
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)}`);
|
|
260
|
+
return undefined;
|
|
223
261
|
}
|
|
224
262
|
try {
|
|
225
263
|
const details = createNativeCompactionDetails({
|
|
@@ -234,8 +272,8 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
|
|
|
234
272
|
});
|
|
235
273
|
return { compaction: createNativeCompactionShimResult({ summary: NATIVE_COMPACTION_SHIM_SUMMARY, firstKeptEntryId: event.preparation.firstKeptEntryId, tokensBefore: event.preparation.tokensBefore, details }) };
|
|
236
274
|
} catch {
|
|
237
|
-
ctx
|
|
238
|
-
return
|
|
275
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, "OpenAI native compaction produced details Pi could not store");
|
|
276
|
+
return undefined;
|
|
239
277
|
}
|
|
240
278
|
}
|
|
241
279
|
|
|
@@ -245,17 +283,49 @@ export async function rewriteCodexCompactedProviderRequest(payload: unknown, ctx
|
|
|
245
283
|
if (!resolution.ok) return undefined;
|
|
246
284
|
const runtime = resolution.runtime;
|
|
247
285
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
248
|
-
const
|
|
286
|
+
const latestNativeCompactionIndex = findLatestNativeCompactionEntryIndex(branchEntries, {
|
|
249
287
|
provider: runtime.provider,
|
|
250
288
|
api: runtime.api,
|
|
251
289
|
baseUrl: runtime.baseUrl,
|
|
252
290
|
});
|
|
253
|
-
if (
|
|
291
|
+
if (latestNativeCompactionIndex === undefined) return undefined;
|
|
254
292
|
if (!runtime.payload) return undefined;
|
|
255
|
-
const rewrite = rewriteResponsesPayloadWithNativeReplay({ model: runtime.currentModel, payload: runtime.payload, branchEntries, compactionEntry:
|
|
293
|
+
const rewrite = rewriteResponsesPayloadWithNativeReplay({ model: runtime.currentModel, payload: runtime.payload, branchEntries, compactionEntry: branchEntries[latestNativeCompactionIndex] as NativeCompactionEntry });
|
|
256
294
|
if (rewrite.ok) return rewrite.rewrittenPayload;
|
|
257
295
|
const detail = rewrite.parity?.mismatches.slice(0, 3).join("; ");
|
|
258
296
|
const message = `OpenAI native compaction replay failed (${rewrite.reason})${detail ? `: ${detail}` : ""}; request was not sent with placeholder compaction context.`;
|
|
259
297
|
ctx.ui.notify(message, "error");
|
|
260
298
|
throw new Error(message);
|
|
261
299
|
}
|
|
300
|
+
|
|
301
|
+
export async function injectPendingNativeWindowIntoPiCompactionRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): Promise<unknown | undefined> {
|
|
302
|
+
const pending = state.pendingPiCompactionNativeWindow;
|
|
303
|
+
if (!pending || pending.window.length === 0) return undefined;
|
|
304
|
+
if (!isResponsesCompatiblePayload(payload)) return undefined;
|
|
305
|
+
if (pending.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
306
|
+
state.pendingPiCompactionNativeWindow = undefined;
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, { enabled: true }, payload);
|
|
311
|
+
if (!resolution.ok) return undefined;
|
|
312
|
+
const runtime = resolution.runtime;
|
|
313
|
+
if (pending.provider !== runtime.provider || pending.api !== runtime.api || pending.baseUrl !== runtime.baseUrl) return undefined;
|
|
314
|
+
|
|
315
|
+
const input = [...payload.input];
|
|
316
|
+
let insertAt = 0;
|
|
317
|
+
while (insertAt < input.length) {
|
|
318
|
+
const item = input[insertAt];
|
|
319
|
+
if (!isRecord(item) || (item.role !== "system" && item.role !== "developer")) break;
|
|
320
|
+
insertAt++;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
...payload,
|
|
325
|
+
input: [
|
|
326
|
+
...input.slice(0, insertAt),
|
|
327
|
+
...pending.window.map((item) => structuredClone(item)),
|
|
328
|
+
...input.slice(insertAt),
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -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
|
}
|
package/src/adapter/state.ts
CHANGED
|
@@ -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
|
}
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1813
|
+
turnActivities.push({ kind: "image", savedImage, imageData });
|
|
1770
1814
|
},
|
|
1771
1815
|
onWebSearchCaptured: (search) => {
|
|
1772
|
-
|
|
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
|
-
|
|
1783
|
-
flushPendingMessages();
|
|
1784
|
-
}
|
|
1831
|
+
activityDispatcher.flushNow();
|
|
1785
1832
|
clearPendingMessages();
|
|
1786
1833
|
closeOpenAICodexWebSocketSessions();
|
|
1787
1834
|
});
|
|
1788
1835
|
|
|
1789
1836
|
pi.on("agent_end", async () => {
|
|
1790
|
-
|
|
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(
|
|
Binary file
|
|
Binary file
|