@pellux/goodvibes-agent 1.0.37 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,14 @@
1
1
  import type { Line } from '../../types/grid.ts';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText } from '../../utils/terminal-width.ts';
2
+ import { getDisplayWidth, truncateDisplay, wrapText } from '../../utils/terminal-width.ts';
3
3
  import {
4
- createOverlayBoxLayout,
5
- createOverlayContentLine,
6
- createOverlayFilledBorderLine,
7
- DEFAULT_OVERLAY_PALETTE,
8
- OVERLAY_GLYPHS,
9
- putOverlayText,
10
- } from '../overlay-box.ts';
11
- import { clamp, drawVerticalRule, fillWidth } from '../fullscreen-primitives.ts';
12
- import { UI_TONES } from '../ui-primitives.ts';
4
+ getFullscreenWorkspaceMetrics,
5
+ renderFullscreenWorkspace,
6
+ stableWindow,
7
+ WORKSPACE_PALETTE as PALETTE,
8
+ type WorkspaceRow,
9
+ } from '../fullscreen-workspace.ts';
10
+ import { GLYPHS } from '../ui-primitives.ts';
13
11
  import {
14
- getOnboardingWizardBodyRows,
15
- getOnboardingWizardVisibleFieldCount,
16
12
  type OnboardingWizardController,
17
13
  type OnboardingWizardFieldDefinition,
18
14
  type OnboardingWizardStepDefinition,
@@ -45,106 +41,49 @@ function stepGlyph(
45
41
  step: OnboardingWizardStepDefinition,
46
42
  stepIndex: number,
47
43
  ): { readonly glyph: string; readonly fg: string } {
48
- if (stepIndex === wizard.stepIndex) {
49
- return { glyph: OVERLAY_GLYPHS.selected, fg: UI_TONES.state.active };
50
- }
44
+ if (stepIndex === wizard.stepIndex) return { glyph: GLYPHS.navigation.selected, fg: PALETTE.info };
51
45
 
52
46
  const total = wizard.getStepFieldCount(stepIndex);
53
47
  const completed = wizard.getCompletedFieldCount(stepIndex);
54
- if (wizard.isStepDirty(stepIndex)) {
55
- return { glyph: '◈', fg: UI_TONES.state.warn };
56
- }
57
- if (total > 0 && completed === total) {
58
- return { glyph: '✓', fg: UI_TONES.state.good };
59
- }
60
- return { glyph: '•', fg: UI_TONES.fg.muted };
48
+ if (wizard.isStepDirty(stepIndex)) return { glyph: GLYPHS.status.review, fg: PALETTE.warn };
49
+ if (total > 0 && completed === total) return { glyph: GLYPHS.status.success, fg: PALETTE.good };
50
+ return { glyph: GLYPHS.status.pending, fg: PALETTE.muted };
61
51
  }
62
52
 
63
- function fieldBadgeTone(
53
+ function fieldRowPrefix(
64
54
  wizard: OnboardingWizardController,
65
55
  field: OnboardingWizardFieldDefinition,
56
+ selected: boolean,
66
57
  ): string {
67
- if (field.kind === 'status') return UI_TONES.state.info;
68
- if (field.kind === 'modelPicker') return UI_TONES.state.info;
69
- if (field.kind === 'acknowledgement') {
70
- const label = wizard.getFieldValueLabel(field);
71
- return label === 'Accepted' ? UI_TONES.state.good : label === 'Pending' ? UI_TONES.state.warn : UI_TONES.fg.muted;
72
- }
73
- if (field.kind === 'checklist') {
74
- return wizard.getFieldValue(field) ? UI_TONES.state.good : UI_TONES.fg.muted;
75
- }
76
- if (field.kind === 'radio') return UI_TONES.state.active;
77
- if (field.kind === 'text' || field.kind === 'masked') {
78
- const missingRequired = wizard.getFieldValueLabel(field) === 'Missing';
79
- if (missingRequired) return UI_TONES.state.warn;
80
- if (field.kind === 'masked') return UI_TONES.state.warn;
81
- }
82
- return UI_TONES.fg.secondary;
58
+ if (selected) return `${GLYPHS.navigation.selected} `;
59
+ if (wizard.isFieldDirty(field.id)) return `${GLYPHS.status.skipped} `;
60
+ if (field.kind === 'checklist') return (wizard.getFieldValue(field) as boolean) ? `${GLYPHS.status.success} ` : '□ ';
61
+ if (field.kind === 'acknowledgement') return (wizard.getFieldValue(field) as boolean) ? `${GLYPHS.status.success} ` : '□ ';
62
+ if (field.kind === 'action') return `${GLYPHS.navigation.next} `;
63
+ if (field.kind === 'radio') return `${GLYPHS.status.active} `;
64
+ return ' ';
83
65
  }
84
66
 
85
- function buildFieldRows(
67
+ function fieldColor(
86
68
  wizard: OnboardingWizardController,
87
- visibleFields: number,
88
- capacity: number,
89
- ): readonly RenderedFieldRow[] {
90
- wizard.ensureSelectionVisible(visibleFields);
91
- const fields = wizard.currentStep.fields;
92
- const rows: RenderedFieldRow[] = [];
93
- if (fields.length === 0 || capacity <= 0) return rows;
94
-
95
- const allRows: RenderedFieldRow[] = [];
96
- fields.forEach((field, absoluteIndex) => {
97
- const spacerRows = Math.max(0, field.spacerBeforeRows ?? 0);
98
- for (let index = 0; index < spacerRows; index += 1) {
99
- allRows.push({ kind: 'empty' });
100
- }
101
- allRows.push({ kind: 'field', field, absoluteIndex });
102
- });
103
-
104
- const selectedFieldIndex = wizard.getSelectedFieldIndex();
105
- const selectedRowIndex = Math.max(0, allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === selectedFieldIndex));
106
- const scrollFieldIndex = wizard.scrollOffsets[wizard.stepIndex] ?? 0;
107
- const scrollRowIndex = allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === scrollFieldIndex);
108
- const maxStart = Math.max(0, allRows.length - capacity);
109
- let start = clamp(scrollRowIndex >= 0 ? scrollRowIndex : 0, 0, maxStart);
110
-
111
- if (selectedRowIndex < start) start = selectedRowIndex;
112
- if (selectedRowIndex >= start + capacity) start = selectedRowIndex - capacity + 1;
113
- start = clamp(start, 0, maxStart);
114
-
115
- if (capacity > 1 && start > 0 && selectedRowIndex === start) start = Math.max(0, start - 1);
116
- if (capacity > 1 && start + capacity < allRows.length && selectedRowIndex === start + capacity - 1) {
117
- start = Math.min(maxStart, start + 1);
118
- }
119
-
120
- rows.push(...allRows.slice(start, start + capacity));
121
- const firstVisibleRow = rows[0];
122
- if (
123
- start > 0
124
- && rows.length > 0
125
- && !(firstVisibleRow?.kind === 'field' && firstVisibleRow.absoluteIndex === selectedFieldIndex)
126
- ) {
127
- rows[0] = {
128
- kind: 'moreAbove',
129
- text: `${OVERLAY_GLYPHS.moreAbove} ${start} more above`,
130
- };
131
- }
132
-
133
- const hiddenBelow = Math.max(0, allRows.length - (start + capacity));
134
- const lastVisibleRow = rows[rows.length - 1];
135
- if (
136
- hiddenBelow > 0
137
- && rows.length > 0
138
- && !(lastVisibleRow?.kind === 'field' && lastVisibleRow.absoluteIndex === selectedFieldIndex)
139
- ) {
140
- rows[rows.length - 1] = {
141
- kind: 'moreBelow',
142
- text: `${OVERLAY_GLYPHS.moreBelow} ${hiddenBelow} more below`,
143
- };
144
- }
69
+ field: OnboardingWizardFieldDefinition,
70
+ selected: boolean,
71
+ ): string {
72
+ if (selected) return PALETTE.text;
73
+ if (field.kind === 'status' || field.kind === 'modelPicker') return PALETTE.info;
74
+ if (field.kind === 'masked') return PALETTE.warn;
75
+ if (field.kind === 'acknowledgement') return (wizard.getFieldValue(field) as boolean) ? PALETTE.good : PALETTE.warn;
76
+ if (field.kind === 'checklist') return (wizard.getFieldValue(field) as boolean) ? PALETTE.good : PALETTE.muted;
77
+ if (wizard.getFieldValueLabel(field) === 'Missing') return PALETTE.warn;
78
+ return PALETTE.text;
79
+ }
145
80
 
146
- while (rows.length < capacity) rows.push({ kind: 'empty' });
147
- return rows.slice(0, capacity);
81
+ function formatEditingValue(value: string, multiline: boolean): string {
82
+ if (!multiline) return value;
83
+ const lines = value.split(/\r?\n/);
84
+ if (lines.length <= 1) return value;
85
+ const preview = lines[lines.length - 1] ?? '';
86
+ return `${preview} (${lines.length} lines)`;
148
87
  }
149
88
 
150
89
  function fieldHint(
@@ -162,7 +101,7 @@ function fieldHint(
162
101
  const editingValue = field.kind === 'masked' && wizard.editBuffer.length > 0
163
102
  ? '•'.repeat(Math.min(12, Math.max(4, wizard.editBuffer.length)))
164
103
  : formatEditingValue(rawValue, field.kind === 'text' && field.multiline === true);
165
- return `Editing: ${editingValue}█`;
104
+ return `Editing: ${editingValue}${GLYPHS.surface.cursor}`;
166
105
  }
167
106
 
168
107
  if (selected && field.kind === 'modelPicker') return `${field.hint} Press Enter to open picker.`;
@@ -175,28 +114,6 @@ function fieldHint(
175
114
  return field.hint;
176
115
  }
177
116
 
178
- function formatEditingValue(value: string, multiline: boolean): string {
179
- if (!multiline) return value;
180
- const lines = value.split(/\r?\n/);
181
- if (lines.length <= 1) return value;
182
- const preview = lines[lines.length - 1] ?? '';
183
- return `${preview} (${lines.length} lines)`;
184
- }
185
-
186
- function fieldRowPrefix(
187
- wizard: OnboardingWizardController,
188
- field: OnboardingWizardFieldDefinition,
189
- selected: boolean,
190
- ): string {
191
- if (selected) return `${OVERLAY_GLYPHS.selected} `;
192
- if (wizard.isFieldDirty(field.id)) return '◇ ';
193
- if (field.kind === 'checklist') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
194
- if (field.kind === 'acknowledgement') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
195
- if (field.kind === 'action') return '▶ ';
196
- if (field.kind === 'radio') return '◉ ';
197
- return ' ';
198
- }
199
-
200
117
  function selectedFieldText(wizard: OnboardingWizardController): {
201
118
  readonly title: string;
202
119
  readonly hint: string;
@@ -212,12 +129,7 @@ function selectedFieldText(wizard: OnboardingWizardController): {
212
129
  }
213
130
 
214
131
  const field = wizard.getSelectedField();
215
- if (!field) {
216
- return {
217
- title: 'Selected: none',
218
- hint: 'No selectable row is active on this screen.',
219
- };
220
- }
132
+ if (!field) return { title: 'Selected: none', hint: 'No selectable row is active on this screen.' };
221
133
 
222
134
  return {
223
135
  title: `Selected: ${field.label} [${wizard.getFieldValueLabel(field)}]`,
@@ -225,54 +137,14 @@ function selectedFieldText(wizard: OnboardingWizardController): {
225
137
  };
226
138
  }
227
139
 
228
- function renderFieldRow(
229
- line: Line,
230
- wizard: OnboardingWizardController,
231
- fieldRow: RenderedFieldRow,
232
- startX: number,
233
- width: number,
234
- ): void {
235
- if (fieldRow.kind === 'empty') return;
236
-
237
- if (fieldRow.kind === 'moreAbove' || fieldRow.kind === 'moreBelow') {
238
- putOverlayText(line, startX + 1, width - 2, truncateDisplay(fieldRow.text, width - 2), {
239
- fg: UI_TONES.fg.muted,
240
- bg: UI_TONES.bg.base,
241
- dim: true,
242
- });
243
- return;
244
- }
245
-
246
- const selected = fieldRow.absoluteIndex === wizard.getSelectedFieldIndex();
247
- const field = fieldRow.field;
248
- const fieldBg = selected ? DEFAULT_OVERLAY_PALETTE.selectedBg : UI_TONES.bg.base;
249
- fillWidth(line, startX, width, fieldBg);
250
-
251
- const badge = truncateDisplay(`[${wizard.getFieldValueLabel(field)}]`, Math.max(8, Math.floor(width * 0.34)));
252
- const badgeWidth = getDisplayWidth(badge);
253
- const labelWidth = Math.max(0, width - badgeWidth - 4);
254
- const prefix = fieldRowPrefix(wizard, field, selected);
255
-
256
- putOverlayText(line, startX + 1, labelWidth, truncateDisplay(`${prefix}${field.label}`, labelWidth), {
257
- fg: UI_TONES.fg.primary,
258
- bg: fieldBg,
259
- bold: selected,
260
- });
261
- putOverlayText(line, startX + width - badgeWidth - 1, badgeWidth, badge, {
262
- fg: fieldBadgeTone(wizard, field),
263
- bg: fieldBg,
264
- bold: selected,
265
- });
266
- }
267
-
268
140
  function footerText(wizard: OnboardingWizardController): string {
269
141
  if (wizard.isEditingTextField()) {
270
142
  return wizard.isEditingMultilineTextField()
271
- ? '[Enter] Save value [Ctrl-J] New line [Esc] Cancel edit [Backspace] Delete char [Del/Ctrl+U] Clear value'
272
- : '[Enter] Save value [Esc] Cancel edit [Backspace] Delete char [Del/Ctrl+U] Clear value';
143
+ ? 'Controls: [Enter] save · [Ctrl-J] new line · [Esc] cancel · [Backspace] delete · [Del/Ctrl+U] clear'
144
+ : 'Controls: [Enter] save · [Esc] cancel · [Backspace] delete · [Del/Ctrl+U] clear';
273
145
  }
274
146
 
275
- return '[Enter] Toggle/open [Esc] Close [Tab/Shift+Tab] Screen [↑↓] Move [Del/Ctrl+U] Clear input';
147
+ return 'Controls: [Enter] toggle/open · [Esc] close · [Tab/Shift+Tab] screen · [Up/Down] move · [Del/Ctrl+U] clear';
276
148
  }
277
149
 
278
150
  function controlsText(wizard: OnboardingWizardController): string {
@@ -281,352 +153,199 @@ function controlsText(wizard: OnboardingWizardController): string {
281
153
  ? 'Controls: Enter saves, Ctrl-J inserts a line, Esc cancels, Backspace deletes, Del clears.'
282
154
  : 'Controls: Enter saves, Esc cancels, Backspace deletes, Del clears.';
283
155
  }
156
+
284
157
  return 'Controls: Enter selects, Del clears, Tab moves.';
285
158
  }
286
159
 
287
- function renderWideLayout(
160
+ function buildFieldRows(
288
161
  wizard: OnboardingWizardController,
289
- width: number,
290
- viewportHeight: number,
291
- layout: ReturnType<typeof createOverlayBoxLayout>,
292
- ): Line[] {
293
- const lines: Line[] = [];
294
- const bodyRows = getOnboardingWizardBodyRows(viewportHeight);
295
- const visibleFields = getOnboardingWizardVisibleFieldCount(viewportHeight);
296
- const currentStep = wizard.currentStep;
297
- const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
298
- const headerBg = UI_TONES.bg.title;
299
- const railBg = UI_TONES.bg.section;
300
- const bodyBg = UI_TONES.bg.base;
301
- const summaryBg = UI_TONES.bg.summary;
302
- const innerLeft = layout.margin + 1;
303
- const availableInner = layout.innerWidth - 2;
304
- const leftWidthBase = layout.innerWidth >= 150 ? 32 : layout.innerWidth >= 108 ? 28 : 24;
305
- const rightWidthBase = layout.innerWidth >= 150 ? 34 : layout.innerWidth >= 108 ? 32 : 24;
306
- const minCenterWidth = layout.innerWidth >= 120 ? 48 : 40;
307
- let leftWidth = Math.min(leftWidthBase, Math.max(20, availableInner - minCenterWidth - 12));
308
- let rightWidth = Math.min(rightWidthBase, Math.max(22, availableInner - minCenterWidth - leftWidth));
309
- let centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
310
-
311
- if (centerWidth < minCenterWidth) {
312
- const deficit = minCenterWidth - centerWidth;
313
- const leftCut = Math.min(Math.max(0, leftWidth - 20), Math.ceil(deficit / 2));
314
- leftWidth -= leftCut;
315
- rightWidth -= Math.min(Math.max(0, rightWidth - 22), deficit - leftCut);
316
- centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
317
- }
318
-
319
- const leftStart = innerLeft;
320
- const leftSeparatorX = leftStart + leftWidth;
321
- const centerStart = leftSeparatorX + 1;
322
- const rightSeparatorX = centerStart + centerWidth;
323
- const rightStart = rightSeparatorX + 1;
324
- const descriptionLines = wrapText(currentStep.description, Math.max(18, centerWidth - 2)).slice(0, 2);
325
- const summaryLines = [
326
- currentStep.summaryTitle,
327
- ...currentStep.summaryLines.slice(0, 2),
328
- `Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
329
- changedScreensLabel(wizard),
330
- ];
331
- const selectedText = selectedFieldText(wizard);
332
- const selectedHintLines = wrapText(selectedText.hint, Math.max(18, centerWidth - 2)).slice(0, 3);
333
- const fieldStartRow = 8;
334
- const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
162
+ visibleFields: number,
163
+ capacity: number,
164
+ ): readonly RenderedFieldRow[] {
165
+ wizard.ensureSelectionVisible(visibleFields);
166
+ const fields = wizard.currentStep.fields;
167
+ const rows: RenderedFieldRow[] = [];
168
+ if (fields.length === 0 || capacity <= 0) return rows;
335
169
 
336
- const topLine = createOverlayFilledBorderLine(
337
- width,
338
- layout,
339
- OVERLAY_GLYPHS.topLeft,
340
- OVERLAY_GLYPHS.horizontal,
341
- OVERLAY_GLYPHS.topRight,
342
- borderFg,
343
- headerBg,
344
- );
345
- putOverlayText(topLine, layout.margin + 2, layout.width - 4, 'Onboarding Wizard', {
346
- fg: UI_TONES.fg.primary,
347
- bg: headerBg,
348
- bold: true,
349
- });
350
- const meta = `${modeLabel(wizard.mode)} ${wizard.stepIndex + 1}/${wizard.steps.length} ${changedScreensLabel(wizard)}`;
351
- putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
352
- fg: UI_TONES.fg.secondary,
353
- bg: headerBg,
354
- });
355
- lines.push(topLine);
356
-
357
- const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
358
- fillWidth(headerLine, leftStart, leftWidth, railBg);
359
- fillWidth(headerLine, centerStart, centerWidth, headerBg);
360
- fillWidth(headerLine, rightStart, rightWidth, summaryBg);
361
- drawVerticalRule(headerLine, leftSeparatorX, borderFg, headerBg);
362
- drawVerticalRule(headerLine, rightSeparatorX, borderFg, headerBg);
363
- putOverlayText(headerLine, leftStart + 1, leftWidth - 2, 'Steps', {
364
- fg: UI_TONES.fg.secondary,
365
- bg: railBg,
366
- bold: true,
367
- });
368
- putOverlayText(headerLine, centerStart + 1, centerWidth - 2, truncateDisplay(currentStep.title, centerWidth - 2), {
369
- fg: UI_TONES.state.active,
370
- bg: headerBg,
371
- bold: true,
372
- });
373
- putOverlayText(headerLine, rightStart + 1, rightWidth - 2, 'Summary', {
374
- fg: UI_TONES.fg.secondary,
375
- bg: summaryBg,
376
- bold: true,
170
+ const allRows: RenderedFieldRow[] = [];
171
+ fields.forEach((field, absoluteIndex) => {
172
+ const spacerRows = Math.max(0, field.spacerBeforeRows ?? 0);
173
+ for (let index = 0; index < spacerRows; index += 1) allRows.push({ kind: 'empty' });
174
+ allRows.push({ kind: 'field', field, absoluteIndex });
377
175
  });
378
- lines.push(headerLine);
379
176
 
380
- lines.push(createOverlayFilledBorderLine(
381
- width,
382
- layout,
383
- OVERLAY_GLYPHS.teeLeft,
384
- OVERLAY_GLYPHS.horizontal,
385
- OVERLAY_GLYPHS.teeRight,
386
- borderFg,
387
- bodyBg,
388
- ));
389
-
390
- for (let row = 0; row < bodyRows; row += 1) {
391
- const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
392
- fillWidth(line, leftStart, leftWidth, railBg);
393
- fillWidth(line, centerStart, centerWidth, bodyBg);
394
- fillWidth(line, rightStart, rightWidth, summaryBg);
395
- drawVerticalRule(line, leftSeparatorX, borderFg);
396
- drawVerticalRule(line, rightSeparatorX, borderFg);
397
-
398
- if (row === 0) {
399
- putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(currentStep.title, centerWidth - 2), {
400
- fg: UI_TONES.fg.primary,
401
- bg: bodyBg,
402
- bold: true,
403
- });
404
- } else if (row === 1) {
405
- putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[0] ?? '', {
406
- fg: UI_TONES.fg.secondary,
407
- bg: bodyBg,
408
- });
409
- } else if (row === 2) {
410
- putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[1] ?? '', {
411
- fg: UI_TONES.fg.secondary,
412
- bg: bodyBg,
413
- });
414
- } else if (row === 3) {
415
- fillWidth(line, centerStart, centerWidth, railBg);
416
- putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(controlsText(wizard), centerWidth - 2), {
417
- fg: UI_TONES.state.info,
418
- bg: railBg,
419
- });
420
- } else if (row === 4) {
421
- fillWidth(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
422
- putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, centerWidth - 2), {
423
- fg: UI_TONES.fg.primary,
424
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
425
- bold: true,
426
- });
427
- } else if (row >= 5 && row <= 7) {
428
- fillWidth(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
429
- putOverlayText(line, centerStart + 1, centerWidth - 2, selectedHintLines[row - 5] ?? '', {
430
- fg: UI_TONES.fg.secondary,
431
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
432
- });
433
- } else {
434
- renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, centerStart, centerWidth);
435
- }
177
+ const selectedFieldIndex = wizard.getSelectedFieldIndex();
178
+ const selectedRowIndex = Math.max(0, allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === selectedFieldIndex));
179
+ const scrollFieldIndex = wizard.scrollOffsets[wizard.stepIndex] ?? 0;
180
+ const scrollRowIndex = allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === scrollFieldIndex);
181
+ const maxStart = Math.max(0, allRows.length - capacity);
182
+ let start = Math.max(0, Math.min(scrollRowIndex >= 0 ? scrollRowIndex : 0, maxStart));
436
183
 
437
- const step = wizard.steps[row] ?? null;
438
- if (step) {
439
- const stepState = stepGlyph(wizard, step, row);
440
- const completion = `${wizard.getCompletedFieldCount(row)}/${wizard.getStepFieldCount(row)}`;
441
- const completionWidth = getDisplayWidth(completion);
442
- const stepLabelWidth = Math.max(0, leftWidth - completionWidth - 4);
443
- putOverlayText(line, leftStart + 1, stepLabelWidth, truncateDisplay(`${stepState.glyph} ${row + 1}. ${step.shortLabel}`, stepLabelWidth), {
444
- fg: stepState.fg,
445
- bg: railBg,
446
- bold: row === wizard.stepIndex,
447
- });
448
- putOverlayText(line, Math.max(leftStart + 1, leftStart + leftWidth - completionWidth - 2), completionWidth, completion, {
449
- fg: wizard.isStepDirty(row) ? UI_TONES.state.warn : UI_TONES.fg.muted,
450
- bg: railBg,
451
- });
452
- }
184
+ if (selectedRowIndex < start) start = selectedRowIndex;
185
+ if (selectedRowIndex >= start + capacity) start = selectedRowIndex - capacity + 1;
186
+ start = Math.max(0, Math.min(start, maxStart));
453
187
 
454
- const summaryText = summaryLines[row];
455
- if (summaryText) {
456
- putOverlayText(line, rightStart + 1, rightWidth - 2, truncateDisplay(summaryText, rightWidth - 2), {
457
- fg: row === 0 ? UI_TONES.state.info : UI_TONES.fg.secondary,
458
- bg: summaryBg,
459
- bold: row === 0,
460
- dim: row > 0,
461
- });
462
- }
188
+ if (capacity > 1 && start > 0 && selectedRowIndex === start) start = Math.max(0, start - 1);
189
+ if (capacity > 1 && start + capacity < allRows.length && selectedRowIndex === start + capacity - 1) {
190
+ start = Math.min(maxStart, start + 1);
191
+ }
463
192
 
464
- lines.push(line);
193
+ rows.push(...allRows.slice(start, start + capacity));
194
+ const firstVisibleRow = rows[0];
195
+ if (
196
+ start > 0
197
+ && rows.length > 0
198
+ && !(firstVisibleRow?.kind === 'field' && firstVisibleRow.absoluteIndex === selectedFieldIndex)
199
+ ) {
200
+ rows[0] = { kind: 'moreAbove', text: `${GLYPHS.navigation.moreAbove} ${start} more above` };
465
201
  }
466
202
 
467
- lines.push(createOverlayFilledBorderLine(
468
- width,
469
- layout,
470
- OVERLAY_GLYPHS.teeLeft,
471
- OVERLAY_GLYPHS.horizontal,
472
- OVERLAY_GLYPHS.teeRight,
473
- borderFg,
474
- bodyBg,
475
- ));
476
-
477
- const footer = createOverlayFilledBorderLine(
478
- width,
479
- layout,
480
- OVERLAY_GLYPHS.bottomLeft,
481
- OVERLAY_GLYPHS.horizontal,
482
- OVERLAY_GLYPHS.bottomRight,
483
- borderFg,
484
- headerBg,
485
- );
486
- putOverlayText(footer, layout.margin + 2, layout.width - 4, truncateDisplay(footerText(wizard), layout.width - 4), {
487
- fg: UI_TONES.fg.muted,
488
- bg: headerBg,
489
- dim: true,
490
- });
491
- lines.push(footer);
203
+ const hiddenBelow = Math.max(0, allRows.length - (start + capacity));
204
+ const lastVisibleRow = rows[rows.length - 1];
205
+ if (
206
+ hiddenBelow > 0
207
+ && rows.length > 0
208
+ && !(lastVisibleRow?.kind === 'field' && lastVisibleRow.absoluteIndex === selectedFieldIndex)
209
+ ) {
210
+ rows[rows.length - 1] = { kind: 'moreBelow', text: `${GLYPHS.navigation.moreBelow} ${hiddenBelow} more below` };
211
+ }
492
212
 
493
- return lines.slice(0, viewportHeight);
213
+ while (rows.length < capacity) rows.push({ kind: 'empty' });
214
+ return rows.slice(0, capacity);
494
215
  }
495
216
 
496
- function renderCollapsedLayout(
497
- wizard: OnboardingWizardController,
217
+ function pushWrapped(
218
+ rows: WorkspaceRow[],
219
+ text: string,
498
220
  width: number,
499
- viewportHeight: number,
500
- layout: ReturnType<typeof createOverlayBoxLayout>,
501
- ): Line[] {
502
- const lines: Line[] = [];
503
- const bodyRows = getOnboardingWizardBodyRows(viewportHeight);
504
- const visibleFields = getOnboardingWizardVisibleFieldCount(viewportHeight);
505
- const currentStep = wizard.currentStep;
506
- const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
507
- const headerBg = UI_TONES.bg.title;
508
- const bodyBg = UI_TONES.bg.base;
509
- const innerStart = layout.margin + 1;
510
- const innerWidth = layout.innerWidth;
511
- const descriptionLines = wrapText(currentStep.description, Math.max(14, innerWidth - 2)).slice(0, 2);
512
- const selectedText = selectedFieldText(wizard);
513
- const selectedHintLines = wrapText(selectedText.hint, Math.max(14, innerWidth - 2)).slice(0, 3);
514
- const fieldStartRow = 8;
515
- const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
516
-
517
- const topLine = createOverlayFilledBorderLine(
518
- width,
519
- layout,
520
- OVERLAY_GLYPHS.topLeft,
521
- OVERLAY_GLYPHS.horizontal,
522
- OVERLAY_GLYPHS.topRight,
523
- borderFg,
524
- headerBg,
525
- );
526
- putOverlayText(topLine, layout.margin + 2, layout.width - 4, 'Onboarding Wizard', {
527
- fg: UI_TONES.fg.primary,
528
- bg: headerBg,
529
- bold: true,
530
- });
531
- const meta = `${wizard.stepIndex + 1}/${wizard.steps.length} • ${changedScreensLabel(wizard)}`;
532
- putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
533
- fg: UI_TONES.fg.secondary,
534
- bg: headerBg,
535
- });
536
- lines.push(topLine);
537
-
538
- const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
539
- fillWidth(headerLine, innerStart, innerWidth, headerBg);
540
- putOverlayText(headerLine, innerStart + 1, innerWidth - 2, fitDisplay(`${modeLabel(wizard.mode)} • ${currentStep.shortLabel}`, innerWidth - 2), {
541
- fg: UI_TONES.state.active,
542
- bg: headerBg,
543
- bold: true,
221
+ options: Partial<WorkspaceRow> = {},
222
+ ): void {
223
+ if (text.length === 0) {
224
+ rows.push({ text: '', kind: 'empty' });
225
+ return;
226
+ }
227
+ for (const line of wrapText(text, Math.max(1, width))) {
228
+ rows.push({ text: line, ...options });
229
+ }
230
+ }
231
+
232
+ function buildStepRows(wizard: OnboardingWizardController, height: number): WorkspaceRow[] {
233
+ const rendered = wizard.steps.map((step, stepIndex): WorkspaceRow => {
234
+ const selected = stepIndex === wizard.stepIndex;
235
+ const state = stepGlyph(wizard, step, stepIndex);
236
+ const completion = `${wizard.getCompletedFieldCount(stepIndex)}/${wizard.getStepFieldCount(stepIndex)}`;
237
+ return {
238
+ text: `${state.glyph} ${stepIndex + 1}. ${step.shortLabel} ${completion}`,
239
+ selected,
240
+ kind: 'item',
241
+ fg: state.fg,
242
+ bold: selected,
243
+ };
544
244
  });
545
- lines.push(headerLine);
546
245
 
547
- lines.push(createOverlayFilledBorderLine(
548
- width,
549
- layout,
550
- OVERLAY_GLYPHS.teeLeft,
551
- OVERLAY_GLYPHS.horizontal,
552
- OVERLAY_GLYPHS.teeRight,
553
- borderFg,
554
- bodyBg,
555
- ));
556
-
557
- for (let row = 0; row < bodyRows; row += 1) {
558
- const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
559
- fillWidth(line, innerStart, innerWidth, bodyBg);
560
-
561
- if (row === 0) {
562
- putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(currentStep.title, innerWidth - 2), {
563
- fg: UI_TONES.fg.primary,
564
- bg: bodyBg,
565
- bold: true,
566
- });
567
- } else if (row === 1) {
568
- putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[0] ?? '', {
569
- fg: UI_TONES.fg.secondary,
570
- bg: bodyBg,
571
- });
572
- } else if (row === 2) {
573
- putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[1] ?? '', {
574
- fg: UI_TONES.fg.secondary,
575
- bg: bodyBg,
576
- });
577
- } else if (row === 3) {
578
- fillWidth(line, innerStart, innerWidth, UI_TONES.bg.section);
579
- putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(controlsText(wizard), innerWidth - 2), {
580
- fg: UI_TONES.state.info,
581
- bg: UI_TONES.bg.section,
582
- });
583
- } else if (row === 4) {
584
- fillWidth(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
585
- putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, innerWidth - 2), {
586
- fg: UI_TONES.fg.primary,
587
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
588
- bold: true,
589
- });
590
- } else if (row >= 5 && row <= 7) {
591
- fillWidth(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
592
- putOverlayText(line, innerStart + 1, innerWidth - 2, selectedHintLines[row - 5] ?? '', {
593
- fg: UI_TONES.fg.secondary,
594
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
595
- });
596
- } else {
597
- renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, innerStart, innerWidth);
246
+ const visible = Math.max(1, height);
247
+ const window = stableWindow(rendered.length, wizard.stepIndex, visible);
248
+ const rows = rendered.slice(window.start, window.end);
249
+ if (window.start > 0 && rows.length > 0) {
250
+ rows[0] = { text: `${GLYPHS.navigation.moreAbove} ${window.start} more step(s) above`, kind: 'more', fg: PALETTE.dim, dim: true };
251
+ }
252
+ if (window.end < rendered.length && rows.length > 0) {
253
+ rows[rows.length - 1] = { text: `${GLYPHS.navigation.moreBelow} ${rendered.length - window.end} more step(s) below`, kind: 'more', fg: PALETTE.dim, dim: true };
254
+ }
255
+ while (rows.length < height) rows.push({ text: '', kind: 'empty' });
256
+ return rows.slice(0, height);
257
+ }
258
+
259
+ function buildContextRows(wizard: OnboardingWizardController, width: number): WorkspaceRow[] {
260
+ const step = wizard.currentStep;
261
+ const selected = selectedFieldText(wizard);
262
+ const rows: WorkspaceRow[] = [
263
+ { text: step.title, fg: PALETTE.title, bold: true },
264
+ ];
265
+ const pushSummaryRows = () => {
266
+ rows.push({ text: 'Summary', fg: PALETTE.subtitle, bold: true });
267
+ rows.push({ text: step.summaryTitle, fg: PALETTE.info, bold: true });
268
+ for (const line of step.summaryLines.slice(0, 4)) {
269
+ pushWrapped(rows, line, width, { fg: PALETTE.text });
598
270
  }
271
+ rows.push({
272
+ text: `Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete · ${changedScreensLabel(wizard)}`,
273
+ fg: wizard.dirtyStepCount > 0 ? PALETTE.warn : PALETTE.muted,
274
+ });
275
+ };
276
+ const pushSelectedRows = () => {
277
+ rows.push({ text: selected.title, fg: PALETTE.info, bold: true });
278
+ pushWrapped(rows, selected.hint, width, { fg: PALETTE.text });
279
+ pushWrapped(rows, controlsText(wizard), width, { fg: PALETTE.info });
280
+ };
599
281
 
600
- lines.push(line);
282
+ pushWrapped(rows, step.description, width, { fg: PALETTE.text });
283
+ rows.push({ text: '', kind: 'empty' });
284
+
285
+ if (wizard.isEditingTextField()) {
286
+ pushSelectedRows();
287
+ rows.push({ text: '', kind: 'empty' });
288
+ pushSummaryRows();
289
+ } else {
290
+ pushSummaryRows();
291
+ rows.push({ text: '', kind: 'empty' });
292
+ pushSelectedRows();
601
293
  }
602
294
 
603
- lines.push(createOverlayFilledBorderLine(
604
- width,
605
- layout,
606
- OVERLAY_GLYPHS.teeLeft,
607
- OVERLAY_GLYPHS.horizontal,
608
- OVERLAY_GLYPHS.teeRight,
609
- borderFg,
610
- bodyBg,
611
- ));
612
-
613
- const footer = createOverlayFilledBorderLine(
614
- width,
615
- layout,
616
- OVERLAY_GLYPHS.bottomLeft,
617
- OVERLAY_GLYPHS.horizontal,
618
- OVERLAY_GLYPHS.bottomRight,
619
- borderFg,
620
- headerBg,
621
- );
622
- putOverlayText(footer, layout.margin + 2, layout.width - 4, truncateDisplay(footerText(wizard), layout.width - 4), {
623
- fg: UI_TONES.fg.muted,
624
- bg: headerBg,
625
- dim: true,
295
+ if (wizard.hydrationPending || wizard.hydrationError !== null || wizard.applyFeedback !== null) {
296
+ rows.push({ text: '', kind: 'empty' });
297
+ }
298
+ if (wizard.hydrationPending) {
299
+ rows.push({ text: 'Loading current settings...', fg: PALETTE.info, bold: true });
300
+ }
301
+ if (wizard.hydrationError !== null) {
302
+ pushWrapped(rows, `Current settings could not load: ${wizard.hydrationError}`, width, { fg: PALETTE.bad, bold: true });
303
+ }
304
+ if (wizard.applyFeedback !== null) {
305
+ const feedbackColor = wizard.applyFeedback.severity === 'error'
306
+ ? PALETTE.bad
307
+ : wizard.applyFeedback.severity === 'warning'
308
+ ? PALETTE.warn
309
+ : PALETTE.info;
310
+ rows.push({ text: wizard.applyFeedback.title, fg: feedbackColor, bold: true });
311
+ pushWrapped(rows, wizard.applyFeedback.summary, width, { fg: PALETTE.text });
312
+ for (const message of wizard.applyFeedback.messages) {
313
+ pushWrapped(rows, message, width, { fg: PALETTE.muted });
314
+ }
315
+ }
316
+
317
+ return rows;
318
+ }
319
+
320
+ function formatFieldRowText(wizard: OnboardingWizardController, field: OnboardingWizardFieldDefinition, selected: boolean, width: number): string {
321
+ const badge = `[${wizard.getFieldValueLabel(field)}]`;
322
+ const badgeWidth = getDisplayWidth(badge);
323
+ const labelWidth = Math.max(1, width - badgeWidth - 2);
324
+ const label = truncateDisplay(`${fieldRowPrefix(wizard, field, selected)}${field.label}`, labelWidth);
325
+ return `${label}${' '.repeat(Math.max(1, width - getDisplayWidth(label) - badgeWidth))}${badge}`;
326
+ }
327
+
328
+ function buildControlRows(wizard: OnboardingWizardController, width: number, height: number): WorkspaceRow[] {
329
+ const visibleFields = Math.max(1, height);
330
+ const fieldRows = buildFieldRows(wizard, visibleFields, height);
331
+ const rows = fieldRows.map((row): WorkspaceRow => {
332
+ if (row.kind === 'empty') return { text: '', kind: 'empty' };
333
+ if (row.kind === 'moreAbove' || row.kind === 'moreBelow') {
334
+ return { text: row.text, kind: 'more', fg: PALETTE.dim, dim: true };
335
+ }
336
+
337
+ const selected = row.absoluteIndex === wizard.getSelectedFieldIndex();
338
+ return {
339
+ text: formatFieldRowText(wizard, row.field, selected, width),
340
+ selected,
341
+ kind: 'item',
342
+ fg: fieldColor(wizard, row.field, selected),
343
+ bold: selected || row.field.kind === 'action',
344
+ };
626
345
  });
627
- lines.push(footer);
628
346
 
629
- return lines.slice(0, viewportHeight);
347
+ while (rows.length < height) rows.push({ text: '', kind: 'empty' });
348
+ return rows.slice(0, height);
630
349
  }
631
350
 
632
351
  export function renderOnboardingWizard(
@@ -634,9 +353,28 @@ export function renderOnboardingWizard(
634
353
  width: number,
635
354
  viewportHeight: number,
636
355
  ): Line[] {
637
- const layout = createOverlayBoxLayout(width, 0, Math.max(20, width));
638
- const collapsed = layout.innerWidth < 86;
639
- return collapsed
640
- ? renderCollapsedLayout(wizard, width, viewportHeight, layout)
641
- : renderWideLayout(wizard, width, viewportHeight, layout);
356
+ const layoutOptions = {
357
+ width,
358
+ height: viewportHeight,
359
+ leftWidth: width < 90 ? undefined : 30,
360
+ contextRatio: 0.42,
361
+ minContextRows: 8,
362
+ };
363
+ const metrics = getFullscreenWorkspaceMetrics(layoutOptions);
364
+
365
+ return renderFullscreenWorkspace({
366
+ width,
367
+ height: viewportHeight,
368
+ title: 'GoodVibes Agent / Onboarding Wizard',
369
+ stateLabel: `${modeLabel(wizard.mode)} · ${wizard.stepIndex + 1}/${wizard.steps.length} · ${changedScreensLabel(wizard)}`,
370
+ leftHeader: 'Steps',
371
+ mainHeader: wizard.currentStep.title,
372
+ leftRows: buildStepRows(wizard, metrics.bodyRows),
373
+ contextRows: buildContextRows(wizard, metrics.contextWidth),
374
+ controlRows: buildControlRows(wizard, metrics.contextWidth, metrics.controlRows),
375
+ footer: footerText(wizard),
376
+ leftWidth: layoutOptions.leftWidth,
377
+ contextRatio: layoutOptions.contextRatio,
378
+ minContextRows: layoutOptions.minContextRows,
379
+ });
642
380
  }