@howaboua/pi-codex-conversion 1.0.29 → 1.0.31-dev.3.4b87b70
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 +1 -1
- package/bin/apply_patch +30 -0
- package/bin/apply_patch.cmd +2 -0
- package/package.json +18 -13
- package/src/adapter/codex-model.ts +1 -1
- package/src/index.ts +4 -21
- package/src/providers/openai-codex-custom-provider.ts +99 -48
- package/src/providers/openai-responses-shared.ts +2 -2
- package/src/tools/apply-patch-binary.ts +21 -0
- package/src/tools/apply-patch-rendering.ts +1 -1
- package/src/tools/apply-patch-tool.ts +2 -2
- package/src/tools/exec-command-tool.ts +2 -2
- package/src/tools/image-generation-tool.ts +2 -2
- package/src/tools/view-image-tool.ts +2 -2
- package/src/tools/web-search-tool.ts +2 -21
- package/src/tools/write-stdin-tool.ts +2 -2
- package/vendor/apply-patch/darwin-arm64/apply_patch +0 -0
- package/vendor/apply-patch/darwin-x64/apply_patch +0 -0
- package/vendor/apply-patch/linux-arm64/apply_patch +0 -0
- package/vendor/apply-patch/linux-x64/apply_patch +0 -0
- 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 @@ This package replaces Pi's default Codex/GPT experience with a narrower Codex-li
|
|
|
9
9
|
- preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
|
|
10
10
|
- renders exec activity with Codex-style command and background-terminal labels
|
|
11
11
|
- renders `apply_patch` calls with Codex-style `Added` / `Edited` / `Deleted` diff blocks and Pi-style colored diff lines
|
|
12
|
-
- targets modern Pi tool/rendering APIs and is aligned with Pi `0.
|
|
12
|
+
- targets modern Pi tool/rendering APIs and is aligned with Pi `0.74.x` / the `@earendil-works/*` package scope
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
package/bin/apply_patch
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
const arch = process.arch;
|
|
10
|
+
const exe = platform === "win32" ? "apply_patch.exe" : "apply_patch";
|
|
11
|
+
const platformArch = `${platform}-${arch}`;
|
|
12
|
+
const binary = join(root, "vendor", "apply-patch", platformArch, exe);
|
|
13
|
+
|
|
14
|
+
if (!existsSync(binary)) {
|
|
15
|
+
console.error(`apply_patch binary is not bundled for ${platformArch}.`);
|
|
16
|
+
console.error("Expected bundled binaries under vendor/apply-patch/<platform>-<arch>/.");
|
|
17
|
+
process.exit(127);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = spawnSync(binary, process.argv.slice(2), {
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
env: process.env,
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (result.error) {
|
|
27
|
+
console.error(result.error.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
process.exit(result.status ?? 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-codex-conversion",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31-dev.3.4b87b70",
|
|
4
4
|
"description": "Codex-oriented tool and prompt adapter for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -21,10 +21,6 @@
|
|
|
21
21
|
"apply-patch"
|
|
22
22
|
],
|
|
23
23
|
"license": "MIT",
|
|
24
|
-
"os": [
|
|
25
|
-
"darwin",
|
|
26
|
-
"linux"
|
|
27
|
-
],
|
|
28
24
|
"pi": {
|
|
29
25
|
"extensions": [
|
|
30
26
|
"./src/index.ts"
|
|
@@ -33,6 +29,9 @@
|
|
|
33
29
|
"files": [
|
|
34
30
|
"src/**/*.ts",
|
|
35
31
|
"src/**/*.md",
|
|
32
|
+
"bin/apply_patch",
|
|
33
|
+
"bin/apply_patch.cmd",
|
|
34
|
+
"vendor/apply-patch/**",
|
|
36
35
|
"available-tools.png",
|
|
37
36
|
"README.md",
|
|
38
37
|
"LICENSE"
|
|
@@ -45,21 +44,24 @@
|
|
|
45
44
|
"publish:dev": "npm publish --tag dev",
|
|
46
45
|
"release:dev": "npm version prerelease --preid dev && npm publish --tag dev",
|
|
47
46
|
"prepack": "npm run check",
|
|
48
|
-
"prepublishOnly": "npm run check"
|
|
47
|
+
"prepublishOnly": "npm run verify:apply-patch-binaries && npm run check",
|
|
48
|
+
"build:apply-patch": "node scripts/build-apply-patch-binary.mjs",
|
|
49
|
+
"sync:apply-patch-source": "node scripts/sync-apply-patch-source.mjs",
|
|
50
|
+
"verify:apply-patch-binaries": "node scripts/verify-apply-patch-binaries.mjs"
|
|
49
51
|
},
|
|
50
52
|
"publishConfig": {
|
|
51
53
|
"access": "public"
|
|
52
54
|
},
|
|
53
55
|
"peerDependencies": {
|
|
54
|
-
"@
|
|
55
|
-
"@
|
|
56
|
-
"@
|
|
57
|
-
"typebox": "
|
|
56
|
+
"@earendil-works/pi-ai": "*",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
58
|
+
"@earendil-works/pi-tui": "*",
|
|
59
|
+
"typebox": "*"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
|
-
"@
|
|
61
|
-
"@
|
|
62
|
-
"@
|
|
62
|
+
"@earendil-works/pi-ai": "^0.74.0",
|
|
63
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
64
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
63
65
|
"tsx": "^4.20.5",
|
|
64
66
|
"typebox": "^1.1.24",
|
|
65
67
|
"typescript": "^5.9.3"
|
|
@@ -69,5 +71,8 @@
|
|
|
69
71
|
"partial-json": "^0.1.7",
|
|
70
72
|
"tree-sitter-bash": "^0.25.1",
|
|
71
73
|
"web-tree-sitter": "^0.26.7"
|
|
74
|
+
},
|
|
75
|
+
"bin": {
|
|
76
|
+
"apply_patch": "./bin/apply_patch"
|
|
72
77
|
}
|
|
73
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
|
|
3
3
|
import {
|
|
4
4
|
CORE_ADAPTER_TOOL_NAMES,
|
|
@@ -24,21 +24,18 @@ import { buildCodexSystemPrompt, extractPiPromptSkills, type PromptSkill } from
|
|
|
24
24
|
import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
|
|
25
25
|
import {
|
|
26
26
|
registerWebSearchTool,
|
|
27
|
-
registerWebSearchSessionNoteRenderer,
|
|
28
27
|
rewriteNativeWebSearchTool,
|
|
29
|
-
shouldShowWebSearchSessionNote,
|
|
30
28
|
supportsNativeWebSearch,
|
|
31
|
-
WEB_SEARCH_SESSION_NOTE_TEXT,
|
|
32
29
|
WEB_SEARCH_SESSION_NOTE_TYPE,
|
|
33
30
|
} from "./tools/web-search-tool.ts";
|
|
34
31
|
import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
|
|
32
|
+
import { ensureBundledApplyPatchOnPath } from "./tools/apply-patch-binary.ts";
|
|
35
33
|
|
|
36
34
|
interface AdapterState {
|
|
37
35
|
enabled: boolean;
|
|
38
36
|
cwd: string;
|
|
39
37
|
previousToolNames?: string[];
|
|
40
38
|
promptSkills: PromptSkill[];
|
|
41
|
-
webSearchNoticeShown: boolean;
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, WEB_SEARCH_TOOL_NAME, IMAGE_GENERATION_TOOL_NAME, VIEW_IMAGE_TOOL_NAME];
|
|
@@ -61,8 +58,9 @@ function isToolCallOnlyAssistantMessage(message: unknown): boolean {
|
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
export default function codexConversion(pi: ExtensionAPI) {
|
|
61
|
+
ensureBundledApplyPatchOnPath();
|
|
64
62
|
const tracker = createExecCommandTracker();
|
|
65
|
-
const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: []
|
|
63
|
+
const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: [] };
|
|
66
64
|
const sessions = createExecSessionManager();
|
|
67
65
|
|
|
68
66
|
registerOpenAICodexCustomProvider(pi, {
|
|
@@ -73,7 +71,6 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
73
71
|
registerWriteStdinTool(pi, sessions);
|
|
74
72
|
registerImageGenerationTool(pi);
|
|
75
73
|
registerWebSearchTool(pi);
|
|
76
|
-
registerWebSearchSessionNoteRenderer(pi);
|
|
77
74
|
|
|
78
75
|
sessions.onSessionExit((sessionId) => {
|
|
79
76
|
tracker.recordSessionFinished(sessionId);
|
|
@@ -81,7 +78,6 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
81
78
|
|
|
82
79
|
pi.on("session_start", async (_event, ctx) => {
|
|
83
80
|
state.cwd = ctx.cwd;
|
|
84
|
-
state.webSearchNoticeShown = false;
|
|
85
81
|
clearApplyPatchRenderState();
|
|
86
82
|
tracker.clear();
|
|
87
83
|
syncAdapter(pi, ctx, state);
|
|
@@ -157,7 +153,6 @@ function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterStat
|
|
|
157
153
|
state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
|
|
158
154
|
|
|
159
155
|
registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
|
|
160
|
-
maybeShowWebSearchSessionNote(pi, ctx, state);
|
|
161
156
|
|
|
162
157
|
if (isCodexLikeContext(ctx)) {
|
|
163
158
|
enableAdapter(pi, ctx, state);
|
|
@@ -227,15 +222,3 @@ export function restoreTools(previousTools: string[], activeTools: string[]): st
|
|
|
227
222
|
function hasAdapterTools(activeTools: string[]): boolean {
|
|
228
223
|
return activeTools.some((toolName) => ADAPTER_TOOL_NAMES.includes(toolName));
|
|
229
224
|
}
|
|
230
|
-
|
|
231
|
-
function maybeShowWebSearchSessionNote(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
232
|
-
if (!shouldShowWebSearchSessionNote(ctx.model, ctx.hasUI, state.webSearchNoticeShown)) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
pi.sendMessage({
|
|
236
|
-
customType: WEB_SEARCH_SESSION_NOTE_TYPE,
|
|
237
|
-
content: WEB_SEARCH_SESSION_NOTE_TEXT,
|
|
238
|
-
display: true,
|
|
239
|
-
});
|
|
240
|
-
state.webSearchNoticeShown = true;
|
|
241
|
-
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Box, Image, Spacer, Text } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, Image, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import {
|
|
4
4
|
createAssistantMessageEventStream,
|
|
5
|
+
appendAssistantMessageDiagnostic,
|
|
5
6
|
clampThinkingLevel,
|
|
7
|
+
createAssistantMessageDiagnostic,
|
|
6
8
|
getEnvApiKey,
|
|
7
9
|
type Api,
|
|
8
10
|
type AssistantMessage,
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
type Context,
|
|
11
13
|
type Model,
|
|
12
14
|
type SimpleStreamOptions,
|
|
13
|
-
} from "@
|
|
15
|
+
} from "@earendil-works/pi-ai";
|
|
14
16
|
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
|
|
15
17
|
import {
|
|
16
18
|
convertResponsesMessages,
|
|
@@ -29,6 +31,7 @@ const BASE_DELAY_MS = 1000;
|
|
|
29
31
|
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
30
32
|
const CODEX_RESPONSE_STATUSES = new Set(["completed", "incomplete", "failed", "cancelled", "queued", "in_progress"]);
|
|
31
33
|
const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
|
|
34
|
+
const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
|
|
32
35
|
const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
33
36
|
const dynamicImport = (specifier: string) => import(specifier);
|
|
34
37
|
let _os: { platform(): string; release(): string; arch(): string } | null = null;
|
|
@@ -412,16 +415,6 @@ function headersToRecord(headers: Headers): Record<string, string> {
|
|
|
412
415
|
return Object.fromEntries(headers.entries());
|
|
413
416
|
}
|
|
414
417
|
|
|
415
|
-
function buildWebSocketCacheKey(url: string, headers: Headers, sessionId: string | undefined): string | undefined {
|
|
416
|
-
if (!sessionId) return undefined;
|
|
417
|
-
const headerFingerprint = Object.entries(headersToRecord(headers))
|
|
418
|
-
.map(([key, value]) => [key.toLowerCase(), value] as const)
|
|
419
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
420
|
-
.map(([key, value]) => `${key}:${value}`)
|
|
421
|
-
.join("\n");
|
|
422
|
-
return `${sessionId}:${shortHash(`${url}\n${headerFingerprint}`)}`;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
418
|
function createCodexRequestId(): string {
|
|
426
419
|
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
427
420
|
return globalThis.crypto.randomUUID();
|
|
@@ -523,7 +516,7 @@ function resolveCodexServiceTier(responseServiceTier: ServiceTier, requestServic
|
|
|
523
516
|
return responseServiceTier ?? requestServiceTier;
|
|
524
517
|
}
|
|
525
518
|
|
|
526
|
-
function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions): ResponsesBody {
|
|
519
|
+
export function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions): ResponsesBody {
|
|
527
520
|
const messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
|
|
528
521
|
includeSystemPrompt: false,
|
|
529
522
|
});
|
|
@@ -532,7 +525,7 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
|
|
|
532
525
|
model: model.id,
|
|
533
526
|
store: false,
|
|
534
527
|
stream: true,
|
|
535
|
-
instructions: context.systemPrompt,
|
|
528
|
+
instructions: context.systemPrompt || "You are a helpful assistant.",
|
|
536
529
|
input: messages,
|
|
537
530
|
text: { verbosity: ((options as { textVerbosity?: string } | undefined)?.textVerbosity ?? "low") as string },
|
|
538
531
|
include: ["reasoning.encrypted_content"],
|
|
@@ -603,7 +596,7 @@ function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
|
|
|
603
596
|
});
|
|
604
597
|
}
|
|
605
598
|
|
|
606
|
-
async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
|
|
599
|
+
export async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
|
|
607
600
|
if (!response.body) return;
|
|
608
601
|
|
|
609
602
|
const reader = response.body.getReader();
|
|
@@ -630,8 +623,8 @@ async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
|
|
|
630
623
|
if (data && data !== "[DONE]") {
|
|
631
624
|
try {
|
|
632
625
|
yield JSON.parse(data) as StreamEventShape;
|
|
633
|
-
} catch {
|
|
634
|
-
|
|
626
|
+
} catch (error) {
|
|
627
|
+
throw new Error(`Invalid Codex SSE JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
635
628
|
}
|
|
636
629
|
}
|
|
637
630
|
}
|
|
@@ -674,6 +667,29 @@ function closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = "do
|
|
|
674
667
|
}
|
|
675
668
|
}
|
|
676
669
|
|
|
670
|
+
export function closeOpenAICodexWebSocketSessions(sessionId?: string): void {
|
|
671
|
+
const closeEntry = (entry: SessionWebSocketCacheEntry) => {
|
|
672
|
+
if (entry.idleTimer) {
|
|
673
|
+
clearTimeout(entry.idleTimer);
|
|
674
|
+
entry.idleTimer = undefined;
|
|
675
|
+
}
|
|
676
|
+
closeWebSocketSilently(entry.socket, 1000, "session_shutdown");
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
if (sessionId) {
|
|
680
|
+
const entry = websocketSessionCache.get(sessionId);
|
|
681
|
+
if (entry) closeEntry(entry);
|
|
682
|
+
websocketSessionCache.delete(sessionId);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
for (const entry of websocketSessionCache.values()) {
|
|
687
|
+
closeEntry(entry);
|
|
688
|
+
}
|
|
689
|
+
websocketSessionCache.clear();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
|
|
677
693
|
function scheduleSessionWebSocketExpiry(cacheKey: string, entry: SessionWebSocketCacheEntry): void {
|
|
678
694
|
if (entry.idleTimer) {
|
|
679
695
|
clearTimeout(entry.idleTimer);
|
|
@@ -700,7 +716,10 @@ function extractWebSocketCloseError(event: unknown): Error {
|
|
|
700
716
|
const code = "code" in event ? (event as { code?: unknown }).code : undefined;
|
|
701
717
|
const reason = "reason" in event ? (event as { reason?: unknown }).reason : undefined;
|
|
702
718
|
const codeText = typeof code === "number" ? ` ${code}` : "";
|
|
703
|
-
|
|
719
|
+
let reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
|
|
720
|
+
if (!reasonText && code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE) {
|
|
721
|
+
reasonText = " message too big";
|
|
722
|
+
}
|
|
704
723
|
return new Error(`WebSocket closed${codeText}${reasonText}`.trim());
|
|
705
724
|
}
|
|
706
725
|
return new Error("WebSocket closed");
|
|
@@ -772,8 +791,7 @@ async function acquireWebSocket(
|
|
|
772
791
|
sessionId: string | undefined,
|
|
773
792
|
signal: AbortSignal | undefined,
|
|
774
793
|
): Promise<AcquiredWebSocket> {
|
|
775
|
-
|
|
776
|
-
if (!cacheKey) {
|
|
794
|
+
if (!sessionId) {
|
|
777
795
|
const socket = await connectWebSocket(url, headers, signal);
|
|
778
796
|
return {
|
|
779
797
|
socket,
|
|
@@ -788,7 +806,7 @@ async function acquireWebSocket(
|
|
|
788
806
|
};
|
|
789
807
|
}
|
|
790
808
|
|
|
791
|
-
const cached = websocketSessionCache.get(
|
|
809
|
+
const cached = websocketSessionCache.get(sessionId);
|
|
792
810
|
if (cached) {
|
|
793
811
|
if (cached.idleTimer) {
|
|
794
812
|
clearTimeout(cached.idleTimer);
|
|
@@ -804,11 +822,11 @@ async function acquireWebSocket(
|
|
|
804
822
|
release: ({ keep } = {}) => {
|
|
805
823
|
if (!keep || !isWebSocketReusable(cached.socket)) {
|
|
806
824
|
closeWebSocketSilently(cached.socket);
|
|
807
|
-
websocketSessionCache.delete(
|
|
825
|
+
websocketSessionCache.delete(sessionId);
|
|
808
826
|
return;
|
|
809
827
|
}
|
|
810
828
|
cached.busy = false;
|
|
811
|
-
scheduleSessionWebSocketExpiry(
|
|
829
|
+
scheduleSessionWebSocketExpiry(sessionId, cached);
|
|
812
830
|
},
|
|
813
831
|
};
|
|
814
832
|
}
|
|
@@ -826,13 +844,13 @@ async function acquireWebSocket(
|
|
|
826
844
|
|
|
827
845
|
if (!isWebSocketReusable(cached.socket)) {
|
|
828
846
|
closeWebSocketSilently(cached.socket);
|
|
829
|
-
websocketSessionCache.delete(
|
|
847
|
+
websocketSessionCache.delete(sessionId);
|
|
830
848
|
}
|
|
831
849
|
}
|
|
832
850
|
|
|
833
851
|
const socket = await connectWebSocket(url, headers, signal);
|
|
834
852
|
const entry: SessionWebSocketCacheEntry = { socket, busy: true };
|
|
835
|
-
websocketSessionCache.set(
|
|
853
|
+
websocketSessionCache.set(sessionId, entry);
|
|
836
854
|
return {
|
|
837
855
|
socket,
|
|
838
856
|
entry,
|
|
@@ -841,13 +859,13 @@ async function acquireWebSocket(
|
|
|
841
859
|
if (!keep || !isWebSocketReusable(entry.socket)) {
|
|
842
860
|
closeWebSocketSilently(entry.socket);
|
|
843
861
|
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
844
|
-
if (websocketSessionCache.get(
|
|
845
|
-
websocketSessionCache.delete(
|
|
862
|
+
if (websocketSessionCache.get(sessionId) === entry) {
|
|
863
|
+
websocketSessionCache.delete(sessionId);
|
|
846
864
|
}
|
|
847
865
|
return;
|
|
848
866
|
}
|
|
849
867
|
entry.busy = false;
|
|
850
|
-
scheduleSessionWebSocketExpiry(
|
|
868
|
+
scheduleSessionWebSocketExpiry(sessionId, entry);
|
|
851
869
|
},
|
|
852
870
|
};
|
|
853
871
|
}
|
|
@@ -951,8 +969,9 @@ async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | unde
|
|
|
951
969
|
done = true;
|
|
952
970
|
}
|
|
953
971
|
queue.push(parsed);
|
|
954
|
-
} catch {
|
|
955
|
-
|
|
972
|
+
} catch (error) {
|
|
973
|
+
failed = new Error(`Invalid Codex WebSocket JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
974
|
+
done = true;
|
|
956
975
|
}
|
|
957
976
|
})
|
|
958
977
|
.catch((error: unknown) => {
|
|
@@ -1033,9 +1052,27 @@ async function* countWebSocketEvents(
|
|
|
1033
1052
|
}
|
|
1034
1053
|
}
|
|
1035
1054
|
|
|
1055
|
+
async function* startWebSocketOutputOnFirstEvent(
|
|
1056
|
+
events: AsyncIterable<StreamEventShape>,
|
|
1057
|
+
output: AssistantMessage,
|
|
1058
|
+
stream: AssistantMessageEventStream,
|
|
1059
|
+
onStart: () => void,
|
|
1060
|
+
): AsyncIterable<StreamEventShape> {
|
|
1061
|
+
let started = false;
|
|
1062
|
+
for await (const event of events) {
|
|
1063
|
+
if (!started) {
|
|
1064
|
+
started = true;
|
|
1065
|
+
onStart();
|
|
1066
|
+
stream.push({ type: "start", partial: output });
|
|
1067
|
+
}
|
|
1068
|
+
yield event;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1036
1072
|
function isRetryableEarlyWebSocketError(error: unknown): boolean {
|
|
1037
1073
|
const message = error instanceof Error ? error.message : String(error);
|
|
1038
|
-
|
|
1074
|
+
if (/message too big/i.test(message)) return false;
|
|
1075
|
+
return /^(?:WebSocket (?:error|closed)(?:\s|$)|Invalid Codex WebSocket JSON)/.test(message);
|
|
1039
1076
|
}
|
|
1040
1077
|
|
|
1041
1078
|
async function* mapCodexEvents(events: AsyncIterable<StreamEventShape>): AsyncIterable<StreamEventShape> {
|
|
@@ -1193,11 +1230,12 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1193
1230
|
let streamStarted = false;
|
|
1194
1231
|
|
|
1195
1232
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1196
|
-
const { socket, entry, release
|
|
1233
|
+
const { socket, entry, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
|
|
1197
1234
|
let keepConnection = true;
|
|
1198
1235
|
let released = false;
|
|
1199
1236
|
let eventCount = 0;
|
|
1200
|
-
const
|
|
1237
|
+
const transport = (options as { transport?: string } | undefined)?.transport ?? "auto";
|
|
1238
|
+
const useCachedContext = transport === "websocket-cached" || transport === "auto";
|
|
1201
1239
|
// ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
|
|
1202
1240
|
// WebSocket continuation still works via connection-scoped previous_response_id state.
|
|
1203
1241
|
const fullBody = body;
|
|
@@ -1211,15 +1249,18 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1211
1249
|
|
|
1212
1250
|
try {
|
|
1213
1251
|
socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
|
|
1214
|
-
if (!streamStarted) {
|
|
1215
|
-
onStart();
|
|
1216
|
-
stream.push({ type: "start", partial: output });
|
|
1217
|
-
streamStarted = true;
|
|
1218
|
-
}
|
|
1219
1252
|
await processCapturedResponsesStream(
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1253
|
+
startWebSocketOutputOnFirstEvent(
|
|
1254
|
+
countWebSocketEvents(parseWebSocket(socket, options?.signal), () => {
|
|
1255
|
+
eventCount++;
|
|
1256
|
+
}),
|
|
1257
|
+
output,
|
|
1258
|
+
stream,
|
|
1259
|
+
() => {
|
|
1260
|
+
streamStarted = true;
|
|
1261
|
+
onStart();
|
|
1262
|
+
},
|
|
1263
|
+
),
|
|
1223
1264
|
output,
|
|
1224
1265
|
stream,
|
|
1225
1266
|
model,
|
|
@@ -1248,11 +1289,10 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1248
1289
|
}
|
|
1249
1290
|
keepConnection = false;
|
|
1250
1291
|
releaseOnce({ keep: false });
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1255
|
-
if (attempt === 0 && reused && eventCount === 0 && !options?.signal?.aborted && isRetryableEarlyWebSocketError(error)) {
|
|
1292
|
+
// If WebSocket fails before the first response event, nothing has been
|
|
1293
|
+
// emitted to the UI/history yet. Retry once on a fresh WebSocket; if that
|
|
1294
|
+
// also fails, the caller can fall back to SSE for `auto` transport.
|
|
1295
|
+
if (attempt === 0 && eventCount === 0 && !streamStarted && !options?.signal?.aborted && isRetryableEarlyWebSocketError(error)) {
|
|
1256
1296
|
continue;
|
|
1257
1297
|
}
|
|
1258
1298
|
throw error;
|
|
@@ -1460,7 +1500,7 @@ function createCodexStream<TApi extends Api>(
|
|
|
1460
1500
|
const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
|
|
1461
1501
|
const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
|
|
1462
1502
|
const bodyJson = JSON.stringify(body);
|
|
1463
|
-
const transport = options?.transport || "
|
|
1503
|
+
const transport = options?.transport || "auto";
|
|
1464
1504
|
|
|
1465
1505
|
if (transport !== "sse") {
|
|
1466
1506
|
let websocketStarted = false;
|
|
@@ -1488,6 +1528,16 @@ function createCodexStream<TApi extends Api>(
|
|
|
1488
1528
|
stream.end();
|
|
1489
1529
|
return;
|
|
1490
1530
|
} catch (error) {
|
|
1531
|
+
appendAssistantMessageDiagnostic(
|
|
1532
|
+
output,
|
|
1533
|
+
createAssistantMessageDiagnostic("provider_transport_failure", error, {
|
|
1534
|
+
configuredTransport: transport,
|
|
1535
|
+
fallbackTransport: websocketStarted ? undefined : "sse",
|
|
1536
|
+
eventsEmitted: websocketStarted,
|
|
1537
|
+
phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
|
|
1538
|
+
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
|
1539
|
+
}),
|
|
1540
|
+
);
|
|
1491
1541
|
if (transport === "websocket" || transport === "websocket-cached" || websocketStarted) {
|
|
1492
1542
|
throw error;
|
|
1493
1543
|
}
|
|
@@ -1656,6 +1706,7 @@ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { g
|
|
|
1656
1706
|
flushPendingMessages();
|
|
1657
1707
|
}
|
|
1658
1708
|
clearPendingMessages();
|
|
1709
|
+
closeOpenAICodexWebSocketSessions();
|
|
1659
1710
|
});
|
|
1660
1711
|
|
|
1661
1712
|
pi.on("agent_end", async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { calculateCost, type Api, type AssistantMessage, type Context, type Model, type Tool, type Usage } from "@
|
|
1
|
+
import { calculateCost, type Api, type AssistantMessage, type Context, type Model, type Tool, type Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ResponseCreateParamsStreaming, ResponseInput, ResponseStreamEvent, Tool as OpenAITool } from "openai/resources/responses/responses.js";
|
|
3
3
|
import { parse as partialParse } from "partial-json";
|
|
4
|
-
import type { AssistantMessageEventStream } from "@
|
|
4
|
+
import type { AssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
5
5
|
|
|
6
6
|
type MessageRole = Context["messages"][number]["role"];
|
|
7
7
|
type Message = Context["messages"][number];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, delimiter, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export function getBundledApplyPatchBinDir(): string {
|
|
6
|
+
return join(dirname(dirname(dirname(fileURLToPath(import.meta.url)))), "bin");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ensureBundledApplyPatchOnPath(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
10
|
+
const binDir = getBundledApplyPatchBinDir();
|
|
11
|
+
const wrapperPath = join(binDir, process.platform === "win32" ? "apply_patch.cmd" : "apply_patch");
|
|
12
|
+
if (!existsSync(wrapperPath)) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const currentPath = env.PATH ?? "";
|
|
16
|
+
const entries = currentPath.split(delimiter).filter(Boolean);
|
|
17
|
+
if (!entries.includes(binDir)) {
|
|
18
|
+
env.PATH = [binDir, ...entries].join(delimiter);
|
|
19
|
+
}
|
|
20
|
+
return binDir;
|
|
21
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAbsolute, relative } from "node:path";
|
|
2
|
-
import { renderDiff } from "@
|
|
2
|
+
import { renderDiff } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { openFileAtPath } from "../patch/paths.ts";
|
|
4
4
|
import { parsePatchActions } from "../patch/parser.ts";
|
|
5
5
|
import type { ParsedPatchAction } from "../patch/types.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import type { ExtensionAPI } from "@
|
|
3
|
-
import { Container, Text } from "@
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { executePatch } from "../patch/core.ts";
|
|
5
5
|
import { ExecutePatchError, type ExecutePatchResult } from "../patch/types.ts";
|
|
6
6
|
import { formatApplyPatchSummary, formatPatchTarget, renderApplyPatchCall } from "./apply-patch-rendering.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { Container, Text } from "@
|
|
3
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { renderExecCommandCall, renderGroupedExecCommandCall } from "./codex-rendering.ts";
|
|
5
5
|
import type { ExecCommandTracker } from "./exec-command-state.ts";
|
|
6
6
|
import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { Container, Text } from "@
|
|
3
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { isOpenAICodexModel } from "../adapter/codex-model.ts";
|
|
5
5
|
|
|
6
6
|
export const IMAGE_GENERATION_UNSUPPORTED_MESSAGE =
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
type ExtensionAPI,
|
|
7
7
|
type ExtensionContext,
|
|
8
8
|
type ToolDefinition,
|
|
9
|
-
} from "@
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { Type, type TSchema } from "typebox";
|
|
11
|
-
import { Text } from "@
|
|
11
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
13
|
const VIEW_IMAGE_UNSUPPORTED_MESSAGE = "view_image is not allowed because you do not support image inputs";
|
|
14
14
|
const DETAIL_DESCRIPTION =
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
3
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { isOpenAICodexModel } from "../adapter/codex-model.ts";
|
|
5
5
|
|
|
6
6
|
export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
|
|
7
7
|
const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
|
|
8
8
|
"web_search is a native openai-codex provider tool and should not execute locally";
|
|
9
9
|
export const WEB_SEARCH_SESSION_NOTE_TYPE = "codex-web-search-session-note";
|
|
10
|
-
export const WEB_SEARCH_SESSION_NOTE_TEXT =
|
|
11
|
-
"Native OpenAI Codex web search is enabled for this session. Search activity is surfaced as merged foldable status messages instead of native tool-call rows.";
|
|
12
10
|
const WEB_SEARCH_MULTIMODAL_CONTENT_TYPES = ["text", "image"] as const;
|
|
13
11
|
|
|
14
12
|
const WEB_SEARCH_PARAMETERS = Type.Unsafe<Record<string, never>>({
|
|
@@ -36,14 +34,6 @@ export function supportsNativeWebSearch(model: ExtensionContext["model"]): boole
|
|
|
36
34
|
return isOpenAICodexModel(model);
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
export function shouldShowWebSearchSessionNote(
|
|
40
|
-
model: ExtensionContext["model"],
|
|
41
|
-
hasUI: boolean,
|
|
42
|
-
alreadyShown: boolean,
|
|
43
|
-
): boolean {
|
|
44
|
-
return hasUI && !alreadyShown && supportsNativeWebSearch(model);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
export function supportsMultimodalNativeWebSearch(model: ExtensionContext["model"]): boolean {
|
|
48
38
|
if (!supportsNativeWebSearch(model)) {
|
|
49
39
|
return false;
|
|
@@ -130,12 +120,3 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
|
|
|
130
120
|
export function registerWebSearchTool(pi: ExtensionAPI): void {
|
|
131
121
|
pi.registerTool(createWebSearchTool());
|
|
132
122
|
}
|
|
133
|
-
|
|
134
|
-
export function registerWebSearchSessionNoteRenderer(pi: ExtensionAPI): void {
|
|
135
|
-
pi.registerMessageRenderer(WEB_SEARCH_SESSION_NOTE_TYPE, (_message, _options, theme) => {
|
|
136
|
-
const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
|
|
137
|
-
box.addChild(new Text(theme.bold("Web search enabled"), 0, 0));
|
|
138
|
-
box.addChild(new Text(`\n${theme.fg("dim", WEB_SEARCH_SESSION_NOTE_TEXT)}`, 0, 0));
|
|
139
|
-
return box;
|
|
140
|
-
});
|
|
141
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { Container, Text } from "@
|
|
3
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { renderWriteStdinCall } from "./codex-rendering.ts";
|
|
5
5
|
import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
|
|
6
6
|
import { formatUnifiedExecResult } from "./unified-exec-format.ts";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|