@pellux/goodvibes-tui 0.19.28 → 0.19.30
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 +12 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/surface-command.ts +46 -11
- package/src/core/orchestrator.ts +5 -1
- package/src/daemon/cli.ts +7 -0
- package/src/input/handler-onboarding.ts +151 -44
- package/src/input/onboarding/handler-onboarding-routes.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +35 -8
- 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 -2
- package/src/input/onboarding/onboarding-wizard-rules.ts +22 -3
- package/src/input/onboarding/onboarding-wizard-state.ts +12 -7
- package/src/input/onboarding/onboarding-wizard-steps.ts +133 -59
- package/src/input/onboarding/onboarding-wizard-types.ts +10 -0
- package/src/input/onboarding/onboarding-wizard.ts +56 -4
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +4 -0
- package/src/main.ts +33 -26
- package/src/renderer/compositor.ts +3 -3
- package/src/renderer/onboarding/onboarding-wizard.ts +38 -21
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +3 -0
- package/src/runtime/bootstrap-core.ts +28 -3
- package/src/runtime/bootstrap.ts +20 -3
- package/src/runtime/onboarding/apply.ts +36 -8
- package/src/runtime/onboarding/derivation.ts +7 -7
- package/src/runtime/onboarding/snapshot.ts +1 -0
- package/src/runtime/onboarding/types.ts +4 -1
- package/src/runtime/onboarding/verify.ts +1 -1
- package/src/runtime/surface-feature-flags.ts +67 -0
- package/src/version.ts +1 -1
package/src/main.ts
CHANGED
|
@@ -177,6 +177,7 @@ async function main() {
|
|
|
177
177
|
};
|
|
178
178
|
|
|
179
179
|
const getViewportHeight = (): number => {
|
|
180
|
+
if (input.onboardingWizard.active) return stdout.rows || 24;
|
|
180
181
|
const promptLines: number = input.getVisiblePromptLineCount(getPromptContentWidth());
|
|
181
182
|
const currentModel = providerRegistry.getCurrentModel();
|
|
182
183
|
return (stdout.rows || 24) - 2 - estimateShellFooterHeight(promptLines, currentModel.contextWindow);
|
|
@@ -551,19 +552,23 @@ async function main() {
|
|
|
551
552
|
composerPendingRisk: composerState.pendingRisk,
|
|
552
553
|
}).lines;
|
|
553
554
|
|
|
555
|
+
const onboardingOwnsScreen = input.onboardingWizard.active;
|
|
556
|
+
const shellHeaderLines = onboardingOwnsScreen ? [] : headerLines;
|
|
557
|
+
const shellFooterLines = onboardingOwnsScreen ? [] : footerLines;
|
|
558
|
+
const panelWidth = !onboardingOwnsScreen && panelManager.isVisible() && panelManager.getAllOpen().length > 0
|
|
559
|
+
? panelManager.getRightWidth(width)
|
|
560
|
+
: 0;
|
|
554
561
|
const shellLayout = createShellLayout({
|
|
555
562
|
width,
|
|
556
563
|
height,
|
|
557
|
-
headerHeight:
|
|
558
|
-
footerHeight:
|
|
559
|
-
panelWidth
|
|
560
|
-
? panelManager.getRightWidth(width)
|
|
561
|
-
: 0,
|
|
564
|
+
headerHeight: shellHeaderLines.length,
|
|
565
|
+
footerHeight: shellFooterLines.length,
|
|
566
|
+
panelWidth,
|
|
562
567
|
});
|
|
563
568
|
const vHeight = shellLayout.body.height;
|
|
564
569
|
const conversationWidth = shellLayout.conversation.width;
|
|
565
570
|
activeConversationWidth = conversationWidth;
|
|
566
|
-
const hasPanelWorkspace = panelManager.isVisible() && panelManager.getAllOpen().length > 0;
|
|
571
|
+
const hasPanelWorkspace = !onboardingOwnsScreen && panelManager.isVisible() && panelManager.getAllOpen().length > 0;
|
|
567
572
|
conversation.setSplashSuppressed(hasPanelWorkspace);
|
|
568
573
|
|
|
569
574
|
// Flush pending renders after updating the width provider and splash posture
|
|
@@ -627,27 +632,29 @@ async function main() {
|
|
|
627
632
|
});
|
|
628
633
|
|
|
629
634
|
// Panel composite data
|
|
630
|
-
const panelComposite =
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
635
|
+
const panelComposite = onboardingOwnsScreen
|
|
636
|
+
? { panelData: undefined, panelWidth: 0 }
|
|
637
|
+
: buildPanelCompositeData(
|
|
638
|
+
panelManager,
|
|
639
|
+
input,
|
|
640
|
+
shellLayout.panel?.width ?? 0,
|
|
641
|
+
shellLayout.panel?.height ?? vHeight,
|
|
642
|
+
);
|
|
636
643
|
|
|
637
644
|
compositor.composite({
|
|
638
645
|
width, height,
|
|
639
|
-
header:
|
|
646
|
+
header: shellHeaderLines,
|
|
640
647
|
viewport,
|
|
641
|
-
footer:
|
|
642
|
-
selection: {
|
|
648
|
+
footer: shellFooterLines,
|
|
649
|
+
selection: onboardingOwnsScreen ? undefined : {
|
|
643
650
|
isCellSelected: (col, row) => selection.isCellSelected(col, row),
|
|
644
651
|
scrollTop,
|
|
645
652
|
lineCount: conversation.history.getLineCount(),
|
|
646
653
|
},
|
|
647
|
-
search: input.searchManager.active ? {
|
|
654
|
+
search: !onboardingOwnsScreen && input.searchManager.active ? {
|
|
648
655
|
manager: input.searchManager,
|
|
649
656
|
scrollTop,
|
|
650
|
-
viewportStartY:
|
|
657
|
+
viewportStartY: shellHeaderLines.length,
|
|
651
658
|
} : undefined,
|
|
652
659
|
panel: panelComposite.panelData,
|
|
653
660
|
panelWidth: panelComposite.panelWidth,
|
|
@@ -675,15 +682,6 @@ async function main() {
|
|
|
675
682
|
render,
|
|
676
683
|
});
|
|
677
684
|
|
|
678
|
-
applyInitialTuiCliState({
|
|
679
|
-
cli,
|
|
680
|
-
input,
|
|
681
|
-
commandRegistry,
|
|
682
|
-
commandContext,
|
|
683
|
-
shellPaths: ctx.services.shellPaths,
|
|
684
|
-
render,
|
|
685
|
-
});
|
|
686
|
-
|
|
687
685
|
// --- Streaming speed + tool preview wiring ---
|
|
688
686
|
const refreshGit = () => gitStatusProvider.refresh().then((info) => { lastGitInfoRef.value = info; render(); }).catch(() => { /* non-fatal */ });
|
|
689
687
|
// Refresh git status after each turn completes or after tool results arrive
|
|
@@ -729,6 +727,15 @@ async function main() {
|
|
|
729
727
|
stdin.setEncoding('utf8');
|
|
730
728
|
stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE);
|
|
731
729
|
|
|
730
|
+
applyInitialTuiCliState({
|
|
731
|
+
cli,
|
|
732
|
+
input,
|
|
733
|
+
commandRegistry,
|
|
734
|
+
commandContext,
|
|
735
|
+
shellPaths: ctx.services.shellPaths,
|
|
736
|
+
render,
|
|
737
|
+
});
|
|
738
|
+
|
|
732
739
|
stdin.on('data', (data: string) => {
|
|
733
740
|
const blocking = handleBlockingShellInput({
|
|
734
741
|
data,
|
|
@@ -86,11 +86,11 @@ export class Compositor {
|
|
|
86
86
|
const leftWidth = hasPanel ? Math.max(1, width - panelWidth - 1) : width;
|
|
87
87
|
const sepX = hasPanel ? leftWidth : -1;
|
|
88
88
|
|
|
89
|
-
// 1. Draw Header
|
|
89
|
+
// 1. Draw Header — always full width
|
|
90
90
|
header.forEach((line, i) => newBuffer.blitLine(i, line));
|
|
91
91
|
|
|
92
|
-
// 2. Draw Viewport
|
|
93
|
-
const viewportStartY =
|
|
92
|
+
// 2. Draw Viewport directly after the supplied header.
|
|
93
|
+
const viewportStartY = header.length;
|
|
94
94
|
const vHeight = Math.max(0, height - header.length - footer.length);
|
|
95
95
|
|
|
96
96
|
// Calculate the offset for bottom-anchored short history
|
|
@@ -60,6 +60,12 @@ function modeLabel(mode: OnboardingWizardController['mode']): string {
|
|
|
60
60
|
return 'New setup';
|
|
61
61
|
}
|
|
62
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
|
+
|
|
63
69
|
function stepGlyph(
|
|
64
70
|
wizard: OnboardingWizardController,
|
|
65
71
|
step: OnboardingWizardStepDefinition,
|
|
@@ -241,17 +247,17 @@ function renderFieldRow(
|
|
|
241
247
|
|
|
242
248
|
function footerText(wizard: OnboardingWizardController): string {
|
|
243
249
|
if (wizard.isEditingTextField()) {
|
|
244
|
-
return '[Enter] Save value [Esc] Cancel edit [Backspace] Delete [
|
|
250
|
+
return '[Enter] Save value [Esc] Cancel edit [Backspace] Delete char [Del/Ctrl+U] Clear value';
|
|
245
251
|
}
|
|
246
252
|
|
|
247
|
-
return '[Enter] Toggle/open
|
|
253
|
+
return '[Enter] Toggle/open [Esc] Close [Tab/Shift+Tab] Screen [↑↓] Move [Del/Ctrl+U] Clear input';
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
function controlsText(wizard: OnboardingWizardController): string {
|
|
251
257
|
if (wizard.isEditingTextField()) {
|
|
252
|
-
return 'Controls: Enter saves this value, Esc cancels editing, Backspace deletes,
|
|
258
|
+
return 'Controls: Enter saves this value, Esc cancels editing, Backspace deletes one character, Delete or Ctrl+U clears the field.';
|
|
253
259
|
}
|
|
254
|
-
return 'Controls: Enter or Space changes the selected row; Tab/Shift+Tab changes screens; arrows move
|
|
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.';
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
function renderWideLayout(
|
|
@@ -271,18 +277,18 @@ function renderWideLayout(
|
|
|
271
277
|
const summaryBg = UI_TONES.bg.summary;
|
|
272
278
|
const innerLeft = layout.margin + 1;
|
|
273
279
|
const availableInner = layout.innerWidth - 2;
|
|
274
|
-
const leftWidthBase = layout.innerWidth >= 108 ?
|
|
275
|
-
const rightWidthBase = layout.innerWidth >= 108 ?
|
|
276
|
-
const minCenterWidth =
|
|
277
|
-
let leftWidth = Math.min(leftWidthBase, Math.max(
|
|
278
|
-
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));
|
|
279
285
|
let centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
|
|
280
286
|
|
|
281
287
|
if (centerWidth < minCenterWidth) {
|
|
282
288
|
const deficit = minCenterWidth - centerWidth;
|
|
283
|
-
const leftCut = Math.min(Math.max(0, leftWidth -
|
|
289
|
+
const leftCut = Math.min(Math.max(0, leftWidth - 20), Math.ceil(deficit / 2));
|
|
284
290
|
leftWidth -= leftCut;
|
|
285
|
-
rightWidth -= Math.min(Math.max(0, rightWidth -
|
|
291
|
+
rightWidth -= Math.min(Math.max(0, rightWidth - 22), deficit - leftCut);
|
|
286
292
|
centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
|
|
287
293
|
}
|
|
288
294
|
|
|
@@ -296,9 +302,9 @@ function renderWideLayout(
|
|
|
296
302
|
currentStep.summaryTitle,
|
|
297
303
|
...currentStep.summaryLines.slice(0, 2),
|
|
298
304
|
`Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
|
|
299
|
-
|
|
305
|
+
changedScreensLabel(wizard),
|
|
300
306
|
];
|
|
301
|
-
const fieldStartRow =
|
|
307
|
+
const fieldStartRow = 6;
|
|
302
308
|
const selectedText = selectedFieldText(wizard);
|
|
303
309
|
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
|
|
304
310
|
|
|
@@ -316,7 +322,7 @@ function renderWideLayout(
|
|
|
316
322
|
bg: headerBg,
|
|
317
323
|
bold: true,
|
|
318
324
|
});
|
|
319
|
-
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)}`;
|
|
320
326
|
putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
|
|
321
327
|
fg: UI_TONES.fg.secondary,
|
|
322
328
|
bg: headerBg,
|
|
@@ -376,19 +382,24 @@ function renderWideLayout(
|
|
|
376
382
|
bg: bodyBg,
|
|
377
383
|
});
|
|
378
384
|
} else if (row === 2) {
|
|
385
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[1] ?? '', {
|
|
386
|
+
fg: UI_TONES.fg.secondary,
|
|
387
|
+
bg: bodyBg,
|
|
388
|
+
});
|
|
389
|
+
} else if (row === 3) {
|
|
379
390
|
fillRange(line, centerStart, centerWidth, railBg);
|
|
380
391
|
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(controlsText(wizard), centerWidth - 2), {
|
|
381
392
|
fg: UI_TONES.state.info,
|
|
382
393
|
bg: railBg,
|
|
383
394
|
});
|
|
384
|
-
} else if (row ===
|
|
395
|
+
} else if (row === 4) {
|
|
385
396
|
fillRange(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
386
397
|
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, centerWidth - 2), {
|
|
387
398
|
fg: UI_TONES.fg.primary,
|
|
388
399
|
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
389
400
|
bold: true,
|
|
390
401
|
});
|
|
391
|
-
} else if (row ===
|
|
402
|
+
} else if (row === 5) {
|
|
392
403
|
fillRange(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
393
404
|
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(selectedText.hint, centerWidth - 2), {
|
|
394
405
|
fg: UI_TONES.fg.secondary,
|
|
@@ -473,7 +484,7 @@ function renderCollapsedLayout(
|
|
|
473
484
|
const innerStart = layout.margin + 1;
|
|
474
485
|
const innerWidth = layout.innerWidth;
|
|
475
486
|
const descriptionLines = wrapText(currentStep.description, Math.max(14, innerWidth - 2)).slice(0, 2);
|
|
476
|
-
const fieldStartRow =
|
|
487
|
+
const fieldStartRow = 6;
|
|
477
488
|
const selectedText = selectedFieldText(wizard);
|
|
478
489
|
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
|
|
479
490
|
|
|
@@ -491,7 +502,7 @@ function renderCollapsedLayout(
|
|
|
491
502
|
bg: headerBg,
|
|
492
503
|
bold: true,
|
|
493
504
|
});
|
|
494
|
-
const meta = `${wizard.stepIndex + 1}/${wizard.steps.length} •
|
|
505
|
+
const meta = `${wizard.stepIndex + 1}/${wizard.steps.length} • ${changedScreensLabel(wizard)}`;
|
|
495
506
|
putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
|
|
496
507
|
fg: UI_TONES.fg.secondary,
|
|
497
508
|
bg: headerBg,
|
|
@@ -533,19 +544,24 @@ function renderCollapsedLayout(
|
|
|
533
544
|
bg: bodyBg,
|
|
534
545
|
});
|
|
535
546
|
} else if (row === 2) {
|
|
547
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[1] ?? '', {
|
|
548
|
+
fg: UI_TONES.fg.secondary,
|
|
549
|
+
bg: bodyBg,
|
|
550
|
+
});
|
|
551
|
+
} else if (row === 3) {
|
|
536
552
|
fillRange(line, innerStart, innerWidth, UI_TONES.bg.section);
|
|
537
553
|
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(controlsText(wizard), innerWidth - 2), {
|
|
538
554
|
fg: UI_TONES.state.info,
|
|
539
555
|
bg: UI_TONES.bg.section,
|
|
540
556
|
});
|
|
541
|
-
} else if (row ===
|
|
557
|
+
} else if (row === 4) {
|
|
542
558
|
fillRange(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
543
559
|
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, innerWidth - 2), {
|
|
544
560
|
fg: UI_TONES.fg.primary,
|
|
545
561
|
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
546
562
|
bold: true,
|
|
547
563
|
});
|
|
548
|
-
} else if (row ===
|
|
564
|
+
} else if (row === 5) {
|
|
549
565
|
fillRange(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
550
566
|
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(selectedText.hint, innerWidth - 2), {
|
|
551
567
|
fg: UI_TONES.fg.secondary,
|
|
@@ -592,7 +608,8 @@ export function renderOnboardingWizard(
|
|
|
592
608
|
width: number,
|
|
593
609
|
viewportHeight: number,
|
|
594
610
|
): Line[] {
|
|
595
|
-
const
|
|
611
|
+
const margin = width >= 64 ? 1 : 0;
|
|
612
|
+
const layout = createOverlayBoxLayout(width, margin, Math.max(20, width - margin * 2));
|
|
596
613
|
const collapsed = layout.innerWidth < 86;
|
|
597
614
|
return collapsed
|
|
598
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.',
|
|
@@ -11,6 +11,7 @@ import { Compositor } from '../renderer/compositor.ts';
|
|
|
11
11
|
import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions/prompt';
|
|
12
12
|
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
13
13
|
import type { ConversationFollowUpItem } from '@pellux/goodvibes-sdk/platform/core/conversation-follow-ups';
|
|
14
|
+
import type { OrchestratorUserInputOptions } from '../core/orchestrator.ts';
|
|
14
15
|
import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/control-plane/gateway';
|
|
15
16
|
import type { MutableRuntimeState } from '@pellux/goodvibes-sdk/platform/runtime/mutable-runtime-state';
|
|
16
17
|
import type { BootstrapOptions } from './context.ts';
|
|
@@ -55,12 +56,32 @@ export interface BootstrapCoreState {
|
|
|
55
56
|
* When non-null, COMPANION_MESSAGE_RECEIVED fires a real LLM turn via
|
|
56
57
|
* orchestrator.handleUserInput() instead of only appending the user message.
|
|
57
58
|
*/
|
|
58
|
-
readonly orchestratorHandleUserInputRef: { value: ((text: string) => void) | null };
|
|
59
|
+
readonly orchestratorHandleUserInputRef: { value: ((text: string, options?: OrchestratorUserInputOptions) => void) | null };
|
|
59
60
|
readonly requestRender: () => void;
|
|
60
61
|
readonly setRenderRequest: (fn: () => void) => void;
|
|
61
62
|
readonly runtimeSessionIdRef: { value: string };
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
export type CompanionMessagePayload = Extract<SessionEvent, { type: 'COMPANION_MESSAGE_RECEIVED' }>;
|
|
66
|
+
|
|
67
|
+
export function companionMessageToOrchestratorInputOptions(
|
|
68
|
+
payload: CompanionMessagePayload,
|
|
69
|
+
): OrchestratorUserInputOptions {
|
|
70
|
+
const metadata = payload.metadata;
|
|
71
|
+
const surface = typeof metadata?.surface === 'string' ? metadata.surface : undefined;
|
|
72
|
+
const topic = typeof metadata?.topic === 'string' ? metadata.topic : undefined;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
origin: {
|
|
76
|
+
source: payload.source,
|
|
77
|
+
messageId: payload.messageId,
|
|
78
|
+
...(surface ? { surface } : {}),
|
|
79
|
+
...(topic ? { topic } : {}),
|
|
80
|
+
...(metadata ? { metadata } : {}),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
export async function initializeBootstrapCore(
|
|
65
86
|
stdout: NodeJS.WriteStream,
|
|
66
87
|
options: BootstrapOptions,
|
|
@@ -362,13 +383,17 @@ export async function initializeBootstrapCore(
|
|
|
362
383
|
// The fallback (ref not yet set) adds the message to the conversation view only —
|
|
363
384
|
// this path is unreachable in practice because the event bus is not connected to
|
|
364
385
|
// any live HTTP traffic until after the orchestrator is wired in bootstrap.ts.
|
|
365
|
-
const orchestratorHandleUserInputRef: {
|
|
386
|
+
const orchestratorHandleUserInputRef: {
|
|
387
|
+
value: ((text: string, options?: OrchestratorUserInputOptions) => void) | null;
|
|
388
|
+
} = { value: null };
|
|
366
389
|
runtimeUnsubs.push(runtimeBus.on<Extract<SessionEvent, { type: 'COMPANION_MESSAGE_RECEIVED' }>>(
|
|
367
390
|
'COMPANION_MESSAGE_RECEIVED',
|
|
368
391
|
({ payload }) => {
|
|
369
392
|
if (orchestratorHandleUserInputRef.value) {
|
|
370
393
|
// Delegate to the orchestrator: adds user message + fires a real LLM turn.
|
|
371
|
-
|
|
394
|
+
// Preserve surface origin metadata so the SDK can correlate replies back
|
|
395
|
+
// to the originating external channel, including ntfy chat topics.
|
|
396
|
+
orchestratorHandleUserInputRef.value(payload.body, companionMessageToOrchestratorInputOptions(payload));
|
|
372
397
|
} else {
|
|
373
398
|
// Fallback: render the user message immediately (orchestrator not yet ready).
|
|
374
399
|
conversation.addUserMessage(payload.body);
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import net from 'node:net';
|
|
14
|
-
import { Orchestrator } from '../core/orchestrator.ts';
|
|
14
|
+
import { Orchestrator, type OrchestratorUserInputOptions } from '../core/orchestrator.ts';
|
|
15
15
|
import { AcpManager } from '@pellux/goodvibes-sdk/platform/acp/manager';
|
|
16
16
|
import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvibes-sdk/platform/providers/tier-prompts';
|
|
17
17
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
|
|
@@ -195,8 +195,10 @@ export async function bootstrapRuntime(
|
|
|
195
195
|
return supplement ? runtime.systemPrompt + '\n\n' + supplement : runtime.systemPrompt;
|
|
196
196
|
},
|
|
197
197
|
hookDispatcher,
|
|
198
|
+
flagManager: services.featureFlags,
|
|
198
199
|
requestRender: () => orchestratorRefs.requestRender(),
|
|
199
200
|
runtimeBus,
|
|
201
|
+
sessionId: runtime.sessionId,
|
|
200
202
|
services: {
|
|
201
203
|
agentManager: services.agentManager,
|
|
202
204
|
wrfcController: services.wrfcController,
|
|
@@ -204,8 +206,8 @@ export async function bootstrapRuntime(
|
|
|
204
206
|
});
|
|
205
207
|
conversationFollowUpRef.value = (item) => orchestrator.enqueueConversationFollowUp(item);
|
|
206
208
|
// Wire orchestratorHandleUserInputRef so COMPANION_MESSAGE_RECEIVED fires a real LLM turn.
|
|
207
|
-
orchestratorHandleUserInputRef.value = (text: string) => {
|
|
208
|
-
orchestrator.handleUserInput(text).catch((err: unknown) => {
|
|
209
|
+
orchestratorHandleUserInputRef.value = (text: string, options?: OrchestratorUserInputOptions) => {
|
|
210
|
+
orchestrator.handleUserInput(text, undefined, options).catch((err: unknown) => {
|
|
209
211
|
logger.debug('companion handleUserInput safety catch', { error: String(err) });
|
|
210
212
|
});
|
|
211
213
|
};
|
|
@@ -370,6 +372,20 @@ export async function bootstrapRuntime(
|
|
|
370
372
|
};
|
|
371
373
|
};
|
|
372
374
|
|
|
375
|
+
const waitForConfigDrivenRestarts = async (handle: ExternalServicesHandle): Promise<void> => {
|
|
376
|
+
const waitForRestart = async (service: unknown): Promise<void> => {
|
|
377
|
+
const maybeRestarting = service as { waitForRestart?: unknown } | null;
|
|
378
|
+
if (typeof maybeRestarting?.waitForRestart === 'function') {
|
|
379
|
+
await maybeRestarting.waitForRestart();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
await Promise.all([
|
|
384
|
+
waitForRestart(handle.daemonServer),
|
|
385
|
+
waitForRestart(handle.httpListener),
|
|
386
|
+
]);
|
|
387
|
+
};
|
|
388
|
+
|
|
373
389
|
let externalServices: ExternalServicesHandle = {
|
|
374
390
|
daemonServer: null,
|
|
375
391
|
httpListener: null,
|
|
@@ -401,6 +417,7 @@ export async function bootstrapRuntime(
|
|
|
401
417
|
// A failed previous startup should not prevent a restart attempt.
|
|
402
418
|
}
|
|
403
419
|
}
|
|
420
|
+
await waitForConfigDrivenRestarts(externalServices);
|
|
404
421
|
await externalServices.stop();
|
|
405
422
|
const previousBindings = externalServiceBindings;
|
|
406
423
|
externalServiceBindings = readExternalServiceBindings();
|
|
@@ -2,6 +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 type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
5
6
|
import {
|
|
6
7
|
getOnboardingRuntimeStatePath,
|
|
7
8
|
readOnboardingRuntimeState,
|
|
@@ -133,11 +134,39 @@ function isMalformedGoodVibesSecretReferenceValue(value: string): boolean {
|
|
|
133
134
|
return normalized.startsWith('goodvibes://') && !isGoodVibesSecretReferenceValue(normalized);
|
|
134
135
|
}
|
|
135
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
|
+
|
|
136
163
|
function validateConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): void {
|
|
137
164
|
if (typeof operation.value === 'string' && isMalformedGoodVibesSecretReferenceValue(operation.value)) {
|
|
138
165
|
throw new Error(`Config key ${operation.key} only accepts goodvibes://secrets/... secret references.`);
|
|
139
166
|
}
|
|
140
167
|
|
|
168
|
+
if (validateFeatureFlagConfigValue(operation)) return;
|
|
169
|
+
|
|
141
170
|
const schema = CONFIG_SCHEMA.find((entry) => entry.key === operation.key);
|
|
142
171
|
if (!schema) {
|
|
143
172
|
const defaultValue = operation.key.split('.').reduce<unknown>((cursor, part) => (
|
|
@@ -204,9 +233,6 @@ function validateAuthOperation(
|
|
|
204
233
|
if (existing && !requiredRoles.every((role) => existing.roles.includes(role))) {
|
|
205
234
|
throw new Error(`Existing local auth user ${username} is missing required role(s): ${requiredRoles.join(', ')}.`);
|
|
206
235
|
}
|
|
207
|
-
if (existing && operation.retireBootstrapCredential) {
|
|
208
|
-
throw new Error('Replacing a bootstrap credential requires a new local admin username.');
|
|
209
|
-
}
|
|
210
236
|
}
|
|
211
237
|
|
|
212
238
|
function validateAcknowledgementOperation(
|
|
@@ -242,7 +268,7 @@ function applyConfigOperation(
|
|
|
242
268
|
};
|
|
243
269
|
}
|
|
244
270
|
|
|
245
|
-
deps.config.setDynamic(operation.key, operation.value);
|
|
271
|
+
deps.config.setDynamic(operation.key as never, operation.value);
|
|
246
272
|
return {
|
|
247
273
|
kind: operation.kind,
|
|
248
274
|
summary: `Updated ${operation.key} in global onboarding settings.`,
|
|
@@ -278,7 +304,9 @@ function applyAuthOperation(
|
|
|
278
304
|
? parseBootstrapCredential(readFileSync(before.bootstrapCredentialPath, 'utf-8'))
|
|
279
305
|
: null;
|
|
280
306
|
|
|
281
|
-
if (
|
|
307
|
+
if (existing) {
|
|
308
|
+
auth.rotatePassword(username, operation.password);
|
|
309
|
+
} else {
|
|
282
310
|
auth.addUser(username, operation.password, operation.roles ?? ['admin']);
|
|
283
311
|
}
|
|
284
312
|
|
|
@@ -296,7 +324,7 @@ function applyAuthOperation(
|
|
|
296
324
|
return {
|
|
297
325
|
kind: operation.kind,
|
|
298
326
|
summary: existing
|
|
299
|
-
? `
|
|
327
|
+
? `Updated local auth user ${username}.`
|
|
300
328
|
: `Created local auth user ${username}.`,
|
|
301
329
|
};
|
|
302
330
|
}
|
|
@@ -391,9 +419,9 @@ async function buildRollbackAction(
|
|
|
391
419
|
);
|
|
392
420
|
}
|
|
393
421
|
|
|
394
|
-
const previous = deps.config.get(operation.key);
|
|
422
|
+
const previous = deps.config.get(operation.key as never);
|
|
395
423
|
return () => {
|
|
396
|
-
deps.config.setDynamic(operation.key, previous);
|
|
424
|
+
deps.config.setDynamic(operation.key as never, previous);
|
|
397
425
|
};
|
|
398
426
|
}
|
|
399
427
|
|
|
@@ -240,22 +240,22 @@ function hasExternalIntegrations(snapshot: OnboardingSnapshotState): boolean {
|
|
|
240
240
|
|
|
241
241
|
function describeLocalTuiOnly(snapshot: OnboardingSnapshotState): string {
|
|
242
242
|
if (!hasAnyServerEnabled(snapshot)) {
|
|
243
|
-
return '
|
|
243
|
+
return 'Use GoodVibes only in this terminal. No browser access, background service, HTTP listener, external app surface, or network setup.';
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
return '
|
|
246
|
+
return 'Turn off browser access, background services, HTTP listeners, external app surfaces, and network setup.';
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function describeBrowserAccess(snapshot: OnboardingSnapshotState): string {
|
|
250
250
|
return snapshot.bindSettings.web.enabled
|
|
251
|
-
? 'Keep the background service and web UI enabled
|
|
252
|
-
: '
|
|
251
|
+
? 'Keep the background service and web UI enabled. Network reachability is controlled on the next screen.'
|
|
252
|
+
: 'Run the background service and web UI. GoodVibes will use the local network by default; you can restrict or customize it next.';
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
function describeRemoteDeviceAccess(snapshot: OnboardingSnapshotState): string {
|
|
256
256
|
return hasRemoteDeviceAccess(snapshot)
|
|
257
|
-
? 'Keep enabled GoodVibes services reachable from other devices on your LAN. Local
|
|
258
|
-
: '
|
|
257
|
+
? 'Keep enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.'
|
|
258
|
+
: 'Make enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.';
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
function describeWebhookIngress(snapshot: OnboardingSnapshotState): string {
|
|
@@ -272,7 +272,7 @@ function describeExternalIntegrations(snapshot: OnboardingSnapshotState): string
|
|
|
272
272
|
]).size;
|
|
273
273
|
|
|
274
274
|
if (integrationCount === 0) {
|
|
275
|
-
return '
|
|
275
|
+
return 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.';
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
return `Review and configure ${integrationCount} detected external app, service, or surface integration signal(s).`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../../config/index.ts';
|
|
2
2
|
import type { SecretsManager, SecretRecord, SecretStorageReview } from '../../config/secrets.ts';
|
|
3
|
+
import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
3
4
|
import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security/user-auth';
|
|
4
5
|
import type { ShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
|
|
5
6
|
import type {
|
|
@@ -45,6 +46,7 @@ export interface OnboardingConfigSnapshot {
|
|
|
45
46
|
readonly network: GoodVibesConfig['network'];
|
|
46
47
|
readonly surfaces: GoodVibesConfig['surfaces'];
|
|
47
48
|
readonly service: GoodVibesConfig['service'];
|
|
49
|
+
readonly featureFlags: GoodVibesConfig['featureFlags'];
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface OnboardingProviderRoutingSnapshot {
|
|
@@ -235,7 +237,7 @@ export interface OnboardingStepDerivationState {
|
|
|
235
237
|
export type OnboardingApplyOperation =
|
|
236
238
|
| {
|
|
237
239
|
readonly kind: 'set-config';
|
|
238
|
-
readonly key: ConfigKey;
|
|
240
|
+
readonly key: ConfigKey | FeatureFlagConfigKey;
|
|
239
241
|
readonly value: unknown;
|
|
240
242
|
readonly scope?: 'global' | 'project';
|
|
241
243
|
}
|
|
@@ -381,6 +383,7 @@ export interface OnboardingApplyDependencies {
|
|
|
381
383
|
| 'getUser'
|
|
382
384
|
| 'inspect'
|
|
383
385
|
| 'revokeSession'
|
|
386
|
+
| 'rotatePassword'
|
|
384
387
|
>;
|
|
385
388
|
readonly shellPaths: OnboardingShellPaths;
|
|
386
389
|
readonly acknowledgementScope?: OnboardingStateScope;
|
|
@@ -43,7 +43,7 @@ function verifyConfigOperation(
|
|
|
43
43
|
deps: OnboardingVerificationDependencies,
|
|
44
44
|
operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>,
|
|
45
45
|
): OnboardingVerificationItem {
|
|
46
|
-
const actual = deps.config.get(operation.key);
|
|
46
|
+
const actual = deps.config.get(operation.key as never);
|
|
47
47
|
const ok = isDeepEqual(actual, operation.value);
|
|
48
48
|
|
|
49
49
|
return {
|