codex-configurator 0.2.4 → 0.2.5
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 +33 -17
- package/index.js +1398 -839
- package/package.json +4 -2
- package/src/appState.js +69 -0
- package/src/components/ConfigNavigator.js +633 -430
- package/src/components/Header.js +31 -50
- package/src/configFeatures.js +30 -165
- package/src/configHelp.js +20 -236
- package/src/configParser.js +117 -32
- package/src/configReference.js +942 -22
- package/src/constants.js +10 -3
- package/src/fileContext.js +118 -0
- package/src/interaction.js +69 -25
- package/src/layout.js +80 -15
- package/src/reference/config-schema.json +2120 -0
- package/src/ui/commands.js +699 -0
- package/src/ui/panes/CommandBar.js +90 -0
- package/src/ui/panes/HelpBubble.js +26 -0
- package/src/ui/panes/LayoutShell.js +17 -0
- package/src/ui/panes/StatusLine.js +80 -0
- package/src/variantPresets.js +268 -0
- package/src/reference/config-reference.json +0 -3494
package/index.js
CHANGED
|
@@ -1,66 +1,73 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, { useEffect, useReducer, useRef, useState } from 'react';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
6
8
|
import { render, useInput, useApp, useStdout, Text, Box } from 'ink';
|
|
7
|
-
import { CONTROL_HINT, EDIT_CONTROL_HINT, FILTER_CONTROL_HINT } from './src/constants.js';
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
buildConfigFileCatalog,
|
|
11
|
+
MAIN_CONFIG_FILE_ID,
|
|
12
|
+
} from './src/fileContext.js';
|
|
13
|
+
import {
|
|
14
|
+
ensureConfigFileExists,
|
|
15
|
+
readConfig,
|
|
16
|
+
getNodeAtPath,
|
|
17
|
+
buildRows,
|
|
18
|
+
deleteValueAtPathPruningEmptyObjects,
|
|
19
|
+
setValueAtPath,
|
|
20
|
+
writeConfig,
|
|
15
21
|
} from './src/configParser.js';
|
|
16
|
-
import { getConfigOptions } from './src/configHelp.js';
|
|
22
|
+
import { getConfigOptions, getConfigVariantMeta } from './src/configHelp.js';
|
|
17
23
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
getReferenceOptionForPath,
|
|
25
|
+
getReferenceCustomIdPlaceholder,
|
|
20
26
|
} from './src/configReference.js';
|
|
21
27
|
import { normalizeCustomPathId } from './src/customPathId.js';
|
|
22
|
-
import { pathToKey, clamp } from './src/layout.js';
|
|
23
28
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} from './src/
|
|
29
|
+
applyVariantSelection,
|
|
30
|
+
buildVariantSelectorOptions,
|
|
31
|
+
isObjectValue,
|
|
32
|
+
objectMatchesVariant,
|
|
33
|
+
resolveMixedVariantBackNavigationPath,
|
|
34
|
+
resolveObjectVariantNavigationPath,
|
|
35
|
+
} from './src/variantPresets.js';
|
|
36
|
+
import {
|
|
37
|
+
APP_MODES,
|
|
38
|
+
APP_STATE_ACTION,
|
|
39
|
+
appStateReducer,
|
|
40
|
+
buildInitialAppState,
|
|
41
|
+
} from './src/appState.js';
|
|
42
|
+
import { pathToKey, clamp, computeListViewportRows } from './src/layout.js';
|
|
31
43
|
import { Header } from './src/components/Header.js';
|
|
32
44
|
import { ConfigNavigator } from './src/components/ConfigNavigator.js';
|
|
33
45
|
import { filterRowsByQuery } from './src/fuzzySearch.js';
|
|
46
|
+
import { executeInputCommand, getModeHint } from './src/ui/commands.js';
|
|
47
|
+
import { CommandBar } from './src/ui/panes/CommandBar.js';
|
|
48
|
+
import { HelpBubble } from './src/ui/panes/HelpBubble.js';
|
|
49
|
+
import { LayoutShell } from './src/ui/panes/LayoutShell.js';
|
|
50
|
+
import { StatusLine } from './src/ui/panes/StatusLine.js';
|
|
34
51
|
|
|
35
52
|
const require = createRequire(import.meta.url);
|
|
36
53
|
const { version: PACKAGE_VERSION = 'unknown' } = require('./package.json');
|
|
37
54
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const isBooleanOnlyOptions = (options) =>
|
|
42
|
-
Array.isArray(options) &&
|
|
43
|
-
options.length === 2 &&
|
|
44
|
-
options.every((option) => typeof option === 'boolean') &&
|
|
45
|
-
options.includes(false) &&
|
|
46
|
-
options.includes(true);
|
|
47
|
-
|
|
48
|
-
const isStringReferenceType = (type) => /^string(?:\s|$)/.test(String(type || '').trim());
|
|
55
|
+
const isStringReferenceType = (type) =>
|
|
56
|
+
/^string(?:\s|$)/.test(String(type || '').trim());
|
|
49
57
|
|
|
50
58
|
const isStringField = (pathSegments, value) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
return isStringReferenceType(getReferenceOptionForPath(pathSegments)?.type);
|
|
56
64
|
};
|
|
57
65
|
|
|
58
66
|
const isCustomIdTableRow = (pathSegments, row) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
row?.kind === 'table' &&
|
|
68
|
+
typeof row?.pathSegment === 'string' &&
|
|
69
|
+
Boolean(getReferenceCustomIdPlaceholder(pathSegments));
|
|
62
70
|
|
|
63
|
-
const isInlineTextMode = (mode) => mode === 'text' || mode === 'add-id';
|
|
64
71
|
const VERSION_COMMAND_TIMEOUT_MS = 3000;
|
|
65
72
|
const UPDATE_COMMAND_TIMEOUT_MS = 180000;
|
|
66
73
|
const COMMAND_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
@@ -68,845 +75,1397 @@ const UPDATE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
|
68
75
|
const CODEX_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_CODEX_BIN';
|
|
69
76
|
const NPM_BIN_ENV_VAR = 'CODEX_CONFIGURATOR_NPM_BIN';
|
|
70
77
|
const CONFIGURATOR_PACKAGE_NAME = 'codex-configurator';
|
|
78
|
+
const FILE_SWITCH_MAX_VISIBLE_ENTRIES = 6;
|
|
79
|
+
const FILE_SWITCH_PANEL_BASE_ROWS = 3;
|
|
80
|
+
const FILE_SWITCH_LAYOUT_EXTRA_GAP_ROWS = 1;
|
|
81
|
+
const ENABLE_ALT_SCREEN = '\u001b[?1049h';
|
|
82
|
+
const DISABLE_ALT_SCREEN = '\u001b[?1049l';
|
|
83
|
+
const HIDE_CURSOR = '\u001b[?25l';
|
|
84
|
+
const SHOW_CURSOR = '\u001b[?25h';
|
|
85
|
+
const CLEAR_SCREEN = '\u001b[2J';
|
|
86
|
+
const CURSOR_HOME = '\u001b[H';
|
|
87
|
+
|
|
88
|
+
const supportsFullScreenTerminal = () =>
|
|
89
|
+
Boolean(process.stdout?.isTTY && process.stderr?.isTTY);
|
|
90
|
+
|
|
91
|
+
const setFullScreenState = (isActive) => {
|
|
92
|
+
if (!supportsFullScreenTerminal()) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isActive) {
|
|
97
|
+
process.stdout.write(
|
|
98
|
+
`${ENABLE_ALT_SCREEN}${CLEAR_SCREEN}${CURSOR_HOME}${HIDE_CURSOR}`,
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
process.stdout.write(`${SHOW_CURSOR}${DISABLE_ALT_SCREEN}`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const activateTerminalMode = () => {
|
|
107
|
+
if (!supportsFullScreenTerminal()) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let isActive = true;
|
|
112
|
+
setFullScreenState(true);
|
|
113
|
+
|
|
114
|
+
const restore = () => {
|
|
115
|
+
if (!isActive) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isActive = false;
|
|
120
|
+
setFullScreenState(false);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
process.once('exit', restore);
|
|
124
|
+
process.once('SIGINT', () => {
|
|
125
|
+
restore();
|
|
126
|
+
process.exit(130);
|
|
127
|
+
});
|
|
128
|
+
process.once('SIGTERM', () => {
|
|
129
|
+
restore();
|
|
130
|
+
process.exit(143);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const computeFileSwitchPanelRows = (entryCount) => {
|
|
137
|
+
const totalEntries = Math.max(0, entryCount);
|
|
138
|
+
const visibleEntries = Math.min(
|
|
139
|
+
FILE_SWITCH_MAX_VISIBLE_ENTRIES,
|
|
140
|
+
totalEntries,
|
|
141
|
+
);
|
|
142
|
+
const hasOverflow = totalEntries > visibleEntries;
|
|
143
|
+
|
|
144
|
+
return FILE_SWITCH_PANEL_BASE_ROWS + visibleEntries + (hasOverflow ? 1 : 0);
|
|
145
|
+
};
|
|
71
146
|
|
|
72
147
|
const runCommandWithResult = (command, args = [], options = {}) =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
148
|
+
new Promise((resolve) => {
|
|
149
|
+
execFile(
|
|
150
|
+
command,
|
|
151
|
+
args,
|
|
152
|
+
{
|
|
153
|
+
encoding: 'utf8',
|
|
154
|
+
timeout: options.timeout || VERSION_COMMAND_TIMEOUT_MS,
|
|
155
|
+
maxBuffer: options.maxBuffer || COMMAND_MAX_BUFFER_BYTES,
|
|
156
|
+
windowsHide: true,
|
|
157
|
+
},
|
|
158
|
+
(error, stdout, stderr) => {
|
|
159
|
+
resolve({
|
|
160
|
+
ok: !error,
|
|
161
|
+
stdout: String(stdout || '').trim(),
|
|
162
|
+
stderr: String(stderr || '').trim(),
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
});
|
|
92
167
|
|
|
93
168
|
const runCommand = async (command, args = [], options = {}) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
169
|
+
const result = await runCommandWithResult(command, args, options);
|
|
170
|
+
if (!result.ok) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
98
173
|
|
|
99
|
-
|
|
174
|
+
return result.stdout;
|
|
100
175
|
};
|
|
101
176
|
|
|
102
177
|
const getConfiguredCommand = (environmentVariableName, fallbackCommand) => {
|
|
103
|
-
|
|
104
|
-
|
|
178
|
+
const configuredCommand = String(
|
|
179
|
+
process.env[environmentVariableName] || '',
|
|
180
|
+
).trim();
|
|
181
|
+
return configuredCommand || fallbackCommand;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const expandTildePath = (value, homeDir = os.homedir()) => {
|
|
185
|
+
const normalized = String(value || '').trim();
|
|
186
|
+
if (!normalized) {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (normalized === '~') {
|
|
191
|
+
return homeDir;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (normalized.startsWith('~/') || normalized.startsWith('~\\')) {
|
|
195
|
+
return path.join(homeDir, normalized.slice(2));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return normalized;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const resolveAgentConfigFilePath = (mainConfigPath, configFileValue) => {
|
|
202
|
+
const normalizedValue = expandTildePath(configFileValue);
|
|
203
|
+
if (!normalizedValue) {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const mainPath =
|
|
208
|
+
String(mainConfigPath || '').trim() ||
|
|
209
|
+
path.resolve(process.cwd(), '.codex', 'config.toml');
|
|
210
|
+
const mainDirectory = path.dirname(mainPath);
|
|
211
|
+
|
|
212
|
+
return path.resolve(mainDirectory, normalizedValue);
|
|
105
213
|
};
|
|
106
214
|
|
|
107
215
|
const getVersionCommands = () => ({
|
|
108
|
-
|
|
109
|
-
|
|
216
|
+
codexCommand: getConfiguredCommand(CODEX_BIN_ENV_VAR, 'codex'),
|
|
217
|
+
npmCommand: getConfiguredCommand(NPM_BIN_ENV_VAR, 'npm'),
|
|
110
218
|
});
|
|
111
219
|
|
|
112
220
|
const getLatestPackageVersion = async (npmCommand, packageName) => {
|
|
113
|
-
|
|
114
|
-
|
|
221
|
+
const latestOutput = await runCommand(npmCommand, [
|
|
222
|
+
'view',
|
|
223
|
+
packageName,
|
|
224
|
+
'version',
|
|
225
|
+
'--json',
|
|
226
|
+
]);
|
|
227
|
+
return normalizeVersion(latestOutput) || latestOutput.trim();
|
|
115
228
|
};
|
|
116
229
|
|
|
117
230
|
const updateGlobalPackageToLatest = async (npmCommand, packageName) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
231
|
+
const result = await runCommandWithResult(
|
|
232
|
+
npmCommand,
|
|
233
|
+
['install', '-g', `${packageName}@latest`],
|
|
234
|
+
{
|
|
235
|
+
timeout: UPDATE_COMMAND_TIMEOUT_MS,
|
|
236
|
+
maxBuffer: UPDATE_MAX_BUFFER_BYTES,
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return result.ok;
|
|
128
241
|
};
|
|
129
242
|
|
|
130
243
|
const getCodexVersion = async (codexCommand) => {
|
|
131
|
-
|
|
132
|
-
|
|
244
|
+
const output = await runCommand(codexCommand, ['--version']);
|
|
245
|
+
const firstLine = output.split('\n')[0]?.trim();
|
|
133
246
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
247
|
+
if (!firstLine) {
|
|
248
|
+
return 'version unavailable';
|
|
249
|
+
}
|
|
137
250
|
|
|
138
|
-
|
|
251
|
+
return firstLine.startsWith('codex') ? firstLine : `version ${firstLine}`;
|
|
139
252
|
};
|
|
140
253
|
|
|
141
254
|
const normalizeVersion = (value) => {
|
|
142
|
-
|
|
143
|
-
|
|
255
|
+
const match = String(value || '').match(
|
|
256
|
+
/(\d+\.\d+\.\d+(?:[-+._][0-9A-Za-z.-]+)*)/,
|
|
257
|
+
);
|
|
258
|
+
return match ? match[1] : '';
|
|
144
259
|
};
|
|
145
260
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (a > b) {
|
|
163
|
-
return 1;
|
|
164
|
-
}
|
|
261
|
+
const parseVersion = (value) => {
|
|
262
|
+
const normalized = normalizeVersion(value);
|
|
263
|
+
const [core, ...suffixParts] = String(normalized || '').split(/[-+]/);
|
|
264
|
+
const suffix = suffixParts.join('-');
|
|
265
|
+
const coreParts = core
|
|
266
|
+
.split('.')
|
|
267
|
+
.map((part) => Number.parseInt(part, 10))
|
|
268
|
+
.filter(Number.isFinite);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
hasSuffix: Boolean(suffix),
|
|
272
|
+
suffix,
|
|
273
|
+
parts: coreParts,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
165
276
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
277
|
+
const comparePreRelease = (leftSuffix, rightSuffix) => {
|
|
278
|
+
const leftParts = String(leftSuffix || '')
|
|
279
|
+
.split('.')
|
|
280
|
+
.filter(Boolean);
|
|
281
|
+
const rightParts = String(rightSuffix || '')
|
|
282
|
+
.split('.')
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
285
|
+
|
|
286
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
287
|
+
const leftPart = leftParts[index];
|
|
288
|
+
const rightPart = rightParts[index];
|
|
289
|
+
const leftNumber = Number.parseInt(leftPart, 10);
|
|
290
|
+
const rightNumber = Number.parseInt(rightPart, 10);
|
|
291
|
+
|
|
292
|
+
const leftIsNumber = Number.isFinite(leftNumber);
|
|
293
|
+
const rightIsNumber = Number.isFinite(rightNumber);
|
|
294
|
+
|
|
295
|
+
if (leftIsNumber && rightIsNumber) {
|
|
296
|
+
if (leftNumber > rightNumber) {
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (leftNumber < rightNumber) {
|
|
301
|
+
return -1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (leftIsNumber && !rightIsNumber) {
|
|
308
|
+
return -1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!leftIsNumber && rightIsNumber) {
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if ((leftPart || '') > (rightPart || '')) {
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if ((leftPart || '') < (rightPart || '')) {
|
|
320
|
+
return -1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return 0;
|
|
325
|
+
};
|
|
170
326
|
|
|
171
|
-
|
|
327
|
+
const compareVersions = (left, right) => {
|
|
328
|
+
const leftVersion = parseVersion(left);
|
|
329
|
+
const rightVersion = parseVersion(right);
|
|
330
|
+
const leftParts = leftVersion.parts;
|
|
331
|
+
const rightParts = rightVersion.parts;
|
|
332
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
333
|
+
|
|
334
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
335
|
+
const a = leftParts[index] || 0;
|
|
336
|
+
const b = rightParts[index] || 0;
|
|
337
|
+
|
|
338
|
+
if (a > b) {
|
|
339
|
+
return 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (a < b) {
|
|
343
|
+
return -1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (leftVersion.hasSuffix !== rightVersion.hasSuffix) {
|
|
348
|
+
return leftVersion.hasSuffix ? -1 : 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (leftVersion.hasSuffix) {
|
|
352
|
+
return comparePreRelease(leftVersion.suffix, rightVersion.suffix);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return 0;
|
|
172
356
|
};
|
|
173
357
|
|
|
174
358
|
const getCodexUpdateStatus = async () => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
359
|
+
const commands = getVersionCommands();
|
|
360
|
+
const installedLabel = await getCodexVersion(commands.codexCommand);
|
|
361
|
+
const installed = normalizeVersion(installedLabel);
|
|
362
|
+
|
|
363
|
+
if (!installed) {
|
|
364
|
+
return {
|
|
365
|
+
installed: installedLabel,
|
|
366
|
+
latest: 'unknown',
|
|
367
|
+
status: 'version check unavailable',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const latestOutput = await runCommand(commands.npmCommand, [
|
|
372
|
+
'view',
|
|
373
|
+
'@openai/codex',
|
|
374
|
+
'version',
|
|
375
|
+
'--json',
|
|
376
|
+
]);
|
|
377
|
+
const latest = normalizeVersion(latestOutput) || latestOutput.trim();
|
|
378
|
+
|
|
379
|
+
if (!latest) {
|
|
380
|
+
return {
|
|
381
|
+
installed,
|
|
382
|
+
latest: 'unknown',
|
|
383
|
+
status: 'version check unavailable',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const comparison = compareVersions(installed, latest);
|
|
388
|
+
if (comparison < 0) {
|
|
389
|
+
return {
|
|
390
|
+
installed,
|
|
391
|
+
latest,
|
|
392
|
+
status: `update available: ${latest}`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
installed,
|
|
398
|
+
latest,
|
|
399
|
+
status: 'up to date',
|
|
400
|
+
};
|
|
217
401
|
};
|
|
218
402
|
|
|
219
403
|
const ensureLatestConfiguratorVersion = async (npmCommand) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
404
|
+
const installed = normalizeVersion(PACKAGE_VERSION);
|
|
405
|
+
if (!installed) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const latest = await getLatestPackageVersion(
|
|
410
|
+
npmCommand,
|
|
411
|
+
CONFIGURATOR_PACKAGE_NAME,
|
|
412
|
+
);
|
|
413
|
+
if (!latest) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (compareVersions(installed, latest) >= 0) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await updateGlobalPackageToLatest(npmCommand, CONFIGURATOR_PACKAGE_NAME);
|
|
235
422
|
};
|
|
236
423
|
|
|
237
424
|
const App = () => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
425
|
+
const initialMainSnapshot = readConfig();
|
|
426
|
+
const initialCatalog = buildConfigFileCatalog(initialMainSnapshot);
|
|
427
|
+
const initialActiveFileId = initialCatalog[0]?.id || MAIN_CONFIG_FILE_ID;
|
|
428
|
+
|
|
429
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
430
|
+
const { stdout } = useStdout();
|
|
431
|
+
const defaultTerminalWidth = 100;
|
|
432
|
+
const defaultTerminalHeight = 24;
|
|
433
|
+
const [terminalSize, setTerminalSize] = useState({
|
|
434
|
+
width: stdout?.columns || defaultTerminalWidth,
|
|
435
|
+
height: stdout?.rows || defaultTerminalHeight,
|
|
436
|
+
});
|
|
437
|
+
const terminalWidth = terminalSize.width || defaultTerminalWidth;
|
|
438
|
+
const terminalHeight = terminalSize.height || defaultTerminalHeight;
|
|
439
|
+
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
const handleResize = () => {
|
|
442
|
+
const nextWidth = process.stdout?.columns || defaultTerminalWidth;
|
|
443
|
+
const nextHeight = process.stdout?.rows || defaultTerminalHeight;
|
|
444
|
+
setTerminalSize({
|
|
445
|
+
width: nextWidth,
|
|
446
|
+
height: nextHeight,
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
process.stdout?.on('resize', handleResize);
|
|
451
|
+
return () => {
|
|
452
|
+
process.stdout?.off('resize', handleResize);
|
|
453
|
+
};
|
|
454
|
+
}, []);
|
|
455
|
+
|
|
456
|
+
const [state, dispatch] = useReducer(
|
|
457
|
+
appStateReducer,
|
|
458
|
+
buildInitialAppState(
|
|
459
|
+
initialMainSnapshot,
|
|
460
|
+
initialCatalog,
|
|
461
|
+
initialActiveFileId,
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
const commandModeLockRef = useRef(false);
|
|
465
|
+
const commandInputRef = useRef('');
|
|
466
|
+
const setAppState = (key, valueOrUpdater) =>
|
|
467
|
+
dispatch({
|
|
468
|
+
type: APP_STATE_ACTION,
|
|
469
|
+
payload: { key, valueOrUpdater },
|
|
470
|
+
});
|
|
471
|
+
const setStateBatch = (updates) =>
|
|
472
|
+
dispatch({ type: APP_STATE_ACTION, payload: { updates } });
|
|
473
|
+
|
|
474
|
+
const setSnapshot = (valueOrUpdater) =>
|
|
475
|
+
setAppState('snapshot', valueOrUpdater);
|
|
476
|
+
const setSnapshotByFileId = (valueOrUpdater) =>
|
|
477
|
+
setAppState('snapshotByFileId', valueOrUpdater);
|
|
478
|
+
const setConfigFileCatalog = (valueOrUpdater) =>
|
|
479
|
+
setAppState('configFileCatalog', valueOrUpdater);
|
|
480
|
+
const setSelectedIndex = (valueOrUpdater) =>
|
|
481
|
+
setAppState('selectedIndex', valueOrUpdater);
|
|
482
|
+
const setScrollOffset = (valueOrUpdater) =>
|
|
483
|
+
setAppState('scrollOffset', valueOrUpdater);
|
|
484
|
+
const setEditMode = (valueOrUpdater) =>
|
|
485
|
+
setAppState('editMode', valueOrUpdater);
|
|
486
|
+
const setFileSwitchIndex = (valueOrUpdater) =>
|
|
487
|
+
setAppState('fileSwitchIndex', valueOrUpdater);
|
|
488
|
+
const setEditError = (valueOrUpdater) =>
|
|
489
|
+
setAppState('editError', valueOrUpdater);
|
|
490
|
+
const setCommandMode = (valueOrUpdater) => {
|
|
491
|
+
const nextValue =
|
|
492
|
+
typeof valueOrUpdater === 'function'
|
|
493
|
+
? Boolean(valueOrUpdater(isCommandMode))
|
|
494
|
+
: Boolean(valueOrUpdater);
|
|
495
|
+
commandModeLockRef.current = nextValue;
|
|
496
|
+
setAppState('isCommandMode', nextValue);
|
|
497
|
+
};
|
|
498
|
+
const setCommandInput = (valueOrUpdater) => {
|
|
499
|
+
const previous = String(commandInputRef.current || '');
|
|
500
|
+
const resolved =
|
|
501
|
+
typeof valueOrUpdater === 'function'
|
|
502
|
+
? valueOrUpdater(previous)
|
|
503
|
+
: valueOrUpdater;
|
|
504
|
+
const next = String(resolved ?? '');
|
|
505
|
+
commandInputRef.current = next;
|
|
506
|
+
setAppState('commandInput', next);
|
|
507
|
+
};
|
|
508
|
+
const setCommandMessage = (valueOrUpdater) =>
|
|
509
|
+
setAppState('commandMessage', valueOrUpdater);
|
|
510
|
+
const setShowHelp = (valueOrUpdater) =>
|
|
511
|
+
setAppState('showHelp', valueOrUpdater);
|
|
512
|
+
const setFilterQuery = (valueOrUpdater) =>
|
|
513
|
+
setAppState('filterQuery', valueOrUpdater);
|
|
514
|
+
const setIsFilterEditing = (valueOrUpdater) =>
|
|
515
|
+
setAppState('isFilterEditing', valueOrUpdater);
|
|
516
|
+
const setCodexVersion = (valueOrUpdater) =>
|
|
517
|
+
setAppState('codexVersion', valueOrUpdater);
|
|
518
|
+
const setCodexVersionStatus = (valueOrUpdater) =>
|
|
519
|
+
setAppState('codexVersionStatus', valueOrUpdater);
|
|
520
|
+
const {
|
|
521
|
+
snapshot,
|
|
522
|
+
snapshotByFileId,
|
|
523
|
+
configFileCatalog,
|
|
524
|
+
activeConfigFileId,
|
|
525
|
+
pathSegments,
|
|
526
|
+
selectedIndex,
|
|
527
|
+
selectionByPath,
|
|
528
|
+
scrollOffset,
|
|
529
|
+
editMode,
|
|
530
|
+
isFileSwitchMode,
|
|
531
|
+
fileSwitchIndex,
|
|
532
|
+
editError,
|
|
533
|
+
filterQuery,
|
|
534
|
+
isFilterEditing,
|
|
535
|
+
isCommandMode,
|
|
536
|
+
commandInput,
|
|
537
|
+
commandMessage,
|
|
538
|
+
showHelp,
|
|
539
|
+
codexVersion,
|
|
540
|
+
codexVersionStatus,
|
|
541
|
+
} = state;
|
|
542
|
+
commandInputRef.current = String(commandInput || '');
|
|
543
|
+
const { exit } = useApp();
|
|
544
|
+
const appMode = isFilterEditing
|
|
545
|
+
? APP_MODES.FILTER
|
|
546
|
+
: isCommandMode
|
|
547
|
+
? APP_MODES.COMMAND
|
|
548
|
+
: isFileSwitchMode
|
|
549
|
+
? APP_MODES.FILE_SWITCH
|
|
550
|
+
: editMode
|
|
551
|
+
? APP_MODES.EDIT
|
|
552
|
+
: APP_MODES.BROWSE;
|
|
553
|
+
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
let isCancelled = false;
|
|
556
|
+
|
|
557
|
+
const loadVersionStatus = async () => {
|
|
558
|
+
const check = await getCodexUpdateStatus();
|
|
559
|
+
|
|
560
|
+
if (isCancelled) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
setCodexVersion(check.installed);
|
|
565
|
+
setCodexVersionStatus(check.status);
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const ensureLatestConfigurator = async () => {
|
|
569
|
+
const commands = getVersionCommands();
|
|
570
|
+
await ensureLatestConfiguratorVersion(commands.npmCommand);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
loadVersionStatus();
|
|
574
|
+
ensureLatestConfigurator();
|
|
575
|
+
|
|
576
|
+
return () => {
|
|
577
|
+
isCancelled = true;
|
|
578
|
+
};
|
|
579
|
+
}, []);
|
|
580
|
+
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
const hasActiveFile = configFileCatalog.some(
|
|
583
|
+
(file) => file.id === activeConfigFileId,
|
|
584
|
+
);
|
|
585
|
+
if (hasActiveFile) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const fallbackFile = configFileCatalog[0];
|
|
590
|
+
if (!fallbackFile) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const fallbackSnapshot =
|
|
595
|
+
snapshotByFileId[fallbackFile.id] ||
|
|
596
|
+
(fallbackFile.kind === 'agent'
|
|
597
|
+
? ensureConfigFileExists(fallbackFile.path)
|
|
598
|
+
: readConfig(fallbackFile.path));
|
|
599
|
+
setStateBatch({
|
|
600
|
+
activeConfigFileId: fallbackFile.id,
|
|
601
|
+
snapshotByFileId: {
|
|
602
|
+
...snapshotByFileId,
|
|
603
|
+
...(snapshotByFileId[fallbackFile.id]
|
|
604
|
+
? {}
|
|
605
|
+
: {
|
|
606
|
+
[fallbackFile.id]: fallbackSnapshot,
|
|
607
|
+
}),
|
|
608
|
+
},
|
|
609
|
+
snapshot: fallbackSnapshot,
|
|
610
|
+
pathSegments: [],
|
|
611
|
+
selectedIndex: 0,
|
|
612
|
+
selectionByPath: {},
|
|
613
|
+
scrollOffset: 0,
|
|
614
|
+
});
|
|
615
|
+
}, [configFileCatalog, activeConfigFileId, snapshotByFileId]);
|
|
616
|
+
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
if (!isFileSwitchMode) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
setFileSwitchIndex((previous) => {
|
|
623
|
+
const maxIndex = Math.max(0, configFileCatalog.length - 1);
|
|
624
|
+
return clamp(previous, 0, maxIndex);
|
|
625
|
+
});
|
|
626
|
+
}, [isFileSwitchMode, configFileCatalog]);
|
|
627
|
+
|
|
628
|
+
const activeConfigFile =
|
|
629
|
+
configFileCatalog.find((file) => file.id === activeConfigFileId) ||
|
|
630
|
+
configFileCatalog[0];
|
|
631
|
+
const activeConfigFilePath = activeConfigFile?.path || snapshot.path;
|
|
632
|
+
const readActiveConfigSnapshot = () => {
|
|
633
|
+
const activeEntry = resolveActiveFileEntry();
|
|
634
|
+
const targetPath = activeEntry?.path || activeConfigFilePath;
|
|
635
|
+
|
|
636
|
+
if (!activeEntry || activeEntry.kind !== 'agent') {
|
|
637
|
+
return readConfig(targetPath);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return ensureConfigFileExists(targetPath);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const currentNode = getNodeAtPath(
|
|
644
|
+
snapshot.ok ? snapshot.data : {},
|
|
645
|
+
pathSegments,
|
|
646
|
+
);
|
|
647
|
+
const allRows = buildRows(currentNode, pathSegments);
|
|
648
|
+
const rows = filterRowsByQuery(allRows, filterQuery);
|
|
649
|
+
const safeSelected =
|
|
650
|
+
rows.length === 0 ? 0 : Math.min(selectedIndex, rows.length - 1);
|
|
651
|
+
const fileSwitchPanelExtraRows =
|
|
652
|
+
isInteractive && isFileSwitchMode
|
|
653
|
+
? computeFileSwitchPanelRows(configFileCatalog.length) +
|
|
654
|
+
FILE_SWITCH_LAYOUT_EXTRA_GAP_ROWS
|
|
655
|
+
: 0;
|
|
656
|
+
const listViewportHeight = computeListViewportRows({
|
|
657
|
+
terminalHeight,
|
|
658
|
+
terminalWidth,
|
|
659
|
+
activeConfigFile,
|
|
660
|
+
packageVersion: PACKAGE_VERSION,
|
|
661
|
+
codexVersion,
|
|
662
|
+
codexVersionStatus,
|
|
663
|
+
isInteractive,
|
|
664
|
+
isCommandMode,
|
|
665
|
+
extraChromeRows: fileSwitchPanelExtraRows,
|
|
666
|
+
});
|
|
667
|
+
const currentPathKey = `${activeConfigFileId}::${pathToKey(pathSegments)}`;
|
|
668
|
+
|
|
669
|
+
const getSavedIndex = (segments, fallback = 0) => {
|
|
670
|
+
const key = `${activeConfigFileId}::${pathToKey(segments)}`;
|
|
671
|
+
const maybe = selectionByPath[key];
|
|
672
|
+
|
|
673
|
+
if (Number.isInteger(maybe)) {
|
|
674
|
+
return maybe;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return fallback;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const adjustScrollForSelection = (
|
|
681
|
+
nextSelection,
|
|
682
|
+
nextViewportHeight,
|
|
683
|
+
totalRows,
|
|
684
|
+
) => {
|
|
685
|
+
const maxOffset = Math.max(0, totalRows - nextViewportHeight);
|
|
686
|
+
const minOffset = 0;
|
|
687
|
+
|
|
688
|
+
setScrollOffset((previous) => {
|
|
689
|
+
if (nextSelection < previous) {
|
|
690
|
+
return clamp(nextSelection, minOffset, maxOffset);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (nextSelection > previous + nextViewportHeight - 1) {
|
|
694
|
+
return clamp(
|
|
695
|
+
nextSelection - nextViewportHeight + 1,
|
|
696
|
+
minOffset,
|
|
697
|
+
maxOffset,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return clamp(previous, minOffset, maxOffset);
|
|
702
|
+
});
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const updateActiveSnapshot = (nextSnapshot) => {
|
|
706
|
+
setSnapshot(nextSnapshot);
|
|
707
|
+
setSnapshotByFileId((previous) => ({
|
|
708
|
+
...previous,
|
|
709
|
+
[activeConfigFileId]: nextSnapshot,
|
|
710
|
+
}));
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const ensureAgentConfigFile = (nextData, editedPath) => {
|
|
714
|
+
if (activeConfigFileId !== MAIN_CONFIG_FILE_ID) {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const isAgentConfigFilePath =
|
|
719
|
+
Array.isArray(editedPath) &&
|
|
720
|
+
editedPath.length === 3 &&
|
|
721
|
+
editedPath[0] === 'agents' &&
|
|
722
|
+
editedPath[2] === 'config_file';
|
|
723
|
+
if (!isAgentConfigFilePath) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const configFileValue = getNodeAtPath(nextData, editedPath);
|
|
728
|
+
if (typeof configFileValue !== 'string' || !configFileValue.trim()) {
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const normalizedTarget = resolveAgentConfigFilePath(
|
|
733
|
+
activeConfigFile?.path || snapshot.path,
|
|
734
|
+
configFileValue,
|
|
735
|
+
);
|
|
736
|
+
if (!normalizedTarget) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const ensureResult = ensureConfigFileExists(normalizedTarget);
|
|
741
|
+
if (!ensureResult.ok) {
|
|
742
|
+
setEditError(ensureResult.error);
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
setSnapshotByFileId((previous) => ({
|
|
747
|
+
...previous,
|
|
748
|
+
[`agent:${normalizedTarget}`]:
|
|
749
|
+
previous[`agent:${normalizedTarget}`] || ensureResult,
|
|
750
|
+
}));
|
|
751
|
+
return true;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
const resolveActiveFileEntry = () =>
|
|
755
|
+
configFileCatalog.find((file) => file.id === activeConfigFileId);
|
|
756
|
+
|
|
757
|
+
const refreshConfigFileCatalog = (mainSnapshot) => {
|
|
758
|
+
const nextCatalog = buildConfigFileCatalog(mainSnapshot);
|
|
759
|
+
setConfigFileCatalog(nextCatalog);
|
|
760
|
+
return nextCatalog;
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const switchConfigFile = (nextFileId) => {
|
|
764
|
+
const nextFile = configFileCatalog.find(
|
|
765
|
+
(file) => file.id === nextFileId,
|
|
766
|
+
);
|
|
767
|
+
if (!nextFile) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const nextSnapshot =
|
|
772
|
+
snapshotByFileId[nextFileId] ||
|
|
773
|
+
(nextFile.kind === 'agent'
|
|
774
|
+
? ensureConfigFileExists(nextFile.path)
|
|
775
|
+
: readConfig(nextFile.path));
|
|
776
|
+
if (!snapshotByFileId[nextFileId]) {
|
|
777
|
+
setSnapshotByFileId((previous) => ({
|
|
778
|
+
...previous,
|
|
779
|
+
[nextFileId]: nextSnapshot,
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (nextFileId === activeConfigFileId) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
setStateBatch({
|
|
788
|
+
activeConfigFileId: nextFileId,
|
|
789
|
+
snapshot: nextSnapshot,
|
|
790
|
+
pathSegments: [],
|
|
791
|
+
selectedIndex: 0,
|
|
792
|
+
scrollOffset: 0,
|
|
793
|
+
editMode: null,
|
|
794
|
+
isFileSwitchMode: false,
|
|
795
|
+
editError: '',
|
|
796
|
+
});
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const beginEditing = (target, targetPath) => {
|
|
800
|
+
const options =
|
|
801
|
+
getConfigOptions(
|
|
802
|
+
targetPath,
|
|
803
|
+
target.key,
|
|
804
|
+
target.value,
|
|
805
|
+
target.kind,
|
|
806
|
+
) || [];
|
|
807
|
+
if (options.length === 0) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
setEditError('');
|
|
812
|
+
setEditMode({
|
|
813
|
+
mode: 'select',
|
|
814
|
+
path: targetPath,
|
|
815
|
+
options,
|
|
816
|
+
selectedOptionIndex: clamp(
|
|
817
|
+
options.findIndex((option) => Object.is(option, target.value)),
|
|
818
|
+
0,
|
|
819
|
+
options.length - 1,
|
|
820
|
+
),
|
|
821
|
+
savedOptionIndex: null,
|
|
822
|
+
});
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const beginTextEditing = (target, targetPath) => {
|
|
826
|
+
setEditError('');
|
|
827
|
+
setEditMode({
|
|
828
|
+
mode: 'text',
|
|
829
|
+
path: targetPath,
|
|
830
|
+
draftValue: typeof target.value === 'string' ? target.value : '',
|
|
831
|
+
savedValue: null,
|
|
832
|
+
});
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const beginAddIdEditing = (targetPath, placeholder = 'id') => {
|
|
836
|
+
setEditError('');
|
|
837
|
+
setEditMode({
|
|
838
|
+
mode: 'add-id',
|
|
839
|
+
path: targetPath,
|
|
840
|
+
placeholder,
|
|
841
|
+
draftValue: '',
|
|
842
|
+
savedValue: null,
|
|
843
|
+
});
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const beginVariantEditing = (target, targetPath, variantMeta) => {
|
|
847
|
+
if (variantMeta?.kind !== 'scalar_object') {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const variantOptions = buildVariantSelectorOptions(variantMeta);
|
|
852
|
+
if (variantOptions.length === 0) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const currentVariantIndex = isObjectValue(target.value)
|
|
857
|
+
? variantOptions.findIndex(
|
|
858
|
+
(option) =>
|
|
859
|
+
option.kind === 'object' &&
|
|
860
|
+
objectMatchesVariant(target.value, option),
|
|
861
|
+
)
|
|
862
|
+
: variantOptions.findIndex(
|
|
863
|
+
(option) =>
|
|
864
|
+
option.kind === 'scalar' &&
|
|
865
|
+
Object.is(option.value, String(target.value)),
|
|
866
|
+
);
|
|
867
|
+
const selectedOptionIndex =
|
|
868
|
+
currentVariantIndex >= 0 ? currentVariantIndex : 0;
|
|
869
|
+
|
|
870
|
+
setEditError('');
|
|
871
|
+
setEditMode({
|
|
872
|
+
mode: 'variant-select',
|
|
873
|
+
key: target.key,
|
|
874
|
+
path: targetPath,
|
|
875
|
+
options: variantOptions.map((option) => option.label),
|
|
876
|
+
variantOptions,
|
|
877
|
+
selectedOptionIndex: clamp(
|
|
878
|
+
selectedOptionIndex,
|
|
879
|
+
0,
|
|
880
|
+
variantOptions.length - 1,
|
|
881
|
+
),
|
|
882
|
+
savedOptionIndex: null,
|
|
883
|
+
});
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
const openPathView = (nextPath, nextData) => {
|
|
887
|
+
const data =
|
|
888
|
+
typeof nextData === 'undefined'
|
|
889
|
+
? snapshot.ok
|
|
890
|
+
? snapshot.data
|
|
891
|
+
: {}
|
|
892
|
+
: nextData;
|
|
893
|
+
const nextNode = getNodeAtPath(data, nextPath);
|
|
894
|
+
const nextRows = buildRows(nextNode, nextPath);
|
|
895
|
+
const nextViewportHeight = computeListViewportRows({
|
|
896
|
+
terminalHeight,
|
|
897
|
+
terminalWidth,
|
|
898
|
+
activeConfigFile,
|
|
899
|
+
packageVersion: PACKAGE_VERSION,
|
|
900
|
+
codexVersion,
|
|
901
|
+
codexVersionStatus,
|
|
902
|
+
isInteractive,
|
|
903
|
+
isCommandMode,
|
|
904
|
+
extraChromeRows: fileSwitchPanelExtraRows,
|
|
905
|
+
});
|
|
906
|
+
const nextSavedIndex = getSavedIndex(nextPath, 0);
|
|
907
|
+
const nextSelected =
|
|
908
|
+
nextRows.length === 0
|
|
909
|
+
? 0
|
|
910
|
+
: clamp(nextSavedIndex, 0, nextRows.length - 1);
|
|
911
|
+
|
|
912
|
+
setStateBatch({
|
|
913
|
+
selectionByPath: {
|
|
914
|
+
...selectionByPath,
|
|
915
|
+
[currentPathKey]: safeSelected,
|
|
916
|
+
},
|
|
917
|
+
pathSegments: nextPath,
|
|
918
|
+
selectedIndex: nextSelected,
|
|
919
|
+
scrollOffset: clamp(
|
|
920
|
+
nextSelected,
|
|
921
|
+
0,
|
|
922
|
+
Math.max(0, nextRows.length - nextViewportHeight),
|
|
923
|
+
),
|
|
924
|
+
});
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const applyEdit = () => {
|
|
928
|
+
if (!editMode || editMode.mode !== 'select') {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const nextIndex = editMode.selectedOptionIndex;
|
|
933
|
+
const nextValue = editMode.options[nextIndex];
|
|
934
|
+
const nextData = setValueAtPath(
|
|
935
|
+
snapshot.ok ? snapshot.data : {},
|
|
936
|
+
editMode.path,
|
|
937
|
+
nextValue,
|
|
938
|
+
);
|
|
939
|
+
if (!ensureAgentConfigFile(nextData, editMode.path)) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const writeResult = writeConfig(nextData, snapshot.path);
|
|
944
|
+
|
|
945
|
+
if (!writeResult.ok) {
|
|
946
|
+
setEditError(writeResult.error);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const nextSnapshot = {
|
|
951
|
+
ok: true,
|
|
952
|
+
path: snapshot.path,
|
|
953
|
+
data: nextData,
|
|
954
|
+
};
|
|
955
|
+
updateActiveSnapshot(nextSnapshot);
|
|
956
|
+
|
|
957
|
+
if (activeConfigFileId === MAIN_CONFIG_FILE_ID) {
|
|
958
|
+
refreshConfigFileCatalog(nextSnapshot);
|
|
959
|
+
}
|
|
960
|
+
setEditMode(null);
|
|
961
|
+
setEditError('');
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const applyTextEdit = () => {
|
|
965
|
+
if (!editMode || editMode.mode !== 'text') {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const nextData = setValueAtPath(
|
|
970
|
+
snapshot.ok ? snapshot.data : {},
|
|
971
|
+
editMode.path,
|
|
972
|
+
editMode.draftValue,
|
|
973
|
+
);
|
|
974
|
+
if (!ensureAgentConfigFile(nextData, editMode.path)) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const writeResult = writeConfig(nextData, snapshot.path);
|
|
979
|
+
|
|
980
|
+
if (!writeResult.ok) {
|
|
981
|
+
setEditError(writeResult.error);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const nextSnapshot = {
|
|
986
|
+
ok: true,
|
|
987
|
+
path: snapshot.path,
|
|
988
|
+
data: nextData,
|
|
989
|
+
};
|
|
990
|
+
updateActiveSnapshot(nextSnapshot);
|
|
991
|
+
|
|
992
|
+
if (activeConfigFileId === MAIN_CONFIG_FILE_ID) {
|
|
993
|
+
refreshConfigFileCatalog(nextSnapshot);
|
|
994
|
+
}
|
|
995
|
+
setEditMode(null);
|
|
996
|
+
setEditError('');
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const applyAddId = () => {
|
|
1000
|
+
if (!editMode || editMode.mode !== 'add-id') {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const nextIdInput = String(editMode.draftValue || '').trim();
|
|
1005
|
+
const placeholder = getReferenceCustomIdPlaceholder(editMode.path);
|
|
1006
|
+
let nextId = nextIdInput;
|
|
1007
|
+
|
|
1008
|
+
if (placeholder === '<path>') {
|
|
1009
|
+
const normalizedPath = normalizeCustomPathId(nextIdInput);
|
|
1010
|
+
if (!normalizedPath.ok) {
|
|
1011
|
+
setEditError(normalizedPath.error);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
nextId = normalizedPath.value;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (!nextId) {
|
|
1019
|
+
setEditError('ID cannot be empty.');
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const nextPath = [...editMode.path, nextId];
|
|
1024
|
+
const data = snapshot.ok ? snapshot.data : {};
|
|
1025
|
+
const existingValue = getNodeAtPath(data, nextPath);
|
|
1026
|
+
|
|
1027
|
+
if (typeof existingValue !== 'undefined') {
|
|
1028
|
+
setEditError(`ID "${nextId}" already exists.`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
openPathView(nextPath, data);
|
|
1033
|
+
setEditMode(null);
|
|
1034
|
+
setEditError('');
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const applyVariantEdit = () => {
|
|
1038
|
+
if (!editMode || editMode.mode !== 'variant-select') {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const selectedVariant = Array.isArray(editMode.variantOptions)
|
|
1043
|
+
? editMode.variantOptions[editMode.selectedOptionIndex]
|
|
1044
|
+
: null;
|
|
1045
|
+
if (!selectedVariant) {
|
|
1046
|
+
setEditMode(null);
|
|
1047
|
+
setEditError('');
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const data = snapshot.ok ? snapshot.data : {};
|
|
1052
|
+
const currentValue = getNodeAtPath(data, editMode.path);
|
|
1053
|
+
const selectionResult = applyVariantSelection({
|
|
1054
|
+
currentValue,
|
|
1055
|
+
selectedVariant,
|
|
1056
|
+
resolveDefaultValue: (requiredKey) => {
|
|
1057
|
+
const requiredPath = [...editMode.path, requiredKey];
|
|
1058
|
+
const requiredOptions =
|
|
1059
|
+
getConfigOptions(
|
|
1060
|
+
requiredPath,
|
|
1061
|
+
requiredKey,
|
|
1062
|
+
undefined,
|
|
1063
|
+
'value',
|
|
1064
|
+
) || [];
|
|
1065
|
+
if (requiredOptions.length > 0) {
|
|
1066
|
+
return requiredOptions[0];
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (
|
|
1070
|
+
isStringReferenceType(
|
|
1071
|
+
getReferenceOptionForPath(requiredPath)?.type,
|
|
1072
|
+
)
|
|
1073
|
+
) {
|
|
1074
|
+
return '';
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return {};
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
const shouldPersistSelection =
|
|
1082
|
+
selectionResult.changed &&
|
|
1083
|
+
(!selectionResult.isObjectSelection ||
|
|
1084
|
+
selectionResult.isObjectVariantSwitch);
|
|
1085
|
+
let nextData = data;
|
|
1086
|
+
if (shouldPersistSelection) {
|
|
1087
|
+
nextData = setValueAtPath(
|
|
1088
|
+
data,
|
|
1089
|
+
editMode.path,
|
|
1090
|
+
selectionResult.nextValue,
|
|
1091
|
+
);
|
|
1092
|
+
const writeResult = writeConfig(nextData, snapshot.path);
|
|
1093
|
+
|
|
1094
|
+
if (!writeResult.ok) {
|
|
1095
|
+
setEditError(writeResult.error);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const nextSnapshot = {
|
|
1100
|
+
ok: true,
|
|
1101
|
+
path: snapshot.path,
|
|
1102
|
+
data: nextData,
|
|
1103
|
+
};
|
|
1104
|
+
updateActiveSnapshot(nextSnapshot);
|
|
1105
|
+
|
|
1106
|
+
if (activeConfigFileId === MAIN_CONFIG_FILE_ID) {
|
|
1107
|
+
refreshConfigFileCatalog(nextSnapshot);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (selectionResult.navigateToObject) {
|
|
1112
|
+
const nextPath = resolveObjectVariantNavigationPath({
|
|
1113
|
+
basePath: editMode.path,
|
|
1114
|
+
nextValue: selectionResult.nextValue,
|
|
1115
|
+
preferredKey:
|
|
1116
|
+
selectedVariant.kind === 'object' &&
|
|
1117
|
+
selectedVariant.requiredKeys.length === 1
|
|
1118
|
+
? selectedVariant.requiredKeys[0]
|
|
1119
|
+
: null,
|
|
1120
|
+
});
|
|
1121
|
+
openPathView(nextPath, nextData);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
setEditError('');
|
|
1125
|
+
setEditMode(null);
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
const reloadActiveConfig = () => {
|
|
1129
|
+
const nextSnapshot = readActiveConfigSnapshot();
|
|
1130
|
+
updateActiveSnapshot(nextSnapshot);
|
|
1131
|
+
setStateBatch({
|
|
1132
|
+
pathSegments: [],
|
|
1133
|
+
selectedIndex: 0,
|
|
1134
|
+
selectionByPath: {},
|
|
1135
|
+
scrollOffset: 0,
|
|
1136
|
+
editMode: null,
|
|
1137
|
+
editError: '',
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
if (activeConfigFileId === MAIN_CONFIG_FILE_ID) {
|
|
1141
|
+
refreshConfigFileCatalog(nextSnapshot);
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const beginFileSwitchMode = () => {
|
|
1146
|
+
if (
|
|
1147
|
+
!Array.isArray(configFileCatalog) ||
|
|
1148
|
+
configFileCatalog.length === 0
|
|
1149
|
+
) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (configFileCatalog.length === 1) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
setStateBatch({
|
|
1158
|
+
editError: '',
|
|
1159
|
+
isFileSwitchMode: true,
|
|
1160
|
+
fileSwitchIndex: Math.max(
|
|
1161
|
+
0,
|
|
1162
|
+
configFileCatalog.findIndex(
|
|
1163
|
+
(file) => file.id === activeConfigFileId,
|
|
1164
|
+
),
|
|
1165
|
+
),
|
|
1166
|
+
});
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
const applyFileSwitch = () => {
|
|
1170
|
+
if (!isFileSwitchMode) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const nextFile = configFileCatalog[fileSwitchIndex];
|
|
1175
|
+
if (!nextFile) {
|
|
1176
|
+
setStateBatch({ isFileSwitchMode: false, editError: '' });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
switchConfigFile(nextFile.id);
|
|
1181
|
+
setStateBatch({ isFileSwitchMode: false, editError: '' });
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const applyBooleanToggle = (target, targetPath) => {
|
|
1185
|
+
const nextValue = !target.value;
|
|
1186
|
+
const data = snapshot.ok ? snapshot.data : {};
|
|
1187
|
+
const nextData = setValueAtPath(data, targetPath, nextValue);
|
|
1188
|
+
|
|
1189
|
+
const writeResult = writeConfig(nextData, snapshot.path);
|
|
1190
|
+
|
|
1191
|
+
if (!writeResult.ok) {
|
|
1192
|
+
setEditError(writeResult.error);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const nextSnapshot = {
|
|
1197
|
+
ok: true,
|
|
1198
|
+
path: snapshot.path,
|
|
1199
|
+
data: nextData,
|
|
1200
|
+
};
|
|
1201
|
+
updateActiveSnapshot(nextSnapshot);
|
|
1202
|
+
|
|
1203
|
+
if (activeConfigFileId === MAIN_CONFIG_FILE_ID) {
|
|
1204
|
+
refreshConfigFileCatalog(nextSnapshot);
|
|
1205
|
+
}
|
|
1206
|
+
setEditError('');
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const unsetValueAtPath = (targetPath) => {
|
|
1210
|
+
const data = snapshot.ok ? snapshot.data : {};
|
|
1211
|
+
const hasConfiguredValue =
|
|
1212
|
+
typeof getNodeAtPath(data, targetPath) !== 'undefined';
|
|
1213
|
+
|
|
1214
|
+
if (!hasConfiguredValue) {
|
|
1215
|
+
setEditError('');
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const nextData = deleteValueAtPathPruningEmptyObjects(data, targetPath);
|
|
1220
|
+
const writeResult = writeConfig(nextData, snapshot.path);
|
|
1221
|
+
|
|
1222
|
+
if (!writeResult.ok) {
|
|
1223
|
+
setEditError(writeResult.error);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const nextSnapshot = {
|
|
1228
|
+
ok: true,
|
|
1229
|
+
path: snapshot.path,
|
|
1230
|
+
data: nextData,
|
|
1231
|
+
};
|
|
1232
|
+
updateActiveSnapshot(nextSnapshot);
|
|
1233
|
+
|
|
1234
|
+
if (activeConfigFileId === MAIN_CONFIG_FILE_ID) {
|
|
1235
|
+
refreshConfigFileCatalog(nextSnapshot);
|
|
1236
|
+
}
|
|
1237
|
+
setEditError('');
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
useInput(
|
|
1241
|
+
(input, key) => {
|
|
1242
|
+
const commandHandled = executeInputCommand({
|
|
1243
|
+
input,
|
|
1244
|
+
key,
|
|
1245
|
+
context: {
|
|
1246
|
+
appMode,
|
|
1247
|
+
isFilterEditing,
|
|
1248
|
+
isFileSwitchMode,
|
|
1249
|
+
isCommandMode,
|
|
1250
|
+
isCommandModeLocked: commandModeLockRef.current,
|
|
1251
|
+
activeConfigFileId,
|
|
1252
|
+
rows,
|
|
1253
|
+
safeSelected,
|
|
1254
|
+
editMode,
|
|
1255
|
+
listViewportHeight,
|
|
1256
|
+
pathSegments,
|
|
1257
|
+
snapshot,
|
|
1258
|
+
currentNode,
|
|
1259
|
+
terminalHeight,
|
|
1260
|
+
selectionByPath,
|
|
1261
|
+
configFileCatalog,
|
|
1262
|
+
fileSwitchIndex,
|
|
1263
|
+
currentPathKey,
|
|
1264
|
+
clamp,
|
|
1265
|
+
setEditMode,
|
|
1266
|
+
setFileSwitchIndex,
|
|
1267
|
+
setIsFilterEditing,
|
|
1268
|
+
setFilterQuery,
|
|
1269
|
+
setShowHelp,
|
|
1270
|
+
setStateBatch,
|
|
1271
|
+
setSelectedIndex,
|
|
1272
|
+
setCommandMode,
|
|
1273
|
+
setCommandInput,
|
|
1274
|
+
getCommandInput: () => commandInputRef.current,
|
|
1275
|
+
setCommandMessage,
|
|
1276
|
+
setEditError,
|
|
1277
|
+
beginAddIdEditing,
|
|
1278
|
+
beginTextEditing,
|
|
1279
|
+
beginVariantEditing,
|
|
1280
|
+
beginEditing,
|
|
1281
|
+
beginFileSwitchMode,
|
|
1282
|
+
applyFileSwitch,
|
|
1283
|
+
applyTextEdit,
|
|
1284
|
+
applyAddId,
|
|
1285
|
+
applyVariantEdit,
|
|
1286
|
+
applyEdit,
|
|
1287
|
+
applyBooleanToggle,
|
|
1288
|
+
unsetValueAtPath,
|
|
1289
|
+
openPathView,
|
|
1290
|
+
reloadActiveConfig,
|
|
1291
|
+
getConfigOptions: getConfigOptions,
|
|
1292
|
+
getConfigVariantMeta,
|
|
1293
|
+
getNodeAtPath,
|
|
1294
|
+
buildRows,
|
|
1295
|
+
isStringField,
|
|
1296
|
+
isCustomIdTableRow,
|
|
1297
|
+
resolveMixedVariantBackNavigationPath,
|
|
1298
|
+
adjustScrollForSelection,
|
|
1299
|
+
getSavedIndex,
|
|
1300
|
+
readActiveConfigSnapshot,
|
|
1301
|
+
refreshConfigFileCatalog,
|
|
1302
|
+
exit,
|
|
1303
|
+
},
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
if (commandHandled) {
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
{ isActive: isInteractive },
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
useEffect(() => {
|
|
1314
|
+
const maxOffset = Math.max(0, rows.length - listViewportHeight);
|
|
1315
|
+
setScrollOffset((previous) => clamp(previous, 0, maxOffset));
|
|
1316
|
+
}, [rows.length, listViewportHeight]);
|
|
1317
|
+
|
|
1318
|
+
const renderFileSwitchPanel = () => {
|
|
1319
|
+
if (!isFileSwitchMode || configFileCatalog.length === 0) {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const totalEntries = configFileCatalog.length;
|
|
1324
|
+
const visibleEntries = Math.min(
|
|
1325
|
+
FILE_SWITCH_MAX_VISIBLE_ENTRIES,
|
|
1326
|
+
totalEntries,
|
|
1327
|
+
);
|
|
1328
|
+
const maxStartIndex = Math.max(0, totalEntries - visibleEntries);
|
|
1329
|
+
const startIndex = clamp(
|
|
1330
|
+
fileSwitchIndex - Math.floor(visibleEntries / 2),
|
|
1331
|
+
0,
|
|
1332
|
+
maxStartIndex,
|
|
1333
|
+
);
|
|
1334
|
+
const endIndex = Math.min(totalEntries, startIndex + visibleEntries);
|
|
1335
|
+
const visibleFiles = configFileCatalog.slice(startIndex, endIndex);
|
|
1336
|
+
const hasOverflow = totalEntries > visibleEntries;
|
|
1337
|
+
|
|
1338
|
+
return React.createElement(
|
|
1339
|
+
Box,
|
|
1340
|
+
{
|
|
1341
|
+
borderStyle: 'round',
|
|
1342
|
+
borderColor: 'cyan',
|
|
1343
|
+
paddingX: 1,
|
|
1344
|
+
flexDirection: 'column',
|
|
1345
|
+
},
|
|
1346
|
+
React.createElement(
|
|
1347
|
+
Text,
|
|
1348
|
+
{ bold: true, color: 'cyan' },
|
|
1349
|
+
'File Switch',
|
|
1350
|
+
),
|
|
1351
|
+
...visibleFiles.map((file, offsetIndex) => {
|
|
1352
|
+
const index = startIndex + offsetIndex;
|
|
1353
|
+
const isSelected = index === fileSwitchIndex;
|
|
1354
|
+
const isActiveFile = file.id === activeConfigFileId;
|
|
1355
|
+
const fileLabel = `${file.label} (${file.kind === 'main' ? 'main' : 'agent'})`;
|
|
1356
|
+
return React.createElement(
|
|
1357
|
+
Text,
|
|
1358
|
+
{
|
|
1359
|
+
key: file.id,
|
|
1360
|
+
color: isSelected
|
|
1361
|
+
? 'yellow'
|
|
1362
|
+
: isActiveFile
|
|
1363
|
+
? 'green'
|
|
1364
|
+
: 'gray',
|
|
1365
|
+
bold: isSelected,
|
|
1366
|
+
wrap: 'truncate-end',
|
|
1367
|
+
},
|
|
1368
|
+
`${isSelected ? '› ' : ' '}${fileLabel}${isActiveFile ? ' [active]' : ''}`,
|
|
1369
|
+
);
|
|
1370
|
+
}),
|
|
1371
|
+
hasOverflow
|
|
1372
|
+
? React.createElement(
|
|
1373
|
+
Text,
|
|
1374
|
+
{
|
|
1375
|
+
color: 'gray',
|
|
1376
|
+
wrap: 'truncate-end',
|
|
1377
|
+
key: 'file-switch-window',
|
|
1378
|
+
},
|
|
1379
|
+
`Showing ${startIndex + 1}-${endIndex} of ${totalEntries}`,
|
|
1380
|
+
)
|
|
1381
|
+
: null,
|
|
1382
|
+
);
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
const commandModeHint = getModeHint({
|
|
1386
|
+
appMode,
|
|
1387
|
+
isCommandMode,
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
if (!isInteractive) {
|
|
1391
|
+
return React.createElement(
|
|
1392
|
+
Box,
|
|
1393
|
+
{ flexDirection: 'column', padding: 1 },
|
|
1394
|
+
React.createElement(Header, {
|
|
1395
|
+
packageVersion: PACKAGE_VERSION,
|
|
1396
|
+
terminalWidth,
|
|
1397
|
+
}),
|
|
1398
|
+
React.createElement(ConfigNavigator, {
|
|
1399
|
+
snapshot,
|
|
1400
|
+
pathSegments,
|
|
1401
|
+
selectedIndex: 0,
|
|
1402
|
+
terminalWidth,
|
|
1403
|
+
listViewportHeight,
|
|
1404
|
+
scrollOffset: 0,
|
|
1405
|
+
editMode: null,
|
|
1406
|
+
editError: editError,
|
|
1407
|
+
filterQuery,
|
|
1408
|
+
isFilterEditing,
|
|
1409
|
+
activeConfigFile: activeConfigFile,
|
|
1410
|
+
}),
|
|
1411
|
+
React.createElement(
|
|
1412
|
+
Text,
|
|
1413
|
+
{ color: 'yellow' },
|
|
1414
|
+
'Non-interactive mode: input is disabled.',
|
|
1415
|
+
),
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
return React.createElement(
|
|
1420
|
+
LayoutShell,
|
|
1421
|
+
null,
|
|
1422
|
+
React.createElement(Header, {
|
|
1423
|
+
packageVersion: PACKAGE_VERSION,
|
|
1424
|
+
terminalWidth,
|
|
1425
|
+
}),
|
|
1426
|
+
React.createElement(ConfigNavigator, {
|
|
1427
|
+
snapshot,
|
|
1428
|
+
pathSegments,
|
|
1429
|
+
selectedIndex: safeSelected,
|
|
1430
|
+
terminalWidth,
|
|
1431
|
+
listViewportHeight,
|
|
1432
|
+
scrollOffset,
|
|
1433
|
+
editMode,
|
|
1434
|
+
editError,
|
|
1435
|
+
filterQuery,
|
|
1436
|
+
isFilterEditing,
|
|
1437
|
+
activeConfigFile: activeConfigFile,
|
|
1438
|
+
}),
|
|
1439
|
+
renderFileSwitchPanel(),
|
|
1440
|
+
React.createElement(CommandBar, {
|
|
1441
|
+
appMode,
|
|
1442
|
+
isCommandMode,
|
|
1443
|
+
commandInput,
|
|
1444
|
+
commandMessage,
|
|
1445
|
+
modeHint: commandModeHint,
|
|
1446
|
+
}),
|
|
1447
|
+
React.createElement(StatusLine, {
|
|
1448
|
+
codexVersion,
|
|
1449
|
+
codexVersionStatus,
|
|
1450
|
+
activeConfigFile,
|
|
1451
|
+
appMode,
|
|
1452
|
+
}),
|
|
1453
|
+
showHelp
|
|
1454
|
+
? React.createElement(
|
|
1455
|
+
Box,
|
|
1456
|
+
{
|
|
1457
|
+
position: 'absolute',
|
|
1458
|
+
bottom: 3,
|
|
1459
|
+
left: 0,
|
|
1460
|
+
right: 0,
|
|
1461
|
+
marginTop: 0,
|
|
1462
|
+
zIndex: 1,
|
|
1463
|
+
},
|
|
1464
|
+
React.createElement(HelpBubble, { appMode }),
|
|
1465
|
+
)
|
|
1466
|
+
: null,
|
|
1467
|
+
);
|
|
910
1468
|
};
|
|
911
1469
|
|
|
1470
|
+
activateTerminalMode();
|
|
912
1471
|
render(React.createElement(App));
|