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 +23 -0
- package/README.md +57 -4
- package/extensions/openai-codex-compat/command-tools.ts +2 -0
- package/extensions/openai-codex-compat/config.ts +2 -56
- package/extensions/openai-codex-compat/index.ts +9 -3
- package/extensions/openai-codex-compat/settings-pane.ts +227 -536
- package/extensions/settings-menu.ts +618 -0
- package/extensions/settings-session.ts +130 -0
- package/extensions/settings-store.ts +240 -0
- package/package.json +1 -1
|
@@ -1,574 +1,265 @@
|
|
|
1
|
-
import { isBoolean, isNumber } from "./value-contracts.ts";
|
|
2
|
-
import {
|
|
3
|
-
getSettingsListTheme,
|
|
4
|
-
type ExtensionContext,
|
|
5
|
-
type KeybindingsManager,
|
|
6
|
-
type Theme,
|
|
7
|
-
} from "@earendil-works/pi-coding-agent";
|
|
8
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
settingsMenu,
|
|
5
|
+
waitForSettingsIdle,
|
|
6
|
+
type SettingsField,
|
|
7
|
+
type SettingsMenuFactory,
|
|
8
|
+
} from "../settings-menu.ts";
|
|
9
|
+
import { SettingsStore } from "../settings-store.ts";
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
fuzzyFilter,
|
|
15
|
-
Input,
|
|
16
|
-
Key,
|
|
17
|
-
matchesKey,
|
|
18
|
-
type SettingItem,
|
|
19
|
-
SettingsList,
|
|
20
|
-
Text,
|
|
21
|
-
truncateToWidth,
|
|
22
|
-
type TUI,
|
|
23
|
-
} from "@earendil-works/pi-tui";
|
|
24
|
-
import { Value } from "typebox/value";
|
|
11
|
+
readSessionSettings,
|
|
12
|
+
sessionSettingsEntry,
|
|
13
|
+
type SettingsSessionContext,
|
|
14
|
+
} from "../settings-session.ts";
|
|
25
15
|
import {
|
|
16
|
+
CONFIG_ENVIRONMENT_VARIABLES,
|
|
26
17
|
CODEX_TOOL_BACKGROUND_SCHEMA,
|
|
27
18
|
CODEX_SHELL_TOOL_SCHEMA,
|
|
28
|
-
CONFIG_ENVIRONMENT_VARIABLES,
|
|
29
19
|
IMAGE_DETAIL_SCHEMA,
|
|
30
20
|
REASONING_MODE_SCHEMA,
|
|
31
21
|
REASONING_SUMMARY_SCHEMA,
|
|
32
22
|
TEXT_VERBOSITY_SCHEMA,
|
|
33
23
|
WEB_SEARCH_MODE_SCHEMA,
|
|
34
|
-
configLayer,
|
|
35
24
|
globalConfigPath,
|
|
36
|
-
|
|
25
|
+
projectConfigPath,
|
|
37
26
|
parseEnvironmentConfig,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
parseConfig,
|
|
28
|
+
resolveConfig,
|
|
29
|
+
loadConfig,
|
|
30
|
+
configLayer,
|
|
41
31
|
type CodexCompatConfig,
|
|
42
|
-
type ConfigLayer,
|
|
43
32
|
} from "./config.ts";
|
|
44
33
|
import type { ConfigContext } from "./config-context.ts";
|
|
45
|
-
import { errorFromThrown } from "./error-from-thrown.ts";
|
|
46
34
|
import { selectedRegistryModel } from "./model-context.ts";
|
|
35
|
+
import { isCodexModel, supportsReasoningMode } from "./request-options.ts";
|
|
36
|
+
import { usesResponsesLite } from "./responses-lite.ts";
|
|
47
37
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
38
|
+
export const settingFields: SettingsField[] = [
|
|
39
|
+
{
|
|
40
|
+
id: "fastMode",
|
|
41
|
+
label: "Fast mode",
|
|
42
|
+
description: "Use the priority service tier and its pricing.",
|
|
43
|
+
choices: [false, true],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "responsesLite",
|
|
47
|
+
label: "Responses Lite",
|
|
48
|
+
description: "Use Responses Lite on supported models.",
|
|
49
|
+
choices: [false, true],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "textVerbosity",
|
|
53
|
+
label: "Text verbosity",
|
|
54
|
+
description: "Set Responses API text.verbosity.",
|
|
55
|
+
choices: [...TEXT_VERBOSITY_SCHEMA.enum],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "reasoningSummary",
|
|
59
|
+
label: "Reasoning summary",
|
|
60
|
+
description: "Choose reasoning summary detail, or omit summaries.",
|
|
61
|
+
choices: [...REASONING_SUMMARY_SCHEMA.enum],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "reasoningMode",
|
|
65
|
+
label: "Reasoning mode",
|
|
66
|
+
description: "Standard or pro execution on supported models, independent of effort.",
|
|
67
|
+
choices: [...REASONING_MODE_SCHEMA.enum],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "toolBackground",
|
|
71
|
+
label: "Codex tool background",
|
|
72
|
+
description: "Choose a subtle surface, Pi status colors, or no background.",
|
|
73
|
+
choices: [...CODEX_TOOL_BACKGROUND_SCHEMA.enum],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "shellTool",
|
|
77
|
+
label: "Command tool",
|
|
78
|
+
description:
|
|
79
|
+
"Unified exec or shell_command. Collect pending output or stop sessions with /ps before switching.",
|
|
80
|
+
choices: [...CODEX_SHELL_TOOL_SCHEMA.enum],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "applyPatch",
|
|
84
|
+
label: "apply_patch tool",
|
|
85
|
+
description: "Replace Pi edit and write tools with Codex apply_patch.",
|
|
86
|
+
choices: [false, true],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "applyPatchDebug",
|
|
90
|
+
label: "apply_patch debug output",
|
|
91
|
+
description: "Show exact model feedback while patch output is collapsed.",
|
|
92
|
+
choices: [false, true],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "applyPatchDiagnostics",
|
|
96
|
+
label: "apply_patch diagnostics capture",
|
|
97
|
+
description:
|
|
98
|
+
"Retain potentially sensitive file snapshots and failed patches in Pi's agent directory.",
|
|
99
|
+
choices: [false, true],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "imageGeneration",
|
|
103
|
+
label: "image_gen.imagegen tool",
|
|
104
|
+
description: "Generate or edit images through Codex.",
|
|
105
|
+
choices: [false, true],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "imageDetail",
|
|
109
|
+
label: "Image result detail",
|
|
110
|
+
description: "Set input_image.detail for returned images.",
|
|
111
|
+
choices: [...IMAGE_DETAIL_SCHEMA.enum],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "webRun",
|
|
115
|
+
label: "web.run tool",
|
|
116
|
+
description: "Use standalone search and browsing instead of hosted web search.",
|
|
117
|
+
choices: [false, true],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "webSearch",
|
|
121
|
+
label: "Web search mode",
|
|
122
|
+
description: "Disabled removes hosted search but leaves enabled web.run cached-only.",
|
|
123
|
+
choices: [...WEB_SEARCH_MODE_SCHEMA.enum],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "autoCompactAtPercent",
|
|
127
|
+
label: "Auto-compact threshold",
|
|
128
|
+
description:
|
|
129
|
+
"An additional percentage trigger. Pi default disables the percentage override, not Pi auto-compaction.",
|
|
130
|
+
choices: [null, 75, 80, 85, 90, 95],
|
|
131
|
+
number: { min: 0, max: 100, exclusiveMin: true, unit: "%" },
|
|
132
|
+
format: (value) => (value === null ? "Pi default" : `${value}%`),
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
export type SettingsChangeContext = { model: Model<Api> | undefined; sessionId: string };
|
|
56
136
|
export type SettingsCallbacks = {
|
|
57
|
-
getConfig
|
|
58
|
-
onChange
|
|
137
|
+
getConfig: (ctx: ConfigContext) => CodexCompatConfig;
|
|
138
|
+
onChange: (config: CodexCompatConfig, ctx: SettingsChangeContext) => void;
|
|
139
|
+
hasPersistentSessions?: () => boolean;
|
|
59
140
|
};
|
|
60
|
-
|
|
61
141
|
export type CodexSettingsContext = ConfigContext &
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
id: string;
|
|
66
|
-
provider: string;
|
|
67
|
-
}
|
|
68
|
-
| undefined;
|
|
69
|
-
modelRegistry: Pick<ExtensionContext["modelRegistry"], "find">;
|
|
70
|
-
sessionManager: Pick<ExtensionContext["sessionManager"], "getSessionId">;
|
|
142
|
+
SettingsSessionContext &
|
|
143
|
+
Pick<ExtensionCommandContext, "mode" | "model" | "isIdle" | "waitForIdle"> & {
|
|
144
|
+
modelRegistry: Pick<ExtensionCommandContext["modelRegistry"], "find">;
|
|
71
145
|
ui: {
|
|
72
|
-
custom: <T>(factory:
|
|
73
|
-
notify:
|
|
146
|
+
custom: <T>(factory: SettingsMenuFactory<T>) => Promise<T>;
|
|
147
|
+
notify: ExtensionCommandContext["ui"]["notify"];
|
|
74
148
|
};
|
|
75
149
|
};
|
|
76
|
-
|
|
77
|
-
export type SettingsComponentFactory<T> = (
|
|
78
|
-
tui: Pick<TUI, "requestRender">,
|
|
79
|
-
theme: Pick<Theme, "bold" | "fg">,
|
|
80
|
-
keybindings: Pick<KeybindingsManager, "matches">,
|
|
81
|
-
done: (result: T) => void,
|
|
82
|
-
) => Component | Promise<Component>;
|
|
83
|
-
|
|
84
|
-
export type SettingsChangeContext = {
|
|
85
|
-
model: Model<Api> | undefined;
|
|
86
|
-
sessionId: string;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
150
|
export type CodexSettingsHandler = (args: string, ctx: CodexSettingsContext) => Promise<void>;
|
|
90
|
-
|
|
91
151
|
export type CodexSettingsApi = {
|
|
152
|
+
appendEntry: (customType: string, data: unknown) => void;
|
|
92
153
|
registerCommand: (
|
|
93
154
|
name: string,
|
|
94
|
-
options: {
|
|
95
|
-
description?: string;
|
|
96
|
-
handler: CodexSettingsHandler;
|
|
97
|
-
},
|
|
155
|
+
options: { description: string; handler: CodexSettingsHandler },
|
|
98
156
|
) => void;
|
|
99
157
|
};
|
|
158
|
+
export const SESSION_SETTINGS_TYPE = "pi-openai-codex-compat:settings";
|
|
100
159
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
function isSearchTextInput(data: string): boolean {
|
|
110
|
-
if (decodeKittyPrintable(data) !== undefined || data.includes("\u001b[200~")) return true;
|
|
111
|
-
if (data.length === 0) return false;
|
|
112
|
-
for (let index = 0; index < data.length; index++) {
|
|
113
|
-
const code = data.charCodeAt(index);
|
|
114
|
-
if (code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) return false;
|
|
115
|
-
}
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function settingItems(
|
|
120
|
-
config: CodexCompatConfig,
|
|
121
|
-
environmentConfig: ConfigLayer = {},
|
|
122
|
-
): SettingItem[] {
|
|
123
|
-
const items: SettingItem[] = [
|
|
124
|
-
{
|
|
125
|
-
id: "fastMode",
|
|
126
|
-
label: "Fast mode",
|
|
127
|
-
description: "Send requests through OpenAI's priority service tier.",
|
|
128
|
-
currentValue: toggleValue(config.fastMode),
|
|
129
|
-
values: ["off", "on"],
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
id: "responsesLite",
|
|
133
|
-
label: "Responses Lite",
|
|
134
|
-
description: "Use Codex's Responses Lite envelope on supported GPT-5.6 models.",
|
|
135
|
-
currentValue: toggleValue(config.responsesLite),
|
|
136
|
-
values: ["off", "on"],
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: "textVerbosity",
|
|
140
|
-
label: "Text verbosity",
|
|
141
|
-
description: "Set Responses API text.verbosity.",
|
|
142
|
-
currentValue: config.textVerbosity,
|
|
143
|
-
values: [...TEXT_VERBOSITY_SCHEMA.enum],
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
id: "reasoningSummary",
|
|
147
|
-
label: "Reasoning summary",
|
|
148
|
-
description: "Choose the reasoning summary detail, or omit summaries.",
|
|
149
|
-
currentValue: config.reasoningSummary,
|
|
150
|
-
values: [...REASONING_SUMMARY_SCHEMA.enum],
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
id: "reasoningMode",
|
|
154
|
-
label: "Reasoning mode",
|
|
155
|
-
description:
|
|
156
|
-
"Choose standard or pro execution independently of reasoning effort. Applied only to GPT-5.6 models.",
|
|
157
|
-
currentValue: config.reasoningMode,
|
|
158
|
-
values: [...REASONING_MODE_SCHEMA.enum],
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
id: "toolBackground",
|
|
162
|
-
label: "Codex tool background",
|
|
163
|
-
description: "Choose a distinct subtle surface, Pi's normal status colors, or no background.",
|
|
164
|
-
currentValue: config.toolBackground,
|
|
165
|
-
values: [...CODEX_TOOL_BACKGROUND_SCHEMA.enum],
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
id: "shellTool",
|
|
169
|
-
label: "Command tool",
|
|
170
|
-
description: "Choose the unified exec pair or legacy shell_command.",
|
|
171
|
-
currentValue: config.shellTool,
|
|
172
|
-
values: [...CODEX_SHELL_TOOL_SCHEMA.enum],
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
id: "applyPatch",
|
|
176
|
-
label: "apply_patch tool",
|
|
177
|
-
description: "Use Codex apply_patch instead of Pi's edit and write tools.",
|
|
178
|
-
currentValue: toggleValue(config.applyPatch),
|
|
179
|
-
values: ["off", "on"],
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
id: "applyPatchDebug",
|
|
183
|
-
label: "apply_patch debug output",
|
|
184
|
-
description: "Show exact model feedback while apply_patch output is collapsed.",
|
|
185
|
-
currentValue: toggleValue(config.applyPatchDebug),
|
|
186
|
-
values: ["off", "on"],
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
id: "applyPatchDiagnostics",
|
|
190
|
-
label: "apply_patch diagnostics capture",
|
|
191
|
-
description:
|
|
192
|
-
"Store failed patches, identifiers, and complete instruction snapshots in Pi's agent directory.",
|
|
193
|
-
currentValue: toggleValue(config.applyPatchDiagnostics),
|
|
194
|
-
values: ["off", "on"],
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
id: "imageGeneration",
|
|
198
|
-
label: "image_gen.imagegen tool",
|
|
199
|
-
description: "Generate or edit images through the standalone Codex image endpoint.",
|
|
200
|
-
currentValue: toggleValue(config.imageGeneration),
|
|
201
|
-
values: ["off", "on"],
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
id: "imageDetail",
|
|
205
|
-
label: "Image result detail",
|
|
206
|
-
description: "Set input_image.detail when image tool results are returned to the model.",
|
|
207
|
-
currentValue: config.imageDetail,
|
|
208
|
-
values: [...IMAGE_DETAIL_SCHEMA.enum],
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
id: "webRun",
|
|
212
|
-
label: "web.run tool",
|
|
213
|
-
description: "Search and browse through the standalone Codex search endpoint.",
|
|
214
|
-
currentValue: toggleValue(config.webRun),
|
|
215
|
-
values: ["off", "on"],
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
id: "webSearch",
|
|
219
|
-
label: "Web search mode",
|
|
220
|
-
description:
|
|
221
|
-
"Control hosted search and web.run access: disabled removes hosted search but keeps web.run cached-only.",
|
|
222
|
-
currentValue: config.webSearch,
|
|
223
|
-
values: [...WEB_SEARCH_MODE_SCHEMA.enum],
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
id: "autoCompactAtPercent",
|
|
227
|
-
label: "Auto-compact threshold",
|
|
228
|
-
description: "Add provider-boundary compaction or rely on Pi's normal compaction threshold.",
|
|
229
|
-
currentValue: thresholdValue(config.autoCompactAtPercent),
|
|
230
|
-
values: ["Pi default", "75%", "80%", "85%", "90%", "95%"],
|
|
231
|
-
},
|
|
232
|
-
];
|
|
233
|
-
|
|
234
|
-
return items.map((item) => {
|
|
235
|
-
if (!isSettingId(item.id)) {
|
|
236
|
-
throw new Error(`Unknown OpenAI Codex setting: ${item.id}`);
|
|
237
|
-
}
|
|
238
|
-
const id = item.id;
|
|
239
|
-
if (!Object.hasOwn(environmentConfig, id)) return item;
|
|
240
|
-
|
|
241
|
-
const variable = CONFIG_ENVIRONMENT_VARIABLES[id];
|
|
242
|
-
const lockedItem = { ...item };
|
|
243
|
-
lockedItem.currentValue = `${item.currentValue} (env)`;
|
|
244
|
-
lockedItem.description = `${item.description} Locked by ${variable}.`;
|
|
245
|
-
delete lockedItem.values;
|
|
246
|
-
return lockedItem;
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function settingPatch(id: string, value: string): ConfigLayer | undefined {
|
|
251
|
-
if (!isSettingId(id)) return undefined;
|
|
252
|
-
switch (id) {
|
|
253
|
-
case "fastMode":
|
|
254
|
-
return value === "on" || value === "off" ? { fastMode: value === "on" } : undefined;
|
|
255
|
-
case "responsesLite":
|
|
256
|
-
return value === "on" || value === "off" ? { responsesLite: value === "on" } : undefined;
|
|
257
|
-
case "textVerbosity":
|
|
258
|
-
if (Value.Check(TEXT_VERBOSITY_SCHEMA, value)) {
|
|
259
|
-
return { textVerbosity: value };
|
|
260
|
-
}
|
|
261
|
-
return undefined;
|
|
262
|
-
case "reasoningSummary":
|
|
263
|
-
if (Value.Check(REASONING_SUMMARY_SCHEMA, value)) {
|
|
264
|
-
return { reasoningSummary: value };
|
|
265
|
-
}
|
|
266
|
-
return undefined;
|
|
267
|
-
case "reasoningMode":
|
|
268
|
-
if (Value.Check(REASONING_MODE_SCHEMA, value)) return { reasoningMode: value };
|
|
269
|
-
return undefined;
|
|
270
|
-
case "toolBackground":
|
|
271
|
-
if (Value.Check(CODEX_TOOL_BACKGROUND_SCHEMA, value)) {
|
|
272
|
-
return { toolBackground: value };
|
|
273
|
-
}
|
|
274
|
-
return undefined;
|
|
275
|
-
case "shellTool":
|
|
276
|
-
if (Value.Check(CODEX_SHELL_TOOL_SCHEMA, value)) {
|
|
277
|
-
return { shellTool: value };
|
|
278
|
-
}
|
|
279
|
-
return undefined;
|
|
280
|
-
case "applyPatch":
|
|
281
|
-
return value === "on" || value === "off" ? { applyPatch: value === "on" } : undefined;
|
|
282
|
-
case "applyPatchDebug":
|
|
283
|
-
return value === "on" || value === "off" ? { applyPatchDebug: value === "on" } : undefined;
|
|
284
|
-
case "applyPatchDiagnostics":
|
|
285
|
-
return value === "on" || value === "off"
|
|
286
|
-
? { applyPatchDiagnostics: value === "on" }
|
|
287
|
-
: undefined;
|
|
288
|
-
case "imageGeneration":
|
|
289
|
-
return value === "on" || value === "off" ? { imageGeneration: value === "on" } : undefined;
|
|
290
|
-
case "imageDetail":
|
|
291
|
-
if (Value.Check(IMAGE_DETAIL_SCHEMA, value)) {
|
|
292
|
-
return { imageDetail: value };
|
|
293
|
-
}
|
|
294
|
-
return undefined;
|
|
295
|
-
case "webRun":
|
|
296
|
-
return value === "on" || value === "off" ? { webRun: value === "on" } : undefined;
|
|
297
|
-
case "webSearch":
|
|
298
|
-
if (Value.Check(WEB_SEARCH_MODE_SCHEMA, value)) {
|
|
299
|
-
return { webSearch: value };
|
|
300
|
-
}
|
|
301
|
-
return undefined;
|
|
302
|
-
case "autoCompactAtPercent": {
|
|
303
|
-
if (value === "Pi default") return { autoCompactAtPercent: null };
|
|
304
|
-
const percent = Number(value.replace(/%$/, ""));
|
|
305
|
-
return Number.isFinite(percent) && percent > 0 && percent <= 100
|
|
306
|
-
? { autoCompactAtPercent: percent }
|
|
307
|
-
: undefined;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function applySettingPatch(config: CodexCompatConfig, patch: ConfigLayer): CodexCompatConfig {
|
|
313
|
-
const next: CodexCompatConfig = { ...config };
|
|
314
|
-
if (isBoolean(patch.fastMode)) next.fastMode = patch.fastMode;
|
|
315
|
-
if (isBoolean(patch.responsesLite)) next.responsesLite = patch.responsesLite;
|
|
316
|
-
if (isBoolean(patch.applyPatch)) next.applyPatch = patch.applyPatch;
|
|
317
|
-
if (isBoolean(patch.applyPatchDebug)) next.applyPatchDebug = patch.applyPatchDebug;
|
|
318
|
-
if (isBoolean(patch.applyPatchDiagnostics)) {
|
|
319
|
-
next.applyPatchDiagnostics = patch.applyPatchDiagnostics;
|
|
320
|
-
}
|
|
321
|
-
if (patch.toolBackground) next.toolBackground = patch.toolBackground;
|
|
322
|
-
if (patch.shellTool) next.shellTool = patch.shellTool;
|
|
323
|
-
if (isBoolean(patch.imageGeneration)) {
|
|
324
|
-
next.imageGeneration = patch.imageGeneration;
|
|
325
|
-
}
|
|
326
|
-
if (patch.imageDetail) next.imageDetail = patch.imageDetail;
|
|
327
|
-
if (isBoolean(patch.webRun)) next.webRun = patch.webRun;
|
|
328
|
-
if (patch.webSearch) next.webSearch = patch.webSearch;
|
|
329
|
-
if (patch.textVerbosity) next.textVerbosity = patch.textVerbosity;
|
|
330
|
-
if (patch.reasoningSummary) next.reasoningSummary = patch.reasoningSummary;
|
|
331
|
-
if (patch.reasoningMode) next.reasoningMode = patch.reasoningMode;
|
|
332
|
-
if (isNumber(patch.autoCompactAtPercent)) {
|
|
333
|
-
next.autoCompactAtPercent = patch.autoCompactAtPercent;
|
|
334
|
-
} else if (patch.autoCompactAtPercent === null) {
|
|
335
|
-
delete next.autoCompactAtPercent;
|
|
336
|
-
}
|
|
337
|
-
return next;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async function showSettings(
|
|
341
|
-
ctx: CodexSettingsContext,
|
|
342
|
-
callbacks: SettingsCallbacks,
|
|
343
|
-
): Promise<void> {
|
|
344
|
-
if (ctx.mode !== "tui") {
|
|
345
|
-
ctx.ui.notify(`/${COMMAND_NAME} requires TUI mode`, "error");
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
let config = callbacks.getConfig?.(ctx) ?? loadConfig(ctx.cwd, ctx.isProjectTrusted());
|
|
350
|
-
let persistedConfig = { ...config };
|
|
351
|
-
let revision = 0;
|
|
352
|
-
let savedRevision = 0;
|
|
353
|
-
let saveQueue = Promise.resolve();
|
|
354
|
-
const environmentConfig = parseEnvironmentConfig();
|
|
355
|
-
const settingsScope =
|
|
356
|
-
writableConfigPath(ctx.cwd, ctx.isProjectTrusted()) === globalConfigPath()
|
|
357
|
-
? "global"
|
|
358
|
-
: "project";
|
|
359
|
-
const changeContext = (): SettingsChangeContext => {
|
|
360
|
-
return {
|
|
361
|
-
model: selectedRegistryModel(ctx),
|
|
362
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
363
|
-
};
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
367
|
-
let closing = false;
|
|
368
|
-
const container = new Container();
|
|
369
|
-
container.addChild(
|
|
370
|
-
new Text(theme.fg("accent", theme.bold("Codex Settings (pi-openai-codex-compat)")), 1, 1),
|
|
371
|
-
);
|
|
372
|
-
const saveStatus = new Text(
|
|
373
|
-
theme.fg(
|
|
374
|
-
"dim",
|
|
375
|
-
`Changes apply to the session immediately · Ctrl+S updates ${settingsScope} settings`,
|
|
376
|
-
),
|
|
377
|
-
1,
|
|
378
|
-
0,
|
|
379
|
-
);
|
|
380
|
-
container.addChild(saveStatus);
|
|
381
|
-
|
|
382
|
-
const queueSave = (closeAfterSave: boolean): void => {
|
|
383
|
-
if (closing) return;
|
|
384
|
-
if (closeAfterSave) closing = true;
|
|
385
|
-
|
|
386
|
-
const snapshotConfig = { ...config };
|
|
387
|
-
const snapshot = withoutEnvironmentOverrides(configLayer(snapshotConfig), environmentConfig);
|
|
388
|
-
const snapshotRevision = revision;
|
|
389
|
-
saveStatus.setText(theme.fg("dim", closeAfterSave ? "Saving and closing…" : "Saving…"));
|
|
390
|
-
saveQueue = saveQueue.then(async () => {
|
|
391
|
-
try {
|
|
392
|
-
const savedPath = await saveConfig(ctx.cwd, ctx.isProjectTrusted(), snapshot);
|
|
393
|
-
if (snapshotRevision >= savedRevision) {
|
|
394
|
-
persistedConfig = snapshotConfig;
|
|
395
|
-
savedRevision = snapshotRevision;
|
|
396
|
-
}
|
|
397
|
-
if (closeAfterSave) {
|
|
398
|
-
done(undefined);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
saveStatus.setText(
|
|
402
|
-
savedRevision === revision
|
|
403
|
-
? theme.fg("success", `Saved to ${savedPath}`)
|
|
404
|
-
: theme.fg("warning", "Unsaved session changes."),
|
|
405
|
-
);
|
|
406
|
-
} catch (cause) {
|
|
407
|
-
const error = errorFromThrown(cause, "Could not save Codex settings.");
|
|
408
|
-
if (closeAfterSave) closing = false;
|
|
409
|
-
const message = error.message;
|
|
410
|
-
saveStatus.setText(theme.fg("error", message));
|
|
411
|
-
ctx.ui.notify(message, "error");
|
|
412
|
-
}
|
|
413
|
-
tui.requestRender();
|
|
414
|
-
});
|
|
415
|
-
tui.requestRender();
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const discardAndClose = (): void => {
|
|
419
|
-
if (closing) return;
|
|
420
|
-
closing = true;
|
|
421
|
-
saveStatus.setText(theme.fg("dim", "Discarding unsaved changes…"));
|
|
422
|
-
saveQueue = saveQueue.then(() => {
|
|
423
|
-
if (revision !== savedRevision) {
|
|
424
|
-
config = { ...persistedConfig };
|
|
425
|
-
revision = savedRevision;
|
|
426
|
-
callbacks.onChange?.(config, changeContext());
|
|
427
|
-
}
|
|
428
|
-
done(undefined);
|
|
429
|
-
});
|
|
430
|
-
tui.requestRender();
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
const listTheme = getSettingsListTheme();
|
|
434
|
-
const items = settingItems(config, environmentConfig);
|
|
435
|
-
const searchInput = new Input();
|
|
436
|
-
let rootFocused = false;
|
|
437
|
-
let searchFocused = false;
|
|
438
|
-
let filteredItems = items;
|
|
439
|
-
const resultListTheme = {
|
|
440
|
-
...listTheme,
|
|
441
|
-
label: (text: string, selected: boolean) =>
|
|
442
|
-
searchFocused && selected ? theme.fg("text", text) : listTheme.label(text, selected),
|
|
443
|
-
value: (text: string, selected: boolean) =>
|
|
444
|
-
searchFocused && selected ? theme.fg("text", text) : listTheme.value(text, selected),
|
|
445
|
-
get cursor(): string {
|
|
446
|
-
return searchFocused ? "→ " : listTheme.cursor;
|
|
447
|
-
},
|
|
448
|
-
};
|
|
449
|
-
const changeSetting = (id: string, value: string): void => {
|
|
450
|
-
const patch = settingPatch(id, value);
|
|
451
|
-
if (!patch) return;
|
|
452
|
-
|
|
453
|
-
config = applySettingPatch(config, patch);
|
|
454
|
-
revision++;
|
|
455
|
-
callbacks.onChange?.(config, changeContext());
|
|
456
|
-
saveStatus.setText(theme.fg("warning", "Unsaved session changes."));
|
|
457
|
-
tui.requestRender();
|
|
458
|
-
};
|
|
459
|
-
const createList = (): SettingsList =>
|
|
460
|
-
new SettingsList(filteredItems, 12, resultListTheme, changeSetting, discardAndClose);
|
|
461
|
-
let list = createList();
|
|
462
|
-
|
|
463
|
-
const setSearchFocused = (focused: boolean): void => {
|
|
464
|
-
searchFocused = focused;
|
|
465
|
-
searchInput.focused = rootFocused && searchFocused;
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
const updateSearch = (data: string): void => {
|
|
469
|
-
const previousQuery = searchInput.getValue();
|
|
470
|
-
searchInput.handleInput(data);
|
|
471
|
-
const query = searchInput.getValue();
|
|
472
|
-
if (query === previousQuery) return;
|
|
473
|
-
filteredItems = fuzzyFilter(items, query, (item) => item.label);
|
|
474
|
-
list = createList();
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
const settingsSelector: Component = {
|
|
478
|
-
render: (width: number) => {
|
|
479
|
-
const query = searchInput.getValue();
|
|
480
|
-
const searchLabel = searchFocused
|
|
481
|
-
? theme.fg("accent", theme.bold("Search"))
|
|
482
|
-
: theme.fg("dim", "Search");
|
|
483
|
-
const searchLines = searchFocused
|
|
484
|
-
? searchInput.render(width)
|
|
485
|
-
: [truncateToWidth(listTheme.hint(query.length > 0 ? ` ${query}` : ""), width, "")];
|
|
486
|
-
const listLines =
|
|
487
|
-
filteredItems.length > 0
|
|
488
|
-
? list.render(width)
|
|
489
|
-
: [truncateToWidth(listTheme.hint(" No matching settings"), width, ""), ""];
|
|
490
|
-
const hint = searchFocused
|
|
491
|
-
? " Type to filter · ↑↓ results · Tab switch · Enter save · Esc discard"
|
|
492
|
-
: " ↑↓ navigate · Type to filter · Space change · Tab search · Enter save · Esc discard";
|
|
493
|
-
if (listLines.length > 0) {
|
|
494
|
-
listLines[listLines.length - 1] = truncateToWidth(listTheme.hint(hint), width, "");
|
|
495
|
-
}
|
|
496
|
-
return [truncateToWidth(searchLabel, width, ""), ...searchLines, "", ...listLines];
|
|
497
|
-
},
|
|
498
|
-
invalidate: () => {
|
|
499
|
-
searchInput.invalidate();
|
|
500
|
-
list.invalidate();
|
|
501
|
-
},
|
|
502
|
-
handleInput: (data: string) => {
|
|
503
|
-
if (matchesKey(data, Key.tab) || matchesKey(data, Key.shift("tab"))) {
|
|
504
|
-
setSearchFocused(!searchFocused);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
if (
|
|
508
|
-
searchFocused &&
|
|
509
|
-
(matchesKey(data, Key.up) ||
|
|
510
|
-
matchesKey(data, Key.down) ||
|
|
511
|
-
keybindings.matches(data, "tui.select.up") ||
|
|
512
|
-
keybindings.matches(data, "tui.select.down"))
|
|
513
|
-
) {
|
|
514
|
-
setSearchFocused(false);
|
|
515
|
-
list.handleInput(data);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
if (!searchFocused) {
|
|
519
|
-
if (!matchesKey(data, Key.space) && isSearchTextInput(data)) {
|
|
520
|
-
setSearchFocused(true);
|
|
521
|
-
updateSearch(data);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
list.handleInput(data);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
updateSearch(data);
|
|
528
|
-
},
|
|
529
|
-
};
|
|
530
|
-
container.addChild(settingsSelector);
|
|
531
|
-
|
|
532
|
-
const component: Component & Focusable = {
|
|
533
|
-
get focused() {
|
|
534
|
-
return rootFocused;
|
|
535
|
-
},
|
|
536
|
-
set focused(focused: boolean) {
|
|
537
|
-
rootFocused = focused;
|
|
538
|
-
searchInput.focused = rootFocused && searchFocused;
|
|
539
|
-
},
|
|
540
|
-
render: (width: number) => container.render(width),
|
|
541
|
-
invalidate: () => container.invalidate(),
|
|
542
|
-
handleInput: (data: string) => {
|
|
543
|
-
if (closing) return;
|
|
544
|
-
if (matchesKey(data, Key.ctrl("s"))) {
|
|
545
|
-
queueSave(false);
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
if (matchesKey(data, Key.enter)) {
|
|
549
|
-
queueSave(true);
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
if (matchesKey(data, Key.escape) || keybindings.matches(data, "tui.select.cancel")) {
|
|
553
|
-
discardAndClose();
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
settingsSelector.handleInput?.(data);
|
|
557
|
-
tui.requestRender();
|
|
558
|
-
},
|
|
559
|
-
};
|
|
560
|
-
return component;
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
await saveQueue;
|
|
160
|
+
export function loadSessionConfig(ctx: ConfigContext & SettingsSessionContext): CodexCompatConfig {
|
|
161
|
+
const saved = readSessionSettings(ctx, SESSION_SETTINGS_TYPE, settingFields);
|
|
162
|
+
return resolveConfig(
|
|
163
|
+
configLayer(loadConfig(ctx.cwd, ctx.isProjectTrusted())),
|
|
164
|
+
parseConfig(saved.values),
|
|
165
|
+
parseEnvironmentConfig(),
|
|
166
|
+
);
|
|
564
167
|
}
|
|
565
168
|
|
|
566
169
|
export default function registerCodexSettings(
|
|
567
170
|
pi: CodexSettingsApi,
|
|
568
|
-
callbacks: SettingsCallbacks
|
|
171
|
+
callbacks: SettingsCallbacks,
|
|
569
172
|
): void {
|
|
570
|
-
pi.registerCommand(
|
|
173
|
+
pi.registerCommand("codex-settings", {
|
|
571
174
|
description: "Configure OpenAI Codex compatibility",
|
|
572
|
-
handler: async (_args, ctx) =>
|
|
175
|
+
handler: async (_args, ctx) => {
|
|
176
|
+
if (ctx.mode !== "tui") {
|
|
177
|
+
ctx.ui.notify("/codex-settings requires TUI mode.", "error");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const environment = parseEnvironmentConfig();
|
|
182
|
+
const locked = Object.fromEntries(
|
|
183
|
+
Object.entries(CONFIG_ENVIRONMENT_VARIABLES).filter(([id]) =>
|
|
184
|
+
Object.hasOwn(environment, id),
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
188
|
+
const trusted = ctx.isProjectTrusted();
|
|
189
|
+
const store = new SettingsStore(
|
|
190
|
+
globalConfigPath(),
|
|
191
|
+
trusted ? projectConfigPath(ctx.cwd) : undefined,
|
|
192
|
+
(global, project) => {
|
|
193
|
+
const config = resolveConfig(parseConfig(global), parseConfig(project), environment);
|
|
194
|
+
return { ...config, autoCompactAtPercent: config.autoCompactAtPercent ?? null };
|
|
195
|
+
},
|
|
196
|
+
locked,
|
|
197
|
+
(data) => ({ ...parseConfig(data) }),
|
|
198
|
+
);
|
|
199
|
+
const snapshot = await store.load();
|
|
200
|
+
const selection = ctx.model;
|
|
201
|
+
const current = callbacks.getConfig(ctx);
|
|
202
|
+
const sessionState = readSessionSettings(ctx, SESSION_SETTINGS_TYPE, settingFields).session;
|
|
203
|
+
for (const id of Object.keys(locked)) delete sessionState.changes[id];
|
|
204
|
+
let appliedShell = current.shellTool;
|
|
205
|
+
const model = selectedRegistryModel(ctx);
|
|
206
|
+
const fields = settingFields.map((field) => {
|
|
207
|
+
const inactive =
|
|
208
|
+
!isCodexModel(model) ||
|
|
209
|
+
(field.id === "responsesLite" && !usesResponsesLite(model.id)) ||
|
|
210
|
+
(field.id === "reasoningMode" && !supportsReasoningMode(model.id));
|
|
211
|
+
return {
|
|
212
|
+
...field,
|
|
213
|
+
description: `${inactive ? "Inactive on this model. " : ""}${field.description}`,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
await ctx.ui.custom(
|
|
217
|
+
settingsMenu({
|
|
218
|
+
title: "Codex Settings",
|
|
219
|
+
store,
|
|
220
|
+
snapshot,
|
|
221
|
+
current: { ...current, autoCompactAtPercent: current.autoCompactAtPercent ?? null },
|
|
222
|
+
session: sessionState,
|
|
223
|
+
fields,
|
|
224
|
+
status: () =>
|
|
225
|
+
isCodexModel(model)
|
|
226
|
+
? "Codex settings apply when applied to session or saved."
|
|
227
|
+
: "Configured preferences are inactive on this provider.",
|
|
228
|
+
prepare: (signal) => waitForSettingsIdle(() => ctx.waitForIdle(), signal),
|
|
229
|
+
guard: (values) => {
|
|
230
|
+
if (ctx.model?.id !== selection?.id || ctx.model?.provider !== selection?.provider) {
|
|
231
|
+
throw new Error("The selected model changed. Reopen settings.");
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
sessionId !== ctx.sessionManager.getSessionId() ||
|
|
235
|
+
trusted !== ctx.isProjectTrusted()
|
|
236
|
+
) {
|
|
237
|
+
throw new Error("The session or project trust changed. Reopen settings.");
|
|
238
|
+
}
|
|
239
|
+
if (!ctx.isIdle()) throw new Error("Pi is busy. Retry when idle.");
|
|
240
|
+
if (values["shellTool"] !== appliedShell && callbacks.hasPersistentSessions?.()) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"Collect pending output with write_stdin or stop running sessions with /ps before changing Command tool.",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
apply: (values, nextSession) => {
|
|
247
|
+
const config = resolveConfig(parseConfig(values), {}, environment);
|
|
248
|
+
pi.appendEntry(
|
|
249
|
+
SESSION_SETTINGS_TYPE,
|
|
250
|
+
sessionSettingsEntry(sessionId, values, nextSession, locked),
|
|
251
|
+
);
|
|
252
|
+
callbacks.onChange(config, { model: selectedRegistryModel(ctx), sessionId });
|
|
253
|
+
appliedShell = config.shellTool;
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
ctx.ui.notify(
|
|
259
|
+
error instanceof Error ? error.message : "Could not open Codex settings.",
|
|
260
|
+
"error",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
573
264
|
});
|
|
574
265
|
}
|