codex-configurator 0.2.4 → 0.2.6

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.
@@ -1,476 +1,679 @@
1
1
  import React from 'react';
2
2
  import { Text, Box } from 'ink';
3
3
  import {
4
- getConfigHelp,
5
- getConfigOptions,
6
- getConfigOptionExplanation,
7
- getConfigDefaultOption,
4
+ getConfigHelp,
5
+ getConfigOptions,
6
+ getConfigOptionExplanation,
7
+ getConfigDefaultOption,
8
+ getConfigVariantMeta,
8
9
  } from '../configHelp.js';
9
10
  import { computePaneWidths, clamp } from '../layout.js';
10
11
  import { getNodeAtPath, buildRows } from '../configParser.js';
11
12
  import { filterRowsByQuery } from '../fuzzySearch.js';
13
+ import {
14
+ buildVariantSelectorOptions,
15
+ isObjectValue,
16
+ objectMatchesVariant,
17
+ } from '../variantPresets.js';
12
18
 
13
19
  const MenuItem = ({ isSelected, isDimmed, isDeprecated, label }) =>
14
- React.createElement(
15
- Text,
16
- {
17
- bold: isSelected,
18
- color: isSelected ? 'yellow' : isDimmed ? 'gray' : 'white',
19
- dimColor: !isSelected && isDimmed,
20
- },
21
- label,
22
- isDeprecated ? React.createElement(Text, { color: 'yellow' }, ' [!]') : null
23
- );
20
+ React.createElement(
21
+ Text,
22
+ {
23
+ bold: isSelected,
24
+ color: isSelected ? 'yellow' : isDimmed ? 'gray' : 'white',
25
+ dimColor: !isSelected && isDimmed,
26
+ wrap: 'truncate-end',
27
+ },
28
+ label,
29
+ isDeprecated
30
+ ? React.createElement(Text, { color: 'yellow' }, ' [!]')
31
+ : null,
32
+ );
24
33
 
25
34
  const formatArrayItem = (value) => {
26
- if (typeof value === 'string') {
27
- return JSON.stringify(value);
28
- }
29
-
30
- if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
31
- return String(value);
32
- }
33
-
34
- if (Array.isArray(value)) {
35
- return `[${value.length} item(s)]`;
36
- }
37
-
38
- if (Object.prototype.toString.call(value) === '[object Object]') {
39
- const keys = Object.keys(value);
40
- if (keys.length === 0) {
41
- return '{}';
42
- }
43
-
44
- return `{${keys.join(', ')}}`;
45
- }
46
-
47
- return String(value);
35
+ if (typeof value === 'string') {
36
+ return JSON.stringify(value);
37
+ }
38
+
39
+ if (
40
+ typeof value === 'number' ||
41
+ typeof value === 'boolean' ||
42
+ value === null
43
+ ) {
44
+ return String(value);
45
+ }
46
+
47
+ if (Array.isArray(value)) {
48
+ return `[${value.length} item(s)]`;
49
+ }
50
+
51
+ if (Object.prototype.toString.call(value) === '[object Object]') {
52
+ const keys = Object.keys(value);
53
+ if (keys.length === 0) {
54
+ return '{}';
55
+ }
56
+
57
+ return `{${keys.join(', ')}}`;
58
+ }
59
+
60
+ return String(value);
48
61
  };
49
62
 
50
63
  const formatOptionValue = (value) => {
51
- if (typeof value === 'string') {
52
- return value;
53
- }
64
+ if (typeof value === 'string') {
65
+ return value;
66
+ }
54
67
 
55
- if (typeof value === 'boolean' || value === null) {
56
- return String(value);
57
- }
68
+ if (typeof value === 'boolean' || value === null) {
69
+ return String(value);
70
+ }
58
71
 
59
- return String(value);
72
+ return String(value);
60
73
  };
61
74
 
62
75
  const truncate = (text, maxLength) =>
63
- text.length <= maxLength ? text : `${text.slice(0, Math.max(0, maxLength - 1))}…`;
76
+ text.length <= maxLength
77
+ ? text
78
+ : `${text.slice(0, Math.max(0, maxLength - 1))}…`;
79
+ const LEFT_PANE_FRAME_ROWS = 4;
64
80
 
65
81
  const isBooleanOnlyOptions = (options) =>
66
- Array.isArray(options) &&
67
- options.length === 2 &&
68
- options.every((option) => typeof option === 'boolean') &&
69
- options.includes(false) &&
70
- options.includes(true);
82
+ Array.isArray(options) &&
83
+ options.length === 2 &&
84
+ options.every((option) => typeof option === 'boolean') &&
85
+ options.includes(false) &&
86
+ options.includes(true);
71
87
 
72
88
  const renderArrayDetails = (rows) => {
73
- const items = rows.slice(0, 5).map((item, index) =>
74
- React.createElement(Text, { key: `array-item-${index}` }, ` ${index + 1}. ${formatArrayItem(item)}`)
75
- );
76
- const overflow = rows.length - items.length;
77
-
78
- return React.createElement(
79
- React.Fragment,
80
- null,
81
- ...items,
82
- overflow > 0 ? React.createElement(Text, { key: 'array-more' }, ` … and ${overflow} more`) : null
83
- );
89
+ const items = rows.slice(0, 5).map((item, index) =>
90
+ React.createElement(
91
+ Text,
92
+ {
93
+ key: `array-item-${index}`,
94
+ wrap: 'truncate-end',
95
+ },
96
+ ` ${index + 1}. ${formatArrayItem(item)}`,
97
+ ),
98
+ );
99
+ const overflow = rows.length - items.length;
100
+
101
+ return React.createElement(
102
+ React.Fragment,
103
+ null,
104
+ ...items,
105
+ overflow > 0
106
+ ? React.createElement(
107
+ Text,
108
+ { key: 'array-more', wrap: 'truncate-end' },
109
+ ` … and ${overflow} more`,
110
+ )
111
+ : null,
112
+ );
84
113
  };
85
114
 
86
115
  const renderTextEditor = (draftValue) =>
87
- React.createElement(
88
- React.Fragment,
89
- null,
90
- React.createElement(Text, { color: 'white' }, `> ${draftValue}`),
91
- React.createElement(Text, { color: 'gray' }, 'Type to edit • Enter: save • Esc: cancel')
92
- );
116
+ React.createElement(
117
+ React.Fragment,
118
+ null,
119
+ React.createElement(
120
+ Text,
121
+ { color: 'white', wrap: 'truncate-end' },
122
+ `> ${draftValue}`,
123
+ ),
124
+ React.createElement(
125
+ Text,
126
+ { color: 'gray', wrap: 'truncate-end' },
127
+ 'Type to edit • Enter: save • Esc: cancel',
128
+ ),
129
+ );
93
130
 
94
131
  const renderIdEditor = (placeholder, draftValue) =>
95
- React.createElement(
96
- React.Fragment,
97
- null,
98
- React.createElement(Text, { color: 'white' }, `${placeholder || 'id'}: ${draftValue}`),
99
- React.createElement(Text, { color: 'gray' }, 'Type id • Enter: create • Esc: cancel')
100
- );
132
+ React.createElement(
133
+ React.Fragment,
134
+ null,
135
+ React.createElement(
136
+ Text,
137
+ { color: 'white', wrap: 'truncate-end' },
138
+ `${placeholder || 'id'}: ${draftValue}`,
139
+ ),
140
+ React.createElement(
141
+ Text,
142
+ { color: 'gray', wrap: 'truncate-end' },
143
+ 'Type id • Enter: create • Esc: cancel',
144
+ ),
145
+ );
101
146
 
102
147
  const formatBreadcrumbSegment = (segment) => {
103
- const value = String(segment);
104
- return /^\d+$/.test(value) ? `[${value}]` : value;
148
+ const value = String(segment);
149
+ return /^\d+$/.test(value) ? `[${value}]` : value;
105
150
  };
106
151
 
107
152
  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
- }
153
+ if (!Array.isArray(pathSegments) || pathSegments.length === 0) {
154
+ return '';
155
+ }
137
156
 
138
- return 'This section groups related settings.';
157
+ return pathSegments.map(formatBreadcrumbSegment).join(' > ');
139
158
  };
140
159
 
141
160
  const renderEditableOptions = (
142
- options,
143
- selectedOptionIndex,
144
- defaultOptionIndex,
145
- optionPathSegments,
146
- rowKey,
147
- savedOptionIndex = null,
148
- showCursor = false
161
+ options,
162
+ selectedOptionIndex,
163
+ defaultOptionIndex,
164
+ rowKey,
165
+ savedOptionIndex = null,
166
+ showCursor = false,
149
167
  ) => {
150
- const optionRows = options.map((option, optionIndex) => {
151
- const optionValueText = formatOptionValue(option);
152
- const prefix = showCursor && optionIndex === selectedOptionIndex ? '▶ ' : ' ';
153
- const valueText = `${prefix}${optionValueText}`;
154
- const explanation = getConfigOptionExplanation(optionPathSegments, rowKey, option);
155
- const isDefault = optionIndex === defaultOptionIndex;
156
- const highlightDefault = selectedOptionIndex < 0 && isDefault;
157
- return { optionIndex, valueText, explanation, optionValueText, isDefault, highlightDefault };
158
- });
159
-
160
- const valueWidth = optionRows.reduce((max, item) => Math.max(max, item.valueText.length), 0);
161
-
162
- return React.createElement(
163
- React.Fragment,
164
- null,
165
- ...optionRows.map(
166
- ({ optionIndex, valueText, explanation, optionValueText, isDefault, highlightDefault }) =>
167
- React.createElement(
168
- Box,
169
- {
170
- key: `option-${rowKey}-${optionIndex}`,
171
- flexDirection: 'column',
172
- },
173
- React.createElement(
174
- Box,
175
- {
176
- flexDirection: 'row',
177
- },
178
- React.createElement(
179
- Text,
180
- {
181
- color: optionIndex === selectedOptionIndex
182
- ? 'yellow'
183
- : highlightDefault
184
- ? 'whiteBright'
185
- : 'white',
186
- bold: optionIndex === selectedOptionIndex || highlightDefault,
187
- },
188
- `${valueText.padEnd(valueWidth, ' ')}`
189
- ),
190
- explanation
191
- ? React.createElement(Text, { color: 'gray' }, ` — ${truncate(explanation, 100)}`)
192
- : null,
193
- isDefault
194
- ? React.createElement(Text, { color: 'cyan' }, ' [default]')
195
- : null
196
- ),
197
- savedOptionIndex === optionIndex
198
- ? React.createElement(Text, { color: 'green' }, ` Saved: ${optionValueText}`)
199
- : null
200
- )
201
- )
202
- );
168
+ const optionRows = options.map((option, optionIndex) => {
169
+ const optionValueText = formatOptionValue(option);
170
+ const prefix =
171
+ showCursor && optionIndex === selectedOptionIndex ? '▶ ' : ' ';
172
+ const valueText = `${prefix}${optionValueText}`;
173
+ const isDefault = optionIndex === defaultOptionIndex;
174
+ const highlightDefault = selectedOptionIndex < 0 && isDefault;
175
+ return {
176
+ optionIndex,
177
+ valueText,
178
+ optionValueText,
179
+ isDefault,
180
+ highlightDefault,
181
+ };
182
+ });
183
+
184
+ const valueWidth = optionRows.reduce(
185
+ (max, item) => Math.max(max, item.valueText.length),
186
+ 0,
187
+ );
188
+
189
+ return React.createElement(
190
+ React.Fragment,
191
+ null,
192
+ ...optionRows.map(
193
+ ({
194
+ optionIndex,
195
+ valueText,
196
+ optionValueText,
197
+ isDefault,
198
+ highlightDefault,
199
+ }) =>
200
+ React.createElement(
201
+ Box,
202
+ {
203
+ key: `option-${rowKey}-${optionIndex}`,
204
+ flexDirection: 'column',
205
+ },
206
+ React.createElement(
207
+ Box,
208
+ {
209
+ flexDirection: 'row',
210
+ },
211
+ React.createElement(
212
+ Text,
213
+ {
214
+ color:
215
+ optionIndex === selectedOptionIndex
216
+ ? 'yellow'
217
+ : highlightDefault
218
+ ? 'whiteBright'
219
+ : 'white',
220
+ bold:
221
+ optionIndex === selectedOptionIndex ||
222
+ highlightDefault,
223
+ wrap: 'truncate-end',
224
+ },
225
+ `${valueText.padEnd(valueWidth, ' ')}`,
226
+ ),
227
+ isDefault
228
+ ? React.createElement(
229
+ Text,
230
+ { color: 'cyan', wrap: 'truncate-end' },
231
+ ' [default]',
232
+ )
233
+ : null,
234
+ ),
235
+ savedOptionIndex === optionIndex
236
+ ? React.createElement(
237
+ Text,
238
+ { color: 'green', wrap: 'truncate-end' },
239
+ ` Saved: ${optionValueText}`,
240
+ )
241
+ : null,
242
+ ),
243
+ ),
244
+ );
203
245
  };
204
246
 
205
247
  const formatConfigHelp = (pathSegments, row) => {
206
- if (row?.kind === 'action' && row?.action === 'add-custom-id') {
207
- return [
208
- {
209
- text: `Create a new custom "${row.placeholder || 'id'}" entry in this section.`,
210
- color: 'white',
211
- bold: false,
212
- showWarningIcon: false,
213
- },
214
- {
215
- text: 'Press Enter to type the id, then Enter again to save.',
216
- color: 'gray',
217
- bold: false,
218
- showWarningIcon: false,
219
- },
220
- ];
221
- }
222
-
223
- const info = getConfigHelp(pathSegments, row.key);
224
- const isSectionRow = row.kind === 'table' || row.kind === 'tableArray';
225
- const defaultCollectionText =
226
- isSectionRow
227
- ? formatSectionSummary(row)
228
- : row.kind === 'array'
229
- ? `This is a list with ${row.value.length} entries.`
230
- : 'This setting affects Codex behavior.';
231
- const short = info?.short || defaultCollectionText;
232
- const usage = isSectionRow ? null : info?.usage;
233
- const isWarning = Boolean(info?.deprecation);
234
- const lines = [{ text: short, color: 'white', bold: false, showWarningIcon: false }];
235
-
236
- if (usage) {
237
- lines.push({
238
- text: isWarning ? usage.replace(/^\[!\]\s*/, '') : usage,
239
- color: 'gray',
240
- bold: false,
241
- showWarningIcon: isWarning,
242
- });
243
- }
244
-
245
- if (row?.key === 'approval_policy' && row?.value === 'on-failure') {
246
- lines.push({
247
- text: 'approval_policy = "on-failure" is deprecated; use "untrusted", "on-request", or "never".',
248
- color: 'gray',
249
- bold: false,
250
- showWarningIcon: true,
251
- });
252
- }
253
-
254
- if (pathSegments?.[pathSegments.length - 1] === 'features' && row?.isDocumented === false) {
255
- lines.push({
256
- text: 'This key is configured in your file but is not in the official feature list.',
257
- color: 'gray',
258
- bold: false,
259
- showWarningIcon: true,
260
- });
261
- }
262
-
263
- if (pathSegments.length === 0 && row?.key === 'model') {
264
- lines.push({
265
- text: 'Model values shown here are curated presets and not a full upstream model catalog.',
266
- color: 'gray',
267
- bold: false,
268
- showWarningIcon: false,
269
- });
270
- }
271
-
272
- return lines;
248
+ if (row?.kind === 'action' && row?.action === 'add-custom-id') {
249
+ return [
250
+ {
251
+ text: `Create a new custom "${row.placeholder || 'id'}" entry in this section.`,
252
+ color: 'white',
253
+ bold: false,
254
+ showWarningIcon: false,
255
+ },
256
+ {
257
+ text: 'Press Enter to type the id, then Enter again to save.',
258
+ color: 'gray',
259
+ bold: false,
260
+ showWarningIcon: false,
261
+ },
262
+ ];
263
+ }
264
+
265
+ if (!row) {
266
+ return [];
267
+ }
268
+
269
+ const info = getConfigHelp(pathSegments, row.key);
270
+ const isSectionRow = row.kind === 'table' || row.kind === 'tableArray';
271
+ const short = String(info?.short || '').trim() || 'no description';
272
+ const usage = isSectionRow ? null : info?.usage;
273
+ const lines = [
274
+ { text: short, color: 'white', bold: false, showWarningIcon: false },
275
+ ];
276
+
277
+ if (usage) {
278
+ lines.push({
279
+ text: usage,
280
+ color: 'gray',
281
+ bold: false,
282
+ showWarningIcon: false,
283
+ });
284
+ }
285
+
286
+ if (
287
+ pathSegments?.[pathSegments.length - 1] === 'features' &&
288
+ row?.isDocumented === false
289
+ ) {
290
+ lines.push({
291
+ text: 'This key is configured in your file but is not in the official feature list.',
292
+ color: 'gray',
293
+ bold: false,
294
+ showWarningIcon: true,
295
+ });
296
+ }
297
+
298
+ if (pathSegments.length === 0 && row?.key === 'model') {
299
+ lines.push({
300
+ text: 'Model values shown here are curated presets and not a full upstream model catalog.',
301
+ color: 'gray',
302
+ bold: false,
303
+ showWarningIcon: false,
304
+ });
305
+ }
306
+
307
+ return lines;
273
308
  };
274
309
 
275
310
  export const ConfigNavigator = ({
276
- snapshot,
277
- pathSegments,
278
- selectedIndex,
279
- scrollOffset,
280
- terminalWidth,
281
- terminalHeight,
282
- editMode,
283
- editError,
284
- filterQuery = '',
285
- isFilterEditing = false,
311
+ snapshot,
312
+ pathSegments,
313
+ selectedIndex,
314
+ scrollOffset,
315
+ terminalWidth,
316
+ listViewportHeight = 20,
317
+ editMode,
318
+ editError,
319
+ filterQuery = '',
320
+ isFilterEditing = false,
286
321
  }) => {
287
- if (!snapshot.ok) {
288
- return React.createElement(
289
- Box,
290
- { flexDirection: 'column', marginTop: 1 },
291
- React.createElement(Text, { color: 'red', bold: true }, 'Unable to read/parse config'),
292
- React.createElement(Text, { color: 'gray' }, snapshot.error),
293
- React.createElement(Text, { color: 'gray' }, `Path: ${snapshot.path}`)
294
- );
295
- }
296
-
297
- const currentNode = getNodeAtPath(snapshot.data, pathSegments);
298
- const allRows = buildRows(currentNode, pathSegments);
299
- const rows = filterRowsByQuery(allRows, filterQuery);
300
- const breadcrumbs = formatBreadcrumbs(pathSegments);
301
- const selected = rows.length === 0 ? 0 : Math.min(selectedIndex, rows.length - 1);
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)) : '';
305
- const terminalListHeight = Math.max(4, (terminalHeight || 24) - 14);
306
- const viewportHeight = Math.max(4, Math.min(rows.length, Math.min(20, terminalListHeight)));
307
- const viewportStart = clamp(scrollOffset, 0, Math.max(0, rows.length - viewportHeight));
308
- const visibleRows = rows.slice(viewportStart, viewportStart + viewportHeight);
309
- const canScrollUp = viewportStart > 0;
310
- const canScrollDown = viewportStart + viewportHeight < rows.length;
311
- const selectedRow = rows[selected] || null;
312
- const selectedPath =
313
- selectedRow && selectedRow.pathSegment != null
314
- ? [...pathSegments, selectedRow.pathSegment]
315
- : pathSegments;
316
- const readOnlyOptions =
317
- selectedRow && selectedRow.kind === 'value'
318
- ? getConfigOptions(selectedPath, selectedRow.key, selectedRow.value, selectedRow.kind) || []
319
- : [];
320
- const readOnlyOptionIndex = readOnlyOptions.findIndex((option) => Object.is(option, selectedRow?.value));
321
- const readOnlyDefaultOption = selectedRow
322
- ? getConfigDefaultOption(selectedPath, selectedRow.key, selectedRow.kind, readOnlyOptions)
323
- : null;
324
- const readOnlyDefaultOptionIndex = readOnlyOptions.findIndex((option) =>
325
- Object.is(option, readOnlyDefaultOption)
326
- );
327
- const shouldShowReadOnlyOptions =
328
- readOnlyOptions.length > 0 &&
329
- !isBooleanOnlyOptions(readOnlyOptions);
330
-
331
- const editRow = rows[selected] || null;
332
- const editDefaultOption = editMode && editMode.mode === 'select' && editRow
333
- ? getConfigDefaultOption(editMode.path, editRow.key, 'value', editMode.options)
334
- : null;
335
- const editDefaultOptionIndex = editMode && editMode.mode === 'select'
336
- ? editMode.options.findIndex((option) => Object.is(option, editDefaultOption))
337
- : -1;
338
-
339
- return React.createElement(
340
- Box,
341
- { flexDirection: 'row', gap: 2 },
342
- React.createElement(
343
- Box,
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,
370
- {
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})`
405
- )
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
419
- ? React.createElement(
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,
458
- null,
459
- React.createElement(Text, { color: 'gray' }, ' '),
460
- renderEditableOptions(
461
- readOnlyOptions,
462
- readOnlyOptionIndex,
463
- readOnlyDefaultOptionIndex,
464
- selectedPath,
465
- selectedRow?.key,
466
- null,
467
- false
468
- )
469
- )
470
- : rows[selected].kind === 'array'
471
- ? renderArrayDetails(rows[selected].value)
472
- : null
473
- )
474
- )
475
- );
322
+ if (!snapshot.ok) {
323
+ return React.createElement(
324
+ Box,
325
+ { flexDirection: 'column', marginTop: 1 },
326
+ React.createElement(
327
+ Text,
328
+ { color: 'red', bold: true },
329
+ 'Unable to read/parse config',
330
+ ),
331
+ React.createElement(Text, { color: 'gray' }, snapshot.error),
332
+ React.createElement(
333
+ Text,
334
+ { color: 'gray' },
335
+ `Path: ${snapshot.path}`,
336
+ ),
337
+ );
338
+ }
339
+
340
+ const currentNode = getNodeAtPath(snapshot.data, pathSegments);
341
+ const allRows = buildRows(currentNode, pathSegments);
342
+ const rows = filterRowsByQuery(allRows, filterQuery);
343
+ const breadcrumbs = formatBreadcrumbs(pathSegments);
344
+ const selected =
345
+ rows.length === 0 ? -1 : clamp(selectedIndex, 0, rows.length - 1);
346
+ const { leftWidth, rightWidth } = computePaneWidths(terminalWidth);
347
+ const breadcrumbLabel = breadcrumbs
348
+ ? truncate(breadcrumbs, Math.max(8, leftWidth - 6))
349
+ : '';
350
+ const maxLeftLabelLength = Math.max(1, leftWidth - 6);
351
+ const contentViewportHeight = Math.max(1, listViewportHeight);
352
+ const paneHeight = contentViewportHeight + LEFT_PANE_FRAME_ROWS;
353
+ const viewportStart = clamp(
354
+ scrollOffset,
355
+ 0,
356
+ Math.max(0, rows.length - contentViewportHeight),
357
+ );
358
+ const visibleRows = rows.slice(
359
+ viewportStart,
360
+ viewportStart + contentViewportHeight,
361
+ );
362
+ const canScrollUp = viewportStart > 0;
363
+ const canScrollDown = viewportStart + contentViewportHeight < rows.length;
364
+ const selectedRow = rows[selected] || null;
365
+ const selectedPath =
366
+ selectedRow && selectedRow.pathSegment != null
367
+ ? [...pathSegments, selectedRow.pathSegment]
368
+ : pathSegments;
369
+ const selectedVariantMeta =
370
+ selectedRow &&
371
+ selectedRow.pathSegment != null &&
372
+ typeof selectedRow.key === 'string'
373
+ ? getConfigVariantMeta(pathSegments, selectedRow.key)
374
+ : null;
375
+ const mixedVariantReadOnlyOptions =
376
+ selectedRow && selectedVariantMeta?.kind === 'scalar_object'
377
+ ? buildVariantSelectorOptions(selectedVariantMeta)
378
+ : [];
379
+ const readOnlyOptions =
380
+ mixedVariantReadOnlyOptions.length > 0
381
+ ? mixedVariantReadOnlyOptions.map((option) => option.label)
382
+ : selectedRow && selectedRow.kind === 'value'
383
+ ? getConfigOptions(
384
+ selectedPath,
385
+ selectedRow.key,
386
+ selectedRow.value,
387
+ selectedRow.kind,
388
+ ) || []
389
+ : [];
390
+ const readOnlyOptionIndex =
391
+ mixedVariantReadOnlyOptions.length > 0
392
+ ? isObjectValue(selectedRow?.value)
393
+ ? mixedVariantReadOnlyOptions.findIndex(
394
+ (option) =>
395
+ option.kind === 'object' &&
396
+ objectMatchesVariant(selectedRow.value, option),
397
+ )
398
+ : mixedVariantReadOnlyOptions.findIndex(
399
+ (option) =>
400
+ option.kind === 'scalar' &&
401
+ Object.is(option.value, String(selectedRow?.value)),
402
+ )
403
+ : readOnlyOptions.findIndex((option) =>
404
+ Object.is(option, selectedRow?.value),
405
+ );
406
+ const readOnlyDefaultOption = selectedRow?.isConfigured
407
+ ? getConfigDefaultOption(
408
+ selectedPath,
409
+ selectedRow.key,
410
+ selectedRow.kind,
411
+ readOnlyOptions,
412
+ )
413
+ : null;
414
+ const readOnlyDefaultOptionIndex = readOnlyOptions.findIndex((option) =>
415
+ Object.is(option, readOnlyDefaultOption),
416
+ );
417
+ const shouldShowReadOnlyOptions =
418
+ readOnlyOptions.length > 0 && !isBooleanOnlyOptions(readOnlyOptions);
419
+
420
+ const editRow = rows[selected] || null;
421
+ const editDefaultOption =
422
+ editMode && editMode.mode === 'select' && editRow?.isConfigured
423
+ ? getConfigDefaultOption(
424
+ editMode.path,
425
+ editRow.key,
426
+ 'value',
427
+ editMode.options,
428
+ )
429
+ : null;
430
+ const editDefaultOptionIndex =
431
+ editMode && editMode.mode === 'select'
432
+ ? editMode.options.findIndex((option) =>
433
+ Object.is(option, editDefaultOption),
434
+ )
435
+ : -1;
436
+ const hoveredOption =
437
+ editMode && editMode.mode === 'select'
438
+ ? editMode.options[editMode.selectedOptionIndex]
439
+ : shouldShowReadOnlyOptions && readOnlyOptionIndex >= 0
440
+ ? readOnlyOptions[readOnlyOptionIndex]
441
+ : null;
442
+ const hoveredOptionSegments =
443
+ editMode && editMode.mode === 'select'
444
+ ? editMode.path.slice(0, -1)
445
+ : selectedPath;
446
+ const hoveredOptionDescription =
447
+ hoveredOption !== null && hoveredOption !== undefined
448
+ ? getConfigOptionExplanation(
449
+ hoveredOptionSegments,
450
+ selectedRow?.key,
451
+ hoveredOption,
452
+ )
453
+ : null;
454
+ const configHelp = formatConfigHelp(pathSegments, selectedRow);
455
+ const hoveredOptionDescriptionLine = hoveredOptionDescription
456
+ ? React.createElement(
457
+ Text,
458
+ {
459
+ color: 'gray',
460
+ key: 'hovered-option-description',
461
+ wrap: 'truncate-end',
462
+ },
463
+ `${formatOptionValue(hoveredOption)}: ${hoveredOptionDescription}`,
464
+ )
465
+ : null;
466
+ const hoveredOptionDescriptionSpacer = hoveredOptionDescriptionLine
467
+ ? React.createElement(
468
+ Text,
469
+ { key: 'hovered-option-description-spacer', color: 'gray' },
470
+ ' ',
471
+ )
472
+ : null;
473
+ const optionSelector = editMode
474
+ ? editMode.mode === 'text'
475
+ ? renderTextEditor(editMode.draftValue)
476
+ : editMode.mode === 'add-id'
477
+ ? renderIdEditor(editMode.placeholder, editMode.draftValue)
478
+ : renderEditableOptions(
479
+ editMode.options,
480
+ editMode.selectedOptionIndex,
481
+ editDefaultOptionIndex,
482
+ selectedRow?.key,
483
+ editMode.savedOptionIndex,
484
+ true,
485
+ )
486
+ : shouldShowReadOnlyOptions
487
+ ? React.createElement(
488
+ React.Fragment,
489
+ null,
490
+ React.createElement(Text, { color: 'gray' }, ' '),
491
+ renderEditableOptions(
492
+ readOnlyOptions,
493
+ readOnlyOptionIndex,
494
+ readOnlyDefaultOptionIndex,
495
+ selectedRow?.key,
496
+ null,
497
+ false,
498
+ ),
499
+ )
500
+ : selectedRow?.kind === 'array'
501
+ ? renderArrayDetails(selectedRow.value)
502
+ : null;
503
+
504
+ return React.createElement(
505
+ Box,
506
+ { flexDirection: 'row', gap: 1 },
507
+ React.createElement(
508
+ Box,
509
+ { flexDirection: 'column', width: leftWidth, height: paneHeight },
510
+ React.createElement(
511
+ Box,
512
+ {
513
+ height: paneHeight,
514
+ flexDirection: 'column',
515
+ borderStyle: 'round',
516
+ borderColor: 'cyan',
517
+ paddingX: 1,
518
+ },
519
+ rows.length === 0
520
+ ? React.createElement(
521
+ Text,
522
+ { color: 'gray' },
523
+ String(filterQuery || '').trim().length > 0
524
+ ? '[no entries match current filter]'
525
+ : '[no entries in this table]',
526
+ )
527
+ : visibleRows.map((row, viewIndex) => {
528
+ const index = viewportStart + viewIndex;
529
+ const showTopCue = canScrollUp && viewIndex === 0;
530
+ const showBottomCue =
531
+ canScrollDown &&
532
+ viewIndex === visibleRows.length - 1;
533
+ const isSelected = index === selected;
534
+ const rawLabel = `${showTopCue ? '↑ ' : showBottomCue ? '↓ ' : ' '}${isSelected ? '▶' : ' '} ${row.label}`;
535
+ const label = truncate(
536
+ rawLabel,
537
+ maxLeftLabelLength,
538
+ );
539
+
540
+ return React.createElement(MenuItem, {
541
+ label,
542
+ isSelected,
543
+ isDimmed:
544
+ !isSelected && row.isConfigured === false,
545
+ isDeprecated: false,
546
+ key: `left-${index}`,
547
+ });
548
+ }),
549
+ ...Array.from(
550
+ {
551
+ length: Math.max(
552
+ 0,
553
+ contentViewportHeight -
554
+ (rows.length === 0 ? 1 : visibleRows.length),
555
+ ),
556
+ },
557
+ (_, fillerIndex) =>
558
+ React.createElement(
559
+ Text,
560
+ {
561
+ color: 'gray',
562
+ key: `left-filler-${fillerIndex}`,
563
+ },
564
+ ' ',
565
+ ),
566
+ ),
567
+ ),
568
+ React.createElement(
569
+ Box,
570
+ { position: 'absolute', top: 0, left: 2 },
571
+ breadcrumbLabel
572
+ ? React.createElement(
573
+ Text,
574
+ {
575
+ color: 'cyan',
576
+ wrap: 'truncate-end',
577
+ backgroundColor: 'black',
578
+ },
579
+ ` ${breadcrumbLabel} `,
580
+ )
581
+ : React.createElement(
582
+ Text,
583
+ { color: 'cyan', backgroundColor: 'black' },
584
+ ' Navigator ',
585
+ ),
586
+ ),
587
+ ),
588
+ React.createElement(
589
+ Box,
590
+ { flexDirection: 'column', width: rightWidth, height: paneHeight },
591
+ React.createElement(
592
+ Box,
593
+ {
594
+ height: paneHeight,
595
+ flexDirection: 'column',
596
+ borderStyle: 'round',
597
+ borderColor: 'gray',
598
+ paddingX: 2,
599
+ },
600
+ React.createElement(
601
+ Box,
602
+ { position: 'absolute', top: 0, left: 2 },
603
+ React.createElement(
604
+ Text,
605
+ { color: 'gray', backgroundColor: 'black' },
606
+ ' Details ',
607
+ ),
608
+ ),
609
+ rows.length === 0
610
+ ? React.createElement(
611
+ React.Fragment,
612
+ null,
613
+ String(filterQuery || '').trim().length > 0
614
+ ? React.createElement(
615
+ Text,
616
+ { color: 'gray', wrap: 'truncate-end' },
617
+ `Filter: ${filterQuery} (${rows.length}/${allRows.length})`,
618
+ )
619
+ : null,
620
+ React.createElement(
621
+ Text,
622
+ { color: 'gray', wrap: 'truncate-end' },
623
+ String(filterQuery || '').trim().length > 0
624
+ ? 'No selection available. Adjust or clear the filter.'
625
+ : 'No selection available.',
626
+ ),
627
+ )
628
+ : React.createElement(
629
+ React.Fragment,
630
+ null,
631
+ String(filterQuery || '').trim().length > 0 ||
632
+ isFilterEditing
633
+ ? React.createElement(
634
+ Text,
635
+ { color: 'cyan', wrap: 'truncate-end' },
636
+ `Filter: ${filterQuery || ''} (${rows.length}/${allRows.length})${isFilterEditing ? ' [editing]' : ''}`,
637
+ )
638
+ : null,
639
+ ...configHelp.map((line, lineIndex) =>
640
+ React.createElement(
641
+ Text,
642
+ {
643
+ key: `help-${selected}-${lineIndex}`,
644
+ color: line.color,
645
+ bold: line.bold,
646
+ wrap: 'wrap',
647
+ },
648
+ line.showWarningIcon
649
+ ? React.createElement(
650
+ Text,
651
+ { color: 'yellow' },
652
+ '[!] ',
653
+ )
654
+ : null,
655
+ line.text,
656
+ ),
657
+ ),
658
+ editError
659
+ ? React.createElement(
660
+ Text,
661
+ { color: 'red', wrap: 'truncate-end' },
662
+ editError,
663
+ )
664
+ : null,
665
+ editMode
666
+ ? React.createElement(
667
+ Text,
668
+ { color: 'gray' },
669
+ ' ',
670
+ )
671
+ : null,
672
+ optionSelector,
673
+ hoveredOptionDescriptionSpacer,
674
+ hoveredOptionDescriptionLine,
675
+ ),
676
+ ),
677
+ ),
678
+ );
476
679
  };