pi-codex-search 0.1.2 → 0.1.3

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/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
- DEFAULT_TOOL_NAME,
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.hasUI) {
35
- await openMainDialog(ctx);
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 openMainDialog(ctx: ExtensionCommandContext): Promise<void> {
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
- while (true) {
73
- const resolved = await loadConfig(ctx.cwd);
74
- const choice = await ctx.ui.select(buildMainTitle(resolved, ctx.cwd), [
75
- "Show current configuration",
76
- `Edit project config (${relative(ctx.cwd, getConfigPath("project", ctx.cwd))})`,
77
- `Edit home config (${homeRelative(getConfigPath("home", ctx.cwd))})`,
78
- "Reset configuration…",
79
- "Done",
80
- ]);
81
-
82
- if (!choice || choice === "Done") break;
83
-
84
- if (choice === "Show current configuration") {
85
- ctx.ui.notify(formatStatus(resolved, ctx.cwd));
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
- if (dirty) {
103
- // ctx is stale after reload — dialog is already closed at this point.
104
- await ctx.reload();
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
- async function editScope(ctx: ExtensionCommandContext, scope: ConfigScope): Promise<boolean> {
109
- const filePath = getConfigPath(scope, ctx.cwd);
110
- const displayPath = scope === "home" ? homeRelative(filePath) : relative(ctx.cwd, filePath);
111
- let saved = false;
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
- while (true) {
114
- const resolved = await loadConfig(ctx.cwd);
115
- const current: PiCodexSearchConfig = { ...resolved.sources[scope] };
116
-
117
- const choice = await ctx.ui.select(`Edit ${scope} config (${displayPath})`, [
118
- `Enabled ${formatValue(current.enabled?.toString(), String(DEFAULT_ENABLED))}`,
119
- `Tool name ${formatValue(current.toolName, DEFAULT_TOOL_NAME)}`,
120
- `Model → ${formatValue(current.model, "auto")}`,
121
- `Base URL → ${formatValue(current.baseUrl, "default")}`,
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
- if (!choice || choice === "Back") return saved;
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
- if (choice.startsWith("Enabled")) {
131
- const value = await ctx.ui.select("Enabled", ["true", "false", "Clear"]);
132
- if (!value) continue;
133
- const next = value === "Clear" ? undefined : value === "true";
134
- if (await applyBooleanField(ctx, scope, current, "enabled", next)) saved = true;
135
- continue;
136
- }
137
- if (choice.startsWith("Tool name")) {
138
- const value = await ctx.ui.input("Tool name (empty to clear)", current.toolName ?? "");
139
- if (value === undefined) continue;
140
- if (await applyTextField(ctx, scope, current, "toolName", value)) saved = true;
141
- continue;
142
- }
143
- if (choice.startsWith("Model")) {
144
- const value = await ctx.ui.input("Codex model id (empty to clear)", current.model ?? "");
145
- if (value === undefined) continue;
146
- if (await applyTextField(ctx, scope, current, "model", value)) saved = true;
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(ctx: ExtensionCommandContext): Promise<boolean> {
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 choice = await ctx.ui.select("Reset configuration", [
189
- `Delete project config (${relative(ctx.cwd, getConfigPath("project", ctx.cwd))})`,
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 buildMainTitle(resolved: ResolvedConfig, cwd: string): string {
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(` toolName = ${resolved.toolName}`);
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 formatValue(value: string | undefined, fallback: string): string {
322
- return value ?? `(default: ${fallback})`;
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 {