@pellux/goodvibes-tui 0.19.24 → 0.19.26
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 +13 -0
- package/README.md +5 -5
- package/bin/goodvibes +10 -0
- package/bin/goodvibes-daemon +10 -0
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +3 -2
- package/src/cli/bundle-command.ts +225 -0
- package/src/cli/completion.ts +90 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +169 -0
- package/src/cli/help.ts +301 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/management-commands.ts +426 -0
- package/src/cli/management.ts +719 -0
- package/src/cli/network-posture.ts +46 -0
- package/src/cli/package-verification.ts +119 -0
- package/src/cli/parser.ts +369 -0
- package/src/cli/provider-classification.ts +107 -0
- package/src/cli/redaction.ts +105 -0
- package/src/cli/service-command.ts +45 -0
- package/src/cli/service-posture.ts +247 -0
- package/src/cli/status.ts +382 -0
- package/src/cli/surface-command.ts +248 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +69 -0
- package/src/cli-flags.ts +18 -55
- package/src/config/index.ts +1 -1
- package/src/config/secrets.ts +44 -0
- package/src/daemon/cli.ts +62 -11
- package/src/input/command-registry.ts +3 -0
- package/src/input/commands/guidance-runtime.ts +9 -4
- package/src/input/commands/local-runtime.ts +21 -7
- package/src/input/commands/local-setup.ts +31 -38
- package/src/input/commands/onboarding-runtime.ts +14 -0
- package/src/input/commands/runtime-services.ts +9 -0
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +8 -1
- package/src/input/handler-feed.ts +13 -8
- package/src/input/handler-interactions.ts +266 -0
- package/src/input/handler-modal-stack.ts +23 -3
- package/src/input/handler-modal-token-routes.ts +23 -1
- package/src/input/handler-onboarding.ts +696 -0
- package/src/input/handler-picker-routes.ts +15 -7
- package/src/input/handler-ui-state.ts +58 -0
- package/src/input/handler.ts +120 -246
- package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
- package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
- package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
- package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
- package/src/input/onboarding/onboarding-wizard.ts +594 -0
- package/src/main.ts +32 -39
- package/src/panels/builtin/operations.ts +0 -10
- package/src/panels/index.ts +0 -1
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
- package/src/runtime/bootstrap-core.ts +1 -0
- package/src/runtime/bootstrap.ts +123 -0
- package/src/runtime/onboarding/apply.ts +685 -0
- package/src/runtime/onboarding/derivation.ts +495 -0
- package/src/runtime/onboarding/index.ts +7 -0
- package/src/runtime/onboarding/markers.ts +161 -0
- package/src/runtime/onboarding/snapshot.ts +400 -0
- package/src/runtime/onboarding/state.ts +140 -0
- package/src/runtime/onboarding/types.ts +402 -0
- package/src/runtime/onboarding/verify.ts +233 -0
- package/src/runtime/ui-services.ts +16 -0
- package/src/shell/ui-openers.ts +12 -2
- package/src/version.ts +1 -1
- package/src/panels/welcome-panel.ts +0 -64
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import type { Line } from '../../types/grid.ts';
|
|
2
|
+
import { createStyledCell } from '../../types/grid.ts';
|
|
3
|
+
import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText } from '../../utils/terminal-width.ts';
|
|
4
|
+
import {
|
|
5
|
+
createOverlayBoxLayout,
|
|
6
|
+
createOverlayContentLine,
|
|
7
|
+
createOverlayFilledBorderLine,
|
|
8
|
+
DEFAULT_OVERLAY_PALETTE,
|
|
9
|
+
OVERLAY_GLYPHS,
|
|
10
|
+
putOverlayText,
|
|
11
|
+
} from '../overlay-box.ts';
|
|
12
|
+
import { UI_TONES } from '../ui-primitives.ts';
|
|
13
|
+
import {
|
|
14
|
+
getOnboardingWizardBodyRows,
|
|
15
|
+
getOnboardingWizardVisibleFieldCount,
|
|
16
|
+
type OnboardingWizardController,
|
|
17
|
+
type OnboardingWizardFieldDefinition,
|
|
18
|
+
type OnboardingWizardStepDefinition,
|
|
19
|
+
} from '../../input/onboarding/onboarding-wizard.ts';
|
|
20
|
+
|
|
21
|
+
type RenderedFieldRow =
|
|
22
|
+
| { readonly kind: 'empty' }
|
|
23
|
+
| { readonly kind: 'moreAbove'; readonly text: string }
|
|
24
|
+
| { readonly kind: 'moreBelow'; readonly text: string }
|
|
25
|
+
| {
|
|
26
|
+
readonly kind: 'field';
|
|
27
|
+
readonly field: OnboardingWizardFieldDefinition;
|
|
28
|
+
readonly absoluteIndex: number;
|
|
29
|
+
readonly line: 0 | 1;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function clamp(value: number, min: number, max: number): number {
|
|
33
|
+
return Math.max(min, Math.min(max, value));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fillRange(line: Line, startX: number, width: number, bg: string): void {
|
|
37
|
+
for (let x = startX; x < Math.min(line.length, startX + width); x += 1) {
|
|
38
|
+
const cell = line[x];
|
|
39
|
+
if (!cell) continue;
|
|
40
|
+
line[x] = createStyledCell(cell.char, {
|
|
41
|
+
fg: cell.fg,
|
|
42
|
+
bg,
|
|
43
|
+
bold: cell.bold,
|
|
44
|
+
dim: cell.dim,
|
|
45
|
+
underline: cell.underline,
|
|
46
|
+
italic: cell.italic,
|
|
47
|
+
strikethrough: cell.strikethrough,
|
|
48
|
+
link: cell.link,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function drawVerticalRule(line: Line, x: number, fg: string, bg = ''): void {
|
|
54
|
+
if (x < 0 || x >= line.length) return;
|
|
55
|
+
line[x] = createStyledCell('│', { fg, bg });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function modeLabel(mode: OnboardingWizardController['mode']): string {
|
|
59
|
+
if (mode === 'edit') return 'Edit existing';
|
|
60
|
+
if (mode === 'reopen') return 'Reopen review';
|
|
61
|
+
return 'New setup';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stepGlyph(
|
|
65
|
+
wizard: OnboardingWizardController,
|
|
66
|
+
step: OnboardingWizardStepDefinition,
|
|
67
|
+
stepIndex: number,
|
|
68
|
+
): { readonly glyph: string; readonly fg: string } {
|
|
69
|
+
if (stepIndex === wizard.stepIndex) {
|
|
70
|
+
return { glyph: OVERLAY_GLYPHS.selected, fg: UI_TONES.state.active };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const total = wizard.getStepFieldCount(stepIndex);
|
|
74
|
+
const completed = wizard.getCompletedFieldCount(stepIndex);
|
|
75
|
+
if (wizard.isStepDirty(stepIndex)) {
|
|
76
|
+
return { glyph: '◈', fg: UI_TONES.state.warn };
|
|
77
|
+
}
|
|
78
|
+
if (total > 0 && completed === total) {
|
|
79
|
+
return { glyph: '✓', fg: UI_TONES.state.good };
|
|
80
|
+
}
|
|
81
|
+
return { glyph: '•', fg: UI_TONES.fg.muted };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function fieldBadgeTone(
|
|
85
|
+
wizard: OnboardingWizardController,
|
|
86
|
+
field: OnboardingWizardFieldDefinition,
|
|
87
|
+
): string {
|
|
88
|
+
if (field.kind === 'status') return UI_TONES.state.info;
|
|
89
|
+
if (field.kind === 'modelPicker') return UI_TONES.state.info;
|
|
90
|
+
if (field.kind === 'acknowledgement') {
|
|
91
|
+
const label = wizard.getFieldValueLabel(field);
|
|
92
|
+
return label === 'Accepted' ? UI_TONES.state.good : label === 'Pending' ? UI_TONES.state.warn : UI_TONES.fg.muted;
|
|
93
|
+
}
|
|
94
|
+
if (field.kind === 'checklist') {
|
|
95
|
+
return wizard.getFieldValue(field) ? UI_TONES.state.good : UI_TONES.fg.muted;
|
|
96
|
+
}
|
|
97
|
+
if (field.kind === 'radio') return UI_TONES.state.active;
|
|
98
|
+
if (field.kind === 'masked') return UI_TONES.state.warn;
|
|
99
|
+
return UI_TONES.fg.secondary;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildFieldRows(
|
|
103
|
+
wizard: OnboardingWizardController,
|
|
104
|
+
visibleFields: number,
|
|
105
|
+
capacity: number,
|
|
106
|
+
): readonly RenderedFieldRow[] {
|
|
107
|
+
const fieldWindow = wizard.getFieldWindow(visibleFields);
|
|
108
|
+
const rows: RenderedFieldRow[] = [];
|
|
109
|
+
|
|
110
|
+
if (fieldWindow.start > 0) {
|
|
111
|
+
rows.push({
|
|
112
|
+
kind: 'moreAbove',
|
|
113
|
+
text: `${OVERLAY_GLYPHS.moreAbove} ${fieldWindow.start} more above`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fieldWindow.fields.forEach((field, index) => {
|
|
118
|
+
const absoluteIndex = fieldWindow.start + index;
|
|
119
|
+
rows.push({ kind: 'field', field, absoluteIndex, line: 0 });
|
|
120
|
+
rows.push({ kind: 'field', field, absoluteIndex, line: 1 });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (fieldWindow.end < fieldWindow.total) {
|
|
124
|
+
rows.push({
|
|
125
|
+
kind: 'moreBelow',
|
|
126
|
+
text: `${OVERLAY_GLYPHS.moreBelow} ${fieldWindow.total - fieldWindow.end} more below`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
while (rows.length < capacity) rows.push({ kind: 'empty' });
|
|
131
|
+
return rows.slice(0, capacity);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fieldHint(
|
|
135
|
+
wizard: OnboardingWizardController,
|
|
136
|
+
field: OnboardingWizardFieldDefinition,
|
|
137
|
+
selected: boolean,
|
|
138
|
+
): string {
|
|
139
|
+
if (
|
|
140
|
+
selected
|
|
141
|
+
&& wizard.isEditingTextField()
|
|
142
|
+
&& wizard.editingFieldId === field.id
|
|
143
|
+
&& (field.kind === 'text' || field.kind === 'masked')
|
|
144
|
+
) {
|
|
145
|
+
const rawValue = wizard.editBuffer.length > 0 ? wizard.editBuffer : field.placeholder;
|
|
146
|
+
const editingValue = field.kind === 'masked' && wizard.editBuffer.length > 0
|
|
147
|
+
? '•'.repeat(Math.min(12, Math.max(4, wizard.editBuffer.length)))
|
|
148
|
+
: rawValue;
|
|
149
|
+
return `Editing: ${editingValue}█`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (selected && field.kind === 'modelPicker') return `${field.hint} Press Enter to open picker.`;
|
|
153
|
+
if (selected && field.kind === 'text') return `${field.hint} Press Enter to edit inline.`;
|
|
154
|
+
if (selected && field.kind === 'masked') return `${field.hint} Press Enter to edit inline.`;
|
|
155
|
+
return field.hint;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderFieldRow(
|
|
159
|
+
line: Line,
|
|
160
|
+
wizard: OnboardingWizardController,
|
|
161
|
+
fieldRow: RenderedFieldRow,
|
|
162
|
+
startX: number,
|
|
163
|
+
width: number,
|
|
164
|
+
): void {
|
|
165
|
+
if (fieldRow.kind === 'empty') return;
|
|
166
|
+
|
|
167
|
+
if (fieldRow.kind === 'moreAbove' || fieldRow.kind === 'moreBelow') {
|
|
168
|
+
putOverlayText(line, startX + 1, width - 2, truncateDisplay(fieldRow.text, width - 2), {
|
|
169
|
+
fg: UI_TONES.fg.muted,
|
|
170
|
+
bg: UI_TONES.bg.base,
|
|
171
|
+
dim: true,
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const selected = fieldRow.absoluteIndex === wizard.getSelectedFieldIndex();
|
|
177
|
+
const field = fieldRow.field;
|
|
178
|
+
const fieldBg = selected ? DEFAULT_OVERLAY_PALETTE.selectedBg : UI_TONES.bg.base;
|
|
179
|
+
fillRange(line, startX, width, fieldBg);
|
|
180
|
+
|
|
181
|
+
if (fieldRow.line === 0) {
|
|
182
|
+
const badge = truncateDisplay(`[${wizard.getFieldValueLabel(field)}]`, Math.max(8, Math.floor(width * 0.38)));
|
|
183
|
+
const badgeWidth = getDisplayWidth(badge);
|
|
184
|
+
const prefix = selected ? `${OVERLAY_GLYPHS.selected} ` : wizard.isFieldDirty(field.id) ? '◇ ' : ' ';
|
|
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
|
+
}
|
|
199
|
+
|
|
200
|
+
putOverlayText(line, startX + 3, Math.max(0, width - 4), truncateDisplay(fieldHint(wizard, field, selected), Math.max(0, width - 4)), {
|
|
201
|
+
fg: selected ? UI_TONES.fg.secondary : UI_TONES.fg.muted,
|
|
202
|
+
bg: fieldBg,
|
|
203
|
+
dim: !selected,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function footerText(wizard: OnboardingWizardController): string {
|
|
208
|
+
if (wizard.isEditingTextField()) {
|
|
209
|
+
return '[Enter] Save [Esc] Cancel [Backspace] Delete [Type] Edit value';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return '[Tab/Shift+Tab] Step [↑↓/j/k] Move [Enter/Space] Toggle/Open [1-9] Jump [Esc] Close';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderWideLayout(
|
|
216
|
+
wizard: OnboardingWizardController,
|
|
217
|
+
width: number,
|
|
218
|
+
viewportHeight: number,
|
|
219
|
+
layout: ReturnType<typeof createOverlayBoxLayout>,
|
|
220
|
+
): Line[] {
|
|
221
|
+
const lines: Line[] = [];
|
|
222
|
+
const bodyRows = getOnboardingWizardBodyRows(viewportHeight);
|
|
223
|
+
const visibleFields = getOnboardingWizardVisibleFieldCount(viewportHeight);
|
|
224
|
+
const currentStep = wizard.currentStep;
|
|
225
|
+
const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
|
|
226
|
+
const headerBg = UI_TONES.bg.title;
|
|
227
|
+
const railBg = UI_TONES.bg.section;
|
|
228
|
+
const bodyBg = UI_TONES.bg.base;
|
|
229
|
+
const summaryBg = UI_TONES.bg.summary;
|
|
230
|
+
const innerLeft = layout.margin + 1;
|
|
231
|
+
const availableInner = layout.innerWidth - 2;
|
|
232
|
+
const leftWidthBase = layout.innerWidth >= 108 ? 22 : 18;
|
|
233
|
+
const rightWidthBase = layout.innerWidth >= 108 ? 30 : 24;
|
|
234
|
+
const minCenterWidth = 34;
|
|
235
|
+
let leftWidth = Math.min(leftWidthBase, Math.max(16, availableInner - minCenterWidth - 12));
|
|
236
|
+
let rightWidth = Math.min(rightWidthBase, Math.max(20, availableInner - minCenterWidth - leftWidth));
|
|
237
|
+
let centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
|
|
238
|
+
|
|
239
|
+
if (centerWidth < minCenterWidth) {
|
|
240
|
+
const deficit = minCenterWidth - centerWidth;
|
|
241
|
+
const leftCut = Math.min(Math.max(0, leftWidth - 16), Math.ceil(deficit / 2));
|
|
242
|
+
leftWidth -= leftCut;
|
|
243
|
+
rightWidth -= Math.min(Math.max(0, rightWidth - 20), deficit - leftCut);
|
|
244
|
+
centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const leftStart = innerLeft;
|
|
248
|
+
const leftSeparatorX = leftStart + leftWidth;
|
|
249
|
+
const centerStart = leftSeparatorX + 1;
|
|
250
|
+
const rightSeparatorX = centerStart + centerWidth;
|
|
251
|
+
const rightStart = rightSeparatorX + 1;
|
|
252
|
+
const descriptionLines = wrapText(currentStep.description, Math.max(18, centerWidth - 2)).slice(0, 2);
|
|
253
|
+
const summaryLines = [
|
|
254
|
+
currentStep.summaryTitle,
|
|
255
|
+
...currentStep.summaryLines,
|
|
256
|
+
`Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
|
|
257
|
+
`Dirty steps ${wizard.dirtyStepCount}`,
|
|
258
|
+
`Pending picker ${wizard.pendingModelPickerTarget ?? 'none'}`,
|
|
259
|
+
];
|
|
260
|
+
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - 3));
|
|
261
|
+
|
|
262
|
+
const topLine = createOverlayFilledBorderLine(
|
|
263
|
+
width,
|
|
264
|
+
layout,
|
|
265
|
+
OVERLAY_GLYPHS.topLeft,
|
|
266
|
+
OVERLAY_GLYPHS.horizontal,
|
|
267
|
+
OVERLAY_GLYPHS.topRight,
|
|
268
|
+
borderFg,
|
|
269
|
+
headerBg,
|
|
270
|
+
);
|
|
271
|
+
putOverlayText(topLine, layout.margin + 2, layout.width - 4, 'Onboarding Wizard', {
|
|
272
|
+
fg: UI_TONES.fg.primary,
|
|
273
|
+
bg: headerBg,
|
|
274
|
+
bold: true,
|
|
275
|
+
});
|
|
276
|
+
const meta = `${modeLabel(wizard.mode)} ${wizard.stepIndex + 1}/${wizard.steps.length} dirty ${wizard.dirtyStepCount}`;
|
|
277
|
+
putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
|
|
278
|
+
fg: UI_TONES.fg.secondary,
|
|
279
|
+
bg: headerBg,
|
|
280
|
+
});
|
|
281
|
+
lines.push(topLine);
|
|
282
|
+
|
|
283
|
+
const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
|
|
284
|
+
fillRange(headerLine, leftStart, leftWidth, railBg);
|
|
285
|
+
fillRange(headerLine, centerStart, centerWidth, headerBg);
|
|
286
|
+
fillRange(headerLine, rightStart, rightWidth, summaryBg);
|
|
287
|
+
drawVerticalRule(headerLine, leftSeparatorX, borderFg, headerBg);
|
|
288
|
+
drawVerticalRule(headerLine, rightSeparatorX, borderFg, headerBg);
|
|
289
|
+
putOverlayText(headerLine, leftStart + 1, leftWidth - 2, 'Steps', {
|
|
290
|
+
fg: UI_TONES.fg.secondary,
|
|
291
|
+
bg: railBg,
|
|
292
|
+
bold: true,
|
|
293
|
+
});
|
|
294
|
+
putOverlayText(headerLine, centerStart + 1, centerWidth - 2, truncateDisplay(currentStep.title, centerWidth - 2), {
|
|
295
|
+
fg: UI_TONES.state.active,
|
|
296
|
+
bg: headerBg,
|
|
297
|
+
bold: true,
|
|
298
|
+
});
|
|
299
|
+
putOverlayText(headerLine, rightStart + 1, rightWidth - 2, 'Summary Rail', {
|
|
300
|
+
fg: UI_TONES.fg.secondary,
|
|
301
|
+
bg: summaryBg,
|
|
302
|
+
bold: true,
|
|
303
|
+
});
|
|
304
|
+
lines.push(headerLine);
|
|
305
|
+
|
|
306
|
+
lines.push(createOverlayFilledBorderLine(
|
|
307
|
+
width,
|
|
308
|
+
layout,
|
|
309
|
+
OVERLAY_GLYPHS.teeLeft,
|
|
310
|
+
OVERLAY_GLYPHS.horizontal,
|
|
311
|
+
OVERLAY_GLYPHS.teeRight,
|
|
312
|
+
borderFg,
|
|
313
|
+
bodyBg,
|
|
314
|
+
));
|
|
315
|
+
|
|
316
|
+
for (let row = 0; row < bodyRows; row += 1) {
|
|
317
|
+
const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
|
|
318
|
+
fillRange(line, leftStart, leftWidth, railBg);
|
|
319
|
+
fillRange(line, centerStart, centerWidth, bodyBg);
|
|
320
|
+
fillRange(line, rightStart, rightWidth, summaryBg);
|
|
321
|
+
drawVerticalRule(line, leftSeparatorX, borderFg);
|
|
322
|
+
drawVerticalRule(line, rightSeparatorX, borderFg);
|
|
323
|
+
|
|
324
|
+
if (row === 0) {
|
|
325
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(currentStep.title, centerWidth - 2), {
|
|
326
|
+
fg: UI_TONES.fg.primary,
|
|
327
|
+
bg: bodyBg,
|
|
328
|
+
bold: true,
|
|
329
|
+
});
|
|
330
|
+
} else if (row === 1) {
|
|
331
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[0] ?? '', {
|
|
332
|
+
fg: UI_TONES.fg.secondary,
|
|
333
|
+
bg: bodyBg,
|
|
334
|
+
});
|
|
335
|
+
} else if (row === 2) {
|
|
336
|
+
const status = fitDisplay(
|
|
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, {
|
|
341
|
+
fg: UI_TONES.fg.secondary,
|
|
342
|
+
bg: bodyBg,
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
renderFieldRow(line, wizard, fieldRows[row - 3] ?? { kind: 'empty' }, centerStart, centerWidth);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const step = wizard.steps[row] ?? null;
|
|
349
|
+
if (step) {
|
|
350
|
+
const stepState = stepGlyph(wizard, step, row);
|
|
351
|
+
const completion = `${wizard.getCompletedFieldCount(row)}/${wizard.getStepFieldCount(row)}`;
|
|
352
|
+
putOverlayText(line, leftStart + 1, leftWidth - 2, truncateDisplay(`${stepState.glyph} ${row + 1}. ${step.shortLabel}`, leftWidth - 2), {
|
|
353
|
+
fg: stepState.fg,
|
|
354
|
+
bg: railBg,
|
|
355
|
+
bold: row === wizard.stepIndex,
|
|
356
|
+
});
|
|
357
|
+
putOverlayText(line, Math.max(leftStart + 1, leftStart + leftWidth - getDisplayWidth(completion) - 2), leftWidth - 2, completion, {
|
|
358
|
+
fg: wizard.isStepDirty(row) ? UI_TONES.state.warn : UI_TONES.fg.muted,
|
|
359
|
+
bg: railBg,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const summaryText = summaryLines[row];
|
|
364
|
+
if (summaryText) {
|
|
365
|
+
putOverlayText(line, rightStart + 1, rightWidth - 2, truncateDisplay(summaryText, rightWidth - 2), {
|
|
366
|
+
fg: row === 0 ? UI_TONES.state.info : UI_TONES.fg.secondary,
|
|
367
|
+
bg: summaryBg,
|
|
368
|
+
bold: row === 0,
|
|
369
|
+
dim: row > 0,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
lines.push(line);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
lines.push(createOverlayFilledBorderLine(
|
|
377
|
+
width,
|
|
378
|
+
layout,
|
|
379
|
+
OVERLAY_GLYPHS.teeLeft,
|
|
380
|
+
OVERLAY_GLYPHS.horizontal,
|
|
381
|
+
OVERLAY_GLYPHS.teeRight,
|
|
382
|
+
borderFg,
|
|
383
|
+
bodyBg,
|
|
384
|
+
));
|
|
385
|
+
|
|
386
|
+
const footer = createOverlayFilledBorderLine(
|
|
387
|
+
width,
|
|
388
|
+
layout,
|
|
389
|
+
OVERLAY_GLYPHS.bottomLeft,
|
|
390
|
+
OVERLAY_GLYPHS.horizontal,
|
|
391
|
+
OVERLAY_GLYPHS.bottomRight,
|
|
392
|
+
borderFg,
|
|
393
|
+
headerBg,
|
|
394
|
+
);
|
|
395
|
+
putOverlayText(footer, layout.margin + 2, layout.width - 4, truncateDisplay(footerText(wizard), layout.width - 4), {
|
|
396
|
+
fg: UI_TONES.fg.muted,
|
|
397
|
+
bg: headerBg,
|
|
398
|
+
dim: true,
|
|
399
|
+
});
|
|
400
|
+
lines.push(footer);
|
|
401
|
+
|
|
402
|
+
return lines.slice(0, viewportHeight);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function renderCollapsedLayout(
|
|
406
|
+
wizard: OnboardingWizardController,
|
|
407
|
+
width: number,
|
|
408
|
+
viewportHeight: number,
|
|
409
|
+
layout: ReturnType<typeof createOverlayBoxLayout>,
|
|
410
|
+
): Line[] {
|
|
411
|
+
const lines: Line[] = [];
|
|
412
|
+
const bodyRows = getOnboardingWizardBodyRows(viewportHeight);
|
|
413
|
+
const visibleFields = getOnboardingWizardVisibleFieldCount(viewportHeight);
|
|
414
|
+
const currentStep = wizard.currentStep;
|
|
415
|
+
const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
|
|
416
|
+
const headerBg = UI_TONES.bg.title;
|
|
417
|
+
const bodyBg = UI_TONES.bg.base;
|
|
418
|
+
const innerStart = layout.margin + 1;
|
|
419
|
+
const innerWidth = layout.innerWidth;
|
|
420
|
+
const descriptionLines = wrapText(currentStep.description, Math.max(14, innerWidth - 2)).slice(0, 2);
|
|
421
|
+
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - 3));
|
|
422
|
+
|
|
423
|
+
const topLine = createOverlayFilledBorderLine(
|
|
424
|
+
width,
|
|
425
|
+
layout,
|
|
426
|
+
OVERLAY_GLYPHS.topLeft,
|
|
427
|
+
OVERLAY_GLYPHS.horizontal,
|
|
428
|
+
OVERLAY_GLYPHS.topRight,
|
|
429
|
+
borderFg,
|
|
430
|
+
headerBg,
|
|
431
|
+
);
|
|
432
|
+
putOverlayText(topLine, layout.margin + 2, layout.width - 4, 'Onboarding Wizard', {
|
|
433
|
+
fg: UI_TONES.fg.primary,
|
|
434
|
+
bg: headerBg,
|
|
435
|
+
bold: true,
|
|
436
|
+
});
|
|
437
|
+
const meta = `${wizard.stepIndex + 1}/${wizard.steps.length} • dirty ${wizard.dirtyStepCount}`;
|
|
438
|
+
putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
|
|
439
|
+
fg: UI_TONES.fg.secondary,
|
|
440
|
+
bg: headerBg,
|
|
441
|
+
});
|
|
442
|
+
lines.push(topLine);
|
|
443
|
+
|
|
444
|
+
const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
|
|
445
|
+
fillRange(headerLine, innerStart, innerWidth, headerBg);
|
|
446
|
+
putOverlayText(headerLine, innerStart + 1, innerWidth - 2, fitDisplay(`${modeLabel(wizard.mode)} • ${currentStep.shortLabel}`, innerWidth - 2), {
|
|
447
|
+
fg: UI_TONES.state.active,
|
|
448
|
+
bg: headerBg,
|
|
449
|
+
bold: true,
|
|
450
|
+
});
|
|
451
|
+
lines.push(headerLine);
|
|
452
|
+
|
|
453
|
+
lines.push(createOverlayFilledBorderLine(
|
|
454
|
+
width,
|
|
455
|
+
layout,
|
|
456
|
+
OVERLAY_GLYPHS.teeLeft,
|
|
457
|
+
OVERLAY_GLYPHS.horizontal,
|
|
458
|
+
OVERLAY_GLYPHS.teeRight,
|
|
459
|
+
borderFg,
|
|
460
|
+
bodyBg,
|
|
461
|
+
));
|
|
462
|
+
|
|
463
|
+
for (let row = 0; row < bodyRows; row += 1) {
|
|
464
|
+
const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
|
|
465
|
+
fillRange(line, innerStart, innerWidth, bodyBg);
|
|
466
|
+
|
|
467
|
+
if (row === 0) {
|
|
468
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(currentStep.title, innerWidth - 2), {
|
|
469
|
+
fg: UI_TONES.fg.primary,
|
|
470
|
+
bg: bodyBg,
|
|
471
|
+
bold: true,
|
|
472
|
+
});
|
|
473
|
+
} else if (row === 1) {
|
|
474
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[0] ?? '', {
|
|
475
|
+
fg: UI_TONES.fg.secondary,
|
|
476
|
+
bg: bodyBg,
|
|
477
|
+
});
|
|
478
|
+
} else if (row === 2) {
|
|
479
|
+
const compactMeta = fitDisplay(
|
|
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, {
|
|
484
|
+
fg: UI_TONES.fg.secondary,
|
|
485
|
+
bg: bodyBg,
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
renderFieldRow(line, wizard, fieldRows[row - 3] ?? { kind: 'empty' }, innerStart, innerWidth);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
lines.push(line);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
lines.push(createOverlayFilledBorderLine(
|
|
495
|
+
width,
|
|
496
|
+
layout,
|
|
497
|
+
OVERLAY_GLYPHS.teeLeft,
|
|
498
|
+
OVERLAY_GLYPHS.horizontal,
|
|
499
|
+
OVERLAY_GLYPHS.teeRight,
|
|
500
|
+
borderFg,
|
|
501
|
+
bodyBg,
|
|
502
|
+
));
|
|
503
|
+
|
|
504
|
+
const footer = createOverlayFilledBorderLine(
|
|
505
|
+
width,
|
|
506
|
+
layout,
|
|
507
|
+
OVERLAY_GLYPHS.bottomLeft,
|
|
508
|
+
OVERLAY_GLYPHS.horizontal,
|
|
509
|
+
OVERLAY_GLYPHS.bottomRight,
|
|
510
|
+
borderFg,
|
|
511
|
+
headerBg,
|
|
512
|
+
);
|
|
513
|
+
putOverlayText(footer, layout.margin + 2, layout.width - 4, truncateDisplay(footerText(wizard), layout.width - 4), {
|
|
514
|
+
fg: UI_TONES.fg.muted,
|
|
515
|
+
bg: headerBg,
|
|
516
|
+
dim: true,
|
|
517
|
+
});
|
|
518
|
+
lines.push(footer);
|
|
519
|
+
|
|
520
|
+
return lines.slice(0, viewportHeight);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function renderOnboardingWizard(
|
|
524
|
+
wizard: OnboardingWizardController,
|
|
525
|
+
width: number,
|
|
526
|
+
viewportHeight: number,
|
|
527
|
+
): Line[] {
|
|
528
|
+
const layout = createOverlayBoxLayout(width, 0, width);
|
|
529
|
+
const collapsed = layout.innerWidth < 86;
|
|
530
|
+
return collapsed
|
|
531
|
+
? renderCollapsedLayout(wizard, width, viewportHeight, layout)
|
|
532
|
+
: renderWideLayout(wizard, width, viewportHeight, layout);
|
|
533
|
+
}
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - lifecycle.ts: save/shutdown helpers
|
|
11
11
|
*/
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import net from 'node:net';
|
|
13
14
|
import { Orchestrator } from '../core/orchestrator.ts';
|
|
14
15
|
import { AcpManager } from '@pellux/goodvibes-sdk/platform/acp/manager';
|
|
15
16
|
import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvibes-sdk/platform/providers/tier-prompts';
|
|
@@ -292,6 +293,83 @@ export async function bootstrapRuntime(
|
|
|
292
293
|
|
|
293
294
|
const deferredStartup = createDeferredStartupCoordinator();
|
|
294
295
|
|
|
296
|
+
interface ExternalServiceBindingSnapshot {
|
|
297
|
+
readonly daemon: {
|
|
298
|
+
readonly host: string;
|
|
299
|
+
readonly port: number;
|
|
300
|
+
};
|
|
301
|
+
readonly httpListener: {
|
|
302
|
+
readonly host: string;
|
|
303
|
+
readonly port: number;
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface ExternalServicePortState {
|
|
308
|
+
readonly daemonPortInUse: boolean;
|
|
309
|
+
readonly httpListenerPortInUse: boolean;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const readExternalServiceBindings = (): ExternalServiceBindingSnapshot => ({
|
|
313
|
+
daemon: {
|
|
314
|
+
host: String(configManager.get('controlPlane.host') ?? '127.0.0.1'),
|
|
315
|
+
port: Number(configManager.get('controlPlane.port') ?? 3421),
|
|
316
|
+
},
|
|
317
|
+
httpListener: {
|
|
318
|
+
host: String(configManager.get('httpListener.host') ?? '127.0.0.1'),
|
|
319
|
+
port: Number(configManager.get('httpListener.port') ?? 3422),
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const getProbeHosts = (host: string): readonly string[] => {
|
|
324
|
+
const normalized = host.trim().toLowerCase();
|
|
325
|
+
if (normalized === '0.0.0.0') return ['127.0.0.1'];
|
|
326
|
+
if (normalized === '::' || normalized === '[::]') return ['::1'];
|
|
327
|
+
if (normalized.length === 0) return ['127.0.0.1'];
|
|
328
|
+
return [host];
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const isTcpPortInUse = async (host: string, port: number): Promise<boolean> => new Promise((resolve) => {
|
|
332
|
+
const socket = new net.Socket();
|
|
333
|
+
let settled = false;
|
|
334
|
+
const finish = (result: boolean): void => {
|
|
335
|
+
if (settled) return;
|
|
336
|
+
settled = true;
|
|
337
|
+
socket.destroy();
|
|
338
|
+
resolve(result);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
socket.setTimeout(250);
|
|
342
|
+
socket.once('connect', () => finish(true));
|
|
343
|
+
socket.once('timeout', () => finish(false));
|
|
344
|
+
socket.once('error', () => finish(false));
|
|
345
|
+
socket.connect(port, host);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const inspectExternalPorts = async (
|
|
349
|
+
bindings: readonly ExternalServiceBindingSnapshot[],
|
|
350
|
+
): Promise<ExternalServicePortState> => {
|
|
351
|
+
const daemonTargets = new Map<string, { readonly host: string; readonly port: number }>();
|
|
352
|
+
const listenerTargets = new Map<string, { readonly host: string; readonly port: number }>();
|
|
353
|
+
for (const binding of bindings) {
|
|
354
|
+
for (const host of getProbeHosts(binding.daemon.host)) {
|
|
355
|
+
daemonTargets.set(`${host}:${binding.daemon.port}`, { host, port: binding.daemon.port });
|
|
356
|
+
}
|
|
357
|
+
for (const host of getProbeHosts(binding.httpListener.host)) {
|
|
358
|
+
listenerTargets.set(`${host}:${binding.httpListener.port}`, { host, port: binding.httpListener.port });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const [daemonResults, listenerResults] = await Promise.all([
|
|
363
|
+
Promise.all([...daemonTargets.values()].map((target) => isTcpPortInUse(target.host, target.port))),
|
|
364
|
+
Promise.all([...listenerTargets.values()].map((target) => isTcpPortInUse(target.host, target.port))),
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
daemonPortInUse: daemonResults.some(Boolean),
|
|
369
|
+
httpListenerPortInUse: listenerResults.some(Boolean),
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
|
|
295
373
|
let externalServices: ExternalServicesHandle = {
|
|
296
374
|
daemonServer: null,
|
|
297
375
|
httpListener: null,
|
|
@@ -299,6 +377,49 @@ export async function bootstrapRuntime(
|
|
|
299
377
|
async stop(): Promise<void> {},
|
|
300
378
|
};
|
|
301
379
|
let externalServicesPromise: Promise<ExternalServicesHandle> | null = null;
|
|
380
|
+
let externalServiceBindings = readExternalServiceBindings();
|
|
381
|
+
let externalServicePortState: ExternalServicePortState = {
|
|
382
|
+
daemonPortInUse: false,
|
|
383
|
+
httpListenerPortInUse: false,
|
|
384
|
+
};
|
|
385
|
+
const inspectExternalServices = () => ({
|
|
386
|
+
daemonRunning: externalServices.daemonServer !== null,
|
|
387
|
+
daemonPortInUse: externalServicePortState.daemonPortInUse,
|
|
388
|
+
httpListenerRunning: externalServices.httpListener !== null,
|
|
389
|
+
httpListenerPortInUse: externalServicePortState.httpListenerPortInUse,
|
|
390
|
+
});
|
|
391
|
+
const platformExternalServices = uiServices.platform as typeof uiServices.platform & {
|
|
392
|
+
externalServices: NonNullable<typeof uiServices.platform.externalServices>;
|
|
393
|
+
};
|
|
394
|
+
platformExternalServices.externalServices = {
|
|
395
|
+
inspect: inspectExternalServices,
|
|
396
|
+
restart: async () => {
|
|
397
|
+
if (externalServicesPromise) {
|
|
398
|
+
try {
|
|
399
|
+
externalServices = await externalServicesPromise;
|
|
400
|
+
} catch {
|
|
401
|
+
// A failed previous startup should not prevent a restart attempt.
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
await externalServices.stop();
|
|
405
|
+
const previousBindings = externalServiceBindings;
|
|
406
|
+
externalServiceBindings = readExternalServiceBindings();
|
|
407
|
+
const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
|
|
408
|
+
const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
409
|
+
externalServicesPromise = startExternalServices(
|
|
410
|
+
configManager,
|
|
411
|
+
runtimeBus,
|
|
412
|
+
hookDispatcher,
|
|
413
|
+
{ sharedDaemonToken: companionTokenRecord.token },
|
|
414
|
+
services,
|
|
415
|
+
);
|
|
416
|
+
externalServices = await externalServicesPromise;
|
|
417
|
+
controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
|
|
418
|
+
externalServicePortState = await inspectExternalPorts([previousBindings, externalServiceBindings]);
|
|
419
|
+
requestRender();
|
|
420
|
+
return inspectExternalServices();
|
|
421
|
+
},
|
|
422
|
+
};
|
|
302
423
|
deferredStartup.schedule({
|
|
303
424
|
label: 'plugins',
|
|
304
425
|
run: async () => {
|
|
@@ -349,6 +470,7 @@ export async function bootstrapRuntime(
|
|
|
349
470
|
if (prune.failedPaths.length > 0) {
|
|
350
471
|
logger.warn(`[bootstrap] Failed to prune ${prune.failedPaths.length} stale operator-token file(s) (permission/race): ${prune.failedPaths.join(', ')}`);
|
|
351
472
|
}
|
|
473
|
+
externalServiceBindings = readExternalServiceBindings();
|
|
352
474
|
externalServicesPromise = startExternalServices(
|
|
353
475
|
configManager,
|
|
354
476
|
runtimeBus,
|
|
@@ -358,6 +480,7 @@ export async function bootstrapRuntime(
|
|
|
358
480
|
);
|
|
359
481
|
externalServices = await externalServicesPromise;
|
|
360
482
|
controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
|
|
483
|
+
externalServicePortState = await inspectExternalPorts([externalServiceBindings]);
|
|
361
484
|
requestRender();
|
|
362
485
|
},
|
|
363
486
|
onError: (error) => {
|