@howaboua/pi-codex-conversion 1.5.10-dev.41.7e2d8d2 → 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 +2 -0
- package/package.json +5 -4
- package/src/adapter/codex-context-budget.ts +93 -0
- package/src/adapter/compaction.ts +9 -1
- package/src/adapter/state.ts +3 -0
- package/src/index.ts +10 -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
|
@@ -57,6 +57,8 @@ The settings UI also has **Usage**, **Overrides**, and **About** tabs. **Usage**
|
|
|
57
57
|
|
|
58
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
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
|
+
|
|
60
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.
|
|
61
63
|
|
|
62
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
|
+
}
|
|
@@ -95,12 +95,20 @@ function clampCodexReasoningEffort(modelId: string, effort: string): string {
|
|
|
95
95
|
return effort;
|
|
96
96
|
}
|
|
97
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
|
+
|
|
98
106
|
function buildCompactionRequestOptions(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState, compactionModel: string): NativeCompactionRequestOptions {
|
|
99
107
|
const tools = buildCompactionTools(pi, ctx, state);
|
|
100
108
|
const reasoning = buildCompactionReasoning(pi, ctx, state, compactionModel);
|
|
101
109
|
return {
|
|
102
110
|
parallel_tool_calls: true,
|
|
103
|
-
prompt_cache_key: ctx.sessionManager.getSessionId(),
|
|
111
|
+
prompt_cache_key: clampOpenAIPromptCacheKey(ctx.sessionManager.getSessionId()),
|
|
104
112
|
...(isOpenAICodexContext(ctx) && state.config.fast ? { service_tier: "priority" } : {}),
|
|
105
113
|
text: { verbosity: state.config.verbosity },
|
|
106
114
|
...(tools ? { tools } : {}),
|
package/src/adapter/state.ts
CHANGED
|
@@ -19,4 +19,7 @@ export interface AdapterState {
|
|
|
19
19
|
promptSkills: PromptSkill[];
|
|
20
20
|
config: CodexConversionConfig;
|
|
21
21
|
pendingPiCompactionNativeWindow?: PendingPiCompactionNativeWindow;
|
|
22
|
+
codexContextBudgetRawWindows?: Record<string, number>;
|
|
23
|
+
codexContextBudgetAdjustedWindows?: Record<string, number>;
|
|
24
|
+
codexContextBudgetReserveTokens?: number;
|
|
22
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);
|
|
@@ -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
|