@pellux/goodvibes-tui 0.19.27 → 0.19.29
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/CHANGELOG.md +11 -0
- package/README.md +3 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +3 -2
- package/src/cli/entrypoint.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/status.ts +9 -9
- package/src/cli/surface-command.ts +46 -11
- package/src/cli/tui-startup.ts +4 -4
- package/src/daemon/cli.ts +7 -0
- package/src/input/handler-interactions.ts +14 -1
- package/src/input/handler-onboarding.ts +161 -118
- package/src/input/handler.ts +1 -1
- package/src/input/onboarding/handler-onboarding-routes.ts +35 -15
- package/src/input/onboarding/onboarding-wizard-apply.ts +35 -25
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -5
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +93 -5
- package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -3
- package/src/input/onboarding/onboarding-wizard-rules.ts +40 -8
- package/src/input/onboarding/onboarding-wizard-state.ts +19 -8
- package/src/input/onboarding/onboarding-wizard-steps.ts +226 -93
- package/src/input/onboarding/onboarding-wizard-types.ts +15 -0
- package/src/input/onboarding/onboarding-wizard.ts +123 -6
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +4 -0
- package/src/main.ts +35 -27
- package/src/renderer/compositor.ts +3 -3
- package/src/renderer/onboarding/onboarding-wizard.ts +141 -57
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +3 -0
- package/src/runtime/bootstrap.ts +15 -0
- package/src/runtime/onboarding/apply.ts +45 -90
- package/src/runtime/onboarding/derivation.ts +7 -7
- package/src/runtime/onboarding/markers.ts +41 -55
- package/src/runtime/onboarding/snapshot.ts +1 -0
- package/src/runtime/onboarding/state.ts +6 -6
- package/src/runtime/onboarding/types.ts +24 -27
- package/src/runtime/onboarding/verify.ts +3 -65
- package/src/runtime/surface-feature-flags.ts +67 -0
- package/src/version.ts +1 -1
|
@@ -26,7 +26,6 @@ type RenderedFieldRow =
|
|
|
26
26
|
readonly kind: 'field';
|
|
27
27
|
readonly field: OnboardingWizardFieldDefinition;
|
|
28
28
|
readonly absoluteIndex: number;
|
|
29
|
-
readonly line: 0 | 1;
|
|
30
29
|
};
|
|
31
30
|
|
|
32
31
|
function clamp(value: number, min: number, max: number): number {
|
|
@@ -61,6 +60,12 @@ function modeLabel(mode: OnboardingWizardController['mode']): string {
|
|
|
61
60
|
return 'New setup';
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
function changedScreensLabel(wizard: OnboardingWizardController): string {
|
|
64
|
+
if (wizard.dirtyStepCount === 0) return 'no changes';
|
|
65
|
+
if (wizard.dirtyStepCount === 1) return '1 changed screen';
|
|
66
|
+
return `${wizard.dirtyStepCount} changed screens`;
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
function stepGlyph(
|
|
65
70
|
wizard: OnboardingWizardController,
|
|
66
71
|
step: OnboardingWizardStepDefinition,
|
|
@@ -95,7 +100,11 @@ function fieldBadgeTone(
|
|
|
95
100
|
return wizard.getFieldValue(field) ? UI_TONES.state.good : UI_TONES.fg.muted;
|
|
96
101
|
}
|
|
97
102
|
if (field.kind === 'radio') return UI_TONES.state.active;
|
|
98
|
-
if (field.kind === 'masked')
|
|
103
|
+
if (field.kind === 'text' || field.kind === 'masked') {
|
|
104
|
+
const missingRequired = wizard.getFieldValueLabel(field) === 'Missing';
|
|
105
|
+
if (missingRequired) return UI_TONES.state.warn;
|
|
106
|
+
if (field.kind === 'masked') return UI_TONES.state.warn;
|
|
107
|
+
}
|
|
99
108
|
return UI_TONES.fg.secondary;
|
|
100
109
|
}
|
|
101
110
|
|
|
@@ -116,8 +125,7 @@ function buildFieldRows(
|
|
|
116
125
|
|
|
117
126
|
fieldWindow.fields.forEach((field, index) => {
|
|
118
127
|
const absoluteIndex = fieldWindow.start + index;
|
|
119
|
-
rows.push({ kind: 'field', field, absoluteIndex
|
|
120
|
-
rows.push({ kind: 'field', field, absoluteIndex, line: 1 });
|
|
128
|
+
rows.push({ kind: 'field', field, absoluteIndex });
|
|
121
129
|
});
|
|
122
130
|
|
|
123
131
|
if (fieldWindow.end < fieldWindow.total) {
|
|
@@ -155,6 +163,48 @@ function fieldHint(
|
|
|
155
163
|
return field.hint;
|
|
156
164
|
}
|
|
157
165
|
|
|
166
|
+
function fieldRowPrefix(
|
|
167
|
+
wizard: OnboardingWizardController,
|
|
168
|
+
field: OnboardingWizardFieldDefinition,
|
|
169
|
+
selected: boolean,
|
|
170
|
+
): string {
|
|
171
|
+
if (selected) return `${OVERLAY_GLYPHS.selected} `;
|
|
172
|
+
if (wizard.isFieldDirty(field.id)) return '◇ ';
|
|
173
|
+
if (field.kind === 'checklist') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
|
|
174
|
+
if (field.kind === 'acknowledgement') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
|
|
175
|
+
if (field.kind === 'action') return '▶ ';
|
|
176
|
+
if (field.kind === 'radio') return '◉ ';
|
|
177
|
+
return ' ';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function selectedFieldText(wizard: OnboardingWizardController): {
|
|
181
|
+
readonly title: string;
|
|
182
|
+
readonly hint: string;
|
|
183
|
+
} {
|
|
184
|
+
if (wizard.isEditingTextField() && wizard.editingFieldId !== null) {
|
|
185
|
+
const editingField = wizard.getFieldById(wizard.editingFieldId);
|
|
186
|
+
if (editingField) {
|
|
187
|
+
return {
|
|
188
|
+
title: `Editing: ${editingField.label}`,
|
|
189
|
+
hint: fieldHint(wizard, editingField, true),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const field = wizard.getSelectedField();
|
|
195
|
+
if (!field) {
|
|
196
|
+
return {
|
|
197
|
+
title: 'Selected: none',
|
|
198
|
+
hint: 'No selectable row is active on this screen.',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
title: `Selected: ${field.label} [${wizard.getFieldValueLabel(field)}]`,
|
|
204
|
+
hint: fieldHint(wizard, field, true),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
158
208
|
function renderFieldRow(
|
|
159
209
|
line: Line,
|
|
160
210
|
wizard: OnboardingWizardController,
|
|
@@ -178,38 +228,36 @@ function renderFieldRow(
|
|
|
178
228
|
const fieldBg = selected ? DEFAULT_OVERLAY_PALETTE.selectedBg : UI_TONES.bg.base;
|
|
179
229
|
fillRange(line, startX, width, fieldBg);
|
|
180
230
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const labelWidth = Math.max(0, width - badgeWidth - 4);
|
|
186
|
-
|
|
187
|
-
putOverlayText(line, startX + 1, labelWidth, truncateDisplay(`${prefix}${field.label}`, labelWidth), {
|
|
188
|
-
fg: UI_TONES.fg.primary,
|
|
189
|
-
bg: fieldBg,
|
|
190
|
-
bold: selected,
|
|
191
|
-
});
|
|
192
|
-
putOverlayText(line, startX + width - badgeWidth - 1, badgeWidth, badge, {
|
|
193
|
-
fg: fieldBadgeTone(wizard, field),
|
|
194
|
-
bg: fieldBg,
|
|
195
|
-
bold: selected,
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
231
|
+
const badge = truncateDisplay(`[${wizard.getFieldValueLabel(field)}]`, Math.max(8, Math.floor(width * 0.34)));
|
|
232
|
+
const badgeWidth = getDisplayWidth(badge);
|
|
233
|
+
const labelWidth = Math.max(0, width - badgeWidth - 4);
|
|
234
|
+
const prefix = fieldRowPrefix(wizard, field, selected);
|
|
199
235
|
|
|
200
|
-
putOverlayText(line, startX +
|
|
201
|
-
fg:
|
|
236
|
+
putOverlayText(line, startX + 1, labelWidth, truncateDisplay(`${prefix}${field.label}`, labelWidth), {
|
|
237
|
+
fg: UI_TONES.fg.primary,
|
|
238
|
+
bg: fieldBg,
|
|
239
|
+
bold: selected,
|
|
240
|
+
});
|
|
241
|
+
putOverlayText(line, startX + width - badgeWidth - 1, badgeWidth, badge, {
|
|
242
|
+
fg: fieldBadgeTone(wizard, field),
|
|
202
243
|
bg: fieldBg,
|
|
203
|
-
|
|
244
|
+
bold: selected,
|
|
204
245
|
});
|
|
205
246
|
}
|
|
206
247
|
|
|
207
248
|
function footerText(wizard: OnboardingWizardController): string {
|
|
208
249
|
if (wizard.isEditingTextField()) {
|
|
209
|
-
return '[Enter] Save [Esc] Cancel [Backspace] Delete [
|
|
250
|
+
return '[Enter] Save value [Esc] Cancel edit [Backspace] Delete char [Del/Ctrl+U] Clear value';
|
|
210
251
|
}
|
|
211
252
|
|
|
212
|
-
return '[
|
|
253
|
+
return '[Enter] Toggle/open [Esc] Close [Tab/Shift+Tab] Screen [↑↓] Move [Del/Ctrl+U] Clear input';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function controlsText(wizard: OnboardingWizardController): string {
|
|
257
|
+
if (wizard.isEditingTextField()) {
|
|
258
|
+
return 'Controls: Enter saves this value, Esc cancels editing, Backspace deletes one character, Delete or Ctrl+U clears the field.';
|
|
259
|
+
}
|
|
260
|
+
return 'Controls: Enter or Space changes the selected row; Delete or Ctrl+U clears selected text inputs; Tab/Shift+Tab changes screens; arrows move.';
|
|
213
261
|
}
|
|
214
262
|
|
|
215
263
|
function renderWideLayout(
|
|
@@ -229,18 +277,18 @@ function renderWideLayout(
|
|
|
229
277
|
const summaryBg = UI_TONES.bg.summary;
|
|
230
278
|
const innerLeft = layout.margin + 1;
|
|
231
279
|
const availableInner = layout.innerWidth - 2;
|
|
232
|
-
const leftWidthBase = layout.innerWidth >= 108 ?
|
|
233
|
-
const rightWidthBase = layout.innerWidth >= 108 ?
|
|
234
|
-
const minCenterWidth =
|
|
235
|
-
let leftWidth = Math.min(leftWidthBase, Math.max(
|
|
236
|
-
let rightWidth = Math.min(rightWidthBase, Math.max(
|
|
280
|
+
const leftWidthBase = layout.innerWidth >= 150 ? 32 : layout.innerWidth >= 108 ? 28 : 24;
|
|
281
|
+
const rightWidthBase = layout.innerWidth >= 150 ? 34 : layout.innerWidth >= 108 ? 32 : 24;
|
|
282
|
+
const minCenterWidth = layout.innerWidth >= 120 ? 48 : 40;
|
|
283
|
+
let leftWidth = Math.min(leftWidthBase, Math.max(20, availableInner - minCenterWidth - 12));
|
|
284
|
+
let rightWidth = Math.min(rightWidthBase, Math.max(22, availableInner - minCenterWidth - leftWidth));
|
|
237
285
|
let centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
|
|
238
286
|
|
|
239
287
|
if (centerWidth < minCenterWidth) {
|
|
240
288
|
const deficit = minCenterWidth - centerWidth;
|
|
241
|
-
const leftCut = Math.min(Math.max(0, leftWidth -
|
|
289
|
+
const leftCut = Math.min(Math.max(0, leftWidth - 20), Math.ceil(deficit / 2));
|
|
242
290
|
leftWidth -= leftCut;
|
|
243
|
-
rightWidth -= Math.min(Math.max(0, rightWidth -
|
|
291
|
+
rightWidth -= Math.min(Math.max(0, rightWidth - 22), deficit - leftCut);
|
|
244
292
|
centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
|
|
245
293
|
}
|
|
246
294
|
|
|
@@ -252,12 +300,13 @@ function renderWideLayout(
|
|
|
252
300
|
const descriptionLines = wrapText(currentStep.description, Math.max(18, centerWidth - 2)).slice(0, 2);
|
|
253
301
|
const summaryLines = [
|
|
254
302
|
currentStep.summaryTitle,
|
|
255
|
-
...currentStep.summaryLines,
|
|
303
|
+
...currentStep.summaryLines.slice(0, 2),
|
|
256
304
|
`Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
|
|
257
|
-
|
|
258
|
-
`Pending picker ${wizard.pendingModelPickerTarget ?? 'none'}`,
|
|
305
|
+
changedScreensLabel(wizard),
|
|
259
306
|
];
|
|
260
|
-
const
|
|
307
|
+
const fieldStartRow = 6;
|
|
308
|
+
const selectedText = selectedFieldText(wizard);
|
|
309
|
+
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
|
|
261
310
|
|
|
262
311
|
const topLine = createOverlayFilledBorderLine(
|
|
263
312
|
width,
|
|
@@ -273,7 +322,7 @@ function renderWideLayout(
|
|
|
273
322
|
bg: headerBg,
|
|
274
323
|
bold: true,
|
|
275
324
|
});
|
|
276
|
-
const meta = `${modeLabel(wizard.mode)} ${wizard.stepIndex + 1}/${wizard.steps.length}
|
|
325
|
+
const meta = `${modeLabel(wizard.mode)} ${wizard.stepIndex + 1}/${wizard.steps.length} ${changedScreensLabel(wizard)}`;
|
|
277
326
|
putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
|
|
278
327
|
fg: UI_TONES.fg.secondary,
|
|
279
328
|
bg: headerBg,
|
|
@@ -296,7 +345,7 @@ function renderWideLayout(
|
|
|
296
345
|
bg: headerBg,
|
|
297
346
|
bold: true,
|
|
298
347
|
});
|
|
299
|
-
putOverlayText(headerLine, rightStart + 1, rightWidth - 2, 'Summary
|
|
348
|
+
putOverlayText(headerLine, rightStart + 1, rightWidth - 2, 'Summary', {
|
|
300
349
|
fg: UI_TONES.fg.secondary,
|
|
301
350
|
bg: summaryBg,
|
|
302
351
|
bold: true,
|
|
@@ -333,28 +382,45 @@ function renderWideLayout(
|
|
|
333
382
|
bg: bodyBg,
|
|
334
383
|
});
|
|
335
384
|
} else if (row === 2) {
|
|
336
|
-
|
|
337
|
-
`Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete • ${modeLabel(wizard.mode)}`,
|
|
338
|
-
centerWidth - 2,
|
|
339
|
-
);
|
|
340
|
-
putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[1] ?? status, {
|
|
385
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[1] ?? '', {
|
|
341
386
|
fg: UI_TONES.fg.secondary,
|
|
342
387
|
bg: bodyBg,
|
|
343
388
|
});
|
|
389
|
+
} else if (row === 3) {
|
|
390
|
+
fillRange(line, centerStart, centerWidth, railBg);
|
|
391
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(controlsText(wizard), centerWidth - 2), {
|
|
392
|
+
fg: UI_TONES.state.info,
|
|
393
|
+
bg: railBg,
|
|
394
|
+
});
|
|
395
|
+
} else if (row === 4) {
|
|
396
|
+
fillRange(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
397
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, centerWidth - 2), {
|
|
398
|
+
fg: UI_TONES.fg.primary,
|
|
399
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
400
|
+
bold: true,
|
|
401
|
+
});
|
|
402
|
+
} else if (row === 5) {
|
|
403
|
+
fillRange(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
404
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(selectedText.hint, centerWidth - 2), {
|
|
405
|
+
fg: UI_TONES.fg.secondary,
|
|
406
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
407
|
+
});
|
|
344
408
|
} else {
|
|
345
|
-
renderFieldRow(line, wizard, fieldRows[row -
|
|
409
|
+
renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, centerStart, centerWidth);
|
|
346
410
|
}
|
|
347
411
|
|
|
348
412
|
const step = wizard.steps[row] ?? null;
|
|
349
413
|
if (step) {
|
|
350
414
|
const stepState = stepGlyph(wizard, step, row);
|
|
351
415
|
const completion = `${wizard.getCompletedFieldCount(row)}/${wizard.getStepFieldCount(row)}`;
|
|
352
|
-
|
|
416
|
+
const completionWidth = getDisplayWidth(completion);
|
|
417
|
+
const stepLabelWidth = Math.max(0, leftWidth - completionWidth - 4);
|
|
418
|
+
putOverlayText(line, leftStart + 1, stepLabelWidth, truncateDisplay(`${stepState.glyph} ${row + 1}. ${step.shortLabel}`, stepLabelWidth), {
|
|
353
419
|
fg: stepState.fg,
|
|
354
420
|
bg: railBg,
|
|
355
421
|
bold: row === wizard.stepIndex,
|
|
356
422
|
});
|
|
357
|
-
putOverlayText(line, Math.max(leftStart + 1, leftStart + leftWidth -
|
|
423
|
+
putOverlayText(line, Math.max(leftStart + 1, leftStart + leftWidth - completionWidth - 2), completionWidth, completion, {
|
|
358
424
|
fg: wizard.isStepDirty(row) ? UI_TONES.state.warn : UI_TONES.fg.muted,
|
|
359
425
|
bg: railBg,
|
|
360
426
|
});
|
|
@@ -418,7 +484,9 @@ function renderCollapsedLayout(
|
|
|
418
484
|
const innerStart = layout.margin + 1;
|
|
419
485
|
const innerWidth = layout.innerWidth;
|
|
420
486
|
const descriptionLines = wrapText(currentStep.description, Math.max(14, innerWidth - 2)).slice(0, 2);
|
|
421
|
-
const
|
|
487
|
+
const fieldStartRow = 6;
|
|
488
|
+
const selectedText = selectedFieldText(wizard);
|
|
489
|
+
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
|
|
422
490
|
|
|
423
491
|
const topLine = createOverlayFilledBorderLine(
|
|
424
492
|
width,
|
|
@@ -434,7 +502,7 @@ function renderCollapsedLayout(
|
|
|
434
502
|
bg: headerBg,
|
|
435
503
|
bold: true,
|
|
436
504
|
});
|
|
437
|
-
const meta = `${wizard.stepIndex + 1}/${wizard.steps.length} •
|
|
505
|
+
const meta = `${wizard.stepIndex + 1}/${wizard.steps.length} • ${changedScreensLabel(wizard)}`;
|
|
438
506
|
putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
|
|
439
507
|
fg: UI_TONES.fg.secondary,
|
|
440
508
|
bg: headerBg,
|
|
@@ -476,16 +544,31 @@ function renderCollapsedLayout(
|
|
|
476
544
|
bg: bodyBg,
|
|
477
545
|
});
|
|
478
546
|
} else if (row === 2) {
|
|
479
|
-
|
|
480
|
-
`${currentStep.summaryTitle} • ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
|
|
481
|
-
innerWidth - 2,
|
|
482
|
-
);
|
|
483
|
-
putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[1] ?? compactMeta, {
|
|
547
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[1] ?? '', {
|
|
484
548
|
fg: UI_TONES.fg.secondary,
|
|
485
549
|
bg: bodyBg,
|
|
486
550
|
});
|
|
551
|
+
} else if (row === 3) {
|
|
552
|
+
fillRange(line, innerStart, innerWidth, UI_TONES.bg.section);
|
|
553
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(controlsText(wizard), innerWidth - 2), {
|
|
554
|
+
fg: UI_TONES.state.info,
|
|
555
|
+
bg: UI_TONES.bg.section,
|
|
556
|
+
});
|
|
557
|
+
} else if (row === 4) {
|
|
558
|
+
fillRange(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
559
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, innerWidth - 2), {
|
|
560
|
+
fg: UI_TONES.fg.primary,
|
|
561
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
562
|
+
bold: true,
|
|
563
|
+
});
|
|
564
|
+
} else if (row === 5) {
|
|
565
|
+
fillRange(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
566
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(selectedText.hint, innerWidth - 2), {
|
|
567
|
+
fg: UI_TONES.fg.secondary,
|
|
568
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
569
|
+
});
|
|
487
570
|
} else {
|
|
488
|
-
renderFieldRow(line, wizard, fieldRows[row -
|
|
571
|
+
renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, innerStart, innerWidth);
|
|
489
572
|
}
|
|
490
573
|
|
|
491
574
|
lines.push(line);
|
|
@@ -525,7 +608,8 @@ export function renderOnboardingWizard(
|
|
|
525
608
|
width: number,
|
|
526
609
|
viewportHeight: number,
|
|
527
610
|
): Line[] {
|
|
528
|
-
const
|
|
611
|
+
const margin = width >= 64 ? 1 : 0;
|
|
612
|
+
const layout = createOverlayBoxLayout(width, margin, Math.max(20, width - margin * 2));
|
|
529
613
|
const collapsed = layout.innerWidth < 86;
|
|
530
614
|
return collapsed
|
|
531
615
|
? renderCollapsedLayout(wizard, width, viewportHeight, layout)
|
|
@@ -75,6 +75,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
75
75
|
permissions: 'Permissions',
|
|
76
76
|
mcp: 'MCP',
|
|
77
77
|
sandbox: 'Sandbox',
|
|
78
|
+
surfaces: 'Surfaces',
|
|
78
79
|
danger: 'Danger',
|
|
79
80
|
tools: 'Tools',
|
|
80
81
|
flags: 'Flags',
|
|
@@ -129,6 +130,14 @@ export const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
|
129
130
|
'web.port': 'Web Port',
|
|
130
131
|
'web.publicBaseUrl': 'Web Public Base URL',
|
|
131
132
|
'web.staticAssetsDir': 'Web Static Assets Dir',
|
|
133
|
+
'surfaces.ntfy.enabled': 'ntfy Enabled',
|
|
134
|
+
'surfaces.ntfy.baseUrl': 'ntfy Base URL',
|
|
135
|
+
'surfaces.ntfy.topic': 'ntfy Default Delivery Topic',
|
|
136
|
+
'surfaces.ntfy.chatTopic': 'ntfy Chat Topic',
|
|
137
|
+
'surfaces.ntfy.agentTopic': 'ntfy Agent Topic',
|
|
138
|
+
'surfaces.ntfy.remoteTopic': 'ntfy Daemon-Only Remote Topic',
|
|
139
|
+
'surfaces.ntfy.token': 'ntfy Token',
|
|
140
|
+
'surfaces.ntfy.defaultPriority': 'ntfy Default Priority',
|
|
132
141
|
};
|
|
133
142
|
|
|
134
143
|
export function getSettingLabel(entry: SettingEntry): string {
|
|
@@ -64,6 +64,7 @@ export function renderSettingsModal(
|
|
|
64
64
|
const isUiTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'ui';
|
|
65
65
|
const isToolsTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'tools';
|
|
66
66
|
const isNetworkTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'network';
|
|
67
|
+
const isSurfacesTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'surfaces';
|
|
67
68
|
let persistentHelpers: import('./modal-factory.ts').ModalHelperRow[] | undefined;
|
|
68
69
|
sections.push({
|
|
69
70
|
type: 'text',
|
|
@@ -79,6 +80,8 @@ export function renderSettingsModal(
|
|
|
79
80
|
? 'Feature flags control staged or experimental behavior. Some changes may require restart.'
|
|
80
81
|
: isToolsTab
|
|
81
82
|
? 'Configure tool LLM routing and helper model. Provider and model fields are optional — empty means use the active provider.'
|
|
83
|
+
: isSurfacesTab
|
|
84
|
+
? 'Configure external app surfaces. Toggle each Enabled setting to auto-start that surface when the service starts.'
|
|
82
85
|
: isNetworkTab
|
|
83
86
|
? 'Configure control-plane and HTTP-listener binding. hostMode local/network use preset hosts; custom enables the host field. Changes trigger auto-restart.'
|
|
84
87
|
: 'Browse and adjust operator-facing runtime settings by category.',
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -370,6 +370,20 @@ export async function bootstrapRuntime(
|
|
|
370
370
|
};
|
|
371
371
|
};
|
|
372
372
|
|
|
373
|
+
const waitForConfigDrivenRestarts = async (handle: ExternalServicesHandle): Promise<void> => {
|
|
374
|
+
const waitForRestart = async (service: unknown): Promise<void> => {
|
|
375
|
+
const maybeRestarting = service as { waitForRestart?: unknown } | null;
|
|
376
|
+
if (typeof maybeRestarting?.waitForRestart === 'function') {
|
|
377
|
+
await maybeRestarting.waitForRestart();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
await Promise.all([
|
|
382
|
+
waitForRestart(handle.daemonServer),
|
|
383
|
+
waitForRestart(handle.httpListener),
|
|
384
|
+
]);
|
|
385
|
+
};
|
|
386
|
+
|
|
373
387
|
let externalServices: ExternalServicesHandle = {
|
|
374
388
|
daemonServer: null,
|
|
375
389
|
httpListener: null,
|
|
@@ -401,6 +415,7 @@ export async function bootstrapRuntime(
|
|
|
401
415
|
// A failed previous startup should not prevent a restart attempt.
|
|
402
416
|
}
|
|
403
417
|
}
|
|
418
|
+
await waitForConfigDrivenRestarts(externalServices);
|
|
404
419
|
await externalServices.stop();
|
|
405
420
|
const previousBindings = externalServiceBindings;
|
|
406
421
|
externalServiceBindings = readExternalServiceBindings();
|
|
@@ -2,12 +2,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
|
|
4
4
|
import { CONFIG_SCHEMA, DEFAULT_CONFIG } from '../../config/index.ts';
|
|
5
|
-
import {
|
|
6
|
-
clearOnboardingCompletionMarker,
|
|
7
|
-
getOnboardingCompletionMarkerPath,
|
|
8
|
-
readOnboardingCompletionMarker,
|
|
9
|
-
writeOnboardingCompletionMarker,
|
|
10
|
-
} from './markers.ts';
|
|
5
|
+
import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
11
6
|
import {
|
|
12
7
|
getOnboardingRuntimeStatePath,
|
|
13
8
|
readOnboardingRuntimeState,
|
|
@@ -27,11 +22,6 @@ function getNow(deps: Pick<OnboardingApplyDependencies, 'clock'>): number {
|
|
|
27
22
|
return deps.clock?.() ?? Date.now();
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
function normalizeCompletionSource(source: string): 'wizard' | 'command' | 'import' | 'unknown' {
|
|
31
|
-
if (source === 'wizard' || source === 'command' || source === 'import') return source;
|
|
32
|
-
return 'unknown';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
25
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
26
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
27
|
}
|
|
@@ -144,11 +134,39 @@ function isMalformedGoodVibesSecretReferenceValue(value: string): boolean {
|
|
|
144
134
|
return normalized.startsWith('goodvibes://') && !isGoodVibesSecretReferenceValue(normalized);
|
|
145
135
|
}
|
|
146
136
|
|
|
137
|
+
function isFeatureFlagConfigKey(key: string): key is FeatureFlagConfigKey {
|
|
138
|
+
return key === 'featureFlags' || key.startsWith('featureFlags.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateFeatureFlagConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): boolean {
|
|
142
|
+
if (!isFeatureFlagConfigKey(operation.key)) return false;
|
|
143
|
+
|
|
144
|
+
if (operation.key === 'featureFlags') {
|
|
145
|
+
if (!isPlainObject(operation.value)) throw new Error('featureFlags expects an object value.');
|
|
146
|
+
for (const [flagId, state] of Object.entries(operation.value)) {
|
|
147
|
+
if (flagId.trim().length === 0) throw new Error('featureFlags cannot contain an empty feature id.');
|
|
148
|
+
if (state !== 'enabled' && state !== 'disabled') {
|
|
149
|
+
throw new Error(`featureFlags.${flagId} expects enabled or disabled.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const flagId = operation.key.slice('featureFlags.'.length);
|
|
156
|
+
if (flagId.trim().length === 0) throw new Error('featureFlags requires a feature id.');
|
|
157
|
+
if (operation.value !== 'enabled' && operation.value !== 'disabled') {
|
|
158
|
+
throw new Error(`Config key ${operation.key} expects enabled or disabled.`);
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
147
163
|
function validateConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): void {
|
|
148
164
|
if (typeof operation.value === 'string' && isMalformedGoodVibesSecretReferenceValue(operation.value)) {
|
|
149
165
|
throw new Error(`Config key ${operation.key} only accepts goodvibes://secrets/... secret references.`);
|
|
150
166
|
}
|
|
151
167
|
|
|
168
|
+
if (validateFeatureFlagConfigValue(operation)) return;
|
|
169
|
+
|
|
152
170
|
const schema = CONFIG_SCHEMA.find((entry) => entry.key === operation.key);
|
|
153
171
|
if (!schema) {
|
|
154
172
|
const defaultValue = operation.key.split('.').reduce<unknown>((cursor, part) => (
|
|
@@ -215,9 +233,6 @@ function validateAuthOperation(
|
|
|
215
233
|
if (existing && !requiredRoles.every((role) => existing.roles.includes(role))) {
|
|
216
234
|
throw new Error(`Existing local auth user ${username} is missing required role(s): ${requiredRoles.join(', ')}.`);
|
|
217
235
|
}
|
|
218
|
-
if (existing && operation.retireBootstrapCredential) {
|
|
219
|
-
throw new Error('Replacing a bootstrap credential requires a new local admin username.');
|
|
220
|
-
}
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
function validateAcknowledgementOperation(
|
|
@@ -234,17 +249,6 @@ function validateAcknowledgementOperation(
|
|
|
234
249
|
}
|
|
235
250
|
}
|
|
236
251
|
|
|
237
|
-
function validateCompletionMarkerOperation(
|
|
238
|
-
deps: OnboardingApplyDependencies,
|
|
239
|
-
operation: Extract<OnboardingApplyOperation, { kind: 'set-completion-marker' }>,
|
|
240
|
-
): void {
|
|
241
|
-
const marker = readOnboardingCompletionMarker(deps.shellPaths, operation.scope);
|
|
242
|
-
if (marker.parseError && !operation.completed) return;
|
|
243
|
-
if (marker.parseError) {
|
|
244
|
-
throw new Error(`Existing ${operation.scope} onboarding marker could not be parsed: ${marker.parseError}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
252
|
function applyConfigOperation(
|
|
249
253
|
deps: OnboardingApplyDependencies,
|
|
250
254
|
operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>,
|
|
@@ -264,7 +268,7 @@ function applyConfigOperation(
|
|
|
264
268
|
};
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
deps.config.setDynamic(operation.key, operation.value);
|
|
271
|
+
deps.config.setDynamic(operation.key as never, operation.value);
|
|
268
272
|
return {
|
|
269
273
|
kind: operation.kind,
|
|
270
274
|
summary: `Updated ${operation.key} in global onboarding settings.`,
|
|
@@ -300,7 +304,9 @@ function applyAuthOperation(
|
|
|
300
304
|
? parseBootstrapCredential(readFileSync(before.bootstrapCredentialPath, 'utf-8'))
|
|
301
305
|
: null;
|
|
302
306
|
|
|
303
|
-
if (
|
|
307
|
+
if (existing) {
|
|
308
|
+
auth.rotatePassword(username, operation.password);
|
|
309
|
+
} else {
|
|
304
310
|
auth.addUser(username, operation.password, operation.roles ?? ['admin']);
|
|
305
311
|
}
|
|
306
312
|
|
|
@@ -318,7 +324,7 @@ function applyAuthOperation(
|
|
|
318
324
|
return {
|
|
319
325
|
kind: operation.kind,
|
|
320
326
|
summary: existing
|
|
321
|
-
? `
|
|
327
|
+
? `Updated local auth user ${username}.`
|
|
322
328
|
: `Created local auth user ${username}.`,
|
|
323
329
|
};
|
|
324
330
|
}
|
|
@@ -413,9 +419,9 @@ async function buildRollbackAction(
|
|
|
413
419
|
);
|
|
414
420
|
}
|
|
415
421
|
|
|
416
|
-
const previous = deps.config.get(operation.key);
|
|
422
|
+
const previous = deps.config.get(operation.key as never);
|
|
417
423
|
return () => {
|
|
418
|
-
deps.config.setDynamic(operation.key, previous);
|
|
424
|
+
deps.config.setDynamic(operation.key as never, previous);
|
|
419
425
|
};
|
|
420
426
|
}
|
|
421
427
|
|
|
@@ -433,9 +439,8 @@ async function buildRollbackAction(
|
|
|
433
439
|
);
|
|
434
440
|
}
|
|
435
441
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
);
|
|
442
|
+
const neverOperation: never = operation;
|
|
443
|
+
throw new Error(`Unsupported onboarding operation: ${JSON.stringify(neverOperation)}`);
|
|
439
444
|
}
|
|
440
445
|
|
|
441
446
|
function applyAcknowledgementOperation(
|
|
@@ -458,35 +463,6 @@ function applyAcknowledgementOperation(
|
|
|
458
463
|
};
|
|
459
464
|
}
|
|
460
465
|
|
|
461
|
-
function applyCompletionMarkerOperation(
|
|
462
|
-
deps: OnboardingApplyDependencies,
|
|
463
|
-
request: OnboardingApplyRequest,
|
|
464
|
-
operation: Extract<OnboardingApplyOperation, { kind: 'set-completion-marker' }>,
|
|
465
|
-
): OnboardingAppliedOperation {
|
|
466
|
-
if (!operation.completed) {
|
|
467
|
-
clearOnboardingCompletionMarker(deps.shellPaths, operation.scope);
|
|
468
|
-
return {
|
|
469
|
-
kind: operation.kind,
|
|
470
|
-
summary: `Cleared ${operation.scope} onboarding completion marker.`,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const updatedAt = getNow(deps);
|
|
475
|
-
writeOnboardingCompletionMarker(deps.shellPaths, {
|
|
476
|
-
scope: operation.scope,
|
|
477
|
-
completedAt: operation.payload?.completedAt,
|
|
478
|
-
updatedAt: operation.payload?.updatedAt ?? updatedAt,
|
|
479
|
-
source: operation.payload?.source ?? normalizeCompletionSource(request.source),
|
|
480
|
-
mode: operation.payload?.mode ?? request.mode,
|
|
481
|
-
workspaceRoot: operation.payload?.workspaceRoot,
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
kind: operation.kind,
|
|
486
|
-
summary: `Wrote ${operation.scope} onboarding completion marker.`,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
466
|
function orderApplyOperations(
|
|
491
467
|
operations: readonly OnboardingApplyOperation[],
|
|
492
468
|
): readonly OnboardingApplyOperation[] {
|
|
@@ -499,7 +475,7 @@ function orderApplyOperations(
|
|
|
499
475
|
operation.kind === 'set-config' && operation.key !== 'storage.secretPolicy'
|
|
500
476
|
));
|
|
501
477
|
const finalOperations = operations.filter((operation) => (
|
|
502
|
-
operation.kind === 'acknowledge'
|
|
478
|
+
operation.kind === 'acknowledge'
|
|
503
479
|
));
|
|
504
480
|
|
|
505
481
|
return [
|
|
@@ -543,7 +519,8 @@ function prevalidateApplyRequest(
|
|
|
543
519
|
continue;
|
|
544
520
|
}
|
|
545
521
|
|
|
546
|
-
|
|
522
|
+
const neverOperation: never = operation;
|
|
523
|
+
throw new Error(`Unsupported onboarding operation: ${JSON.stringify(neverOperation)}`);
|
|
547
524
|
} catch (error) {
|
|
548
525
|
errors.push({
|
|
549
526
|
kind: operation.kind,
|
|
@@ -560,22 +537,9 @@ function getVerificationFailureKind(itemId: string): OnboardingApplyOperation['k
|
|
|
560
537
|
if (itemId.startsWith('secret:')) return 'set-secret';
|
|
561
538
|
if (itemId.startsWith('auth:')) return 'ensure-auth-user';
|
|
562
539
|
if (itemId.startsWith('acknowledge:')) return 'acknowledge';
|
|
563
|
-
if (itemId.startsWith('marker:')) return 'set-completion-marker';
|
|
564
540
|
return 'set-config';
|
|
565
541
|
}
|
|
566
542
|
|
|
567
|
-
function splitCompletionMarkerOperations(
|
|
568
|
-
operations: readonly OnboardingApplyOperation[],
|
|
569
|
-
): {
|
|
570
|
-
readonly preMarkerOperations: readonly OnboardingApplyOperation[];
|
|
571
|
-
readonly markerOperations: readonly OnboardingApplyOperation[];
|
|
572
|
-
} {
|
|
573
|
-
return {
|
|
574
|
-
preMarkerOperations: operations.filter((operation) => operation.kind !== 'set-completion-marker'),
|
|
575
|
-
markerOperations: operations.filter((operation) => operation.kind === 'set-completion-marker'),
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
543
|
export async function applyOnboardingRequest(
|
|
580
544
|
deps: OnboardingApplyDependencies,
|
|
581
545
|
request: OnboardingApplyRequest,
|
|
@@ -593,7 +557,6 @@ export async function applyOnboardingRequest(
|
|
|
593
557
|
}
|
|
594
558
|
|
|
595
559
|
const orderedOperations = orderApplyOperations(request.operations);
|
|
596
|
-
const { preMarkerOperations, markerOperations } = splitCompletionMarkerOperations(orderedOperations);
|
|
597
560
|
const rollbacks: RollbackAction[] = [];
|
|
598
561
|
|
|
599
562
|
const applyOperations = async (operations: readonly OnboardingApplyOperation[]): Promise<boolean> => {
|
|
@@ -625,8 +588,8 @@ export async function applyOnboardingRequest(
|
|
|
625
588
|
continue;
|
|
626
589
|
}
|
|
627
590
|
|
|
628
|
-
|
|
629
|
-
|
|
591
|
+
const neverOperation: never = operation;
|
|
592
|
+
throw new Error(`Unsupported onboarding operation: ${JSON.stringify(neverOperation)}`);
|
|
630
593
|
} catch (error) {
|
|
631
594
|
const rollbackErrors = await runRollbacks([...rollbacks, rollback]);
|
|
632
595
|
applied.length = 0;
|
|
@@ -660,19 +623,11 @@ export async function applyOnboardingRequest(
|
|
|
660
623
|
return false;
|
|
661
624
|
};
|
|
662
625
|
|
|
663
|
-
if (!await applyOperations(
|
|
664
|
-
return { ok: false, applied, skipped, errors };
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (!await verifyOrRollback(preMarkerOperations)) {
|
|
668
|
-
return { ok: false, applied, skipped, errors };
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (!await applyOperations(markerOperations)) {
|
|
626
|
+
if (!await applyOperations(orderedOperations)) {
|
|
672
627
|
return { ok: false, applied, skipped, errors };
|
|
673
628
|
}
|
|
674
629
|
|
|
675
|
-
if (!await verifyOrRollback(
|
|
630
|
+
if (!await verifyOrRollback(orderedOperations)) {
|
|
676
631
|
return { ok: false, applied, skipped, errors };
|
|
677
632
|
}
|
|
678
633
|
|