pi-openai-codex-compat 0.0.1 → 0.0.2
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,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.2 - 2026-08-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add transient `PI_OPENAI_CODEX_COMPAT_*` environment overrides for every compatibility setting, including common boolean aliases, explicit `off`/`default` percentage-compaction values, and an unambiguous `WEB_SEARCH_MODE` name.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Make `Enter` save and close `/codex-settings`, make `Escape` discard unsaved changes and close, and retain `Ctrl+S` as save without closing.
|
|
14
|
+
|
|
5
15
|
## 0.0.1 - 2026-08-03
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ OpenAI Codex compatibility for [Pi](https://github.com/earendil-works/pi-mono),
|
|
|
12
12
|
- **Dedicated Codex tool UI**: renders `apply_patch`, `image_gen.imagegen`, and `web.run` on a shared configurable surface with compact summaries and `Ctrl+O` expansion.
|
|
13
13
|
- **Hosted web-search fallback**: injects native `web_search` only when `web.run` is inactive, with cached, indexed, or live modes.
|
|
14
14
|
- **Native request controls**: configures Responses API text verbosity, reasoning summaries, and GPT-5.6 standard/pro reasoning mode.
|
|
15
|
-
- **Session-local settings pane**: `/codex-settings` changes every compatibility setting for the current session; `Ctrl+S`
|
|
15
|
+
- **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.
|
|
16
16
|
- **Compact footer indicators**: non-default Codex request modes are appended to the model side of Pi's normal second footer line.
|
|
17
17
|
|
|
18
18
|
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.
|
|
@@ -143,7 +143,7 @@ A trusted project can override it at:
|
|
|
143
143
|
<project>/.pi/openai-codex-compat.json
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
Each session inherits the effective file-backed settings. Open `/codex-settings` to make immediate session-local changes. Press `
|
|
146
|
+
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.
|
|
147
147
|
|
|
148
148
|
The effective settings are printed once when a TUI session starts. The footer shows `fast` and `pro` only when enabled, and shows text verbosity or reasoning summary only when they differ from their defaults.
|
|
149
149
|
|
|
@@ -181,7 +181,36 @@ Defaults:
|
|
|
181
181
|
| `reasoningSummary` | `auto`, `concise`, `detailed`, `off` | `auto` | Sets `reasoning.summary` when reasoning is enabled; `off` omits the summary parameter. |
|
|
182
182
|
| `reasoningMode` | `standard`, `pro` | `standard` | Sets `reasoning.mode` on GPT-5.6 models independently of Pi's reasoning-effort control. |
|
|
183
183
|
|
|
184
|
-
Invalid 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 `Ctrl+S`
|
|
184
|
+
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.
|
|
185
|
+
|
|
186
|
+
Every setting can also be overridden for one Pi process with an environment variable:
|
|
187
|
+
|
|
188
|
+
| Setting | Environment variable |
|
|
189
|
+
| ---------------------- | ------------------------------------------------ |
|
|
190
|
+
| `fastMode` | `PI_OPENAI_CODEX_COMPAT_FAST_MODE` |
|
|
191
|
+
| `toolBackground` | `PI_OPENAI_CODEX_COMPAT_TOOL_BACKGROUND` |
|
|
192
|
+
| `applyPatch` | `PI_OPENAI_CODEX_COMPAT_APPLY_PATCH` |
|
|
193
|
+
| `imageGeneration` | `PI_OPENAI_CODEX_COMPAT_IMAGE_GENERATION` |
|
|
194
|
+
| `imageDetail` | `PI_OPENAI_CODEX_COMPAT_IMAGE_DETAIL` |
|
|
195
|
+
| `webRun` | `PI_OPENAI_CODEX_COMPAT_WEB_RUN` |
|
|
196
|
+
| `autoCompactAtPercent` | `PI_OPENAI_CODEX_COMPAT_AUTO_COMPACT_AT_PERCENT` |
|
|
197
|
+
| `webSearch` | `PI_OPENAI_CODEX_COMPAT_WEB_SEARCH_MODE` |
|
|
198
|
+
| `textVerbosity` | `PI_OPENAI_CODEX_COMPAT_TEXT_VERBOSITY` |
|
|
199
|
+
| `reasoningSummary` | `PI_OPENAI_CODEX_COMPAT_REASONING_SUMMARY` |
|
|
200
|
+
| `reasoningMode` | `PI_OPENAI_CODEX_COMPAT_REASONING_MODE` |
|
|
201
|
+
|
|
202
|
+
Environment variables have the highest precedence: defaults < global JSON < trusted-project JSON < environment. Boolean values accept `true`/`false`, `1`/`0`, `on`/`off`, or `enabled`/`disabled`. Other settings use the values in the defaults table; `PI_OPENAI_CODEX_COMPAT_AUTO_COMPACT_AT_PERCENT=off` and `PI_OPENAI_CODEX_COMPAT_AUTO_COMPACT_AT_PERCENT=default` explicitly select Pi's default compaction lifecycle.
|
|
203
|
+
|
|
204
|
+
Environment-controlled rows are marked `(env)` and locked in `/codex-settings`. Saving the pane does not copy their effective values into JSON, so CLI overrides remain transient. Invalid environment values fail fast with the variable name and accepted values.
|
|
205
|
+
|
|
206
|
+
For example:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
PI_OPENAI_CODEX_COMPAT_WEB_RUN=off \
|
|
210
|
+
PI_OPENAI_CODEX_COMPAT_IMAGE_DETAIL=high \
|
|
211
|
+
PI_OPENAI_CODEX_COMPAT_AUTO_COMPACT_AT_PERCENT=90 \
|
|
212
|
+
pi
|
|
213
|
+
```
|
|
185
214
|
|
|
186
215
|
## Native compaction
|
|
187
216
|
|
|
@@ -262,7 +291,7 @@ The tool generates new images with `gpt-image-2` or edits up to five local/recen
|
|
|
262
291
|
~/.pi/agent/generated_images/<session-id>/<call-id>.png
|
|
263
292
|
```
|
|
264
293
|
|
|
265
|
-
The active Pi agent directory replaces `~/.pi/agent` when configured differently. Turning `imageGeneration` off removes the tool immediately for the current session; `Ctrl+S` in `/codex-settings` persists the value.
|
|
294
|
+
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.
|
|
266
295
|
|
|
267
296
|
Pi also persists the returned image content in tool-result history so later image edits and provider replay remain self-contained. Generated-image turns therefore increase the session file by approximately the base64 image size in addition to the saved PNG artifact.
|
|
268
297
|
|
|
@@ -11,6 +11,8 @@ export type ReasoningMode = "standard" | "pro";
|
|
|
11
11
|
export type ImageDetail = "auto" | "low" | "high" | "original";
|
|
12
12
|
export type CodexToolBackground = "subtle" | "status" | "none";
|
|
13
13
|
|
|
14
|
+
export const ENV_PREFIX = "PI_OPENAI_CODEX_COMPAT_";
|
|
15
|
+
|
|
14
16
|
export interface CodexCompatConfig {
|
|
15
17
|
/** Send OpenAI Codex requests through the priority service tier. */
|
|
16
18
|
fastMode: boolean;
|
|
@@ -36,6 +38,20 @@ export interface CodexCompatConfig {
|
|
|
36
38
|
reasoningMode: ReasoningMode;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
export const CONFIG_ENVIRONMENT_VARIABLES = {
|
|
42
|
+
fastMode: `${ENV_PREFIX}FAST_MODE`,
|
|
43
|
+
applyPatch: `${ENV_PREFIX}APPLY_PATCH`,
|
|
44
|
+
toolBackground: `${ENV_PREFIX}TOOL_BACKGROUND`,
|
|
45
|
+
imageGeneration: `${ENV_PREFIX}IMAGE_GENERATION`,
|
|
46
|
+
imageDetail: `${ENV_PREFIX}IMAGE_DETAIL`,
|
|
47
|
+
webRun: `${ENV_PREFIX}WEB_RUN`,
|
|
48
|
+
autoCompactAtPercent: `${ENV_PREFIX}AUTO_COMPACT_AT_PERCENT`,
|
|
49
|
+
webSearch: `${ENV_PREFIX}WEB_SEARCH_MODE`,
|
|
50
|
+
textVerbosity: `${ENV_PREFIX}TEXT_VERBOSITY`,
|
|
51
|
+
reasoningSummary: `${ENV_PREFIX}REASONING_SUMMARY`,
|
|
52
|
+
reasoningMode: `${ENV_PREFIX}REASONING_MODE`,
|
|
53
|
+
} as const satisfies Record<keyof CodexCompatConfig, string>;
|
|
54
|
+
|
|
39
55
|
export type ConfigLayer = {
|
|
40
56
|
fastMode?: boolean;
|
|
41
57
|
applyPatch?: boolean;
|
|
@@ -71,10 +87,136 @@ const REASONING_MODES = new Set<ReasoningMode>(["standard", "pro"]);
|
|
|
71
87
|
const IMAGE_DETAILS = new Set<ImageDetail>(["auto", "low", "high", "original"]);
|
|
72
88
|
const CODEX_TOOL_BACKGROUNDS = new Set<CodexToolBackground>(["subtle", "status", "none"]);
|
|
73
89
|
|
|
90
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
91
|
+
|
|
74
92
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
75
93
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
function invalidEnvironmentValue(name: string, value: string, expected: string): never {
|
|
97
|
+
throw new Error(`Invalid ${name}=${JSON.stringify(value)}; expected ${expected}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function environmentBoolean(environment: Environment, name: string): boolean | undefined {
|
|
101
|
+
const raw = environment[name];
|
|
102
|
+
if (raw === undefined) return undefined;
|
|
103
|
+
|
|
104
|
+
switch (raw.trim().toLowerCase()) {
|
|
105
|
+
case "1":
|
|
106
|
+
case "enabled":
|
|
107
|
+
case "on":
|
|
108
|
+
case "true":
|
|
109
|
+
return true;
|
|
110
|
+
case "0":
|
|
111
|
+
case "disabled":
|
|
112
|
+
case "off":
|
|
113
|
+
case "false":
|
|
114
|
+
return false;
|
|
115
|
+
default:
|
|
116
|
+
return invalidEnvironmentValue(name, raw, "true, false, 1, 0, on, off, enabled, or disabled");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function environmentEnum<T extends string>(
|
|
121
|
+
environment: Environment,
|
|
122
|
+
name: string,
|
|
123
|
+
values: ReadonlySet<T>,
|
|
124
|
+
): T | undefined {
|
|
125
|
+
const raw = environment[name];
|
|
126
|
+
if (raw === undefined) return undefined;
|
|
127
|
+
|
|
128
|
+
const value = raw.trim();
|
|
129
|
+
if (values.has(value as T)) return value as T;
|
|
130
|
+
return invalidEnvironmentValue(name, raw, [...values].join(", "));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse explicit process-level overrides. Unlike invalid JSON settings, invalid
|
|
135
|
+
* environment values fail fast so a CLI test cannot silently exercise a
|
|
136
|
+
* different configuration than requested.
|
|
137
|
+
*/
|
|
138
|
+
export function parseEnvironmentConfig(environment: Environment = process.env): ConfigLayer {
|
|
139
|
+
const layer: ConfigLayer = {};
|
|
140
|
+
|
|
141
|
+
const fastMode = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.fastMode);
|
|
142
|
+
if (fastMode !== undefined) layer.fastMode = fastMode;
|
|
143
|
+
|
|
144
|
+
const applyPatch = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.applyPatch);
|
|
145
|
+
if (applyPatch !== undefined) layer.applyPatch = applyPatch;
|
|
146
|
+
|
|
147
|
+
const toolBackground = environmentEnum(
|
|
148
|
+
environment,
|
|
149
|
+
CONFIG_ENVIRONMENT_VARIABLES.toolBackground,
|
|
150
|
+
CODEX_TOOL_BACKGROUNDS,
|
|
151
|
+
);
|
|
152
|
+
if (toolBackground !== undefined) layer.toolBackground = toolBackground;
|
|
153
|
+
|
|
154
|
+
const imageGeneration = environmentBoolean(
|
|
155
|
+
environment,
|
|
156
|
+
CONFIG_ENVIRONMENT_VARIABLES.imageGeneration,
|
|
157
|
+
);
|
|
158
|
+
if (imageGeneration !== undefined) layer.imageGeneration = imageGeneration;
|
|
159
|
+
|
|
160
|
+
const imageDetail = environmentEnum(
|
|
161
|
+
environment,
|
|
162
|
+
CONFIG_ENVIRONMENT_VARIABLES.imageDetail,
|
|
163
|
+
IMAGE_DETAILS,
|
|
164
|
+
);
|
|
165
|
+
if (imageDetail !== undefined) layer.imageDetail = imageDetail;
|
|
166
|
+
|
|
167
|
+
const webRun = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.webRun);
|
|
168
|
+
if (webRun !== undefined) layer.webRun = webRun;
|
|
169
|
+
|
|
170
|
+
const thresholdName = CONFIG_ENVIRONMENT_VARIABLES.autoCompactAtPercent;
|
|
171
|
+
const rawThreshold = environment[thresholdName];
|
|
172
|
+
if (rawThreshold !== undefined) {
|
|
173
|
+
const value = rawThreshold.trim();
|
|
174
|
+
if (value.toLowerCase() === "off" || value.toLowerCase() === "default") {
|
|
175
|
+
layer.autoCompactAtPercent = null;
|
|
176
|
+
} else {
|
|
177
|
+
const threshold = Number(value);
|
|
178
|
+
if (value.length === 0 || !Number.isFinite(threshold) || threshold <= 0 || threshold > 100) {
|
|
179
|
+
invalidEnvironmentValue(
|
|
180
|
+
thresholdName,
|
|
181
|
+
rawThreshold,
|
|
182
|
+
"a number greater than 0 and at most 100, off, or default",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
layer.autoCompactAtPercent = threshold;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const webSearch = environmentEnum(
|
|
190
|
+
environment,
|
|
191
|
+
CONFIG_ENVIRONMENT_VARIABLES.webSearch,
|
|
192
|
+
WEB_SEARCH_MODES,
|
|
193
|
+
);
|
|
194
|
+
if (webSearch !== undefined) layer.webSearch = webSearch;
|
|
195
|
+
|
|
196
|
+
const textVerbosity = environmentEnum(
|
|
197
|
+
environment,
|
|
198
|
+
CONFIG_ENVIRONMENT_VARIABLES.textVerbosity,
|
|
199
|
+
TEXT_VERBOSITIES,
|
|
200
|
+
);
|
|
201
|
+
if (textVerbosity !== undefined) layer.textVerbosity = textVerbosity;
|
|
202
|
+
|
|
203
|
+
const reasoningSummary = environmentEnum(
|
|
204
|
+
environment,
|
|
205
|
+
CONFIG_ENVIRONMENT_VARIABLES.reasoningSummary,
|
|
206
|
+
REASONING_SUMMARIES,
|
|
207
|
+
);
|
|
208
|
+
if (reasoningSummary !== undefined) layer.reasoningSummary = reasoningSummary;
|
|
209
|
+
|
|
210
|
+
const reasoningMode = environmentEnum(
|
|
211
|
+
environment,
|
|
212
|
+
CONFIG_ENVIRONMENT_VARIABLES.reasoningMode,
|
|
213
|
+
REASONING_MODES,
|
|
214
|
+
);
|
|
215
|
+
if (reasoningMode !== undefined) layer.reasoningMode = reasoningMode;
|
|
216
|
+
|
|
217
|
+
return layer;
|
|
218
|
+
}
|
|
219
|
+
|
|
78
220
|
export function parseConfig(value: unknown): ConfigLayer {
|
|
79
221
|
if (!isRecord(value)) return {};
|
|
80
222
|
|
|
@@ -157,8 +299,9 @@ function readConfig(filePath: string): ConfigLayer {
|
|
|
157
299
|
export function resolveConfig(
|
|
158
300
|
globalConfig: ConfigLayer,
|
|
159
301
|
projectConfig: ConfigLayer,
|
|
302
|
+
environmentConfig: ConfigLayer = {},
|
|
160
303
|
): CodexCompatConfig {
|
|
161
|
-
const merged = { ...globalConfig, ...projectConfig };
|
|
304
|
+
const merged = { ...globalConfig, ...projectConfig, ...environmentConfig };
|
|
162
305
|
return {
|
|
163
306
|
...DEFAULT_CONFIG,
|
|
164
307
|
...(typeof merged.fastMode === "boolean" ? { fastMode: merged.fastMode } : {}),
|
|
@@ -193,10 +336,14 @@ export function writableConfigPath(cwd: string, projectTrusted: boolean): string
|
|
|
193
336
|
return projectTrusted && existsSync(projectPath) ? projectPath : globalConfigPath();
|
|
194
337
|
}
|
|
195
338
|
|
|
196
|
-
export function loadConfig(
|
|
339
|
+
export function loadConfig(
|
|
340
|
+
cwd: string,
|
|
341
|
+
projectTrusted: boolean,
|
|
342
|
+
environment: Environment = process.env,
|
|
343
|
+
): CodexCompatConfig {
|
|
197
344
|
const globalConfig = readConfig(globalConfigPath());
|
|
198
345
|
const projectConfig = projectTrusted ? readConfig(projectConfigPath(cwd)) : {};
|
|
199
|
-
return resolveConfig(globalConfig, projectConfig);
|
|
346
|
+
return resolveConfig(globalConfig, projectConfig, parseEnvironmentConfig(environment));
|
|
200
347
|
}
|
|
201
348
|
|
|
202
349
|
export function configLayer(config: CodexCompatConfig): ConfigLayer {
|
|
@@ -215,6 +362,18 @@ export function configLayer(config: CodexCompatConfig): ConfigLayer {
|
|
|
215
362
|
};
|
|
216
363
|
}
|
|
217
364
|
|
|
365
|
+
/** Keep process-level overrides transient when an effective config is saved. */
|
|
366
|
+
export function withoutEnvironmentOverrides(
|
|
367
|
+
layer: ConfigLayer,
|
|
368
|
+
environmentConfig: ConfigLayer,
|
|
369
|
+
): ConfigLayer {
|
|
370
|
+
const result = { ...layer };
|
|
371
|
+
for (const key of Object.keys(environmentConfig) as (keyof ConfigLayer)[]) {
|
|
372
|
+
delete result[key];
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
218
377
|
async function readWritableConfig(filePath: string): Promise<Record<string, unknown>> {
|
|
219
378
|
try {
|
|
220
379
|
const value = JSON.parse(await readFile(filePath, "utf8")) as unknown;
|
|
@@ -30,7 +30,7 @@ function settingsSummary(ctx: ExtensionContext, config: CodexCompatConfig): stri
|
|
|
30
30
|
`reasoning summary: ${config.reasoningSummary}`,
|
|
31
31
|
`auto-compact threshold: ${config.autoCompactAtPercent ?? "Pi default"}`,
|
|
32
32
|
`save target: ${writableConfigPath(ctx.cwd, ctx.isProjectTrusted())}`,
|
|
33
|
-
"settings: /codex-settings (
|
|
33
|
+
"settings: /codex-settings (Enter saves & closes; Esc discards; Ctrl+S saves)",
|
|
34
34
|
].join("\n");
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -10,11 +10,15 @@ import {
|
|
|
10
10
|
type SettingItem,
|
|
11
11
|
SettingsList,
|
|
12
12
|
Text,
|
|
13
|
+
truncateToWidth,
|
|
13
14
|
} from "@earendil-works/pi-tui";
|
|
14
15
|
import {
|
|
16
|
+
CONFIG_ENVIRONMENT_VARIABLES,
|
|
15
17
|
configLayer,
|
|
16
18
|
loadConfig,
|
|
19
|
+
parseEnvironmentConfig,
|
|
17
20
|
saveConfig,
|
|
21
|
+
withoutEnvironmentOverrides,
|
|
18
22
|
writableConfigPath,
|
|
19
23
|
type CodexCompatConfig,
|
|
20
24
|
type ConfigLayer,
|
|
@@ -48,8 +52,11 @@ function thresholdValue(value: number | undefined): string {
|
|
|
48
52
|
return value === undefined ? "Pi default" : `${value}%`;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
export function settingItems(
|
|
52
|
-
|
|
55
|
+
export function settingItems(
|
|
56
|
+
config: CodexCompatConfig,
|
|
57
|
+
environmentConfig: ConfigLayer = {},
|
|
58
|
+
): SettingItem[] {
|
|
59
|
+
const items: SettingItem[] = [
|
|
53
60
|
{
|
|
54
61
|
id: "fastMode",
|
|
55
62
|
label: "Fast mode",
|
|
@@ -130,6 +137,18 @@ export function settingItems(config: CodexCompatConfig): SettingItem[] {
|
|
|
130
137
|
values: ["Pi default", "75%", "80%", "85%", "90%", "95%"],
|
|
131
138
|
},
|
|
132
139
|
];
|
|
140
|
+
|
|
141
|
+
return items.map((item) => {
|
|
142
|
+
const id = item.id as SettingId;
|
|
143
|
+
if (!Object.hasOwn(environmentConfig, id)) return item;
|
|
144
|
+
|
|
145
|
+
const variable = CONFIG_ENVIRONMENT_VARIABLES[id];
|
|
146
|
+
const lockedItem = { ...item };
|
|
147
|
+
lockedItem.currentValue = `${item.currentValue} (env)`;
|
|
148
|
+
lockedItem.description = `${item.description} Locked by ${variable}.`;
|
|
149
|
+
delete lockedItem.values;
|
|
150
|
+
return lockedItem;
|
|
151
|
+
});
|
|
133
152
|
}
|
|
134
153
|
|
|
135
154
|
export function settingPatch(id: string, value: string): ConfigLayer | undefined {
|
|
@@ -212,18 +231,21 @@ async function showSettings(
|
|
|
212
231
|
}
|
|
213
232
|
|
|
214
233
|
let config = callbacks.getConfig?.(ctx) ?? loadConfig(ctx.cwd, ctx.isProjectTrusted());
|
|
234
|
+
let persistedConfig = { ...config };
|
|
215
235
|
let revision = 0;
|
|
216
236
|
let savedRevision = 0;
|
|
217
237
|
let saveQueue = Promise.resolve();
|
|
218
238
|
const filePath = writableConfigPath(ctx.cwd, ctx.isProjectTrusted());
|
|
239
|
+
const environmentConfig = parseEnvironmentConfig();
|
|
219
240
|
|
|
220
|
-
await ctx.ui.custom((tui, theme,
|
|
241
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
242
|
+
let closing = false;
|
|
221
243
|
const container = new Container();
|
|
222
244
|
container.addChild(
|
|
223
245
|
new Text(theme.fg("accent", theme.bold("OpenAI Codex Compatibility Settings")), 1, 1),
|
|
224
246
|
);
|
|
225
247
|
container.addChild(
|
|
226
|
-
new Text(theme.fg("dim", `
|
|
248
|
+
new Text(theme.fg("dim", `Ctrl+S saves without closing to ${filePath}`), 1, 0),
|
|
227
249
|
);
|
|
228
250
|
const saveStatus = new Text(
|
|
229
251
|
theme.fg("dim", "Changes apply to this session immediately."),
|
|
@@ -232,10 +254,61 @@ async function showSettings(
|
|
|
232
254
|
);
|
|
233
255
|
container.addChild(saveStatus);
|
|
234
256
|
|
|
257
|
+
const queueSave = (closeAfterSave: boolean): void => {
|
|
258
|
+
if (closing) return;
|
|
259
|
+
if (closeAfterSave) closing = true;
|
|
260
|
+
|
|
261
|
+
const snapshotConfig = { ...config };
|
|
262
|
+
const snapshot = withoutEnvironmentOverrides(configLayer(snapshotConfig), environmentConfig);
|
|
263
|
+
const snapshotRevision = revision;
|
|
264
|
+
saveStatus.setText(theme.fg("dim", closeAfterSave ? "Saving and closing…" : "Saving…"));
|
|
265
|
+
saveQueue = saveQueue.then(async () => {
|
|
266
|
+
try {
|
|
267
|
+
const savedPath = await saveConfig(ctx.cwd, ctx.isProjectTrusted(), snapshot);
|
|
268
|
+
if (snapshotRevision >= savedRevision) {
|
|
269
|
+
persistedConfig = snapshotConfig;
|
|
270
|
+
savedRevision = snapshotRevision;
|
|
271
|
+
}
|
|
272
|
+
if (closeAfterSave) {
|
|
273
|
+
done(undefined);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
saveStatus.setText(
|
|
277
|
+
savedRevision === revision
|
|
278
|
+
? theme.fg("success", `Saved to ${savedPath}`)
|
|
279
|
+
: theme.fg("warning", "Unsaved session changes."),
|
|
280
|
+
);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (closeAfterSave) closing = false;
|
|
283
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
284
|
+
saveStatus.setText(theme.fg("error", message));
|
|
285
|
+
ctx.ui.notify(message, "error");
|
|
286
|
+
}
|
|
287
|
+
tui.requestRender();
|
|
288
|
+
});
|
|
289
|
+
tui.requestRender();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const discardAndClose = (): void => {
|
|
293
|
+
if (closing) return;
|
|
294
|
+
closing = true;
|
|
295
|
+
saveStatus.setText(theme.fg("dim", "Discarding unsaved changes…"));
|
|
296
|
+
saveQueue = saveQueue.then(() => {
|
|
297
|
+
if (revision !== savedRevision) {
|
|
298
|
+
config = { ...persistedConfig };
|
|
299
|
+
revision = savedRevision;
|
|
300
|
+
callbacks.onChange?.(config, ctx);
|
|
301
|
+
}
|
|
302
|
+
done(undefined);
|
|
303
|
+
});
|
|
304
|
+
tui.requestRender();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const listTheme = getSettingsListTheme();
|
|
235
308
|
const list = new SettingsList(
|
|
236
|
-
settingItems(config),
|
|
309
|
+
settingItems(config, environmentConfig),
|
|
237
310
|
12,
|
|
238
|
-
|
|
311
|
+
listTheme,
|
|
239
312
|
(id, value) => {
|
|
240
313
|
const patch = settingPatch(id, value);
|
|
241
314
|
if (!patch) return;
|
|
@@ -246,36 +319,42 @@ async function showSettings(
|
|
|
246
319
|
saveStatus.setText(theme.fg("warning", "Unsaved session changes."));
|
|
247
320
|
tui.requestRender();
|
|
248
321
|
},
|
|
249
|
-
|
|
322
|
+
discardAndClose,
|
|
250
323
|
{ enableSearch: true },
|
|
251
324
|
);
|
|
252
|
-
container.addChild(
|
|
325
|
+
container.addChild({
|
|
326
|
+
render: (width: number) => {
|
|
327
|
+
const lines = list.render(width);
|
|
328
|
+
if (lines.length > 0) {
|
|
329
|
+
lines[lines.length - 1] = truncateToWidth(
|
|
330
|
+
listTheme.hint(
|
|
331
|
+
" Type to search · Space changes · Enter save & close · Esc discard & close",
|
|
332
|
+
),
|
|
333
|
+
width,
|
|
334
|
+
"",
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return lines;
|
|
338
|
+
},
|
|
339
|
+
invalidate: () => list.invalidate(),
|
|
340
|
+
handleInput: (data: string) => list.handleInput(data),
|
|
341
|
+
});
|
|
253
342
|
|
|
254
343
|
return {
|
|
255
344
|
render: (width: number) => container.render(width),
|
|
256
345
|
invalidate: () => container.invalidate(),
|
|
257
346
|
handleInput: (data: string) => {
|
|
347
|
+
if (closing) return;
|
|
258
348
|
if (matchesKey(data, Key.ctrl("s"))) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
? theme.fg("success", `Saved to ${savedPath}`)
|
|
269
|
-
: theme.fg("warning", "Unsaved session changes."),
|
|
270
|
-
);
|
|
271
|
-
} catch (error) {
|
|
272
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
273
|
-
saveStatus.setText(theme.fg("error", message));
|
|
274
|
-
ctx.ui.notify(message, "error");
|
|
275
|
-
}
|
|
276
|
-
tui.requestRender();
|
|
277
|
-
});
|
|
278
|
-
tui.requestRender();
|
|
349
|
+
queueSave(false);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (matchesKey(data, Key.enter)) {
|
|
353
|
+
queueSave(true);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (matchesKey(data, Key.escape) || keybindings.matches(data, "tui.select.cancel")) {
|
|
357
|
+
discardAndClose();
|
|
279
358
|
return;
|
|
280
359
|
}
|
|
281
360
|
list.handleInput(data);
|