codex-configurator 0.2.1 → 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 +19 -5
- package/index.js +133 -42
- package/package.json +1 -1
- package/src/components/ConfigNavigator.js +178 -89
- package/src/components/Header.js +29 -9
- 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,6 +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.
|
|
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`).
|
|
50
55
|
|
|
51
56
|
## TOML-aware navigation
|
|
52
57
|
|
|
@@ -96,12 +101,21 @@ When the log exceeds the size limit, it is rotated to `<log-path>.1` before writ
|
|
|
96
101
|
|
|
97
102
|
## Version checks
|
|
98
103
|
|
|
99
|
-
Codex version checks are
|
|
100
|
-
|
|
104
|
+
Codex version checks are always enabled.
|
|
105
|
+
By default, the app runs global `codex` and `npm` from `PATH`.
|
|
106
|
+
You can override either command with:
|
|
101
107
|
|
|
102
|
-
- `
|
|
103
|
-
- `
|
|
104
|
-
|
|
108
|
+
- `CODEX_CONFIGURATOR_CODEX_BIN=<command-or-path>`
|
|
109
|
+
- `CODEX_CONFIGURATOR_NPM_BIN=<command-or-path>`
|
|
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).
|
|
105
119
|
|
|
106
120
|
## Upstream reference
|
|
107
121
|
|
package/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
|
-
import
|
|
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,10 @@ 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';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const { version: PACKAGE_VERSION = 'unknown' } = require('./package.json');
|
|
33
37
|
|
|
34
38
|
const computeListViewportHeight = (rows, terminalRows) =>
|
|
35
39
|
Math.max(4, Math.min(rows.length, Math.min(20, Math.max(4, terminalRows - 14))));
|
|
@@ -58,57 +62,69 @@ const isCustomIdTableRow = (pathSegments, row) =>
|
|
|
58
62
|
|
|
59
63
|
const isInlineTextMode = (mode) => mode === 'text' || mode === 'add-id';
|
|
60
64
|
const VERSION_COMMAND_TIMEOUT_MS = 3000;
|
|
61
|
-
const
|
|
62
|
-
const
|
|
65
|
+
const UPDATE_COMMAND_TIMEOUT_MS = 180000;
|
|
66
|
+
const COMMAND_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
67
|
+
const UPDATE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
63
68
|
const CODEX_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_CODEX_BIN';
|
|
64
69
|
const NPM_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_NPM_BIN';
|
|
70
|
+
const CONFIGURATOR_PACKAGE_NAME = 'codex-configurator';
|
|
65
71
|
|
|
66
|
-
const
|
|
72
|
+
const runCommandWithResult = (command, args = [], options = {}) =>
|
|
67
73
|
new Promise((resolve) => {
|
|
68
74
|
execFile(
|
|
69
75
|
command,
|
|
70
76
|
args,
|
|
71
77
|
{
|
|
72
78
|
encoding: 'utf8',
|
|
73
|
-
timeout: VERSION_COMMAND_TIMEOUT_MS,
|
|
74
|
-
maxBuffer:
|
|
79
|
+
timeout: options.timeout || VERSION_COMMAND_TIMEOUT_MS,
|
|
80
|
+
maxBuffer: options.maxBuffer || COMMAND_MAX_BUFFER_BYTES,
|
|
75
81
|
windowsHide: true,
|
|
76
82
|
},
|
|
77
|
-
(error, stdout) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
resolve(String(stdout || '').trim());
|
|
83
|
+
(error, stdout, stderr) => {
|
|
84
|
+
resolve({
|
|
85
|
+
ok: !error,
|
|
86
|
+
stdout: String(stdout || '').trim(),
|
|
87
|
+
stderr: String(stderr || '').trim(),
|
|
88
|
+
});
|
|
84
89
|
}
|
|
85
90
|
);
|
|
86
91
|
});
|
|
87
92
|
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
93
|
+
const runCommand = async (command, args = [], options = {}) => {
|
|
94
|
+
const result = await runCommandWithResult(command, args, options);
|
|
95
|
+
if (!result.ok) {
|
|
91
96
|
return '';
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
return
|
|
99
|
+
return result.stdout;
|
|
95
100
|
};
|
|
96
101
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
const getConfiguredCommand = (environmentVariableName, fallbackCommand) => {
|
|
103
|
+
const configuredCommand = String(process.env[environmentVariableName] || '').trim();
|
|
104
|
+
return configuredCommand || fallbackCommand;
|
|
105
|
+
};
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
+
const getVersionCommands = () => ({
|
|
108
|
+
codexCommand: getConfiguredCommand(CODEX_BIN_ENV_VAR, 'codex'),
|
|
109
|
+
npmCommand: getConfiguredCommand(NPM_BIN_ENV_VAR, 'npm'),
|
|
110
|
+
});
|
|
107
111
|
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
110
119
|
npmCommand,
|
|
111
|
-
|
|
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;
|
|
112
128
|
};
|
|
113
129
|
|
|
114
130
|
const getCodexVersion = async (codexCommand) => {
|
|
@@ -157,14 +173,6 @@ const compareVersions = (left, right) => {
|
|
|
157
173
|
|
|
158
174
|
const getCodexUpdateStatus = async () => {
|
|
159
175
|
const commands = getVersionCommands();
|
|
160
|
-
if (!commands) {
|
|
161
|
-
return {
|
|
162
|
-
installed: VERSION_DISABLED_LABEL,
|
|
163
|
-
latest: 'unknown',
|
|
164
|
-
status: '',
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
176
|
const installedLabel = await getCodexVersion(commands.codexCommand);
|
|
169
177
|
const installed = normalizeVersion(installedLabel);
|
|
170
178
|
|
|
@@ -208,6 +216,24 @@ const getCodexUpdateStatus = async () => {
|
|
|
208
216
|
};
|
|
209
217
|
};
|
|
210
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
|
+
|
|
211
237
|
const App = () => {
|
|
212
238
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
213
239
|
const { stdout } = useStdout();
|
|
@@ -221,6 +247,8 @@ const App = () => {
|
|
|
221
247
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
222
248
|
const [editMode, setEditMode] = useState(null);
|
|
223
249
|
const [editError, setEditError] = useState('');
|
|
250
|
+
const [filterQuery, setFilterQuery] = useState('');
|
|
251
|
+
const [isFilterEditing, setIsFilterEditing] = useState(false);
|
|
224
252
|
const [codexVersion, setCodexVersion] = useState('version loading...');
|
|
225
253
|
const [codexVersionStatus, setCodexVersionStatus] = useState('');
|
|
226
254
|
const { exit } = useApp();
|
|
@@ -239,7 +267,13 @@ const App = () => {
|
|
|
239
267
|
setCodexVersionStatus(check.status);
|
|
240
268
|
};
|
|
241
269
|
|
|
270
|
+
const ensureLatestConfigurator = async () => {
|
|
271
|
+
const commands = getVersionCommands();
|
|
272
|
+
await ensureLatestConfiguratorVersion(commands.npmCommand);
|
|
273
|
+
};
|
|
274
|
+
|
|
242
275
|
loadVersionStatus();
|
|
276
|
+
ensureLatestConfigurator();
|
|
243
277
|
|
|
244
278
|
return () => {
|
|
245
279
|
isCancelled = true;
|
|
@@ -247,7 +281,8 @@ const App = () => {
|
|
|
247
281
|
}, []);
|
|
248
282
|
|
|
249
283
|
const currentNode = getNodeAtPath(snapshot.ok ? snapshot.data : {}, pathSegments);
|
|
250
|
-
const
|
|
284
|
+
const allRows = buildRows(currentNode, pathSegments);
|
|
285
|
+
const rows = filterRowsByQuery(allRows, filterQuery);
|
|
251
286
|
const safeSelected = rows.length === 0 ? 0 : Math.min(selectedIndex, rows.length - 1);
|
|
252
287
|
const listViewportHeight = computeListViewportHeight(rows, terminalHeight);
|
|
253
288
|
const currentPathKey = pathToKey(pathSegments);
|
|
@@ -464,11 +499,51 @@ const App = () => {
|
|
|
464
499
|
useInput((input, key) => {
|
|
465
500
|
const isTextEditing = isInlineTextMode(editMode?.mode);
|
|
466
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
|
+
|
|
467
537
|
if (input === 'q' && !isTextEditing) {
|
|
468
538
|
exit();
|
|
469
539
|
return;
|
|
470
540
|
}
|
|
471
541
|
|
|
542
|
+
if (!editMode && input === '/') {
|
|
543
|
+
setIsFilterEditing(true);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
472
547
|
if (editMode) {
|
|
473
548
|
if (isInlineTextMode(editMode.mode)) {
|
|
474
549
|
if (key.return) {
|
|
@@ -785,7 +860,11 @@ const App = () => {
|
|
|
785
860
|
return React.createElement(
|
|
786
861
|
Box,
|
|
787
862
|
{ flexDirection: 'column', padding: 1 },
|
|
788
|
-
React.createElement(Header, {
|
|
863
|
+
React.createElement(Header, {
|
|
864
|
+
codexVersion,
|
|
865
|
+
codexVersionStatus,
|
|
866
|
+
packageVersion: PACKAGE_VERSION,
|
|
867
|
+
}),
|
|
789
868
|
React.createElement(ConfigNavigator, {
|
|
790
869
|
snapshot,
|
|
791
870
|
pathSegments,
|
|
@@ -795,6 +874,8 @@ const App = () => {
|
|
|
795
874
|
scrollOffset: 0,
|
|
796
875
|
editMode: null,
|
|
797
876
|
editError: editError,
|
|
877
|
+
filterQuery,
|
|
878
|
+
isFilterEditing,
|
|
798
879
|
}),
|
|
799
880
|
React.createElement(Text, { color: 'yellow' }, 'Non-interactive mode: input is disabled.')
|
|
800
881
|
);
|
|
@@ -803,7 +884,11 @@ const App = () => {
|
|
|
803
884
|
return React.createElement(
|
|
804
885
|
Box,
|
|
805
886
|
{ flexDirection: 'column', padding: 1 },
|
|
806
|
-
React.createElement(Header, {
|
|
887
|
+
React.createElement(Header, {
|
|
888
|
+
codexVersion,
|
|
889
|
+
codexVersionStatus,
|
|
890
|
+
packageVersion: PACKAGE_VERSION,
|
|
891
|
+
}),
|
|
807
892
|
React.createElement(ConfigNavigator, {
|
|
808
893
|
snapshot,
|
|
809
894
|
pathSegments,
|
|
@@ -813,8 +898,14 @@ const App = () => {
|
|
|
813
898
|
scrollOffset,
|
|
814
899
|
editMode,
|
|
815
900
|
editError,
|
|
901
|
+
filterQuery,
|
|
902
|
+
isFilterEditing,
|
|
816
903
|
}),
|
|
817
|
-
React.createElement(
|
|
904
|
+
React.createElement(
|
|
905
|
+
Text,
|
|
906
|
+
{ color: 'gray' },
|
|
907
|
+
isFilterEditing ? FILTER_CONTROL_HINT : editMode ? EDIT_CONTROL_HINT : CONTROL_HINT
|
|
908
|
+
)
|
|
818
909
|
);
|
|
819
910
|
};
|
|
820
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/components/Header.js
CHANGED
|
@@ -10,7 +10,7 @@ const WORDMARK = [
|
|
|
10
10
|
' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝',
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
export const Header = ({ codexVersion, codexVersionStatus }) =>
|
|
13
|
+
export const Header = ({ codexVersion, codexVersionStatus, packageVersion }) =>
|
|
14
14
|
React.createElement(
|
|
15
15
|
Box,
|
|
16
16
|
{
|
|
@@ -20,14 +20,34 @@ export const Header = ({ codexVersion, codexVersionStatus }) =>
|
|
|
20
20
|
},
|
|
21
21
|
React.createElement(
|
|
22
22
|
Box,
|
|
23
|
-
{ flexDirection: '
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
{ flexDirection: 'row', marginBottom: 1, gap: 0, alignItems: 'flex-end' },
|
|
24
|
+
React.createElement(
|
|
25
|
+
Box,
|
|
26
|
+
{ flexDirection: 'column' },
|
|
27
|
+
...WORDMARK.map((line, index) =>
|
|
28
|
+
React.createElement(Text, { color: 'magentaBright', bold: true, key: `word-${index}` }, line)
|
|
29
|
+
)
|
|
30
|
+
),
|
|
31
|
+
React.createElement(
|
|
32
|
+
Box,
|
|
33
|
+
{ marginLeft: 1 },
|
|
34
|
+
React.createElement(Text, { color: 'gray', bold: true }, `v${packageVersion || 'unknown'}`)
|
|
35
|
+
)
|
|
36
|
+
),
|
|
28
37
|
React.createElement(
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
|
|
38
|
+
Box,
|
|
39
|
+
{ flexDirection: 'row' },
|
|
40
|
+
React.createElement(Text, { color: 'gray' }, `Codex ${codexVersion}`),
|
|
41
|
+
codexVersionStatus
|
|
42
|
+
? codexVersionStatus === 'up to date'
|
|
43
|
+
? React.createElement(
|
|
44
|
+
React.Fragment,
|
|
45
|
+
null,
|
|
46
|
+
React.createElement(Text, { color: 'gray' }, ' ('),
|
|
47
|
+
React.createElement(Text, { color: 'green' }, '✓'),
|
|
48
|
+
React.createElement(Text, { color: 'gray' }, ` ${codexVersionStatus})`)
|
|
49
|
+
)
|
|
50
|
+
: React.createElement(Text, { color: 'gray' }, ` (${codexVersionStatus})`)
|
|
51
|
+
: null
|
|
32
52
|
)
|
|
33
53
|
);
|
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
|
+
};
|