pi-codex-search 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -19
- package/index.ts +761 -110
- package/package.json +10 -5
- package/scripts/codex-e2e.ts +797 -0
- package/src/codex.ts +86 -392
- package/src/command.ts +415 -194
- package/src/config.ts +77 -4
- package/src/cookies.ts +131 -0
- package/src/errors.ts +56 -0
- package/src/modes/responses.ts +310 -0
- package/src/modes/standalone.ts +378 -0
- package/src/modes/types.ts +41 -0
- package/src/ref-store.ts +74 -0
- package/src/transport.ts +110 -0
- package/src/ua.ts +67 -0
package/src/command.ts
CHANGED
|
@@ -1,27 +1,198 @@
|
|
|
1
1
|
import { relative } from "node:path";
|
|
2
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
type Component,
|
|
5
|
+
Input,
|
|
6
|
+
type SelectItem,
|
|
7
|
+
SelectList,
|
|
8
|
+
type SettingItem,
|
|
9
|
+
SettingsList,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
import { type CodexModel, extractAccountIdFromToken, fetchCodexModels } from "./codex.ts";
|
|
3
12
|
import {
|
|
4
13
|
type ConfigScope,
|
|
14
|
+
DEFAULT_BATCH_SIZE,
|
|
5
15
|
DEFAULT_ENABLED,
|
|
6
16
|
DEFAULT_FRESHNESS,
|
|
7
17
|
DEFAULT_SEARCH_CONTEXT_SIZE,
|
|
8
|
-
|
|
18
|
+
DEFAULT_STANDALONE_ENABLED,
|
|
19
|
+
STANDALONE_TOOL_NAME,
|
|
9
20
|
deleteConfig,
|
|
10
|
-
type Freshness,
|
|
11
21
|
getConfigPath,
|
|
22
|
+
isProjectTrustedContext,
|
|
12
23
|
loadConfig,
|
|
24
|
+
MAX_BATCH_SIZE,
|
|
25
|
+
MIN_BATCH_SIZE,
|
|
13
26
|
type PiCodexSearchConfig,
|
|
14
27
|
type ResolvedConfig,
|
|
15
28
|
saveConfig,
|
|
16
29
|
} from "./config.ts";
|
|
17
|
-
import type { SearchContextSize } from "./codex.ts";
|
|
18
30
|
|
|
19
31
|
const COMMAND_NAME = "codex-search-settings";
|
|
20
32
|
const SUBCOMMANDS = ["status", "reset"] as const;
|
|
33
|
+
const OPENAI_CODEX_PROVIDER = "openai-codex";
|
|
34
|
+
|
|
35
|
+
const DEFAULT_SUFFIX = " (default)";
|
|
36
|
+
const defaultTag = (value: string): string => `${value}${DEFAULT_SUFFIX}`;
|
|
37
|
+
const isDefaultTag = (value: string): boolean => value.endsWith(DEFAULT_SUFFIX);
|
|
38
|
+
|
|
39
|
+
function normalizeStandaloneSearchContext(config: PiCodexSearchConfig): boolean {
|
|
40
|
+
const standaloneEnabled = config.standaloneEnabled ?? config.searchApi === "standalone";
|
|
41
|
+
if (!standaloneEnabled || config.searchContextSize !== "low") return false;
|
|
42
|
+
config.searchContextSize = DEFAULT_SEARCH_CONTEXT_SIZE;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface CycleField {
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
description: string;
|
|
50
|
+
values(cfg: PiCodexSearchConfig): string[];
|
|
51
|
+
get(cfg: PiCodexSearchConfig): string;
|
|
52
|
+
apply(cfg: PiCodexSearchConfig, value: string): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface TextField {
|
|
56
|
+
id: string;
|
|
57
|
+
label: string;
|
|
58
|
+
description: string;
|
|
59
|
+
/** Shown (tagged as default) when the field is not set in the active scope. */
|
|
60
|
+
defaultDisplay: string;
|
|
61
|
+
get(cfg: PiCodexSearchConfig): string | undefined;
|
|
62
|
+
apply(cfg: PiCodexSearchConfig, value: string): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The default value is shown first as "<value> (default)"; selecting it clears the field.
|
|
66
|
+
const CYCLE_FIELDS: CycleField[] = [
|
|
67
|
+
{
|
|
68
|
+
id: "enabled",
|
|
69
|
+
label: "Enabled",
|
|
70
|
+
description: "Register the search tool at session start",
|
|
71
|
+
values: () => [defaultTag(String(DEFAULT_ENABLED)), "false"],
|
|
72
|
+
get: (c) =>
|
|
73
|
+
c.enabled === undefined || c.enabled === DEFAULT_ENABLED
|
|
74
|
+
? defaultTag(String(DEFAULT_ENABLED))
|
|
75
|
+
: String(c.enabled),
|
|
76
|
+
apply: (c, v) => {
|
|
77
|
+
if (isDefaultTag(v)) delete c.enabled;
|
|
78
|
+
else c.enabled = v === "true";
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "standaloneEnabled",
|
|
83
|
+
label: "Standalone web tool",
|
|
84
|
+
description:
|
|
85
|
+
"Register codex_standalone_web for page open/find/click/screenshot and domain lookups",
|
|
86
|
+
values: () => [defaultTag(String(DEFAULT_STANDALONE_ENABLED)), "true", "false"],
|
|
87
|
+
get: (c) =>
|
|
88
|
+
c.standaloneEnabled === undefined && c.searchApi !== "standalone"
|
|
89
|
+
? defaultTag(String(DEFAULT_STANDALONE_ENABLED))
|
|
90
|
+
: String(c.standaloneEnabled ?? c.searchApi === "standalone"),
|
|
91
|
+
apply: (c, v) => {
|
|
92
|
+
delete c.searchApi;
|
|
93
|
+
if (isDefaultTag(v)) delete c.standaloneEnabled;
|
|
94
|
+
else c.standaloneEnabled = v === "true";
|
|
95
|
+
normalizeStandaloneSearchContext(c);
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "freshness",
|
|
100
|
+
label: "Freshness",
|
|
101
|
+
description: "live / indexed / cached web access",
|
|
102
|
+
values: () => [defaultTag(DEFAULT_FRESHNESS), "cached", "indexed"],
|
|
103
|
+
get: (c) =>
|
|
104
|
+
c.freshness === undefined || c.freshness === DEFAULT_FRESHNESS
|
|
105
|
+
? defaultTag(DEFAULT_FRESHNESS)
|
|
106
|
+
: c.freshness,
|
|
107
|
+
apply: (c, v) => {
|
|
108
|
+
if (isDefaultTag(v)) delete c.freshness;
|
|
109
|
+
else c.freshness = v as PiCodexSearchConfig["freshness"];
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "searchContextSize",
|
|
114
|
+
label: "Search context size",
|
|
115
|
+
description: "Amount of web context to retrieve",
|
|
116
|
+
values: (c) =>
|
|
117
|
+
c.standaloneEnabled || c.searchApi === "standalone"
|
|
118
|
+
? [defaultTag(DEFAULT_SEARCH_CONTEXT_SIZE), "high"]
|
|
119
|
+
: [defaultTag(DEFAULT_SEARCH_CONTEXT_SIZE), "low", "high"],
|
|
120
|
+
get: (c) =>
|
|
121
|
+
c.searchContextSize === undefined || c.searchContextSize === DEFAULT_SEARCH_CONTEXT_SIZE
|
|
122
|
+
? defaultTag(DEFAULT_SEARCH_CONTEXT_SIZE)
|
|
123
|
+
: c.searchContextSize,
|
|
124
|
+
apply: (c, v) => {
|
|
125
|
+
if (isDefaultTag(v)) delete c.searchContextSize;
|
|
126
|
+
else c.searchContextSize = v as PiCodexSearchConfig["searchContextSize"];
|
|
127
|
+
normalizeStandaloneSearchContext(c);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const TEXT_FIELDS: TextField[] = [
|
|
133
|
+
{
|
|
134
|
+
id: "model",
|
|
135
|
+
label: "Model",
|
|
136
|
+
description: "Pick from models loaded via /codex/models (default = auto-select)",
|
|
137
|
+
defaultDisplay: "auto",
|
|
138
|
+
get: (c) => c.model,
|
|
139
|
+
apply: (c, v) => {
|
|
140
|
+
if (v) c.model = v;
|
|
141
|
+
else delete c.model;
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "baseUrl",
|
|
146
|
+
label: "Base URL",
|
|
147
|
+
description: "Codex backend base URL",
|
|
148
|
+
defaultDisplay: "built-in",
|
|
149
|
+
get: (c) => c.baseUrl,
|
|
150
|
+
apply: (c, v) => {
|
|
151
|
+
if (v) c.baseUrl = v;
|
|
152
|
+
else delete c.baseUrl;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "clientVersion",
|
|
157
|
+
label: "Client version",
|
|
158
|
+
description: "Client version sent to /codex/models",
|
|
159
|
+
defaultDisplay: "built-in",
|
|
160
|
+
get: (c) => c.clientVersion,
|
|
161
|
+
apply: (c, v) => {
|
|
162
|
+
if (v) c.clientVersion = v;
|
|
163
|
+
else delete c.clientVersion;
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "batchSize",
|
|
168
|
+
label: "Max batch size",
|
|
169
|
+
description: `Max queries per tool call (${MIN_BATCH_SIZE}-${MAX_BATCH_SIZE})`,
|
|
170
|
+
defaultDisplay: String(DEFAULT_BATCH_SIZE),
|
|
171
|
+
get: (c) => (c.batchSize === undefined ? undefined : String(c.batchSize)),
|
|
172
|
+
apply: (c, v) => {
|
|
173
|
+
const trimmed = v.trim();
|
|
174
|
+
if (!trimmed) {
|
|
175
|
+
delete c.batchSize;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const parsed = Number(trimmed);
|
|
179
|
+
if (!Number.isInteger(parsed) || parsed < MIN_BATCH_SIZE || parsed > MAX_BATCH_SIZE) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Max batch size must be an integer between ${MIN_BATCH_SIZE} and ${MAX_BATCH_SIZE}.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
c.batchSize = parsed;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
function textDisplay(field: TextField, cfg: PiCodexSearchConfig): string {
|
|
190
|
+
return field.get(cfg) ?? defaultTag(field.defaultDisplay);
|
|
191
|
+
}
|
|
21
192
|
|
|
22
193
|
export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
23
194
|
pi.registerCommand(COMMAND_NAME, {
|
|
24
|
-
description: "Configure pi-codex-search (tool name, model, defaults, freshness).",
|
|
195
|
+
description: "Configure pi-codex-search (tool name, model, API, defaults, freshness).",
|
|
25
196
|
getArgumentCompletions(prefix) {
|
|
26
197
|
const lower = prefix.toLowerCase();
|
|
27
198
|
const matches = SUBCOMMANDS.filter((name) => name.startsWith(lower));
|
|
@@ -31,8 +202,8 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
31
202
|
const trimmed = args.trim();
|
|
32
203
|
try {
|
|
33
204
|
if (!trimmed) {
|
|
34
|
-
if (ctx.
|
|
35
|
-
await
|
|
205
|
+
if (ctx.mode === "tui") {
|
|
206
|
+
await openSettingsMenu(ctx);
|
|
36
207
|
return;
|
|
37
208
|
}
|
|
38
209
|
await printStatus(ctx);
|
|
@@ -44,7 +215,7 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
44
215
|
}
|
|
45
216
|
if (trimmed === "reset") {
|
|
46
217
|
if (ctx.hasUI) {
|
|
47
|
-
await openResetMenu(ctx);
|
|
218
|
+
await openResetMenu(ctx, isProjectTrustedContext(ctx));
|
|
48
219
|
} else {
|
|
49
220
|
notify(
|
|
50
221
|
ctx,
|
|
@@ -66,130 +237,247 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
66
237
|
});
|
|
67
238
|
}
|
|
68
239
|
|
|
69
|
-
async function
|
|
240
|
+
async function openSettingsMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
241
|
+
const isProjectTrusted = isProjectTrustedContext(ctx);
|
|
242
|
+
const resolved = await loadConfig(ctx.cwd, isProjectTrusted);
|
|
243
|
+
const drafts: Record<ConfigScope, PiCodexSearchConfig> = {
|
|
244
|
+
project: { ...resolved.sources.project },
|
|
245
|
+
home: { ...resolved.sources.home },
|
|
246
|
+
};
|
|
247
|
+
let scope: ConfigScope = isProjectTrusted ? "project" : "home";
|
|
70
248
|
let dirty = false;
|
|
249
|
+
let saveQueue: Promise<void> = Promise.resolve();
|
|
71
250
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (choice.startsWith("Edit project")) {
|
|
89
|
-
if (await editScope(ctx, "project")) dirty = true;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (choice.startsWith("Edit home")) {
|
|
93
|
-
if (await editScope(ctx, "home")) dirty = true;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (choice.startsWith("Reset")) {
|
|
97
|
-
if (await openResetMenu(ctx)) dirty = true;
|
|
98
|
-
continue;
|
|
251
|
+
for (const currentScope of ["home", "project"] as const) {
|
|
252
|
+
if (currentScope === "project" && !isProjectTrusted) continue;
|
|
253
|
+
if (normalizeStandaloneSearchContext(drafts[currentScope])) {
|
|
254
|
+
const currentDraft = { ...drafts[currentScope] };
|
|
255
|
+
saveQueue = saveQueue
|
|
256
|
+
.catch(() => undefined)
|
|
257
|
+
.then(async () => {
|
|
258
|
+
try {
|
|
259
|
+
await saveConfig(currentScope, ctx.cwd, currentDraft);
|
|
260
|
+
dirty = true;
|
|
261
|
+
} catch (error: unknown) {
|
|
262
|
+
ctx.ui.notify((error as Error).message, "error");
|
|
263
|
+
}
|
|
264
|
+
});
|
|
99
265
|
}
|
|
100
266
|
}
|
|
101
267
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
268
|
+
let models: CodexModel[] = [];
|
|
269
|
+
|
|
270
|
+
await ctx.ui.custom<void>((_tui, theme, _keybindings, done) => {
|
|
271
|
+
const settingsTheme = buildSettingsTheme(theme);
|
|
272
|
+
const selectTheme = buildSelectTheme(theme);
|
|
273
|
+
|
|
274
|
+
let list: SettingsList;
|
|
275
|
+
|
|
276
|
+
const refreshDisplays = () => {
|
|
277
|
+
scopeItem.description = formatScopeDescription(scope, ctx.cwd);
|
|
278
|
+
for (const f of CYCLE_FIELDS) {
|
|
279
|
+
const item = items.find((candidate) => candidate.id === f.id);
|
|
280
|
+
if (item) item.values = f.values(drafts[scope]);
|
|
281
|
+
list.updateValue(f.id, f.get(drafts[scope]));
|
|
282
|
+
}
|
|
283
|
+
for (const f of TEXT_FIELDS) list.updateValue(f.id, textDisplay(f, drafts[scope]));
|
|
284
|
+
list.invalidate();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const save = () => {
|
|
288
|
+
if (scope === "project" && !isProjectTrusted) {
|
|
289
|
+
ctx.ui.notify("Project config cannot be saved until the project is trusted.", "warning");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const currentScope = scope;
|
|
293
|
+
const currentDraft = { ...drafts[scope] };
|
|
294
|
+
saveQueue = saveQueue
|
|
295
|
+
.catch(() => undefined)
|
|
296
|
+
.then(async () => {
|
|
297
|
+
try {
|
|
298
|
+
await saveConfig(currentScope, ctx.cwd, currentDraft);
|
|
299
|
+
dirty = true;
|
|
300
|
+
} catch (error: unknown) {
|
|
301
|
+
ctx.ui.notify((error as Error).message, "error");
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const onChange = (id: string, newValue: string) => {
|
|
307
|
+
if (id === "scope") {
|
|
308
|
+
scope = newValue as ConfigScope;
|
|
309
|
+
refreshDisplays();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const cycle = CYCLE_FIELDS.find((f) => f.id === id);
|
|
313
|
+
if (cycle) {
|
|
314
|
+
cycle.apply(drafts[scope], newValue);
|
|
315
|
+
refreshDisplays();
|
|
316
|
+
save();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const text = TEXT_FIELDS.find((f) => f.id === id);
|
|
320
|
+
if (text) {
|
|
321
|
+
try {
|
|
322
|
+
text.apply(drafts[scope], newValue.trim());
|
|
323
|
+
} catch (error: unknown) {
|
|
324
|
+
ctx.ui.notify((error as Error).message, "error");
|
|
325
|
+
list.updateValue(id, textDisplay(text, drafts[scope]));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
list.updateValue(id, textDisplay(text, drafts[scope]));
|
|
329
|
+
save();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const scopeItem: SettingItem = {
|
|
334
|
+
id: "scope",
|
|
335
|
+
label: "Config scope",
|
|
336
|
+
description: isProjectTrusted
|
|
337
|
+
? formatScopeDescription(scope, ctx.cwd)
|
|
338
|
+
: "Project config disabled until the project is trusted; editing home config only",
|
|
339
|
+
currentValue: scope,
|
|
340
|
+
values: isProjectTrusted ? ["project", "home"] : ["home"],
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const items: SettingItem[] = [
|
|
344
|
+
scopeItem,
|
|
345
|
+
...CYCLE_FIELDS.map(
|
|
346
|
+
(f): SettingItem => ({
|
|
347
|
+
id: f.id,
|
|
348
|
+
label: f.label,
|
|
349
|
+
description: f.description,
|
|
350
|
+
currentValue: f.get(drafts[scope]),
|
|
351
|
+
values: f.values(drafts[scope]),
|
|
352
|
+
}),
|
|
353
|
+
),
|
|
354
|
+
...TEXT_FIELDS.map(
|
|
355
|
+
(f): SettingItem => ({
|
|
356
|
+
id: f.id,
|
|
357
|
+
label: f.label,
|
|
358
|
+
description: f.description,
|
|
359
|
+
currentValue: textDisplay(f, drafts[scope]),
|
|
360
|
+
submenu:
|
|
361
|
+
f.id === "model"
|
|
362
|
+
? (_current, submenuDone) =>
|
|
363
|
+
buildModelSelect(models, drafts[scope].model, selectTheme, submenuDone)
|
|
364
|
+
: (_current, submenuDone) => {
|
|
365
|
+
const input = new Input();
|
|
366
|
+
input.setValue(f.get(drafts[scope]) ?? "");
|
|
367
|
+
input.onSubmit = (value) => submenuDone(value);
|
|
368
|
+
input.onEscape = () => submenuDone();
|
|
369
|
+
return input;
|
|
370
|
+
},
|
|
371
|
+
}),
|
|
372
|
+
),
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
const modelItem = items.find((i) => i.id === "model");
|
|
376
|
+
if (modelItem) modelItem.description = "Loading models via /codex/models…";
|
|
377
|
+
|
|
378
|
+
loadModels(ctx, resolved)
|
|
379
|
+
.then((loaded) => {
|
|
380
|
+
models = loaded;
|
|
381
|
+
if (modelItem) {
|
|
382
|
+
modelItem.description =
|
|
383
|
+
loaded.length === 0
|
|
384
|
+
? "No models loaded from /codex/models (default = auto-select)"
|
|
385
|
+
: `Pick from ${loaded.length} model${loaded.length === 1 ? "" : "s"} loaded via /codex/models (default = auto-select)`;
|
|
386
|
+
}
|
|
387
|
+
list.invalidate();
|
|
388
|
+
})
|
|
389
|
+
.catch((error: unknown) => {
|
|
390
|
+
if (modelItem) {
|
|
391
|
+
modelItem.description =
|
|
392
|
+
"Could not load models from /codex/models (default = auto-select)";
|
|
393
|
+
}
|
|
394
|
+
list.invalidate();
|
|
395
|
+
ctx.ui.notify(`Could not load model list: ${(error as Error).message}`, "warning");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
list = new SettingsList(items, items.length, settingsTheme, onChange, () => done(), {
|
|
399
|
+
enableSearch: true,
|
|
400
|
+
});
|
|
401
|
+
return list as Component;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await saveQueue;
|
|
405
|
+
if (dirty) await ctx.reload();
|
|
106
406
|
}
|
|
107
407
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
408
|
+
function buildSettingsTheme(theme: Theme) {
|
|
409
|
+
return {
|
|
410
|
+
label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
|
|
411
|
+
value: (text: string, selected: boolean) =>
|
|
412
|
+
selected ? theme.bold(theme.fg("accent", text)) : theme.fg("muted", text),
|
|
413
|
+
description: (text: string) => theme.fg("dim", text),
|
|
414
|
+
cursor: theme.fg("accent", "> "),
|
|
415
|
+
hint: (text: string) => theme.fg("dim", text),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
112
418
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`Client version → ${formatValue(current.clientVersion, "default")}`,
|
|
123
|
-
`Search context size → ${formatValue(current.searchContextSize, DEFAULT_SEARCH_CONTEXT_SIZE)}`,
|
|
124
|
-
`Freshness → ${formatValue(current.freshness, DEFAULT_FRESHNESS)}`,
|
|
125
|
-
"Back",
|
|
126
|
-
]);
|
|
419
|
+
function buildSelectTheme(theme: Theme) {
|
|
420
|
+
return {
|
|
421
|
+
selectedPrefix: (text: string) => theme.fg("accent", text),
|
|
422
|
+
selectedText: (text: string) => theme.bold(theme.fg("accent", text)),
|
|
423
|
+
description: (text: string) => theme.fg("dim", text),
|
|
424
|
+
scrollInfo: (text: string) => theme.fg("dim", text),
|
|
425
|
+
noMatch: (text: string) => theme.fg("warning", text),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
127
428
|
|
|
128
|
-
|
|
429
|
+
function buildModelSelect(
|
|
430
|
+
models: CodexModel[],
|
|
431
|
+
current: string | undefined,
|
|
432
|
+
theme: ReturnType<typeof buildSelectTheme>,
|
|
433
|
+
done: (value?: string) => void,
|
|
434
|
+
): Component {
|
|
435
|
+
const items: SelectItem[] = [
|
|
436
|
+
{ value: "", label: defaultTag("auto"), description: "Auto-select from /codex/models" },
|
|
437
|
+
...models.map((m): SelectItem => ({ value: m.id, label: m.id, description: m.name })),
|
|
438
|
+
];
|
|
439
|
+
const select = new SelectList(items, Math.min(items.length, 10), theme);
|
|
440
|
+
const currentIndex = items.findIndex((i) => i.value === (current ?? ""));
|
|
441
|
+
if (currentIndex >= 0) select.setSelectedIndex(currentIndex);
|
|
442
|
+
select.onSelect = (item) => done(item.value);
|
|
443
|
+
select.onCancel = () => done();
|
|
444
|
+
return select;
|
|
445
|
+
}
|
|
129
446
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (choice.startsWith("Base URL")) {
|
|
150
|
-
const value = await ctx.ui.input(
|
|
151
|
-
"Codex backend base URL (empty to clear)",
|
|
152
|
-
current.baseUrl ?? "",
|
|
153
|
-
);
|
|
154
|
-
if (value === undefined) continue;
|
|
155
|
-
if (await applyTextField(ctx, scope, current, "baseUrl", value)) saved = true;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (choice.startsWith("Client version")) {
|
|
159
|
-
const value = await ctx.ui.input(
|
|
160
|
-
"Client version sent to /codex/models (empty to clear)",
|
|
161
|
-
current.clientVersion ?? "",
|
|
162
|
-
);
|
|
163
|
-
if (value === undefined) continue;
|
|
164
|
-
if (await applyTextField(ctx, scope, current, "clientVersion", value)) saved = true;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (choice.startsWith("Search context size")) {
|
|
168
|
-
const value = await ctx.ui.select("Search context size", ["low", "medium", "high", "Clear"]);
|
|
169
|
-
if (!value) continue;
|
|
170
|
-
const next = value === "Clear" ? undefined : (value as SearchContextSize);
|
|
171
|
-
if (await applyEnumField(ctx, scope, current, "searchContextSize", next)) saved = true;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (choice.startsWith("Freshness")) {
|
|
175
|
-
const value = await ctx.ui.select("Freshness", ["live", "cached", "Clear"]);
|
|
176
|
-
if (!value) continue;
|
|
177
|
-
const next = value === "Clear" ? undefined : (value as Freshness);
|
|
178
|
-
if (await applyEnumField(ctx, scope, current, "freshness", next)) saved = true;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
447
|
+
async function loadModels(
|
|
448
|
+
ctx: ExtensionCommandContext,
|
|
449
|
+
resolved: ResolvedConfig,
|
|
450
|
+
): Promise<CodexModel[]> {
|
|
451
|
+
const token = await ctx.modelRegistry.getApiKeyForProvider(OPENAI_CODEX_PROVIDER);
|
|
452
|
+
if (!token) return [];
|
|
453
|
+
const credential = ctx.modelRegistry.authStorage.get(OPENAI_CODEX_PROVIDER);
|
|
454
|
+
const accountId =
|
|
455
|
+
credential?.type === "oauth" && typeof credential.accountId === "string"
|
|
456
|
+
? credential.accountId
|
|
457
|
+
: extractAccountIdFromToken(token);
|
|
458
|
+
if (!accountId) return [];
|
|
459
|
+
|
|
460
|
+
const opts: Parameters<typeof fetchCodexModels>[0] = { token, accountId };
|
|
461
|
+
if (resolved.baseUrl !== undefined) opts.baseUrl = resolved.baseUrl;
|
|
462
|
+
if (resolved.clientVersion !== undefined) opts.clientVersion = resolved.clientVersion;
|
|
463
|
+
return fetchCodexModels(opts);
|
|
182
464
|
}
|
|
183
465
|
|
|
184
|
-
async function openResetMenu(
|
|
466
|
+
async function openResetMenu(
|
|
467
|
+
ctx: ExtensionCommandContext,
|
|
468
|
+
isProjectTrusted: boolean,
|
|
469
|
+
): Promise<boolean> {
|
|
185
470
|
let removed = false;
|
|
186
471
|
|
|
187
472
|
while (true) {
|
|
188
|
-
const
|
|
189
|
-
|
|
473
|
+
const options = [
|
|
474
|
+
...(isProjectTrusted
|
|
475
|
+
? [`Delete project config (${relative(ctx.cwd, getConfigPath("project", ctx.cwd))})`]
|
|
476
|
+
: []),
|
|
190
477
|
`Delete home config (${homeRelative(getConfigPath("home", ctx.cwd))})`,
|
|
191
478
|
"Back",
|
|
192
|
-
]
|
|
479
|
+
];
|
|
480
|
+
const choice = await ctx.ui.select("Reset configuration", options);
|
|
193
481
|
|
|
194
482
|
if (!choice || choice === "Back") return removed;
|
|
195
483
|
|
|
@@ -211,94 +499,25 @@ async function openResetMenu(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
|
211
499
|
}
|
|
212
500
|
}
|
|
213
501
|
|
|
214
|
-
async function applyTextField(
|
|
215
|
-
ctx: ExtensionCommandContext,
|
|
216
|
-
scope: ConfigScope,
|
|
217
|
-
current: PiCodexSearchConfig,
|
|
218
|
-
key: "toolName" | "model" | "baseUrl" | "clientVersion",
|
|
219
|
-
value: string,
|
|
220
|
-
): Promise<boolean> {
|
|
221
|
-
const next: PiCodexSearchConfig = { ...current };
|
|
222
|
-
const trimmed = value.trim();
|
|
223
|
-
if (trimmed.length === 0) {
|
|
224
|
-
delete next[key];
|
|
225
|
-
} else {
|
|
226
|
-
next[key] = trimmed;
|
|
227
|
-
}
|
|
228
|
-
return await persist(ctx, scope, next, key);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function applyEnumField<K extends "searchContextSize" | "freshness">(
|
|
232
|
-
ctx: ExtensionCommandContext,
|
|
233
|
-
scope: ConfigScope,
|
|
234
|
-
current: PiCodexSearchConfig,
|
|
235
|
-
key: K,
|
|
236
|
-
value: PiCodexSearchConfig[K] | undefined,
|
|
237
|
-
): Promise<boolean> {
|
|
238
|
-
const next: PiCodexSearchConfig = { ...current };
|
|
239
|
-
if (value === undefined) {
|
|
240
|
-
delete next[key];
|
|
241
|
-
} else {
|
|
242
|
-
next[key] = value;
|
|
243
|
-
}
|
|
244
|
-
return await persist(ctx, scope, next, key);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function applyBooleanField(
|
|
248
|
-
ctx: ExtensionCommandContext,
|
|
249
|
-
scope: ConfigScope,
|
|
250
|
-
current: PiCodexSearchConfig,
|
|
251
|
-
key: "enabled",
|
|
252
|
-
value: boolean | undefined,
|
|
253
|
-
): Promise<boolean> {
|
|
254
|
-
const next: PiCodexSearchConfig = { ...current };
|
|
255
|
-
if (value === undefined) {
|
|
256
|
-
delete next[key];
|
|
257
|
-
} else {
|
|
258
|
-
next[key] = value;
|
|
259
|
-
}
|
|
260
|
-
return await persist(ctx, scope, next, key);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function persist(
|
|
264
|
-
ctx: ExtensionCommandContext,
|
|
265
|
-
scope: ConfigScope,
|
|
266
|
-
next: PiCodexSearchConfig,
|
|
267
|
-
field: string,
|
|
268
|
-
): Promise<boolean> {
|
|
269
|
-
try {
|
|
270
|
-
const filePath = await saveConfig(scope, ctx.cwd, next);
|
|
271
|
-
ctx.ui.notify(`Saved ${field} to ${filePath}.`);
|
|
272
|
-
return true;
|
|
273
|
-
} catch (error) {
|
|
274
|
-
ctx.ui.notify((error as Error).message, "error");
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
502
|
async function printStatus(ctx: ExtensionCommandContext): Promise<void> {
|
|
280
|
-
const resolved = await loadConfig(ctx.cwd);
|
|
503
|
+
const resolved = await loadConfig(ctx.cwd, isProjectTrustedContext(ctx));
|
|
281
504
|
notify(ctx, formatStatus(resolved, ctx.cwd));
|
|
282
505
|
}
|
|
283
506
|
|
|
284
|
-
function
|
|
285
|
-
return [
|
|
286
|
-
"Codex Search settings",
|
|
287
|
-
`Effective: enabled=${resolved.enabled}, tool=${resolved.toolName}, model=${resolved.model ?? "(auto)"}, freshness=${resolved.defaultFreshness}, contextSize=${resolved.defaultSearchContextSize}`,
|
|
288
|
-
`Project file: ${relative(cwd, getConfigPath("project", cwd))}${resolved.sources.project ? "" : " (absent)"}`,
|
|
289
|
-
`Home file: ${homeRelative(getConfigPath("home", cwd))}${resolved.sources.home ? "" : " (absent)"}`,
|
|
290
|
-
].join("\n");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function formatStatus(resolved: ResolvedConfig, cwd: string): string {
|
|
507
|
+
export function formatStatus(resolved: ResolvedConfig, cwd: string): string {
|
|
294
508
|
const lines = ["Codex Search settings:"];
|
|
295
509
|
lines.push(` enabled = ${resolved.enabled}`);
|
|
296
|
-
lines.push(`
|
|
510
|
+
lines.push(` searchToolName = codex_search`);
|
|
511
|
+
lines.push(` standaloneToolName = ${STANDALONE_TOOL_NAME}`);
|
|
297
512
|
lines.push(` model = ${resolved.model ?? "(auto from /codex/models)"}`);
|
|
298
513
|
lines.push(` baseUrl = ${resolved.baseUrl ?? "(default)"}`);
|
|
299
514
|
lines.push(` clientVersion = ${resolved.clientVersion ?? "(default)"}`);
|
|
300
515
|
lines.push(` searchContextSize = ${resolved.defaultSearchContextSize}`);
|
|
301
516
|
lines.push(` freshness = ${resolved.defaultFreshness}`);
|
|
517
|
+
lines.push(` searchApi = responses`);
|
|
518
|
+
lines.push(` standaloneEnabled = ${resolved.standaloneEnabled}`);
|
|
519
|
+
lines.push(` maxBatchSize = ${resolved.batchSize}`);
|
|
520
|
+
lines.push(` standaloneBatchSize = 1`);
|
|
302
521
|
lines.push("");
|
|
303
522
|
lines.push("Sources (env > project > home):");
|
|
304
523
|
lines.push(` env = ${describeSource(resolved.sources.env)}`);
|
|
@@ -318,8 +537,10 @@ function describeSource(config: PiCodexSearchConfig | undefined): string {
|
|
|
318
537
|
return keys.sort().join(", ");
|
|
319
538
|
}
|
|
320
539
|
|
|
321
|
-
function
|
|
322
|
-
|
|
540
|
+
function formatScopeDescription(scope: ConfigScope, cwd: string): string {
|
|
541
|
+
const filePath = getConfigPath(scope, cwd);
|
|
542
|
+
const displayPath = scope === "home" ? homeRelative(filePath) : relative(cwd, filePath);
|
|
543
|
+
return `Writes to the ${scope} config file: ${displayPath}`;
|
|
323
544
|
}
|
|
324
545
|
|
|
325
546
|
function homeRelative(filePath: string): string {
|