pi-openai-codex-compat 0.0.6 → 0.0.7

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.
@@ -209,6 +209,7 @@ export async function processCodexStream(
209
209
  ): Promise<void> {
210
210
  let terminal = false;
211
211
  const slots = new Map<number, OutputSlot>();
212
+ const completedOutputItems = new Set<string>();
212
213
  const reasoningById = new Map<string, ThinkingContent>();
213
214
  const applyMessagePhaseStopReason = (item: JsonRecord): void => {
214
215
  if (item.type === "message" && item["phase"] === "final_answer") {
@@ -318,6 +319,79 @@ export async function processCodexStream(
318
319
  const slotFor = (index: number, item: JsonRecord): OutputSlot | undefined =>
319
320
  slots.get(index) ?? createSlot(index, item);
320
321
 
322
+ const outputItemKey = (index: number, item: JsonRecord): string => {
323
+ if (typeof item.id === "string") return `id:${item.id}`;
324
+ if (typeof item["call_id"] === "string") {
325
+ return `call:${String(item.type)}:${item["call_id"]}`;
326
+ }
327
+ return `index:${String(index)}:${String(item.type)}`;
328
+ };
329
+
330
+ const completeOutputItem = (index: number, item: JsonRecord): void => {
331
+ const key = outputItemKey(index, item);
332
+ if (completedOutputItems.has(key)) return;
333
+ completedOutputItems.add(key);
334
+ applyMessagePhaseStopReason(item);
335
+ const slot = slotFor(index, item);
336
+ if (item.type === "reasoning" && slot?.type === "thinking") {
337
+ slot.block.thinking = reasoningText(item) || slot.block.thinking;
338
+ slot.block.thinkingSignature = JSON.stringify(item);
339
+ if (typeof item.id === "string") reasoningById.set(item.id, slot.block);
340
+ stream.push({
341
+ type: "thinking_end",
342
+ contentIndex: slot.contentIndex,
343
+ content: slot.block.thinking,
344
+ partial: output,
345
+ });
346
+ trackCompleted(slot);
347
+ slots.delete(index);
348
+ } else if (item.type === "message" && slot?.type === "text") {
349
+ slot.block.text = itemContentText(item);
350
+ if (typeof item.id === "string") {
351
+ slot.block.textSignature = encodeTextSignature(item.id, item["phase"]);
352
+ }
353
+ stream.push({
354
+ type: "text_end",
355
+ contentIndex: slot.contentIndex,
356
+ content: slot.block.text,
357
+ partial: output,
358
+ });
359
+ trackCompleted(slot);
360
+ slots.delete(index);
361
+ } else if (
362
+ item.type === "function_call" &&
363
+ slot?.type === "toolCall" &&
364
+ slot.block.partialJson !== undefined
365
+ ) {
366
+ slot.block.name = piToolCallName(item);
367
+ const argumentsJson =
368
+ typeof item.arguments === "string" ? item.arguments : slot.block.partialJson || "{}";
369
+ slot.block.arguments = parseStreamingJson(argumentsJson);
370
+ delete slot.block.partialJson;
371
+ stream.push({
372
+ type: "toolcall_end",
373
+ contentIndex: slot.contentIndex,
374
+ toolCall: slot.block,
375
+ partial: output,
376
+ });
377
+ trackCompleted(slot);
378
+ slots.delete(index);
379
+ } else if (item.type === "custom_tool_call" && slot?.type === "toolCall") {
380
+ slot.block.name = piToolCallName(item);
381
+ const input = typeof item["input"] === "string" ? item["input"] : customInput(slot.block);
382
+ pushToolDelta(slot, appendCustomInput(slot.block, input, true));
383
+ delete slot.block.customInput;
384
+ stream.push({
385
+ type: "toolcall_end",
386
+ contentIndex: slot.contentIndex,
387
+ toolCall: slot.block,
388
+ partial: output,
389
+ });
390
+ trackCompleted(slot);
391
+ slots.delete(index);
392
+ }
393
+ };
394
+
321
395
  const finalize = (response: JsonRecord): void => {
322
396
  terminal = true;
323
397
  if (typeof response.id === "string") output.responseId = response.id;
@@ -350,7 +424,9 @@ export async function processCodexStream(
350
424
  output.usage,
351
425
  typeof response.service_tier === "string" ? response.service_tier : undefined,
352
426
  );
353
- for (const item of responseItems(response["output"])) {
427
+ const terminalItems = responseItems(response["output"]);
428
+ terminalItems.forEach((item, index) => completeOutputItem(index, item));
429
+ for (const item of terminalItems) {
354
430
  if (item.type !== "reasoning" || typeof item.id !== "string") continue;
355
431
  const block = reasoningById.get(item.id);
356
432
  if (!block?.thinkingSignature || typeof item.encrypted_content !== "string") continue;
@@ -455,66 +531,7 @@ export async function processCodexStream(
455
531
  if (!slot || typeof event["input"] !== "string") continue;
456
532
  pushToolDelta(slot, appendCustomInput(slot.block, event["input"], true));
457
533
  } else if (event.type === "response.output_item.done" && isObject(event.item)) {
458
- const item = event.item;
459
- applyMessagePhaseStopReason(item);
460
- const slot = slotFor(index, item);
461
- if (item.type === "reasoning" && slot?.type === "thinking") {
462
- slot.block.thinking = reasoningText(item) || slot.block.thinking;
463
- slot.block.thinkingSignature = JSON.stringify(item);
464
- if (typeof item.id === "string") reasoningById.set(item.id, slot.block);
465
- stream.push({
466
- type: "thinking_end",
467
- contentIndex: slot.contentIndex,
468
- content: slot.block.thinking,
469
- partial: output,
470
- });
471
- trackCompleted(slot);
472
- slots.delete(index);
473
- } else if (item.type === "message" && slot?.type === "text") {
474
- slot.block.text = itemContentText(item);
475
- if (typeof item.id === "string") {
476
- slot.block.textSignature = encodeTextSignature(item.id, item["phase"]);
477
- }
478
- stream.push({
479
- type: "text_end",
480
- contentIndex: slot.contentIndex,
481
- content: slot.block.text,
482
- partial: output,
483
- });
484
- trackCompleted(slot);
485
- slots.delete(index);
486
- } else if (
487
- item.type === "function_call" &&
488
- slot?.type === "toolCall" &&
489
- slot.block.partialJson !== undefined
490
- ) {
491
- slot.block.name = piToolCallName(item);
492
- const argumentsJson =
493
- typeof item.arguments === "string" ? item.arguments : slot.block.partialJson || "{}";
494
- slot.block.arguments = parseStreamingJson(argumentsJson);
495
- delete slot.block.partialJson;
496
- stream.push({
497
- type: "toolcall_end",
498
- contentIndex: slot.contentIndex,
499
- toolCall: slot.block,
500
- partial: output,
501
- });
502
- trackCompleted(slot);
503
- slots.delete(index);
504
- } else if (item.type === "custom_tool_call" && slot?.type === "toolCall") {
505
- slot.block.name = piToolCallName(item);
506
- const input = typeof item["input"] === "string" ? item["input"] : customInput(slot.block);
507
- pushToolDelta(slot, appendCustomInput(slot.block, input, true));
508
- delete slot.block.customInput;
509
- stream.push({
510
- type: "toolcall_end",
511
- contentIndex: slot.contentIndex,
512
- toolCall: slot.block,
513
- partial: output,
514
- });
515
- trackCompleted(slot);
516
- slots.delete(index);
517
- }
534
+ completeOutputItem(index, event.item);
518
535
  } else if (
519
536
  (event.type === "response.completed" || event.type === "response.incomplete") &&
520
537
  isObject(event.response)
@@ -25,11 +25,17 @@ import { convertResponsesMessages } from "./vendor/pi-ai/openai-responses-serial
25
25
  export const CHECKPOINT_ENTRY_TYPE = "openai-codex-compat-remote-compaction";
26
26
  export const CHECKPOINT_FORMAT_VERSION = 1;
27
27
 
28
+ export type CompactionDecision = {
29
+ reason: "manual" | "threshold" | "overflow" | "provider-boundary";
30
+ willRetry: boolean;
31
+ };
32
+
28
33
  export type CheckpointData = {
29
34
  kind: typeof CHECKPOINT_ENTRY_TYPE;
30
35
  version: typeof CHECKPOINT_FORMAT_VERSION;
31
36
  modelId: string;
32
37
  history: ResponsesItem[];
38
+ compactionDecision?: CompactionDecision;
33
39
  };
34
40
 
35
41
  export type CheckpointSearch =
@@ -180,11 +186,31 @@ export function parseCheckpoint(value: unknown): CheckpointData | undefined {
180
186
  return undefined;
181
187
  }
182
188
 
189
+ const rawDecision = value["compactionDecision"];
190
+ let compactionDecision: CompactionDecision | undefined;
191
+ if (rawDecision !== undefined) {
192
+ if (
193
+ !isObject(rawDecision) ||
194
+ (rawDecision["reason"] !== "manual" &&
195
+ rawDecision["reason"] !== "threshold" &&
196
+ rawDecision["reason"] !== "overflow" &&
197
+ rawDecision["reason"] !== "provider-boundary") ||
198
+ typeof rawDecision["willRetry"] !== "boolean"
199
+ ) {
200
+ return undefined;
201
+ }
202
+ compactionDecision = {
203
+ reason: rawDecision["reason"],
204
+ willRetry: rawDecision["willRetry"],
205
+ };
206
+ }
207
+
183
208
  return {
184
209
  kind: CHECKPOINT_ENTRY_TYPE,
185
210
  version: CHECKPOINT_FORMAT_VERSION,
186
211
  modelId: value.modelId,
187
212
  history,
213
+ ...(compactionDecision ? { compactionDecision } : {}),
188
214
  };
189
215
  }
190
216
 
@@ -229,6 +255,7 @@ export function checkpointData(
229
255
  inputHistory: readonly ResponsesItem[],
230
256
  compactionItem: ResponsesItem,
231
257
  postCompactionTail: readonly ResponsesItem[] = [],
258
+ compactionDecision?: CompactionDecision,
232
259
  ): CheckpointData {
233
260
  return {
234
261
  kind: CHECKPOINT_ENTRY_TYPE,
@@ -238,6 +265,7 @@ export function checkpointData(
238
265
  ...installCompactionItem(inputHistory, compactionItem),
239
266
  ...postCompactionTail.map((item) => structuredClone(item)),
240
267
  ],
268
+ ...(compactionDecision ? { compactionDecision: { ...compactionDecision } } : {}),
241
269
  };
242
270
  }
243
271
 
@@ -20,6 +20,8 @@ export interface CodexCompatConfig {
20
20
  responsesLite: boolean;
21
21
  /** Replace Pi's active edit and write tools with the extension's apply_patch tool. */
22
22
  applyPatch: boolean;
23
+ /** Show exact model-facing apply_patch feedback while its TUI result is collapsed. */
24
+ applyPatchDebug: boolean;
23
25
  /** Select the shared background surface for extension-owned Codex tools. */
24
26
  toolBackground: CodexToolBackground;
25
27
  /** Expose the standalone Codex image-generation namespace tool. */
@@ -44,6 +46,7 @@ export const CONFIG_ENVIRONMENT_VARIABLES = {
44
46
  fastMode: `${ENV_PREFIX}FAST_MODE`,
45
47
  responsesLite: `${ENV_PREFIX}RESPONSES_LITE`,
46
48
  applyPatch: `${ENV_PREFIX}APPLY_PATCH`,
49
+ applyPatchDebug: `${ENV_PREFIX}APPLY_PATCH_DEBUG`,
47
50
  toolBackground: `${ENV_PREFIX}TOOL_BACKGROUND`,
48
51
  imageGeneration: `${ENV_PREFIX}IMAGE_GENERATION`,
49
52
  imageDetail: `${ENV_PREFIX}IMAGE_DETAIL`,
@@ -59,6 +62,7 @@ export type ConfigLayer = {
59
62
  fastMode?: boolean;
60
63
  responsesLite?: boolean;
61
64
  applyPatch?: boolean;
65
+ applyPatchDebug?: boolean;
62
66
  toolBackground?: CodexToolBackground;
63
67
  imageGeneration?: boolean;
64
68
  imageDetail?: ImageDetail;
@@ -75,6 +79,7 @@ export const DEFAULT_CONFIG: CodexCompatConfig = {
75
79
  fastMode: false,
76
80
  responsesLite: false,
77
81
  applyPatch: true,
82
+ applyPatchDebug: false,
78
83
  toolBackground: "subtle",
79
84
  imageGeneration: true,
80
85
  imageDetail: "auto",
@@ -152,6 +157,12 @@ export function parseEnvironmentConfig(environment: Environment = process.env):
152
157
  const applyPatch = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.applyPatch);
153
158
  if (applyPatch !== undefined) layer.applyPatch = applyPatch;
154
159
 
160
+ const applyPatchDebug = environmentBoolean(
161
+ environment,
162
+ CONFIG_ENVIRONMENT_VARIABLES.applyPatchDebug,
163
+ );
164
+ if (applyPatchDebug !== undefined) layer.applyPatchDebug = applyPatchDebug;
165
+
155
166
  const toolBackground = environmentEnum(
156
167
  environment,
157
168
  CONFIG_ENVIRONMENT_VARIABLES.toolBackground,
@@ -239,6 +250,9 @@ export function parseConfig(value: unknown): ConfigLayer {
239
250
  const applyPatch = value["applyPatch"];
240
251
  if (typeof applyPatch === "boolean") layer.applyPatch = applyPatch;
241
252
 
253
+ const applyPatchDebug = value["applyPatchDebug"];
254
+ if (typeof applyPatchDebug === "boolean") layer.applyPatchDebug = applyPatchDebug;
255
+
242
256
  const toolBackground = value["toolBackground"];
243
257
  if (
244
258
  typeof toolBackground === "string" &&
@@ -318,6 +332,9 @@ export function resolveConfig(
318
332
  ...(typeof merged.fastMode === "boolean" ? { fastMode: merged.fastMode } : {}),
319
333
  ...(typeof merged.responsesLite === "boolean" ? { responsesLite: merged.responsesLite } : {}),
320
334
  ...(typeof merged.applyPatch === "boolean" ? { applyPatch: merged.applyPatch } : {}),
335
+ ...(typeof merged.applyPatchDebug === "boolean"
336
+ ? { applyPatchDebug: merged.applyPatchDebug }
337
+ : {}),
321
338
  ...(merged.toolBackground ? { toolBackground: merged.toolBackground } : {}),
322
339
  ...(typeof merged.imageGeneration === "boolean"
323
340
  ? { imageGeneration: merged.imageGeneration }
@@ -363,6 +380,7 @@ export function configLayer(config: CodexCompatConfig): ConfigLayer {
363
380
  fastMode: config.fastMode,
364
381
  responsesLite: config.responsesLite,
365
382
  applyPatch: config.applyPatch,
383
+ applyPatchDebug: config.applyPatchDebug,
366
384
  toolBackground: config.toolBackground,
367
385
  imageGeneration: config.imageGeneration,
368
386
  imageDetail: config.imageDetail,
@@ -4,7 +4,7 @@ import {
4
4
  type ExtensionContext,
5
5
  type ReadonlyFooterDataProvider,
6
6
  } from "@earendil-works/pi-coding-agent";
7
- import type { Component, TUI } from "@earendil-works/pi-tui";
7
+ import type { Component } from "@earendil-works/pi-tui";
8
8
  import type { CodexCompatConfig } from "./config.ts";
9
9
  import { isCodexModel } from "./request-options.ts";
10
10
 
@@ -74,16 +74,15 @@ function footerSession(ctx: ExtensionContext, resolveConfig: ConfigResolver): Fo
74
74
 
75
75
  class CodexFooter implements Component {
76
76
  private readonly footer: FooterComponent;
77
- private readonly unsubscribe: () => void;
78
77
 
79
78
  constructor(
80
- tui: TUI,
81
79
  footerData: ReadonlyFooterDataProvider,
82
80
  ctx: ExtensionContext,
83
81
  resolveConfig: ConfigResolver,
84
82
  ) {
85
83
  this.footer = new FooterComponent(footerSession(ctx, resolveConfig), footerData);
86
- this.unsubscribe = footerData.onBranchChange(() => tui.requestRender());
84
+ // Pi does not expose its live auto-compaction state to extension footers.
85
+ this.footer.setAutoCompactEnabled(false);
87
86
  }
88
87
 
89
88
  render(width: number): string[] {
@@ -95,14 +94,11 @@ class CodexFooter implements Component {
95
94
  }
96
95
 
97
96
  dispose(): void {
98
- this.unsubscribe();
99
97
  this.footer.dispose();
100
98
  }
101
99
  }
102
100
 
103
101
  export function installCodexFooter(ctx: ExtensionContext, resolveConfig: ConfigResolver): void {
104
102
  if (ctx.mode !== "tui") return;
105
- ctx.ui.setFooter(
106
- (tui, _theme, footerData) => new CodexFooter(tui, footerData, ctx, resolveConfig),
107
- );
103
+ ctx.ui.setFooter((_tui, _theme, footerData) => new CodexFooter(footerData, ctx, resolveConfig));
108
104
  }
@@ -32,6 +32,7 @@ function settingsSummary(ctx: ExtensionContext, config: CodexCompatConfig): stri
32
32
  `reasoning mode: ${config.reasoningMode}`,
33
33
  `Codex tool background: ${config.toolBackground}`,
34
34
  `apply_patch: ${config.applyPatch ? "on" : "off"}`,
35
+ `apply_patch debug output: ${config.applyPatchDebug ? "on" : "off"}`,
35
36
  `image_gen.imagegen: ${config.imageGeneration ? "on" : "off"}`,
36
37
  `image result detail: ${config.imageDetail}`,
37
38
  `web.run: ${config.webRun ? "on" : "off"}`,
@@ -51,6 +52,8 @@ export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
51
52
  return activeConfig;
52
53
  };
53
54
  const resolveToolBackground = () => activeConfig?.toolBackground ?? DEFAULT_CONFIG.toolBackground;
55
+ const resolveApplyPatchDebug = () =>
56
+ activeConfig?.applyPatchDebug ?? DEFAULT_CONFIG.applyPatchDebug;
54
57
 
55
58
  pi.on("session_start", (event, ctx) => {
56
59
  activeConfig = loadConfig(ctx.cwd, ctx.isProjectTrusted());
@@ -60,7 +63,7 @@ export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
60
63
  }
61
64
  });
62
65
 
63
- registerCodexTools(pi, resolveConfig, resolveToolBackground);
66
+ registerCodexTools(pi, resolveConfig, resolveToolBackground, resolveApplyPatchDebug);
64
67
  registerCodexThreadLineage(pi);
65
68
  const codexProvider = registerCodexProvider(pi, resolveConfig);
66
69
  registerCodexRequestOptions(pi, resolveConfig);
@@ -188,6 +188,10 @@ export default function registerRemoteCompaction(
188
188
  template,
189
189
  priority: config.fastMode,
190
190
  compactionMetadata: compactionMetadata(event.reason),
191
+ compactionDecision: {
192
+ reason: event.reason,
193
+ willRetry: event.willRetry,
194
+ },
191
195
  });
192
196
 
193
197
  return {
@@ -34,6 +34,7 @@ type SettingId =
34
34
  | "reasoningMode"
35
35
  | "toolBackground"
36
36
  | "applyPatch"
37
+ | "applyPatchDebug"
37
38
  | "imageGeneration"
38
39
  | "imageDetail"
39
40
  | "webRun"
@@ -108,6 +109,13 @@ export function settingItems(
108
109
  currentValue: toggleValue(config.applyPatch),
109
110
  values: ["off", "on"],
110
111
  },
112
+ {
113
+ id: "applyPatchDebug",
114
+ label: "apply_patch debug output",
115
+ description: "Show exact model feedback while apply_patch output is collapsed.",
116
+ currentValue: toggleValue(config.applyPatchDebug),
117
+ values: ["off", "on"],
118
+ },
111
119
  {
112
120
  id: "imageGeneration",
113
121
  label: "image_gen.imagegen tool",
@@ -185,6 +193,8 @@ export function settingPatch(id: string, value: string): ConfigLayer | undefined
185
193
  return undefined;
186
194
  case "applyPatch":
187
195
  return value === "on" || value === "off" ? { applyPatch: value === "on" } : undefined;
196
+ case "applyPatchDebug":
197
+ return value === "on" || value === "off" ? { applyPatchDebug: value === "on" } : undefined;
188
198
  case "imageGeneration":
189
199
  return value === "on" || value === "off" ? { imageGeneration: value === "on" } : undefined;
190
200
  case "imageDetail":
@@ -214,6 +224,7 @@ function applySettingPatch(config: CodexCompatConfig, patch: ConfigLayer): Codex
214
224
  if (typeof patch.fastMode === "boolean") next.fastMode = patch.fastMode;
215
225
  if (typeof patch.responsesLite === "boolean") next.responsesLite = patch.responsesLite;
216
226
  if (typeof patch.applyPatch === "boolean") next.applyPatch = patch.applyPatch;
227
+ if (typeof patch.applyPatchDebug === "boolean") next.applyPatchDebug = patch.applyPatchDebug;
217
228
  if (patch.toolBackground) next.toolBackground = patch.toolBackground;
218
229
  if (typeof patch.imageGeneration === "boolean") {
219
230
  next.imageGeneration = patch.imageGeneration;
@@ -63,8 +63,9 @@ export default function registerCodexTools(
63
63
  pi: ExtensionAPI,
64
64
  resolveConfig: ConfigResolver,
65
65
  resolveToolBackground: CodexToolBackgroundResolver = () => DEFAULT_CONFIG.toolBackground,
66
+ resolveApplyPatchDebug: () => boolean = () => DEFAULT_CONFIG.applyPatchDebug,
66
67
  ): void {
67
- registerApplyPatch(pi, resolveToolBackground);
68
+ registerApplyPatch(pi, resolveToolBackground, resolveApplyPatchDebug);
68
69
  registerImageGeneration(pi, resolveConfig, resolveToolBackground);
69
70
  registerWebRun(pi, resolveConfig, resolveToolBackground);
70
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-codex-compat",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "files": [
23
23
  "extensions",
24
+ "APPLY_PATCH_INSTRUCTION_FEEDBACK.md",
24
25
  "README.md",
25
26
  "CHANGELOG.md",
26
27
  "LICENSE",
@@ -33,19 +34,24 @@
33
34
  "test": "node --test --test-concurrency=1 test/*.test.ts",
34
35
  "test:live:codex": "PI_CODEX_LIVE_TEST=1 node --test --test-concurrency=1 test/codex-host.live.test.ts",
35
36
  "pack:dry": "npm pack --dry-run --allow-directory=all",
37
+ "release": "node scripts/release.ts",
36
38
  "fmt": "oxfmt",
37
39
  "lint": "oxlint",
38
40
  "lint:fix": "oxlint --fix"
39
41
  },
42
+ "dependencies": {
43
+ "@2h2d/tree-sitter-wasms": "0.2.1",
44
+ "web-tree-sitter": "0.26.11"
45
+ },
40
46
  "devDependencies": {
41
47
  "@earendil-works/pi-ai": "0.84.1",
42
48
  "@earendil-works/pi-coding-agent": "0.84.1",
43
49
  "@earendil-works/pi-tui": "0.84.1",
44
50
  "@types/node": "22.20.1",
45
- "oxfmt": "0.60.0",
46
- "oxlint": "1.75.0",
51
+ "oxfmt": "0.62.0",
52
+ "oxlint": "1.77.0",
47
53
  "oxlint-tsgolint": "7.0.2001",
48
- "typebox": "1.3.7",
54
+ "typebox": "1.3.11",
49
55
  "typescript": "7.0.2"
50
56
  },
51
57
  "peerDependencies": {