@pellux/goodvibes-tui 0.19.23 → 0.19.25
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 +21 -0
- package/README.md +5 -5
- package/bin/goodvibes +5 -0
- package/bin/goodvibes-daemon +5 -0
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/completion.ts +89 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +155 -0
- package/src/cli/help.ts +122 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/management-commands.ts +576 -0
- package/src/cli/management.ts +693 -0
- package/src/cli/parser.ts +367 -0
- package/src/cli/status.ts +112 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +63 -0
- package/src/cli-flags.ts +17 -55
- package/src/config/index.ts +1 -1
- package/src/config/secrets.ts +44 -0
- package/src/core/conversation.ts +36 -13
- 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/panels/panel-manager.ts +6 -2
- 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/renderer/panel-composite.ts +42 -5
- package/src/renderer/panel-workspace-bar.ts +5 -1
- 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
|
+
}
|
|
@@ -24,9 +24,9 @@ import { renderPanelWorkspaceBar } from './panel-workspace-bar.ts';
|
|
|
24
24
|
* path (e.g. deferred lazy-load) should call `this.invalidate()` AFTER the
|
|
25
25
|
* trailing work so the next frame picks it up.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* The fix (now applied): snapshot `needsRender` before calling `render()` and
|
|
28
|
+
* only call `markRendered()` when no concurrent invalidation occurred during
|
|
29
|
+
* the render pass — preserving any mid-render invalidation.
|
|
30
30
|
*/
|
|
31
31
|
interface PanelRenderCache {
|
|
32
32
|
lines: Line[];
|
|
@@ -34,15 +34,52 @@ interface PanelRenderCache {
|
|
|
34
34
|
height: number;
|
|
35
35
|
}
|
|
36
36
|
const panelRenderCache = new WeakMap<Panel, PanelRenderCache>();
|
|
37
|
+
/**
|
|
38
|
+
* Per-panel render-generation counter. Incremented whenever invalidate() fires
|
|
39
|
+
* on the panel (tracked here externally since Panel does not expose a version).
|
|
40
|
+
* We monkey-patch each panel the first time it enters renderPanel() by wrapping
|
|
41
|
+
* its invalidate() to bump the counter stored in this map.
|
|
42
|
+
*
|
|
43
|
+
* Race-guard: snapshot the generation before render(), compare after. If it
|
|
44
|
+
* changed, a mid-render invalidation occurred — leave needsRender=true.
|
|
45
|
+
*/
|
|
46
|
+
const panelRenderGen = new WeakMap<Panel, { gen: number }>();
|
|
47
|
+
|
|
48
|
+
function getRenderGenState(panel: Panel): { gen: number } {
|
|
49
|
+
let state = panelRenderGen.get(panel);
|
|
50
|
+
if (!state) {
|
|
51
|
+
state = { gen: 0 };
|
|
52
|
+
panelRenderGen.set(panel, state);
|
|
53
|
+
// Wrap invalidate() to bump generation counter.
|
|
54
|
+
const origInvalidate = panel.invalidate.bind(panel);
|
|
55
|
+
panel.invalidate = () => {
|
|
56
|
+
state!.gen++;
|
|
57
|
+
origInvalidate();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
37
62
|
|
|
38
63
|
/** R2: Render a panel, skipping if nothing changed. Returns cached lines on a skip. */
|
|
39
|
-
function renderPanel(panel: Panel, width: number, height: number): Line[] {
|
|
64
|
+
export function renderPanel(panel: Panel, width: number, height: number): Line[] {
|
|
40
65
|
const cached = panelRenderCache.get(panel);
|
|
41
66
|
if (cached && !panel.needsRender && cached.width === width && cached.height === height) {
|
|
42
67
|
return cached.lines;
|
|
43
68
|
}
|
|
69
|
+
// Snapshot render-generation counter BEFORE calling render(). If an event
|
|
70
|
+
// listener fires during render() and calls panel.invalidate(), the generation
|
|
71
|
+
// counter will be bumped. We compare after render to detect mid-render races.
|
|
72
|
+
const genState = getRenderGenState(panel);
|
|
73
|
+
const genBefore = genState.gen;
|
|
44
74
|
const lines = panel.render(width, height);
|
|
45
|
-
|
|
75
|
+
// Only call markRendered() when no mid-render invalidation occurred.
|
|
76
|
+
// If the generation changed during render(), a concurrent invalidate() fired —
|
|
77
|
+
// leave needsRender=true so the next frame re-renders with the new state.
|
|
78
|
+
if (genState.gen === genBefore) {
|
|
79
|
+
panel.markRendered();
|
|
80
|
+
}
|
|
81
|
+
// If gen changed, needsRender is already true (invalidate() set it); do not
|
|
82
|
+
// call markRendered() — the next frame will pick it up.
|
|
46
83
|
panelRenderCache.set(panel, { lines, width, height });
|
|
47
84
|
return lines;
|
|
48
85
|
}
|
|
@@ -17,7 +17,11 @@ export function renderPanelWorkspaceBar(
|
|
|
17
17
|
return renderTabStrip({
|
|
18
18
|
width,
|
|
19
19
|
tabs: tabs.map((tab) => ({
|
|
20
|
-
|
|
20
|
+
// tab.active = selected in its own pane (drives highlighted background).
|
|
21
|
+
// tab.focused = has keyboard focus (drives brighter text / focus indicator).
|
|
22
|
+
// A tab can be active-but-not-focused (selected in the unfocused pane) or
|
|
23
|
+
// active-and-focused (selected in the focused pane).
|
|
24
|
+
label: `${tab.pane === 'bottom' ? 'v' : '^'} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
|
|
21
25
|
active: tab.active,
|
|
22
26
|
})),
|
|
23
27
|
prefixLabel: ' PANELS ',
|