pi-openai-codex-compat 0.0.16 → 0.0.17

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 CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.17 - 2026-09-25
6
+
7
+ ### Fixed
8
+
9
+ - Separate menu sections with blank rows, using compact spacing on short terminals.
10
+ - Align setting names on the left and values in a stable column on the right.
11
+ Render the search placeholder in muted gray instead of ordinary input text.
12
+ Keep Save and Discard hints visible in narrow terminals.
13
+ - Align settings with the Anthropic menu: Enter changes values, Ctrl+S saves and
14
+ applies drafts, and Escape discards unapplied drafts without changing active tools.
15
+ - Save only changed overrides, preserve inheritance and external edits, and
16
+ report conflicting saves without overwriting them.
17
+ - Block command-tool switches while persistent command sessions remain instead
18
+ of terminating them while browsing settings.
19
+
20
+ ### Added
21
+
22
+ - Apply to session without changing configuration files. Reopening settings shows
23
+ active session values, including after reload, restart, or resume.
24
+ Ctrl+S can save session-only changes later.
25
+ - Custom percentage editing, configuration-source and model-applicability labels,
26
+ inherited-value resets, and consistent keyboard, focus, and mouse support.
27
+
5
28
  ## 0.0.16 - 2026-09-23
6
29
 
7
30
  ### Added
package/README.md CHANGED
@@ -14,7 +14,7 @@ OpenAI Codex compatibility for [Pi](https://github.com/earendil-works/pi-mono),
14
14
  - **Dedicated Codex tool UI**: renders command tools, `apply_patch`, `image_gen.imagegen`, and `web.run` on a shared configurable surface with compact summaries and `Ctrl+O` expansion.
15
15
  - **Hosted web-search fallback**: injects native `web_search` only when `web.run` is inactive, with cached, indexed, or live modes.
16
16
  - **Native request controls**: configures Responses API text verbosity, reasoning summaries, and standard/pro reasoning mode on supported models.
17
- - **Session-local settings pane**: `/codex-settings` changes every compatibility setting for the current session; `Enter` persists and closes, `Escape` discards unsaved changes and closes, and `Ctrl+S` persists without closing.
17
+ - **Draft settings pane**: `/codex-settings` stages edits before applying them. Apply to session stores them with the session without changing configuration files. Ctrl+S saves and applies them. Escape discards only unapplied drafts.
18
18
  - **Session-aware footer**: shows the current Pi session ID on the first line and appends non-default Codex request modes to the model side of Pi's normal second line.
19
19
 
20
20
  Pi provides the Codex OAuth flow and model catalog. At session start, this package overrides the built-in `openai-codex` runtime under the same provider id so ordinary responses and remote compaction share one transport, parser, native-history store, and sticky WebSocket session.
@@ -173,7 +173,56 @@ A trusted project can override it at:
173
173
  <project>/.pi/openai-codex-compat.json
174
174
  ```
175
175
 
176
- Each session inherits the effective file-backed settings. Open `/codex-settings` to make immediate session-local changes. Press `Enter` to persist and close, `Escape` to discard unsaved changes and close, or `Ctrl+S` to persist without closing. The global file is the normal save target, while an existing trusted project override remains the target for that project. After `Ctrl+S`, later unsaved changes can still be discarded back to the values from that save.
176
+ Each session inherits the effective file-backed settings. `/codex-settings`
177
+ uses the same interaction contract as `/anthropic-settings`:
178
+
179
+ - **Enter** changes the selected value or opens its editor. It never implicitly saves.
180
+ - **Space** activates a result or inserts a space when search has focus.
181
+ - **Tab / Shift+Tab** move between search, results, and action controls.
182
+ - **F1** opens scrollable details, full errors, and the exact save target.
183
+ - Edits remain drafts until explicitly applied or saved.
184
+ - **Apply to session** applies drafts without writing configuration files and stays open.
185
+ - **Ctrl+S** saves and applies changes without closing.
186
+ - **Save and close** saves and applies changes, then closes on success.
187
+ - **Escape** cancels a field editor or discards unapplied drafts and closes the main menu.
188
+ It does not undo settings already applied to the session.
189
+
190
+ Search matches labels, configuration keys, and descriptions. The percentage
191
+ editor supports presets and custom fractional values. **Use inherited value**
192
+ removes an override. This differs from **Pi default**, which explicitly disables
193
+ an inherited percentage trigger. Rows show configuration sources, environment
194
+ locks, and model applicability. Pi's remapped selection and cancel keys are respected.
195
+
196
+ The global file is the normal save target. An existing trusted project override
197
+ is the target for that project. The target stays fixed while the menu is open.
198
+ Saving changes only edited overrides, preserves unknown keys and inherited values,
199
+ and preserves non-conflicting external edits. Conflicting edits are rejected for review.
200
+ Opening or closing without changes does not create a configuration file.
201
+ Reopening shows active session values, not freshly read file values. `*` marks
202
+ an unapplied draft. `~` marks an active session override or a value that differs
203
+ from the saved configuration. Details shows the saved value when it differs.
204
+ Ctrl+S also persists changes previously applied only to the session.
205
+ Session settings are stored as extension state entries in the Pi session,
206
+ outside model context. They survive menu closure, tree navigation, extension
207
+ reload, process restart, and switching away and back. Resuming the same session
208
+ restores its settings before provider operations. A new session, fork, or clone
209
+ has a separate identity and starts from configuration files.
210
+ Pi's `--no-session` mode cannot be resumed after exit. Pi also defers creating a
211
+ new session file until its first assistant message.
212
+ Environment-controlled fields remain locked and take precedence over restored
213
+ session preferences. Their values are not copied into session settings.
214
+
215
+ Applying and saving wait for Pi to become idle. Collect pending output with `write_stdin` or
216
+ stop running command sessions through `/ps` before applying or saving a command-tool switch.
217
+ Browsing settings and discarding drafts
218
+ never terminate those sessions. Escape cancels a pending operation without discarding
219
+ the draft. Failures leave the menu open. If a save reaches disk but cannot
220
+ be applied to the session, the menu reports that distinction and Ctrl+S retries
221
+ application. Cooperating writers use a `.settings-lock` file. An interrupted
222
+ writer can leave a lock that requires review; locks are not automatically
223
+ deleted. External editors do not participate in that lock.
224
+ Session overrides retain their original conflict checks across menu reopenings.
225
+ Conflicting external edits remain protected after reload or restart.
177
226
 
178
227
  The effective settings are printed once when a TUI session starts. The footer shows the current Pi session ID alongside the working directory and optional session name. It shows `fast` and `pro` only when enabled, and shows text verbosity or reasoning summary only when they differ from their defaults.
179
228
 
@@ -225,7 +274,9 @@ Responses Lite supports exactly `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`,
225
274
  standard reasoning mode even when `pro` is configured. Both controls remain
226
275
  opt-in. GPT-6 Sol and Luna require Pi 0.87.1's model catalog.
227
276
 
228
- Invalid JSON setting values are ignored and invalid JSON does not prevent Pi from starting. The settings pane never writes on ordinary changes, refuses to overwrite invalid JSON when `Enter` or `Ctrl+S` attempts to save, and retains unknown keys when saving. Project configuration is read only when the project is trusted.
277
+ Invalid JSON setting values are ignored and invalid JSON does not prevent Pi from
278
+ starting. The settings pane reports malformed JSON instead of overwriting it.
279
+ Project configuration is read only when the project is trusted.
229
280
 
230
281
  Every setting can also be overridden for one Pi process with an environment variable:
231
282
 
@@ -533,7 +584,9 @@ The tool generates new images with `gpt-image-2` or edits up to five local/recen
533
584
  ~/.pi/agent/generated_images/<session-id>/<call-id>.png
534
585
  ```
535
586
 
536
- The active Pi agent directory replaces `~/.pi/agent` when configured differently. Turning `imageGeneration` off removes the tool immediately for the current session; `Enter` or `Ctrl+S` in `/codex-settings` persists the value.
587
+ The active Pi agent directory replaces `~/.pi/agent` when configured differently.
588
+ Saving `imageGeneration: false` through `/codex-settings` removes the tool from
589
+ the current session.
537
590
 
538
591
  The tool registers the server-reserved schema directly with a model-facing absolute-path annotation that names the supported image formats. OpenAI rejects additional schema keywords for image-count bounds and selector exclusivity, so the executor enforces those constraints before filesystem or network access. Local paths are lexically normalized before reading. A one-line system-prompt snippet and four high-signal guidelines cover normal model use. Local images are inspected with Pi's `read` tool, and generated image content is displayed and saved automatically without Codex Code Mode wrappers.
539
592
 
@@ -59,6 +59,7 @@ export { formatBackgroundProcesses } from "./background-process-browser.ts";
59
59
  export type CommandToolsApi = Pick<ExtensionAPI, "on" | "registerCommand" | "registerTool">;
60
60
 
61
61
  export type CommandToolsController = {
62
+ hasPersistentSessions: () => boolean;
62
63
  terminateUnifiedExecSessions: () => void;
63
64
  };
64
65
 
@@ -352,6 +353,7 @@ export default function registerCommandTools(
352
353
  pi.on("session_shutdown", () => manager.terminateAll());
353
354
 
354
355
  return {
356
+ hasPersistentSessions: () => manager.activeSessionCount() > 0,
355
357
  terminateUnifiedExecSessions() {
356
358
  manager.terminateAll().catch((error: unknown) => {
357
359
  console.error("Could not terminate unified exec sessions.", error);
@@ -1,12 +1,10 @@
1
1
  import { isBoolean, isNumber, nodeErrorCode } from "./value-contracts.ts";
2
- import { randomUUID } from "node:crypto";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
- import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
5
- import { basename, dirname, join } from "node:path";
3
+ import { join } from "node:path";
6
4
  import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
7
5
  import type { Static } from "typebox";
8
6
  import { Value } from "typebox/value";
9
- import { isObject, type JsonRecord } from "./codex-protocol.ts";
7
+ import { isObject } from "./codex-protocol.ts";
10
8
 
11
9
  export const WEB_SEARCH_MODE_SCHEMA = {
12
10
  enum: ["disabled", "cached", "indexed", "live"],
@@ -465,55 +463,3 @@ export function withoutEnvironmentOverrides(
465
463
  }
466
464
  return result;
467
465
  }
468
-
469
- async function readWritableConfig(filePath: string): Promise<JsonRecord> {
470
- try {
471
- const value: unknown = JSON.parse(await readFile(filePath, "utf8"));
472
- if (!isObject(value)) throw new Error("the root value must be a JSON object");
473
- return value;
474
- } catch (error) {
475
- if (nodeErrorCode(error) === "ENOENT") return {};
476
- const detail = error instanceof Error ? error.message : String(error);
477
- throw new Error(`Cannot update ${filePath}: ${detail}`, { cause: error });
478
- }
479
- }
480
-
481
- /**
482
- * Merge known setting changes into the dedicated extension file. Unknown keys
483
- * are retained, and an invalid existing file is never overwritten.
484
- */
485
- export async function saveConfig(
486
- cwd: string,
487
- projectTrusted: boolean,
488
- patch: ConfigLayer,
489
- ): Promise<string> {
490
- const filePath = writableConfigPath(cwd, projectTrusted);
491
- const current = await readWritableConfig(filePath);
492
- const next = { ...current, ...patch };
493
- const directory = dirname(filePath);
494
- const temporaryPath = join(
495
- directory,
496
- `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`,
497
- );
498
-
499
- await mkdir(directory, { recursive: true });
500
- const existingMode = await stat(filePath)
501
- .then((metadata) => metadata.mode & 0o777)
502
- .catch((error: unknown) => {
503
- if (nodeErrorCode(error) === "ENOENT") return 0o600;
504
- throw error;
505
- });
506
-
507
- try {
508
- await writeFile(temporaryPath, `${JSON.stringify(next, null, 2)}\n`, {
509
- encoding: "utf8",
510
- flag: "wx",
511
- mode: existingMode,
512
- });
513
- await rename(temporaryPath, filePath);
514
- } finally {
515
- await rm(temporaryPath, { force: true });
516
- }
517
-
518
- return filePath;
519
- }
@@ -12,7 +12,7 @@ import registerOutputLimitContinuation, {
12
12
  } from "./output-limit-continuation.ts";
13
13
  import registerRemoteCompaction, { remoteCompactionApi } from "./remote-compaction.ts";
14
14
  import registerCodexRequestOptions from "./request-options.ts";
15
- import registerCodexSettings from "./settings-pane.ts";
15
+ import registerCodexSettings, { loadSessionConfig } from "./settings-pane.ts";
16
16
  import registerCodexThreadLineage, { codexThreadLineageApi } from "./codex-thread-lineage.ts";
17
17
  import registerCodexTools, { syncCodexTools } from "./tools.ts";
18
18
 
@@ -26,8 +26,11 @@ export {
26
26
  export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
27
27
  requirePiTranscriptRuntime(piAi, piCodingAgent);
28
28
  let activeConfig: CodexCompatConfig | undefined;
29
+ let sessionContext: Parameters<typeof loadSessionConfig>[0] | undefined;
29
30
  const resolveConfig = (ctx: ConfigContext): CodexCompatConfig => {
30
- activeConfig ??= loadConfig(ctx.cwd, ctx.isProjectTrusted());
31
+ activeConfig ??= sessionContext
32
+ ? loadSessionConfig(sessionContext)
33
+ : loadConfig(ctx.cwd, ctx.isProjectTrusted());
31
34
  return activeConfig;
32
35
  };
33
36
  const resolveToolBackground = () => activeConfig?.toolBackground ?? DEFAULT_CONFIG.toolBackground;
@@ -37,7 +40,9 @@ export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
37
40
  activeConfig?.applyPatchDiagnostics ?? DEFAULT_CONFIG.applyPatchDiagnostics;
38
41
 
39
42
  pi.on("session_start", (event, ctx) => {
40
- activeConfig = loadConfig(ctx.cwd, ctx.isProjectTrusted());
43
+ sessionContext = ctx;
44
+ activeConfig = undefined;
45
+ activeConfig = resolveConfig(ctx);
41
46
  installCodexFooter(ctx, resolveConfig);
42
47
  if (event.reason !== "reload" && ctx.mode === "tui") {
43
48
  ctx.ui.notify("pi-openai-codex-compat loaded · /codex-settings", "info");
@@ -60,6 +65,7 @@ export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
60
65
  registerCodexModelPolicy(modelPolicy, resolveConfig, commandTools);
61
66
  registerCodexSettings(pi, {
62
67
  getConfig: resolveConfig,
68
+ hasPersistentSessions: commandTools.hasPersistentSessions,
63
69
  onChange(config, ctx) {
64
70
  activeConfig = config;
65
71
  codexProvider.updateSessionConfig(ctx.sessionId, config);