@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.
- package/CHANGELOG.md +13 -0
- package/README.md +6 -2
- package/package.json +4 -4
- package/src/adapter/activation.ts +38 -5
- package/src/adapter/compact-client.ts +257 -0
- package/src/adapter/compaction-output.ts +80 -0
- package/src/adapter/compaction-runtime.ts +272 -0
- package/src/adapter/compaction.ts +261 -0
- package/src/adapter/config.ts +27 -0
- package/src/adapter/context-filter.ts +20 -0
- package/src/adapter/details-store.ts +151 -0
- package/src/adapter/payload-rewrite.ts +550 -0
- package/src/adapter/provider-request.ts +4 -2
- package/src/adapter/serializer.ts +288 -0
- package/src/adapter/skills.ts +8 -0
- package/src/adapter/tool-set.ts +7 -3
- package/src/adapter/types.ts +220 -0
- package/src/codex-settings/command.ts +16 -3
- package/src/codex-settings/ui.ts +140 -48
- package/src/index.ts +49 -24
- package/src/providers/openai-codex-custom-provider.ts +81 -4
- package/src/providers/openai-responses-shared.ts +2 -0
- package/src/tools/web-search-tool.ts +22 -9
- package/vendor/apply-patch/win32-arm64/apply_patch.exe +0 -0
- package/vendor/apply-patch/win32-x64/apply_patch.exe +0 -0
package/src/codex-settings/ui.ts
CHANGED
|
@@ -1,67 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
}
|
|
39
|
-
settingsList.updateValue(id, previousValue);
|
|
40
|
-
}
|
|
38
|
+
}, done, () => tui.requestRender());
|
|
41
39
|
tui.requestRender();
|
|
42
|
-
}
|
|
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) =>
|
|
63
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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("
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 ===
|
|
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 = "
|
|
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
|
-
"
|
|
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 ===
|
|
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
|
|
93
|
-
label:
|
|
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(
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|