@howaboua/pi-codex-conversion 1.5.5 → 1.5.6-dev.32.699e826

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.
@@ -1,67 +1,65 @@
1
- import { DynamicBorder, getSettingsListTheme, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Box, Container, SettingsList, Spacer, Text, type SettingItem } from "@earendil-works/pi-tui";
3
- import { DEFAULT_CODEX_CONVERSION_CONFIG, normalizeCodexVerbosity, type CodexConversionConfig } from "../adapter/config.ts";
1
+ import { getSettingsListTheme, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { SettingsList, truncateToWidth, type SettingItem } from "@earendil-works/pi-tui";
3
+ import {
4
+ COMPACTION_MODELS,
5
+ COMPACTION_REASONING_LEVELS,
6
+ DEFAULT_CODEX_CONVERSION_CONFIG,
7
+ normalizeCodexVerbosity,
8
+ normalizeCompactionModel,
9
+ normalizeCompactionReasoning,
10
+ type CodexConversionConfig,
11
+ } from "../adapter/config.ts";
4
12
  import { CHANGELOG_URL, DISCORD_URL, GITHUB_URL, ISSUE_URL, openExternalUrl } from "./links.ts";
5
13
 
6
14
  export interface CodexSettingsScreenOptions {
7
15
  initialConfig: CodexConversionConfig;
8
16
  onChange: (nextConfig: CodexConversionConfig) => boolean;
17
+ initialTab?: SettingsTab;
9
18
  }
10
19
 
20
+ type SettingsTab = "general" | "compaction" | "overrides";
21
+
22
+ const TAB_ORDER: readonly SettingsTab[] = ["general", "compaction", "overrides"];
23
+
11
24
  export async function openCodexSettingsScreen(ctx: ExtensionContext, options: CodexSettingsScreenOptions): Promise<void> {
12
25
  let draft = { ...options.initialConfig };
26
+ let activeTab: SettingsTab = options.initialTab ?? "general";
27
+
13
28
  await ctx.ui.custom<void>((tui, theme, _kb, done) => {
14
- const buildItems = (): SettingItem[] => [
15
- { id: "useOnAllModels", label: "Use on all models", currentValue: draft.useOnAllModels ? "on" : "off", values: ["off", "on"] },
16
- { id: "statusLine", label: "Statusline", currentValue: draft.statusLine ? "on" : "off", values: ["off", "on"] },
17
- { id: "fast", label: "Fast mode", currentValue: draft.fast ? "on" : "off", values: ["off", "on"] },
18
- { id: "webSearch", label: "Web search", currentValue: draft.webSearch ? "on" : "off", values: ["off", "on"] },
19
- { id: "imageGeneration", label: "Image generation", currentValue: draft.imageGeneration ? "on" : "off", values: ["off", "on"] },
20
- { id: "verbosity", label: "Verbosity", currentValue: draft.verbosity, values: ["low", "medium", "high"] },
21
- ];
29
+ let settingsList = createSettingsList(activeTab, draft, options, (nextDraft) => {
30
+ draft = nextDraft;
31
+ }, done, () => tui.requestRender());
22
32
 
23
- const container = new Container();
24
- const panel = new Box(1, 0);
25
- panel.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
26
- let settingsList: SettingsList;
27
- settingsList = new SettingsList(buildItems(), 6, getSettingsListTheme(), (id, value) => {
28
- const nextDraft = { ...draft };
29
- const previousValue = buildItems().find((item) => item.id === id)?.currentValue;
30
- if (id === "useOnAllModels") nextDraft.useOnAllModels = value === "on";
31
- if (id === "statusLine") nextDraft.statusLine = value === "on";
32
- if (id === "fast") nextDraft.fast = value === "on";
33
- if (id === "webSearch") nextDraft.webSearch = value === "on";
34
- if (id === "imageGeneration") nextDraft.imageGeneration = value === "on";
35
- if (id === "verbosity") nextDraft.verbosity = normalizeCodexVerbosity(value) ?? DEFAULT_CODEX_CONVERSION_CONFIG.verbosity;
36
- if (options.onChange(nextDraft)) {
33
+ const switchTab = () => {
34
+ const currentIndex = TAB_ORDER.indexOf(activeTab);
35
+ activeTab = TAB_ORDER[(currentIndex + 1) % TAB_ORDER.length] ?? "general";
36
+ settingsList = createSettingsList(activeTab, draft, options, (nextDraft) => {
37
37
  draft = nextDraft;
38
- } else if (previousValue !== undefined) {
39
- settingsList.updateValue(id, previousValue);
40
- }
38
+ }, done, () => tui.requestRender());
41
39
  tui.requestRender();
42
- }, () => done(undefined));
43
- panel.addChild(settingsList);
44
- panel.addChild(new DynamicBorder((text) => theme.fg("dim", text)));
45
- panel.addChild(
46
- new Text(
47
- [
48
- `${theme.bold("g")} github ${theme.fg("dim", GITHUB_URL)}`,
49
- `${theme.bold("c")} changes ${theme.fg("dim", CHANGELOG_URL)}`,
50
- `${theme.bold("d")} discord ${theme.fg("dim", DISCORD_URL)}`,
51
- `${theme.bold("i")} issue ${theme.fg("dim", ISSUE_URL)}`,
52
- ].join("\n"),
53
- 0,
54
- 0,
55
- ),
56
- );
57
- panel.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
58
- container.addChild(new Spacer(1));
59
- container.addChild(panel);
40
+ };
60
41
 
61
42
  return {
62
- render: (width: number) => container.render(width),
63
- invalidate: () => container.invalidate(),
43
+ render: (width: number) =>
44
+ [
45
+ rule(width, theme, "accent"),
46
+ formatTabs(activeTab, theme),
47
+ rule(width, theme, "borderMuted"),
48
+ ...(activeTab === "compaction" ? formatCompactionNotes(theme) : []),
49
+ ...(activeTab === "overrides" ? formatOverridesNotes(theme) : []),
50
+ "",
51
+ ...settingsList.render(width),
52
+ rule(width, theme, "borderMuted"),
53
+ ...formatLinks(theme),
54
+ rule(width, theme, "accent"),
55
+ theme.fg("dim", " Tab to switch sections · g/c/d/i open links"),
56
+ ].map((line) => truncateToWidth(line, width, "")),
57
+ invalidate: () => settingsList.invalidate(),
64
58
  handleInput: (data: string) => {
59
+ if (data === "\t") {
60
+ switchTab();
61
+ return;
62
+ }
65
63
  if (handleLinkKey(data, ctx)) return;
66
64
  settingsList.handleInput?.(data);
67
65
  tui.requestRender();
@@ -70,6 +68,100 @@ export async function openCodexSettingsScreen(ctx: ExtensionContext, options: Co
70
68
  });
71
69
  }
72
70
 
71
+ function formatCompactionNotes(theme: Theme): string[] {
72
+ return [
73
+ theme.fg("dim", " Beta: native OpenAI Responses compaction is experimental. Please report any issues."),
74
+ theme.fg("error", " Warning: do not turn this off mid-session; old context may be much less reliable."),
75
+ ];
76
+ }
77
+
78
+ function formatOverridesNotes(theme: Theme): string[] {
79
+ return [
80
+ theme.fg("dim", " Advanced tool-surface overrides."),
81
+ ];
82
+ }
83
+
84
+ function rule(width: number, theme: Theme, color: "accent" | "borderMuted"): string {
85
+ return theme.fg(color, "─".repeat(Math.max(0, width)));
86
+ }
87
+
88
+ function createSettingsList(
89
+ tab: SettingsTab,
90
+ draft: CodexConversionConfig,
91
+ options: CodexSettingsScreenOptions,
92
+ onDraftChanged: (draft: CodexConversionConfig) => void,
93
+ done: (value?: void) => void,
94
+ requestRender: () => void,
95
+ ): SettingsList {
96
+ let settingsList: SettingsList;
97
+ settingsList = new SettingsList(buildItems(tab, draft), 8, getSettingsListTheme(), (id, value) => {
98
+ const nextDraft = applySettingChange(id, value, draft);
99
+ const previousValue = buildItems(tab, draft).find((item) => item.id === id)?.currentValue;
100
+ if (options.onChange(nextDraft)) {
101
+ onDraftChanged(nextDraft);
102
+ draft = nextDraft;
103
+ } else if (previousValue !== undefined) {
104
+ settingsList.updateValue(id, previousValue);
105
+ }
106
+ requestRender();
107
+ }, () => done(undefined));
108
+ return settingsList;
109
+ }
110
+
111
+ function buildItems(tab: SettingsTab, draft: CodexConversionConfig): SettingItem[] {
112
+ if (tab === "compaction") {
113
+ return [
114
+ { id: "responsesCompaction", label: "Responses compaction", currentValue: (draft.responsesCompaction ?? false) ? "on" : "off", values: ["off", "on"] },
115
+ { id: "compactionModel", label: "Model", currentValue: draft.compactionModel, values: [...COMPACTION_MODELS] },
116
+ { id: "compactionReasoning", label: "Reasoning", currentValue: draft.compactionReasoning, values: [...COMPACTION_REASONING_LEVELS] },
117
+ ];
118
+ }
119
+
120
+ if (tab === "overrides") {
121
+ return [
122
+ { id: "applyPatchOnly", label: "Apply patch only", currentValue: draft.applyPatchOnly ? "on" : "off", values: ["off", "on"] },
123
+ ];
124
+ }
125
+
126
+ return [
127
+ { id: "useOnAllModels", label: "Use on all models", currentValue: draft.useOnAllModels ? "on" : "off", values: ["off", "on"] },
128
+ { id: "statusLine", label: "Statusline", currentValue: draft.statusLine ? "on" : "off", values: ["off", "on"] },
129
+ { id: "fast", label: "Fast mode", currentValue: draft.fast ? "on" : "off", values: ["off", "on"] },
130
+ { id: "webSearch", label: "Web search", currentValue: draft.webSearch ? "on" : "off", values: ["off", "on"] },
131
+ { id: "imageGeneration", label: "Image generation", currentValue: draft.imageGeneration ? "on" : "off", values: ["off", "on"] },
132
+ { id: "verbosity", label: "Verbosity", currentValue: draft.verbosity, values: ["low", "medium", "high"] },
133
+ ];
134
+ }
135
+
136
+ function applySettingChange(id: string, value: string, draft: CodexConversionConfig): CodexConversionConfig {
137
+ const nextDraft = { ...draft };
138
+ if (id === "applyPatchOnly") nextDraft.applyPatchOnly = value === "on";
139
+ if (id === "useOnAllModels") nextDraft.useOnAllModels = value === "on";
140
+ if (id === "statusLine") nextDraft.statusLine = value === "on";
141
+ if (id === "fast") nextDraft.fast = value === "on";
142
+ if (id === "webSearch") nextDraft.webSearch = value === "on";
143
+ if (id === "imageGeneration") nextDraft.imageGeneration = value === "on";
144
+ if (id === "responsesCompaction") nextDraft.responsesCompaction = value === "on";
145
+ if (id === "compactionModel") nextDraft.compactionModel = normalizeCompactionModel(value) ?? DEFAULT_CODEX_CONVERSION_CONFIG.compactionModel;
146
+ if (id === "compactionReasoning") nextDraft.compactionReasoning = normalizeCompactionReasoning(value) ?? DEFAULT_CODEX_CONVERSION_CONFIG.compactionReasoning;
147
+ if (id === "verbosity") nextDraft.verbosity = normalizeCodexVerbosity(value) ?? DEFAULT_CODEX_CONVERSION_CONFIG.verbosity;
148
+ return nextDraft;
149
+ }
150
+
151
+ function formatTabs(activeTab: SettingsTab, theme: Theme): string {
152
+ const renderTab = (tab: SettingsTab, label: string) => activeTab === tab ? theme.bold(label) : theme.fg("dim", label);
153
+ return ` ${renderTab("general", "General")} ${theme.fg("dim", "/")} ${renderTab("compaction", "Compaction")} ${theme.fg("dim", "/")} ${renderTab("overrides", "Overrides")}`;
154
+ }
155
+
156
+ function formatLinks(theme: Theme): string[] {
157
+ return [
158
+ `${theme.bold("g")} github ${theme.fg("dim", GITHUB_URL)}`,
159
+ `${theme.bold("c")} changes ${theme.fg("dim", CHANGELOG_URL)}`,
160
+ `${theme.bold("d")} discord ${theme.fg("dim", DISCORD_URL)}`,
161
+ `${theme.bold("i")} issue ${theme.fg("dim", ISSUE_URL)}`,
162
+ ];
163
+ }
164
+
73
165
  function handleLinkKey(data: string, ctx: ExtensionContext): boolean {
74
166
  const target = getLinkTarget(data);
75
167
  if (!target) return false;
package/src/index.ts CHANGED
@@ -1,26 +1,27 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Text, truncateToWidth } from "@earendil-works/pi-tui";
2
3
  import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
3
4
  import { clearApplyPatchRenderState, registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
4
5
  import { createExecCommandTracker } from "./tools/exec-command-state.ts";
5
6
  import { registerExecCommandTool } from "./tools/exec-command-tool.ts";
6
7
  import { createExecSessionManager } from "./tools/exec-session-manager.ts";
7
- import {
8
- IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
9
- WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
10
- registerOpenAICodexCustomProvider,
11
- } from "./providers/openai-codex-custom-provider.ts";
8
+ import { registerOpenAICodexCustomProvider } from "./providers/openai-codex-custom-provider.ts";
12
9
  import { registerImageGenerationTool } from "./tools/image-generation-tool.ts";
13
10
  import { buildCodexSystemPrompt, extractPiPromptSkills, resolvePromptSkills } from "./prompt/build-system-prompt.ts";
14
11
  import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
15
- import { registerWebSearchTool, WEB_SEARCH_SESSION_NOTE_TYPE } from "./tools/web-search-tool.ts";
12
+ import { registerWebSearchTool } from "./tools/web-search-tool.ts";
16
13
  import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
17
14
  import { ensureBundledApplyPatchOnPath } from "./tools/apply-patch-binary.ts";
18
15
  import { readCodexConversionConfig } from "./adapter/config.ts";
19
16
  import { syncAdapter, mergeAdapterTools, restoreTools, stripAdapterTools, shouldUseCodexAdapter } from "./adapter/activation.ts";
20
17
  import { rewriteCodexProviderRequest } from "./adapter/provider-request.ts";
21
- import { getCodexSkillPaths } from "./adapter/skills.ts";
18
+ import { handleCodexSessionBeforeCompact } from "./adapter/compaction.ts";
19
+ import { isNativeCompactionDetails, NATIVE_COMPACTION_DISPLAY_MESSAGE_TYPE, NATIVE_COMPACTION_DISPLAY_TEXT } from "./adapter/types.ts";
20
+ import { isAdapterContextExcludedCustomMessage } from "./adapter/context-filter.ts";
21
+ import { getCodexSkillPaths, hasNoSkillsFlag } from "./adapter/skills.ts";
22
22
  import type { AdapterState } from "./adapter/state.ts";
23
23
  import { registerCodexCommand } from "./codex-settings/command.ts";
24
+ import { WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
24
25
 
25
26
  function getCommandArg(args: unknown): string | undefined {
26
27
  if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
@@ -44,13 +45,16 @@ export default function codexConversion(pi: ExtensionAPI) {
44
45
  const tracker = createExecCommandTracker();
45
46
  const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: [], config: readCodexConversionConfig() };
46
47
  const sessions = createExecSessionManager();
47
- let nativeWebSearchRegistered = false;
48
+ const registeredNativeWebSearchTools = new Set<string>();
48
49
  let nativeImageGenerationRegistered = false;
49
50
 
50
51
  function ensureOptionalNativeToolsRegistered(config = state.config): void {
51
- if (config.webSearch && !nativeWebSearchRegistered) {
52
- registerWebSearchTool(pi);
53
- nativeWebSearchRegistered = true;
52
+ if (config.webSearch) {
53
+ const webSearchToolName = WEB_SEARCH_TOOL_NAME;
54
+ if (!registeredNativeWebSearchTools.has(webSearchToolName)) {
55
+ registerWebSearchTool(pi, webSearchToolName);
56
+ registeredNativeWebSearchTools.add(webSearchToolName);
57
+ }
54
58
  }
55
59
  if (config.imageGeneration && !nativeImageGenerationRegistered) {
56
60
  registerImageGenerationTool(pi);
@@ -60,6 +64,10 @@ export default function codexConversion(pi: ExtensionAPI) {
60
64
 
61
65
  registerOpenAICodexCustomProvider(pi, {
62
66
  getCurrentCwd: () => state.cwd,
67
+ getNativeToolRewriteConfig: () => ({
68
+ webSearch: !state.config.applyPatchOnly && state.config.webSearch,
69
+ imageGeneration: !state.config.applyPatchOnly && state.config.imageGeneration,
70
+ }),
63
71
  });
64
72
  registerApplyPatchTool(pi);
65
73
  registerExecCommandTool(pi, tracker, sessions);
@@ -67,6 +75,16 @@ export default function codexConversion(pi: ExtensionAPI) {
67
75
  ensureOptionalNativeToolsRegistered();
68
76
  registerCodexCommand(pi, state, ensureOptionalNativeToolsRegistered);
69
77
 
78
+ pi.registerMessageRenderer(NATIVE_COMPACTION_DISPLAY_MESSAGE_TYPE, (message, _options, theme) => {
79
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
80
+ box.addChild(new Text(theme.fg("customMessageLabel", theme.bold("[compaction]")), 0, 0));
81
+ const content = typeof message.content === "string" ? message.content : NATIVE_COMPACTION_DISPLAY_TEXT;
82
+ box.addChild(new Text(`\n${theme.fg("customMessageText", content)}`, 0, 0));
83
+ const render = box.render.bind(box);
84
+ box.render = (width) => render(width).map((line) => truncateToWidth(line, width, ""));
85
+ return box;
86
+ });
87
+
70
88
  sessions.onSessionExit((sessionId) => {
71
89
  tracker.recordSessionFinished(sessionId);
72
90
  });
@@ -83,6 +101,7 @@ export default function codexConversion(pi: ExtensionAPI) {
83
101
  });
84
102
 
85
103
  pi.on("resources_discover", async (event) => {
104
+ if (hasNoSkillsFlag()) return undefined;
86
105
  const skillPaths = getCodexSkillPaths(event.cwd);
87
106
  return skillPaths.length > 0 ? { skillPaths } : undefined;
88
107
  });
@@ -124,7 +143,7 @@ export default function codexConversion(pi: ExtensionAPI) {
124
143
  if (!shouldUseCodexAdapter(ctx, state.config)) {
125
144
  return undefined;
126
145
  }
127
- const skills = resolvePromptSkills(event.systemPromptOptions?.skills, state.promptSkills);
146
+ const skills = resolvePromptSkills(event.systemPromptOptions?.skills, hasNoSkillsFlag() ? [] : state.promptSkills);
128
147
  return {
129
148
  systemPrompt: buildCodexSystemPrompt(event.systemPrompt, {
130
149
  skills,
@@ -138,19 +157,25 @@ export default function codexConversion(pi: ExtensionAPI) {
138
157
  return rewriteCodexProviderRequest(event.payload, ctx, state);
139
158
  });
140
159
 
141
- pi.on("context", async (event) => {
142
- return {
143
- messages: event.messages.filter(
144
- (message) =>
145
- !(
146
- message.role === "custom" &&
147
- (message.customType === WEB_SEARCH_SESSION_NOTE_TYPE ||
148
- message.customType === WEB_SEARCH_ACTIVITY_MESSAGE_TYPE ||
149
- message.customType === IMAGE_SAVE_DISPLAY_MESSAGE_TYPE)
150
- ),
151
- ),
152
- };
160
+ pi.on("session_before_compact", async (event, ctx) => {
161
+ state.cwd = ctx.cwd;
162
+ return handleCodexSessionBeforeCompact(event, ctx, state, pi);
163
+ });
164
+
165
+ pi.on("session_compact", async (event) => {
166
+ if (!event.fromExtension || !isNativeCompactionDetails(event.compactionEntry.details)) return;
167
+ pi.sendMessage(
168
+ {
169
+ customType: NATIVE_COMPACTION_DISPLAY_MESSAGE_TYPE,
170
+ content: NATIVE_COMPACTION_DISPLAY_TEXT,
171
+ display: true,
172
+ details: { compactionEntryId: event.compactionEntry.id },
173
+ },
174
+ { triggerTurn: false },
175
+ );
153
176
  });
177
+
178
+ pi.on("context", async (event) => ({ messages: event.messages.filter((message) => !isAdapterContextExcludedCustomMessage(message)) }));
154
179
  }
155
180
 
156
181
  export { getCodexSkillPaths, mergeAdapterTools, restoreTools, stripAdapterTools };
@@ -17,8 +17,12 @@ import type { ResponseCreateParamsStreaming } from "openai/resources/responses/r
17
17
  import {
18
18
  convertResponsesMessages,
19
19
  convertResponsesTools,
20
+ CODEX_TOOL_CALL_PROVIDERS,
20
21
  processResponsesStream,
21
22
  } from "./openai-responses-shared.ts";
23
+ import { WEB_SEARCH_TOOL_NAME } from "../adapter/tool-set.ts";
24
+ import { rewriteNativeImageGenerationTool } from "../tools/image-generation-tool.ts";
25
+ import { rewriteNativeWebSearchTool } from "../tools/web-search-tool.ts";
22
26
 
23
27
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
24
28
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
@@ -28,7 +32,6 @@ const OPENAI_CODEX_IMAGE_DIR = ".pi/openai-codex-images";
28
32
  const OPENAI_CODEX_LATEST_IMAGE_NAME = "latest.png";
29
33
  const MAX_RETRIES = 3;
30
34
  const BASE_DELAY_MS = 1000;
31
- const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
32
35
  const CODEX_RESPONSE_STATUSES = new Set(["completed", "incomplete", "failed", "cancelled", "queued", "in_progress"]);
33
36
  const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
34
37
  const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
@@ -109,6 +112,19 @@ interface SessionWebSocketCacheEntry {
109
112
  continuation?: CachedWebSocketContinuationState;
110
113
  }
111
114
 
115
+ export interface OpenAICodexWebSocketDebugStats {
116
+ requests: number;
117
+ connectionsCreated: number;
118
+ connectionsReused: number;
119
+ cachedContextRequests: number;
120
+ storeTrueRequests: number;
121
+ fullContextRequests: number;
122
+ deltaRequests: number;
123
+ lastInputItems: number;
124
+ lastDeltaInputItems?: number;
125
+ lastPreviousResponseId?: string;
126
+ }
127
+
112
128
  interface AcquiredWebSocket {
113
129
  socket: WebSocketLike;
114
130
  entry?: SessionWebSocketCacheEntry;
@@ -166,6 +182,38 @@ interface ResponseEnvelope {
166
182
  type ServiceTier = ResponseCreateParamsStreaming["service_tier"];
167
183
 
168
184
  const websocketSessionCache = new Map<string, SessionWebSocketCacheEntry>();
185
+ const websocketDebugStats = new Map<string, OpenAICodexWebSocketDebugStats>();
186
+
187
+ function getOrCreateWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats {
188
+ let stats = websocketDebugStats.get(sessionId);
189
+ if (!stats) {
190
+ stats = {
191
+ requests: 0,
192
+ connectionsCreated: 0,
193
+ connectionsReused: 0,
194
+ cachedContextRequests: 0,
195
+ storeTrueRequests: 0,
196
+ fullContextRequests: 0,
197
+ deltaRequests: 0,
198
+ lastInputItems: 0,
199
+ };
200
+ websocketDebugStats.set(sessionId, stats);
201
+ }
202
+ return stats;
203
+ }
204
+
205
+ export function getOpenAICodexWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats | undefined {
206
+ const stats = websocketDebugStats.get(sessionId);
207
+ return stats ? { ...stats } : undefined;
208
+ }
209
+
210
+ export function resetOpenAICodexWebSocketDebugStats(sessionId?: string): void {
211
+ if (sessionId) {
212
+ websocketDebugStats.delete(sessionId);
213
+ return;
214
+ }
215
+ websocketDebugStats.clear();
216
+ }
169
217
 
170
218
  class NonRetryableProviderError extends Error {}
171
219
 
@@ -550,7 +598,7 @@ export function buildRequestBody<TApi extends Api>(model: Model<TApi>, context:
550
598
 
551
599
  if (context.tools && context.tools.length > 0) {
552
600
  body.tools = convertResponsesTools(context.tools, { strict: null });
553
- const hasWebSearchTool = context.tools.some((tool) => tool.name === "web_search");
601
+ const hasWebSearchTool = context.tools.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
554
602
  if (hasWebSearchTool) {
555
603
  body.include.push("web_search_call.action.sources", "web_search_call.results");
556
604
  }
@@ -680,6 +728,7 @@ export function closeOpenAICodexWebSocketSessions(sessionId?: string): void {
680
728
  const entry = websocketSessionCache.get(sessionId);
681
729
  if (entry) closeEntry(entry);
682
730
  websocketSessionCache.delete(sessionId);
731
+ websocketDebugStats.delete(sessionId);
683
732
  return;
684
733
  }
685
734
 
@@ -687,6 +736,7 @@ export function closeOpenAICodexWebSocketSessions(sessionId?: string): void {
687
736
  closeEntry(entry);
688
737
  }
689
738
  websocketSessionCache.clear();
739
+ websocketDebugStats.clear();
690
740
  }
691
741
 
692
742
 
@@ -1230,7 +1280,7 @@ async function processWebSocketStream<TApi extends Api>(
1230
1280
  let streamStarted = false;
1231
1281
 
1232
1282
  for (let attempt = 0; attempt < 2; attempt++) {
1233
- const { socket, entry, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1283
+ const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1234
1284
  let keepConnection = true;
1235
1285
  let released = false;
1236
1286
  let eventCount = 0;
@@ -1240,6 +1290,24 @@ async function processWebSocketStream<TApi extends Api>(
1240
1290
  // WebSocket continuation still works via connection-scoped previous_response_id state.
1241
1291
  const fullBody = body;
1242
1292
  const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
1293
+ const stats = options?.sessionId ? getOrCreateWebSocketDebugStats(options.sessionId) : undefined;
1294
+ if (stats) {
1295
+ stats.requests++;
1296
+ if (reused) stats.connectionsReused++;
1297
+ else stats.connectionsCreated++;
1298
+ if (useCachedContext) stats.cachedContextRequests++;
1299
+ if (requestBody.store === true) stats.storeTrueRequests++;
1300
+ stats.lastInputItems = requestBody.input?.length ?? 0;
1301
+ if (requestBody.previous_response_id) {
1302
+ stats.deltaRequests++;
1303
+ stats.lastDeltaInputItems = requestBody.input?.length ?? 0;
1304
+ stats.lastPreviousResponseId = requestBody.previous_response_id;
1305
+ } else {
1306
+ stats.fullContextRequests++;
1307
+ stats.lastDeltaInputItems = undefined;
1308
+ stats.lastPreviousResponseId = undefined;
1309
+ }
1310
+ }
1243
1311
 
1244
1312
  const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
1245
1313
  if (released) return;
@@ -1472,6 +1540,7 @@ function createCodexStream<TApi extends Api>(
1472
1540
  options: SimpleStreamOptions | undefined,
1473
1541
  deps: {
1474
1542
  getCurrentCwd: () => string;
1543
+ getNativeToolRewriteConfig?: () => { webSearch: boolean; imageGeneration: boolean };
1475
1544
  onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
1476
1545
  onWebSearchCaptured?: (search: SurfacedWebSearch) => void;
1477
1546
  },
@@ -1495,6 +1564,13 @@ function createCodexStream<TApi extends Api>(
1495
1564
  if (nextBody !== undefined) {
1496
1565
  body = nextBody as ResponsesBody;
1497
1566
  }
1567
+ const nativeToolRewriteConfig = deps.getNativeToolRewriteConfig?.();
1568
+ if (nativeToolRewriteConfig?.webSearch) {
1569
+ body = rewriteNativeWebSearchTool(body, model) as ResponsesBody;
1570
+ }
1571
+ if (nativeToolRewriteConfig?.imageGeneration) {
1572
+ body = rewriteNativeImageGenerationTool(body, model) as ResponsesBody;
1573
+ }
1498
1574
 
1499
1575
  const websocketRequestId = options?.sessionId || createCodexRequestId();
1500
1576
  const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
@@ -1626,7 +1702,7 @@ function createCodexStream<TApi extends Api>(
1626
1702
  return stream;
1627
1703
  }
1628
1704
 
1629
- export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { getCurrentCwd: () => string }): void {
1705
+ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { getCurrentCwd: () => string; getNativeToolRewriteConfig?: () => { webSearch: boolean; imageGeneration: boolean } }): void {
1630
1706
  const pendingActivities: PendingActivity[] = [];
1631
1707
  const imagePreviewCache = new Map<string, CachedImagePreview>();
1632
1708
  let pendingFlushTimer: ReturnType<typeof setTimeout> | undefined;
@@ -1688,6 +1764,7 @@ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { g
1688
1764
  streamSimple: (model, context, streamOptions) =>
1689
1765
  createCodexStream(model, context, streamOptions, {
1690
1766
  getCurrentCwd: options.getCurrentCwd,
1767
+ getNativeToolRewriteConfig: options.getNativeToolRewriteConfig,
1691
1768
  onImageSaved: (savedImage, imageData) => {
1692
1769
  pendingActivities.push({ kind: "image", savedImage, imageData });
1693
1770
  },
@@ -40,6 +40,8 @@ interface ConvertResponsesToolsOptions {
40
40
  strict?: boolean | null;
41
41
  }
42
42
 
43
+ export const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
44
+
43
45
  function shortHash(str: string): string {
44
46
  let h1 = 0xdeadbeef;
45
47
  let h2 = 0x41c6ce57;
@@ -2,10 +2,11 @@ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-w
2
2
  import { Type } from "typebox";
3
3
  import { Container, Text } from "@earendil-works/pi-tui";
4
4
  import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
+ import { WEB_SEARCH_TOOL_NAME } from "../adapter/tool-set.ts";
5
6
 
6
- export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
7
+ export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web.run is only available with the openai-codex provider";
7
8
  const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
8
- "web_search is a native openai-codex provider tool and should not execute locally";
9
+ "web.run is a native openai-codex provider tool and should not execute locally";
9
10
  export const WEB_SEARCH_SESSION_NOTE_TYPE = "codex-web-search-session-note";
10
11
  const WEB_SEARCH_MULTIMODAL_CONTENT_TYPES = ["text", "image"] as const;
11
12
 
@@ -20,6 +21,7 @@ interface FunctionToolPayload {
20
21
  }
21
22
 
22
23
  interface ResponsesPayload {
24
+ include?: unknown;
23
25
  tools?: unknown[];
24
26
  [key: string]: unknown;
25
27
  }
@@ -43,7 +45,7 @@ export function supportsMultimodalNativeWebSearch(model: ExtensionContext["model
43
45
  }
44
46
 
45
47
  function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
46
- return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
48
+ return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === WEB_SEARCH_TOOL_NAME;
47
49
  }
48
50
 
49
51
  function createEmptyResultComponent(): Container {
@@ -80,17 +82,28 @@ export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionCon
80
82
  if (!rewritten) {
81
83
  return payload;
82
84
  }
85
+ const existingInclude: unknown[] = Array.isArray((payload as ResponsesPayload).include)
86
+ ? [...((payload as ResponsesPayload).include as unknown[])]
87
+ : [];
88
+ const include = [
89
+ ...existingInclude,
90
+ ...[
91
+ "web_search_call.action.sources",
92
+ "web_search_call.results",
93
+ ].filter((item) => !existingInclude.includes(item)),
94
+ ];
83
95
 
84
96
  return {
85
97
  ...(payload as ResponsesPayload),
98
+ include,
86
99
  tools: nextTools,
87
100
  };
88
101
  }
89
102
 
90
- export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
103
+ export function createWebSearchTool(name: string = WEB_SEARCH_TOOL_NAME): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
91
104
  return {
92
- name: "web_search",
93
- label: "web_search",
105
+ name,
106
+ label: name,
94
107
  description:
95
108
  "Search the web for sources relevant to the current task. Use it when you need up-to-date information, external references, or broader context beyond the workspace.",
96
109
  promptSnippet:
@@ -104,7 +117,7 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
104
117
  throw new Error(WEB_SEARCH_LOCAL_EXECUTION_MESSAGE);
105
118
  },
106
119
  renderCall(_args, theme) {
107
- return new Text(`${theme.fg("toolTitle", theme.bold("web_search"))}`, 0, 0);
120
+ return new Text(`${theme.fg("toolTitle", theme.bold(name))}`, 0, 0);
108
121
  },
109
122
  renderResult(result, { expanded }, theme) {
110
123
  if (!expanded) {
@@ -117,6 +130,6 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
117
130
  };
118
131
  }
119
132
 
120
- export function registerWebSearchTool(pi: ExtensionAPI): void {
121
- pi.registerTool(createWebSearchTool());
133
+ export function registerWebSearchTool(pi: ExtensionAPI, name: string = WEB_SEARCH_TOOL_NAME): void {
134
+ pi.registerTool(createWebSearchTool(name));
122
135
  }