codex-configurator 0.2.2 → 0.2.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/README.md +14 -1
- package/index.js +119 -13
- package/package.json +1 -1
- package/src/components/ConfigNavigator.js +178 -89
- package/src/configHelp.js +97 -2
- package/src/configReference.js +12 -0
- package/src/constants.js +2 -1
- package/src/fuzzySearch.js +39 -0
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ npm start
|
|
|
38
38
|
- `PgUp` `PgDn`: move one page up/down
|
|
39
39
|
- `Home` `End`: jump to first/last item
|
|
40
40
|
- `Enter`: open selected table; for boolean settings, toggle directly; for string settings, open inline input; for other preset values, open picker
|
|
41
|
+
- `/`: start fuzzy filter mode for the current list
|
|
41
42
|
- `Del`: unset selected value or remove selected custom `<id>` entry from `config.toml`
|
|
42
43
|
- `←` / `Backspace`: move up one level (to parent table)
|
|
43
44
|
- `r`: reload the active config file
|
|
@@ -47,7 +48,10 @@ The right-hand pane shows what each setting means, plus a picker when a value ha
|
|
|
47
48
|
Deprecated settings are marked with a `[!]` warning marker; only that marker is highlighted.
|
|
48
49
|
Model picker entries are curated presets maintained by this project.
|
|
49
50
|
In select lists, `[default]` marks the default option.
|
|
50
|
-
The header banner shows the configurator package version (for example `v0.2.
|
|
51
|
+
The header banner shows the configurator package version (for example `v0.2.2`) next to the wordmark.
|
|
52
|
+
Section help includes purpose-driven guidance (what a section is for), not generic placeholder copy.
|
|
53
|
+
Fuzzy filter mode matches section/value rows by label and key as you type.
|
|
54
|
+
The list panel shows a breadcrumb title on its top border with your current path (for example `projects > /home/me/repo`).
|
|
51
55
|
|
|
52
56
|
## TOML-aware navigation
|
|
53
57
|
|
|
@@ -104,6 +108,15 @@ You can override either command with:
|
|
|
104
108
|
- `CODEX_CONFIGURATOR_CODEX_BIN=<command-or-path>`
|
|
105
109
|
- `CODEX_CONFIGURATOR_NPM_BIN=<command-or-path>`
|
|
106
110
|
|
|
111
|
+
## Self-update behavior
|
|
112
|
+
|
|
113
|
+
On startup, the app checks the npm registry for the latest `codex-configurator` version.
|
|
114
|
+
If the running version is older, it automatically runs:
|
|
115
|
+
|
|
116
|
+
- `npm install -g codex-configurator@latest`
|
|
117
|
+
|
|
118
|
+
This uses the `npm` command from `PATH` (or `CODEX_CONFIGURATOR_NPM_BIN` if set).
|
|
119
|
+
|
|
107
120
|
## Upstream reference
|
|
108
121
|
|
|
109
122
|
- Codex configuration reference: https://developers.openai.com/codex/config-reference/
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
|
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
6
|
import { render, useInput, useApp, useStdout, Text, Box } from 'ink';
|
|
7
|
-
import { CONTROL_HINT, EDIT_CONTROL_HINT } from './src/constants.js';
|
|
7
|
+
import { CONTROL_HINT, EDIT_CONTROL_HINT, FILTER_CONTROL_HINT } from './src/constants.js';
|
|
8
8
|
import {
|
|
9
9
|
readConfig,
|
|
10
10
|
getNodeAtPath,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from './src/interaction.js';
|
|
31
31
|
import { Header } from './src/components/Header.js';
|
|
32
32
|
import { ConfigNavigator } from './src/components/ConfigNavigator.js';
|
|
33
|
+
import { filterRowsByQuery } from './src/fuzzySearch.js';
|
|
33
34
|
|
|
34
35
|
const require = createRequire(import.meta.url);
|
|
35
36
|
const { version: PACKAGE_VERSION = 'unknown' } = require('./package.json');
|
|
@@ -61,31 +62,43 @@ const isCustomIdTableRow = (pathSegments, row) =>
|
|
|
61
62
|
|
|
62
63
|
const isInlineTextMode = (mode) => mode === 'text' || mode === 'add-id';
|
|
63
64
|
const VERSION_COMMAND_TIMEOUT_MS = 3000;
|
|
65
|
+
const UPDATE_COMMAND_TIMEOUT_MS = 180000;
|
|
66
|
+
const COMMAND_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
67
|
+
const UPDATE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
64
68
|
const CODEX_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_CODEX_BIN';
|
|
65
69
|
const NPM_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_NPM_BIN';
|
|
70
|
+
const CONFIGURATOR_PACKAGE_NAME = 'codex-configurator';
|
|
66
71
|
|
|
67
|
-
const
|
|
72
|
+
const runCommandWithResult = (command, args = [], options = {}) =>
|
|
68
73
|
new Promise((resolve) => {
|
|
69
74
|
execFile(
|
|
70
75
|
command,
|
|
71
76
|
args,
|
|
72
77
|
{
|
|
73
78
|
encoding: 'utf8',
|
|
74
|
-
timeout: VERSION_COMMAND_TIMEOUT_MS,
|
|
75
|
-
maxBuffer:
|
|
79
|
+
timeout: options.timeout || VERSION_COMMAND_TIMEOUT_MS,
|
|
80
|
+
maxBuffer: options.maxBuffer || COMMAND_MAX_BUFFER_BYTES,
|
|
76
81
|
windowsHide: true,
|
|
77
82
|
},
|
|
78
|
-
(error, stdout) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
resolve(String(stdout || '').trim());
|
|
83
|
+
(error, stdout, stderr) => {
|
|
84
|
+
resolve({
|
|
85
|
+
ok: !error,
|
|
86
|
+
stdout: String(stdout || '').trim(),
|
|
87
|
+
stderr: String(stderr || '').trim(),
|
|
88
|
+
});
|
|
85
89
|
}
|
|
86
90
|
);
|
|
87
91
|
});
|
|
88
92
|
|
|
93
|
+
const runCommand = async (command, args = [], options = {}) => {
|
|
94
|
+
const result = await runCommandWithResult(command, args, options);
|
|
95
|
+
if (!result.ok) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result.stdout;
|
|
100
|
+
};
|
|
101
|
+
|
|
89
102
|
const getConfiguredCommand = (environmentVariableName, fallbackCommand) => {
|
|
90
103
|
const configuredCommand = String(process.env[environmentVariableName] || '').trim();
|
|
91
104
|
return configuredCommand || fallbackCommand;
|
|
@@ -96,6 +109,24 @@ const getVersionCommands = () => ({
|
|
|
96
109
|
npmCommand: getConfiguredCommand(NPM_BIN_ENV_VAR, 'npm'),
|
|
97
110
|
});
|
|
98
111
|
|
|
112
|
+
const getLatestPackageVersion = async (npmCommand, packageName) => {
|
|
113
|
+
const latestOutput = await runCommand(npmCommand, ['view', packageName, 'version', '--json']);
|
|
114
|
+
return normalizeVersion(latestOutput) || latestOutput.trim();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const updateGlobalPackageToLatest = async (npmCommand, packageName) => {
|
|
118
|
+
const result = await runCommandWithResult(
|
|
119
|
+
npmCommand,
|
|
120
|
+
['install', '-g', `${packageName}@latest`],
|
|
121
|
+
{
|
|
122
|
+
timeout: UPDATE_COMMAND_TIMEOUT_MS,
|
|
123
|
+
maxBuffer: UPDATE_MAX_BUFFER_BYTES,
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return result.ok;
|
|
128
|
+
};
|
|
129
|
+
|
|
99
130
|
const getCodexVersion = async (codexCommand) => {
|
|
100
131
|
const output = await runCommand(codexCommand, ['--version']);
|
|
101
132
|
const firstLine = output.split('\n')[0]?.trim();
|
|
@@ -185,6 +216,24 @@ const getCodexUpdateStatus = async () => {
|
|
|
185
216
|
};
|
|
186
217
|
};
|
|
187
218
|
|
|
219
|
+
const ensureLatestConfiguratorVersion = async (npmCommand) => {
|
|
220
|
+
const installed = normalizeVersion(PACKAGE_VERSION);
|
|
221
|
+
if (!installed) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const latest = await getLatestPackageVersion(npmCommand, CONFIGURATOR_PACKAGE_NAME);
|
|
226
|
+
if (!latest) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (compareVersions(installed, latest) >= 0) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await updateGlobalPackageToLatest(npmCommand, CONFIGURATOR_PACKAGE_NAME);
|
|
235
|
+
};
|
|
236
|
+
|
|
188
237
|
const App = () => {
|
|
189
238
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
190
239
|
const { stdout } = useStdout();
|
|
@@ -198,6 +247,8 @@ const App = () => {
|
|
|
198
247
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
199
248
|
const [editMode, setEditMode] = useState(null);
|
|
200
249
|
const [editError, setEditError] = useState('');
|
|
250
|
+
const [filterQuery, setFilterQuery] = useState('');
|
|
251
|
+
const [isFilterEditing, setIsFilterEditing] = useState(false);
|
|
201
252
|
const [codexVersion, setCodexVersion] = useState('version loading...');
|
|
202
253
|
const [codexVersionStatus, setCodexVersionStatus] = useState('');
|
|
203
254
|
const { exit } = useApp();
|
|
@@ -216,7 +267,13 @@ const App = () => {
|
|
|
216
267
|
setCodexVersionStatus(check.status);
|
|
217
268
|
};
|
|
218
269
|
|
|
270
|
+
const ensureLatestConfigurator = async () => {
|
|
271
|
+
const commands = getVersionCommands();
|
|
272
|
+
await ensureLatestConfiguratorVersion(commands.npmCommand);
|
|
273
|
+
};
|
|
274
|
+
|
|
219
275
|
loadVersionStatus();
|
|
276
|
+
ensureLatestConfigurator();
|
|
220
277
|
|
|
221
278
|
return () => {
|
|
222
279
|
isCancelled = true;
|
|
@@ -224,7 +281,8 @@ const App = () => {
|
|
|
224
281
|
}, []);
|
|
225
282
|
|
|
226
283
|
const currentNode = getNodeAtPath(snapshot.ok ? snapshot.data : {}, pathSegments);
|
|
227
|
-
const
|
|
284
|
+
const allRows = buildRows(currentNode, pathSegments);
|
|
285
|
+
const rows = filterRowsByQuery(allRows, filterQuery);
|
|
228
286
|
const safeSelected = rows.length === 0 ? 0 : Math.min(selectedIndex, rows.length - 1);
|
|
229
287
|
const listViewportHeight = computeListViewportHeight(rows, terminalHeight);
|
|
230
288
|
const currentPathKey = pathToKey(pathSegments);
|
|
@@ -441,11 +499,51 @@ const App = () => {
|
|
|
441
499
|
useInput((input, key) => {
|
|
442
500
|
const isTextEditing = isInlineTextMode(editMode?.mode);
|
|
443
501
|
|
|
502
|
+
if (isFilterEditing) {
|
|
503
|
+
if (key.return || key.escape) {
|
|
504
|
+
setIsFilterEditing(false);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if ((key.ctrl && key.name === 'u') || input === '\u0015') {
|
|
509
|
+
setFilterQuery('');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (isDeleteKey(input, key) || isBackspaceKey(input, key)) {
|
|
514
|
+
setFilterQuery((previous) => previous.slice(0, -1));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (
|
|
519
|
+
key.rightArrow ||
|
|
520
|
+
key.leftArrow ||
|
|
521
|
+
key.upArrow ||
|
|
522
|
+
key.downArrow ||
|
|
523
|
+
isPageUpKey(input, key) ||
|
|
524
|
+
isPageDownKey(input, key) ||
|
|
525
|
+
isHomeKey(input, key) ||
|
|
526
|
+
isEndKey(input, key)
|
|
527
|
+
) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!key.ctrl && !key.meta && input.length > 0) {
|
|
532
|
+
setFilterQuery((previous) => `${previous}${input}`);
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
444
537
|
if (input === 'q' && !isTextEditing) {
|
|
445
538
|
exit();
|
|
446
539
|
return;
|
|
447
540
|
}
|
|
448
541
|
|
|
542
|
+
if (!editMode && input === '/') {
|
|
543
|
+
setIsFilterEditing(true);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
449
547
|
if (editMode) {
|
|
450
548
|
if (isInlineTextMode(editMode.mode)) {
|
|
451
549
|
if (key.return) {
|
|
@@ -776,6 +874,8 @@ const App = () => {
|
|
|
776
874
|
scrollOffset: 0,
|
|
777
875
|
editMode: null,
|
|
778
876
|
editError: editError,
|
|
877
|
+
filterQuery,
|
|
878
|
+
isFilterEditing,
|
|
779
879
|
}),
|
|
780
880
|
React.createElement(Text, { color: 'yellow' }, 'Non-interactive mode: input is disabled.')
|
|
781
881
|
);
|
|
@@ -798,8 +898,14 @@ const App = () => {
|
|
|
798
898
|
scrollOffset,
|
|
799
899
|
editMode,
|
|
800
900
|
editError,
|
|
901
|
+
filterQuery,
|
|
902
|
+
isFilterEditing,
|
|
801
903
|
}),
|
|
802
|
-
React.createElement(
|
|
904
|
+
React.createElement(
|
|
905
|
+
Text,
|
|
906
|
+
{ color: 'gray' },
|
|
907
|
+
isFilterEditing ? FILTER_CONTROL_HINT : editMode ? EDIT_CONTROL_HINT : CONTROL_HINT
|
|
908
|
+
)
|
|
803
909
|
);
|
|
804
910
|
};
|
|
805
911
|
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '../configHelp.js';
|
|
9
9
|
import { computePaneWidths, clamp } from '../layout.js';
|
|
10
10
|
import { getNodeAtPath, buildRows } from '../configParser.js';
|
|
11
|
+
import { filterRowsByQuery } from '../fuzzySearch.js';
|
|
11
12
|
|
|
12
13
|
const MenuItem = ({ isSelected, isDimmed, isDeprecated, label }) =>
|
|
13
14
|
React.createElement(
|
|
@@ -98,6 +99,45 @@ const renderIdEditor = (placeholder, draftValue) =>
|
|
|
98
99
|
React.createElement(Text, { color: 'gray' }, 'Type id • Enter: create • Esc: cancel')
|
|
99
100
|
);
|
|
100
101
|
|
|
102
|
+
const formatBreadcrumbSegment = (segment) => {
|
|
103
|
+
const value = String(segment);
|
|
104
|
+
return /^\d+$/.test(value) ? `[${value}]` : value;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const formatBreadcrumbs = (pathSegments) => {
|
|
108
|
+
if (!Array.isArray(pathSegments) || pathSegments.length === 0) {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return pathSegments.map(formatBreadcrumbSegment).join(' > ');
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const formatSectionSummary = (row) => {
|
|
116
|
+
if (row?.kind === 'table') {
|
|
117
|
+
const entryCount =
|
|
118
|
+
Object.prototype.toString.call(row?.value) === '[object Object]'
|
|
119
|
+
? Object.keys(row.value).length
|
|
120
|
+
: 0;
|
|
121
|
+
|
|
122
|
+
if (entryCount === 0) {
|
|
123
|
+
return 'Empty section.';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `Section with ${entryCount} configured ${entryCount === 1 ? 'entry' : 'entries'}.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (row?.kind === 'tableArray') {
|
|
130
|
+
const entryCount = Array.isArray(row?.value) ? row.value.length : 0;
|
|
131
|
+
if (entryCount === 0) {
|
|
132
|
+
return 'Section with no entries.';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return `Section list with ${entryCount} ${entryCount === 1 ? 'entry' : 'entries'}.`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return 'This section groups related settings.';
|
|
139
|
+
};
|
|
140
|
+
|
|
101
141
|
const renderEditableOptions = (
|
|
102
142
|
options,
|
|
103
143
|
selectedOptionIndex,
|
|
@@ -181,14 +221,15 @@ const formatConfigHelp = (pathSegments, row) => {
|
|
|
181
221
|
}
|
|
182
222
|
|
|
183
223
|
const info = getConfigHelp(pathSegments, row.key);
|
|
224
|
+
const isSectionRow = row.kind === 'table' || row.kind === 'tableArray';
|
|
184
225
|
const defaultCollectionText =
|
|
185
|
-
|
|
186
|
-
?
|
|
226
|
+
isSectionRow
|
|
227
|
+
? formatSectionSummary(row)
|
|
187
228
|
: row.kind === 'array'
|
|
188
229
|
? `This is a list with ${row.value.length} entries.`
|
|
189
230
|
: 'This setting affects Codex behavior.';
|
|
190
231
|
const short = info?.short || defaultCollectionText;
|
|
191
|
-
const usage = info?.usage;
|
|
232
|
+
const usage = isSectionRow ? null : info?.usage;
|
|
192
233
|
const isWarning = Boolean(info?.deprecation);
|
|
193
234
|
const lines = [{ text: short, color: 'white', bold: false, showWarningIcon: false }];
|
|
194
235
|
|
|
@@ -240,6 +281,8 @@ export const ConfigNavigator = ({
|
|
|
240
281
|
terminalHeight,
|
|
241
282
|
editMode,
|
|
242
283
|
editError,
|
|
284
|
+
filterQuery = '',
|
|
285
|
+
isFilterEditing = false,
|
|
243
286
|
}) => {
|
|
244
287
|
if (!snapshot.ok) {
|
|
245
288
|
return React.createElement(
|
|
@@ -252,9 +295,13 @@ export const ConfigNavigator = ({
|
|
|
252
295
|
}
|
|
253
296
|
|
|
254
297
|
const currentNode = getNodeAtPath(snapshot.data, pathSegments);
|
|
255
|
-
const
|
|
298
|
+
const allRows = buildRows(currentNode, pathSegments);
|
|
299
|
+
const rows = filterRowsByQuery(allRows, filterQuery);
|
|
300
|
+
const breadcrumbs = formatBreadcrumbs(pathSegments);
|
|
256
301
|
const selected = rows.length === 0 ? 0 : Math.min(selectedIndex, rows.length - 1);
|
|
257
|
-
const
|
|
302
|
+
const paneSizingRows = breadcrumbs ? [...rows, { label: breadcrumbs }] : rows;
|
|
303
|
+
const { leftWidth, rightWidth } = computePaneWidths(terminalWidth, paneSizingRows);
|
|
304
|
+
const breadcrumbLabel = breadcrumbs ? truncate(breadcrumbs, Math.max(8, leftWidth - 6)) : '';
|
|
258
305
|
const terminalListHeight = Math.max(4, (terminalHeight || 24) - 14);
|
|
259
306
|
const viewportHeight = Math.max(4, Math.min(rows.length, Math.min(20, terminalListHeight)));
|
|
260
307
|
const viewportStart = clamp(scrollOffset, 0, Math.max(0, rows.length - viewportHeight));
|
|
@@ -292,96 +339,138 @@ export const ConfigNavigator = ({
|
|
|
292
339
|
return React.createElement(
|
|
293
340
|
Box,
|
|
294
341
|
{ flexDirection: 'row', gap: 2 },
|
|
295
|
-
React.createElement(
|
|
296
|
-
Box,
|
|
297
|
-
{ flexDirection: 'column', width: leftWidth },
|
|
298
342
|
React.createElement(
|
|
299
343
|
Box,
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
),
|
|
328
|
-
React.createElement(
|
|
329
|
-
Box,
|
|
330
|
-
{ flexDirection: 'column', width: rightWidth, marginTop: 2 },
|
|
331
|
-
rows.length === 0
|
|
332
|
-
? React.createElement(Text, { color: 'gray' }, 'No selection available.')
|
|
333
|
-
: React.createElement(
|
|
334
|
-
React.Fragment,
|
|
335
|
-
null,
|
|
336
|
-
...formatConfigHelp(pathSegments, rows[selected]).map((line, lineIndex) =>
|
|
337
|
-
React.createElement(
|
|
338
|
-
Text,
|
|
344
|
+
{ flexDirection: 'column', width: leftWidth },
|
|
345
|
+
React.createElement(
|
|
346
|
+
Box,
|
|
347
|
+
{
|
|
348
|
+
flexDirection: 'column',
|
|
349
|
+
borderStyle: 'single',
|
|
350
|
+
borderColor: 'gray',
|
|
351
|
+
padding: 1,
|
|
352
|
+
},
|
|
353
|
+
rows.length === 0
|
|
354
|
+
? React.createElement(
|
|
355
|
+
Text,
|
|
356
|
+
{ color: 'gray' },
|
|
357
|
+
String(filterQuery || '').trim().length > 0
|
|
358
|
+
? '[no entries match current filter]'
|
|
359
|
+
: '[no entries in this table]'
|
|
360
|
+
)
|
|
361
|
+
: visibleRows.map((row, viewIndex) => {
|
|
362
|
+
const index = viewportStart + viewIndex;
|
|
363
|
+
const showTopCue = canScrollUp && viewIndex === 0;
|
|
364
|
+
const showBottomCue = canScrollDown && viewIndex === visibleRows.length - 1;
|
|
365
|
+
const isSelected = index === selected;
|
|
366
|
+
const label = `${showTopCue ? '↑ ' : showBottomCue ? '↓ ' : ' '}${isSelected ? '▶' : ' '} ${row.label}`;
|
|
367
|
+
|
|
368
|
+
return React.createElement(
|
|
369
|
+
MenuItem,
|
|
339
370
|
{
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
:
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
371
|
+
label,
|
|
372
|
+
isSelected,
|
|
373
|
+
isDimmed: !isSelected && row.isConfigured === false,
|
|
374
|
+
isDeprecated: row.isDeprecated,
|
|
375
|
+
key: `left-${index}`,
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
})
|
|
379
|
+
),
|
|
380
|
+
React.createElement(
|
|
381
|
+
Box,
|
|
382
|
+
{ position: 'absolute', top: 0, left: 0 },
|
|
383
|
+
breadcrumbLabel
|
|
384
|
+
? React.createElement(
|
|
385
|
+
Text,
|
|
386
|
+
null,
|
|
387
|
+
React.createElement(Text, { color: 'gray' }, '┌── '),
|
|
388
|
+
React.createElement(Text, { color: 'cyan' }, `${breadcrumbLabel} `)
|
|
389
|
+
)
|
|
390
|
+
: null
|
|
391
|
+
)
|
|
392
|
+
),
|
|
393
|
+
React.createElement(
|
|
394
|
+
Box,
|
|
395
|
+
{ flexDirection: 'column', width: rightWidth, marginTop: 1 },
|
|
396
|
+
rows.length === 0
|
|
397
|
+
? React.createElement(
|
|
398
|
+
React.Fragment,
|
|
399
|
+
null,
|
|
400
|
+
String(filterQuery || '').trim().length > 0
|
|
401
|
+
? React.createElement(
|
|
402
|
+
Text,
|
|
403
|
+
{ color: 'gray' },
|
|
404
|
+
`Filter: ${filterQuery} (${rows.length}/${allRows.length})`
|
|
365
405
|
)
|
|
366
|
-
|
|
406
|
+
: null,
|
|
407
|
+
React.createElement(
|
|
408
|
+
Text,
|
|
409
|
+
{ color: 'gray' },
|
|
410
|
+
String(filterQuery || '').trim().length > 0
|
|
411
|
+
? 'No selection available. Adjust or clear the filter.'
|
|
412
|
+
: 'No selection available.'
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
: React.createElement(
|
|
416
|
+
React.Fragment,
|
|
417
|
+
null,
|
|
418
|
+
String(filterQuery || '').trim().length > 0 || isFilterEditing
|
|
367
419
|
? React.createElement(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
420
|
+
Text,
|
|
421
|
+
{ color: 'cyan' },
|
|
422
|
+
`Filter: ${filterQuery || ''} (${rows.length}/${allRows.length})${isFilterEditing ? ' [editing]' : ''}`
|
|
423
|
+
)
|
|
424
|
+
: null,
|
|
425
|
+
...formatConfigHelp(pathSegments, rows[selected]).map((line, lineIndex) =>
|
|
426
|
+
React.createElement(
|
|
427
|
+
Text,
|
|
428
|
+
{
|
|
429
|
+
key: `help-${selected}-${lineIndex}`,
|
|
430
|
+
color: line.color,
|
|
431
|
+
bold: line.bold,
|
|
432
|
+
},
|
|
433
|
+
line.showWarningIcon
|
|
434
|
+
? React.createElement(Text, { color: 'yellow' }, '[!] ')
|
|
435
|
+
: null,
|
|
436
|
+
line.text
|
|
437
|
+
)
|
|
438
|
+
),
|
|
439
|
+
editError ? React.createElement(Text, { color: 'red' }, editError) : null,
|
|
440
|
+
editMode ? React.createElement(Text, { color: 'gray' }, ' ') : null,
|
|
441
|
+
editMode
|
|
442
|
+
? editMode.mode === 'text'
|
|
443
|
+
? renderTextEditor(editMode.draftValue)
|
|
444
|
+
: editMode.mode === 'add-id'
|
|
445
|
+
? renderIdEditor(editMode.placeholder, editMode.draftValue)
|
|
446
|
+
: renderEditableOptions(
|
|
447
|
+
editMode.options,
|
|
448
|
+
editMode.selectedOptionIndex,
|
|
449
|
+
editDefaultOptionIndex,
|
|
450
|
+
editMode.path.slice(0, -1),
|
|
451
|
+
rows[selected].key,
|
|
452
|
+
editMode.savedOptionIndex,
|
|
453
|
+
true
|
|
454
|
+
)
|
|
455
|
+
: shouldShowReadOnlyOptions
|
|
456
|
+
? React.createElement(
|
|
457
|
+
React.Fragment,
|
|
377
458
|
null,
|
|
378
|
-
|
|
459
|
+
React.createElement(Text, { color: 'gray' }, ' '),
|
|
460
|
+
renderEditableOptions(
|
|
461
|
+
readOnlyOptions,
|
|
462
|
+
readOnlyOptionIndex,
|
|
463
|
+
readOnlyDefaultOptionIndex,
|
|
464
|
+
selectedPath,
|
|
465
|
+
selectedRow?.key,
|
|
466
|
+
null,
|
|
467
|
+
false
|
|
468
|
+
)
|
|
379
469
|
)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)
|
|
470
|
+
: rows[selected].kind === 'array'
|
|
471
|
+
? renderArrayDetails(rows[selected].value)
|
|
472
|
+
: null
|
|
473
|
+
)
|
|
474
|
+
)
|
|
386
475
|
);
|
|
387
476
|
};
|
package/src/configHelp.js
CHANGED
|
@@ -2,7 +2,12 @@ import {
|
|
|
2
2
|
getConfigFeatureDefinition,
|
|
3
3
|
getConfigFeatureDefinitionOrFallback,
|
|
4
4
|
} from './configFeatures.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getReferenceOptionForPath,
|
|
7
|
+
getReferenceCustomIdPlaceholder,
|
|
8
|
+
getReferenceDescendantOptions,
|
|
9
|
+
getReferenceTableDefinitions,
|
|
10
|
+
} from './configReference.js';
|
|
6
11
|
|
|
7
12
|
const CONFIG_VALUE_OPTIONS = {
|
|
8
13
|
model: [
|
|
@@ -54,6 +59,24 @@ const CONFIG_OPTION_EXPLANATIONS = {
|
|
|
54
59
|
untrusted: 'Limits risky actions and prompts more often.',
|
|
55
60
|
},
|
|
56
61
|
};
|
|
62
|
+
const SECTION_PURPOSE_OVERRIDES = {
|
|
63
|
+
agents: 'Named agent definitions and per-agent configuration file references.',
|
|
64
|
+
apps: 'Per-app enablement rules and disable reasons.',
|
|
65
|
+
features: 'Feature flags for optional and experimental Codex behavior.',
|
|
66
|
+
feedback: 'Feedback submission settings for Codex surfaces.',
|
|
67
|
+
history: 'Session history retention policy and on-disk size limits.',
|
|
68
|
+
mcp_servers: 'MCP server definitions, transport settings, and authentication configuration.',
|
|
69
|
+
model_providers: 'Model provider definitions, API endpoints, and credential settings.',
|
|
70
|
+
notice: 'Visibility toggles for startup and migration notices.',
|
|
71
|
+
otel: 'OpenTelemetry exporter configuration for telemetry and traces.',
|
|
72
|
+
profiles: 'Named profile overrides you can select per session.',
|
|
73
|
+
projects: 'Project/worktree trust settings scoped by filesystem path.',
|
|
74
|
+
sandbox_workspace_write: 'Workspace-write sandbox behavior, writable roots, and network access rules.',
|
|
75
|
+
shell_environment_policy: 'Shell environment inheritance and variable override policy.',
|
|
76
|
+
skills: 'Skill discovery and loading controls.',
|
|
77
|
+
tools: 'Tool-related configuration, including legacy compatibility flags.',
|
|
78
|
+
tui: 'Terminal UI behavior, notifications, and presentation settings.',
|
|
79
|
+
};
|
|
57
80
|
|
|
58
81
|
const makePathSegments = (segments, key) => {
|
|
59
82
|
const normalizedSegments = Array.isArray(segments)
|
|
@@ -78,6 +101,73 @@ const getContextEntry = (segments, key, candidates) => {
|
|
|
78
101
|
|
|
79
102
|
const getReferenceEntry = (segments, key) => getReferenceOptionForPath(makePathSegments(segments, key));
|
|
80
103
|
|
|
104
|
+
const formatPlaceholderLabel = (placeholder) => {
|
|
105
|
+
const cleaned = String(placeholder || '')
|
|
106
|
+
.replace(/^</, '')
|
|
107
|
+
.replace(/>$/, '');
|
|
108
|
+
|
|
109
|
+
return cleaned || 'id';
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const toFirstSentence = (text) => {
|
|
113
|
+
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
114
|
+
if (!normalized) {
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const periodIndex = normalized.indexOf('. ');
|
|
119
|
+
if (periodIndex < 0) {
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalized.slice(0, periodIndex + 1);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const getSectionPurposeDescription = (descendantOptions) => {
|
|
127
|
+
for (const option of descendantOptions) {
|
|
128
|
+
const firstSentence = toFirstSentence(option?.description);
|
|
129
|
+
if (firstSentence) {
|
|
130
|
+
return firstSentence;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return '';
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const buildInferredSectionHelp = (segments, key) => {
|
|
138
|
+
const sectionPath = makePathSegments(segments, key);
|
|
139
|
+
const childDefinitions = getReferenceTableDefinitions(sectionPath);
|
|
140
|
+
const descendantOptions = getReferenceDescendantOptions(sectionPath);
|
|
141
|
+
const customPlaceholder = getReferenceCustomIdPlaceholder(sectionPath);
|
|
142
|
+
const parentCustomPlaceholder = getReferenceCustomIdPlaceholder(sectionPath.slice(0, -1));
|
|
143
|
+
|
|
144
|
+
if (childDefinitions.length === 0 && descendantOptions.length === 0 && !customPlaceholder) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const rootOverride = sectionPath.length === 1
|
|
149
|
+
? SECTION_PURPOSE_OVERRIDES[sectionPath[0]]
|
|
150
|
+
: null;
|
|
151
|
+
const customLabel = customPlaceholder ? formatPlaceholderLabel(customPlaceholder) : '';
|
|
152
|
+
const bestPurposeDescription = getSectionPurposeDescription(descendantOptions);
|
|
153
|
+
const dynamicEntryLabel = parentCustomPlaceholder
|
|
154
|
+
? formatPlaceholderLabel(parentCustomPlaceholder)
|
|
155
|
+
: '';
|
|
156
|
+
const short = rootOverride
|
|
157
|
+
|| (dynamicEntryLabel
|
|
158
|
+
? `Configuration for this ${dynamicEntryLabel} entry.`
|
|
159
|
+
: '')
|
|
160
|
+
|| bestPurposeDescription
|
|
161
|
+
|| (customLabel
|
|
162
|
+
? `Section for custom ${customLabel} entries.`
|
|
163
|
+
: 'Section with related settings.');
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
short,
|
|
167
|
+
usage: null,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
81
171
|
const getReferenceUsage = (entry) => {
|
|
82
172
|
if (entry.deprecated) {
|
|
83
173
|
return 'This option is deprecated in the official configuration reference.';
|
|
@@ -92,7 +182,7 @@ const getReferenceUsage = (entry) => {
|
|
|
92
182
|
}
|
|
93
183
|
|
|
94
184
|
if (entry.type === 'table') {
|
|
95
|
-
return '
|
|
185
|
+
return 'Press Enter to open this section and edit nested settings.';
|
|
96
186
|
}
|
|
97
187
|
|
|
98
188
|
if (entry.type.startsWith('array<')) {
|
|
@@ -144,6 +234,11 @@ export const getConfigHelp = (segments, key) => {
|
|
|
144
234
|
return referenceHelp;
|
|
145
235
|
}
|
|
146
236
|
|
|
237
|
+
const inferredSectionHelp = buildInferredSectionHelp(segments, key);
|
|
238
|
+
if (inferredSectionHelp) {
|
|
239
|
+
return inferredSectionHelp;
|
|
240
|
+
}
|
|
241
|
+
|
|
147
242
|
return null;
|
|
148
243
|
};
|
|
149
244
|
|
package/src/configReference.js
CHANGED
|
@@ -218,3 +218,15 @@ export const getReferenceCustomIdPlaceholder = (pathSegments = []) => {
|
|
|
218
218
|
const [firstMatch] = [...placeholders];
|
|
219
219
|
return firstMatch || null;
|
|
220
220
|
};
|
|
221
|
+
|
|
222
|
+
export const getReferenceDescendantOptions = (pathSegments = []) => {
|
|
223
|
+
const normalizedPath = normalizeSegments(pathSegments);
|
|
224
|
+
|
|
225
|
+
return referenceOptions
|
|
226
|
+
.filter(
|
|
227
|
+
(option) =>
|
|
228
|
+
pathPrefixMatches(option.keyPath, normalizedPath) &&
|
|
229
|
+
option.keyPath.length > normalizedPath.length
|
|
230
|
+
)
|
|
231
|
+
.sort((left, right) => left.keyPath.length - right.keyPath.length || left.key.localeCompare(right.key));
|
|
232
|
+
};
|
package/src/constants.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const CONTROL_HINT = '↑/↓ move • PgUp/PgDn page • Home/End jump • Enter: open section or edit • Del: unset value • ←/Backspace: back • r: reload • q: quit';
|
|
1
|
+
export const CONTROL_HINT = '↑/↓ move • PgUp/PgDn page • Home/End jump • Enter: open section or edit • /: filter • Del: unset value • ←/Backspace: back • r: reload • q: quit';
|
|
2
2
|
export const EDIT_CONTROL_HINT = '↑/↓ choose • PgUp/PgDn page • Home/End jump • Enter: save • Esc/Backspace/←: cancel • Del: delete char (text input) • r: reload • q: quit';
|
|
3
|
+
export const FILTER_CONTROL_HINT = 'Type filter • Enter/Esc: done • Del/Backspace: delete • Ctrl+U: clear';
|
|
3
4
|
export const BRAND = 'CODEX CONFIGURATOR';
|
|
4
5
|
export const CONFIG_TAGS = ['Node.js', 'React', 'Ink', 'TOML'];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const normalizeText = (value) => String(value || '').toLowerCase();
|
|
2
|
+
|
|
3
|
+
export const isFuzzyMatch = (query, text) => {
|
|
4
|
+
const normalizedQuery = normalizeText(query).trim();
|
|
5
|
+
if (!normalizedQuery) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const normalizedText = normalizeText(text);
|
|
10
|
+
let queryIndex = 0;
|
|
11
|
+
|
|
12
|
+
for (let textIndex = 0; textIndex < normalizedText.length; textIndex += 1) {
|
|
13
|
+
if (normalizedText[textIndex] === normalizedQuery[queryIndex]) {
|
|
14
|
+
queryIndex += 1;
|
|
15
|
+
if (queryIndex >= normalizedQuery.length) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const rowSearchableTexts = (row) => [
|
|
25
|
+
row?.label,
|
|
26
|
+
row?.key,
|
|
27
|
+
row?.preview,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const filterRowsByQuery = (rows, query) => {
|
|
31
|
+
const normalizedQuery = String(query || '').trim();
|
|
32
|
+
if (!normalizedQuery) {
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return rows.filter((row) =>
|
|
37
|
+
rowSearchableTexts(row).some((value) => isFuzzyMatch(normalizedQuery, value))
|
|
38
|
+
);
|
|
39
|
+
};
|