@pellux/goodvibes-agent 1.0.38 → 1.0.40

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Product-facing release notes for GoodVibes Agent.
4
4
 
5
+ ## 1.0.40 - 2026-06-04
6
+
7
+ - Onboarding now uses a dedicated fullscreen workspace renderer copied from the settings-style workspace, so setup can be tuned without changing Settings, MCP, or Agent workspaces.
8
+ - First-run setup keeps the settings-style left rail, detail pane, field pane, and footer while the shell gives it the full terminal height, hiding the prompt/input area until setup is complete.
9
+
10
+ ## 1.0.39 - 2026-06-04
11
+
12
+ - Removed the body-scoped onboarding overlay fallback so onboarding can only render through the shell fullscreen composite.
13
+ - Added regression coverage for the fullscreen onboarding composite and nested model workspace composite.
14
+
5
15
  ## 1.0.38 - 2026-06-04
6
16
 
7
17
  - Onboarding now uses the shared fullscreen workspace surface so it owns the full terminal height, including the composer area.
@@ -817087,7 +817087,7 @@ var createStyledCell = (char, overrides = {}) => ({
817087
817087
  // src/version.ts
817088
817088
  import { readFileSync } from "fs";
817089
817089
  import { join } from "path";
817090
- var _version = "1.0.38";
817090
+ var _version = "1.0.40";
817091
817091
  try {
817092
817092
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf-8"));
817093
817093
  _version = typeof pkg.version === "string" ? pkg.version : _version;
@@ -906062,311 +906062,13 @@ function renderAutocompleteOverlay(autocomplete, width, viewportHeight = 24) {
906062
906062
  return lines;
906063
906063
  }
906064
906064
 
906065
- // src/renderer/onboarding/onboarding-wizard.ts
906066
- function modeLabel(mode) {
906067
- if (mode === "edit")
906068
- return "Edit existing";
906069
- if (mode === "reopen")
906070
- return "Reopen review";
906071
- return "New setup";
906072
- }
906073
- function changedScreensLabel(wizard) {
906074
- if (wizard.dirtyStepCount === 0)
906075
- return "no changes";
906076
- if (wizard.dirtyStepCount === 1)
906077
- return "1 changed screen";
906078
- return `${wizard.dirtyStepCount} changed screens`;
906079
- }
906080
- function stepGlyph(wizard, step, stepIndex) {
906081
- if (stepIndex === wizard.stepIndex)
906082
- return { glyph: GLYPHS.navigation.selected, fg: FULLSCREEN_PALETTE.info };
906083
- const total = wizard.getStepFieldCount(stepIndex);
906084
- const completed = wizard.getCompletedFieldCount(stepIndex);
906085
- if (wizard.isStepDirty(stepIndex))
906086
- return { glyph: GLYPHS.status.review, fg: FULLSCREEN_PALETTE.warn };
906087
- if (total > 0 && completed === total)
906088
- return { glyph: GLYPHS.status.success, fg: FULLSCREEN_PALETTE.good };
906089
- return { glyph: GLYPHS.status.pending, fg: FULLSCREEN_PALETTE.muted };
906090
- }
906091
- function fieldRowPrefix(wizard, field, selected) {
906092
- if (selected)
906093
- return `${GLYPHS.navigation.selected} `;
906094
- if (wizard.isFieldDirty(field.id))
906095
- return `${GLYPHS.status.skipped} `;
906096
- if (field.kind === "checklist")
906097
- return wizard.getFieldValue(field) ? `${GLYPHS.status.success} ` : "\u25A1 ";
906098
- if (field.kind === "acknowledgement")
906099
- return wizard.getFieldValue(field) ? `${GLYPHS.status.success} ` : "\u25A1 ";
906100
- if (field.kind === "action")
906101
- return `${GLYPHS.navigation.next} `;
906102
- if (field.kind === "radio")
906103
- return `${GLYPHS.status.active} `;
906104
- return " ";
906105
- }
906106
- function fieldColor(wizard, field, selected) {
906107
- if (selected)
906108
- return FULLSCREEN_PALETTE.text;
906109
- if (field.kind === "status" || field.kind === "modelPicker")
906110
- return FULLSCREEN_PALETTE.info;
906111
- if (field.kind === "masked")
906112
- return FULLSCREEN_PALETTE.warn;
906113
- if (field.kind === "acknowledgement")
906114
- return wizard.getFieldValue(field) ? FULLSCREEN_PALETTE.good : FULLSCREEN_PALETTE.warn;
906115
- if (field.kind === "checklist")
906116
- return wizard.getFieldValue(field) ? FULLSCREEN_PALETTE.good : FULLSCREEN_PALETTE.muted;
906117
- if (wizard.getFieldValueLabel(field) === "Missing")
906118
- return FULLSCREEN_PALETTE.warn;
906119
- return FULLSCREEN_PALETTE.text;
906120
- }
906121
- function formatEditingValue(value, multiline) {
906122
- if (!multiline)
906123
- return value;
906124
- const lines = value.split(/\r?\n/);
906125
- if (lines.length <= 1)
906126
- return value;
906127
- const preview4 = lines[lines.length - 1] ?? "";
906128
- return `${preview4} (${lines.length} lines)`;
906129
- }
906130
- function fieldHint(wizard, field, selected) {
906131
- if (selected && wizard.isEditingTextField() && wizard.editingFieldId === field.id && (field.kind === "text" || field.kind === "masked")) {
906132
- const rawValue = wizard.editBuffer.length > 0 ? wizard.editBuffer : field.placeholder;
906133
- const editingValue = field.kind === "masked" && wizard.editBuffer.length > 0 ? "\u2022".repeat(Math.min(12, Math.max(4, wizard.editBuffer.length))) : formatEditingValue(rawValue, field.kind === "text" && field.multiline === true);
906134
- return `Editing: ${editingValue}${GLYPHS.surface.cursor}`;
906135
- }
906136
- if (selected && field.kind === "modelPicker")
906137
- return `${field.hint} Press Enter to open picker.`;
906138
- if (selected && field.kind === "text") {
906139
- return field.multiline === true ? `${field.hint} Press Enter to edit; Ctrl-J inserts a new line.` : `${field.hint} Press Enter to edit inline.`;
906140
- }
906141
- if (selected && field.kind === "masked")
906142
- return `${field.hint} Press Enter to edit inline.`;
906143
- return field.hint;
906144
- }
906145
- function selectedFieldText(wizard) {
906146
- if (wizard.isEditingTextField() && wizard.editingFieldId !== null) {
906147
- const editingField = wizard.getFieldById(wizard.editingFieldId);
906148
- if (editingField) {
906149
- return {
906150
- title: `Editing: ${editingField.label}`,
906151
- hint: fieldHint(wizard, editingField, true)
906152
- };
906153
- }
906154
- }
906155
- const field = wizard.getSelectedField();
906156
- if (!field)
906157
- return { title: "Selected: none", hint: "No selectable row is active on this screen." };
906158
- return {
906159
- title: `Selected: ${field.label} [${wizard.getFieldValueLabel(field)}]`,
906160
- hint: fieldHint(wizard, field, true)
906161
- };
906162
- }
906163
- function footerText4(wizard) {
906164
- if (wizard.isEditingTextField()) {
906165
- return wizard.isEditingMultilineTextField() ? "Controls: [Enter] save \xB7 [Ctrl-J] new line \xB7 [Esc] cancel \xB7 [Backspace] delete \xB7 [Del/Ctrl+U] clear" : "Controls: [Enter] save \xB7 [Esc] cancel \xB7 [Backspace] delete \xB7 [Del/Ctrl+U] clear";
906166
- }
906167
- return "Controls: [Enter] toggle/open \xB7 [Esc] close \xB7 [Tab/Shift+Tab] screen \xB7 [Up/Down] move \xB7 [Del/Ctrl+U] clear";
906168
- }
906169
- function controlsText(wizard) {
906170
- if (wizard.isEditingTextField()) {
906171
- return wizard.isEditingMultilineTextField() ? "Controls: Enter saves, Ctrl-J inserts a line, Esc cancels, Backspace deletes, Del clears." : "Controls: Enter saves, Esc cancels, Backspace deletes, Del clears.";
906172
- }
906173
- return "Controls: Enter selects, Del clears, Tab moves.";
906174
- }
906175
- function buildFieldRows(wizard, visibleFields, capacity) {
906176
- wizard.ensureSelectionVisible(visibleFields);
906177
- const fields = wizard.currentStep.fields;
906178
- const rows = [];
906179
- if (fields.length === 0 || capacity <= 0)
906180
- return rows;
906181
- const allRows = [];
906182
- fields.forEach((field, absoluteIndex) => {
906183
- const spacerRows = Math.max(0, field.spacerBeforeRows ?? 0);
906184
- for (let index = 0;index < spacerRows; index += 1)
906185
- allRows.push({ kind: "empty" });
906186
- allRows.push({ kind: "field", field, absoluteIndex });
906187
- });
906188
- const selectedFieldIndex = wizard.getSelectedFieldIndex();
906189
- const selectedRowIndex = Math.max(0, allRows.findIndex((row) => row.kind === "field" && row.absoluteIndex === selectedFieldIndex));
906190
- const scrollFieldIndex = wizard.scrollOffsets[wizard.stepIndex] ?? 0;
906191
- const scrollRowIndex = allRows.findIndex((row) => row.kind === "field" && row.absoluteIndex === scrollFieldIndex);
906192
- const maxStart = Math.max(0, allRows.length - capacity);
906193
- let start2 = Math.max(0, Math.min(scrollRowIndex >= 0 ? scrollRowIndex : 0, maxStart));
906194
- if (selectedRowIndex < start2)
906195
- start2 = selectedRowIndex;
906196
- if (selectedRowIndex >= start2 + capacity)
906197
- start2 = selectedRowIndex - capacity + 1;
906198
- start2 = Math.max(0, Math.min(start2, maxStart));
906199
- if (capacity > 1 && start2 > 0 && selectedRowIndex === start2)
906200
- start2 = Math.max(0, start2 - 1);
906201
- if (capacity > 1 && start2 + capacity < allRows.length && selectedRowIndex === start2 + capacity - 1) {
906202
- start2 = Math.min(maxStart, start2 + 1);
906203
- }
906204
- rows.push(...allRows.slice(start2, start2 + capacity));
906205
- const firstVisibleRow = rows[0];
906206
- if (start2 > 0 && rows.length > 0 && !(firstVisibleRow?.kind === "field" && firstVisibleRow.absoluteIndex === selectedFieldIndex)) {
906207
- rows[0] = { kind: "moreAbove", text: `${GLYPHS.navigation.moreAbove} ${start2} more above` };
906208
- }
906209
- const hiddenBelow = Math.max(0, allRows.length - (start2 + capacity));
906210
- const lastVisibleRow = rows[rows.length - 1];
906211
- if (hiddenBelow > 0 && rows.length > 0 && !(lastVisibleRow?.kind === "field" && lastVisibleRow.absoluteIndex === selectedFieldIndex)) {
906212
- rows[rows.length - 1] = { kind: "moreBelow", text: `${GLYPHS.navigation.moreBelow} ${hiddenBelow} more below` };
906213
- }
906214
- while (rows.length < capacity)
906215
- rows.push({ kind: "empty" });
906216
- return rows.slice(0, capacity);
906217
- }
906218
- function pushWrapped(rows, text, width, options = {}) {
906219
- if (text.length === 0) {
906220
- rows.push({ text: "", kind: "empty" });
906221
- return;
906222
- }
906223
- for (const line2 of wrapText(text, Math.max(1, width))) {
906224
- rows.push({ text: line2, ...options });
906225
- }
906226
- }
906227
- function buildStepRows(wizard, height) {
906228
- const rendered = wizard.steps.map((step, stepIndex) => {
906229
- const selected = stepIndex === wizard.stepIndex;
906230
- const state4 = stepGlyph(wizard, step, stepIndex);
906231
- const completion = `${wizard.getCompletedFieldCount(stepIndex)}/${wizard.getStepFieldCount(stepIndex)}`;
906232
- return {
906233
- text: `${state4.glyph} ${stepIndex + 1}. ${step.shortLabel} ${completion}`,
906234
- selected,
906235
- kind: "item",
906236
- fg: state4.fg,
906237
- bold: selected
906238
- };
906239
- });
906240
- const visible = Math.max(1, height);
906241
- const window3 = stableWindow(rendered.length, wizard.stepIndex, visible);
906242
- const rows = rendered.slice(window3.start, window3.end);
906243
- if (window3.start > 0 && rows.length > 0) {
906244
- rows[0] = { text: `${GLYPHS.navigation.moreAbove} ${window3.start} more step(s) above`, kind: "more", fg: FULLSCREEN_PALETTE.dim, dim: true };
906245
- }
906246
- if (window3.end < rendered.length && rows.length > 0) {
906247
- rows[rows.length - 1] = { text: `${GLYPHS.navigation.moreBelow} ${rendered.length - window3.end} more step(s) below`, kind: "more", fg: FULLSCREEN_PALETTE.dim, dim: true };
906248
- }
906249
- while (rows.length < height)
906250
- rows.push({ text: "", kind: "empty" });
906251
- return rows.slice(0, height);
906252
- }
906253
- function buildContextRows2(wizard, width) {
906254
- const step = wizard.currentStep;
906255
- const selected = selectedFieldText(wizard);
906256
- const rows = [
906257
- { text: step.title, fg: FULLSCREEN_PALETTE.title, bold: true }
906258
- ];
906259
- const pushSummaryRows = () => {
906260
- rows.push({ text: "Summary", fg: FULLSCREEN_PALETTE.subtitle, bold: true });
906261
- rows.push({ text: step.summaryTitle, fg: FULLSCREEN_PALETTE.info, bold: true });
906262
- for (const line2 of step.summaryLines.slice(0, 4)) {
906263
- pushWrapped(rows, line2, width, { fg: FULLSCREEN_PALETTE.text });
906264
- }
906265
- rows.push({
906266
- text: `Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete \xB7 ${changedScreensLabel(wizard)}`,
906267
- fg: wizard.dirtyStepCount > 0 ? FULLSCREEN_PALETTE.warn : FULLSCREEN_PALETTE.muted
906268
- });
906269
- };
906270
- const pushSelectedRows = () => {
906271
- rows.push({ text: selected.title, fg: FULLSCREEN_PALETTE.info, bold: true });
906272
- pushWrapped(rows, selected.hint, width, { fg: FULLSCREEN_PALETTE.text });
906273
- pushWrapped(rows, controlsText(wizard), width, { fg: FULLSCREEN_PALETTE.info });
906274
- };
906275
- pushWrapped(rows, step.description, width, { fg: FULLSCREEN_PALETTE.text });
906276
- rows.push({ text: "", kind: "empty" });
906277
- if (wizard.isEditingTextField()) {
906278
- pushSelectedRows();
906279
- rows.push({ text: "", kind: "empty" });
906280
- pushSummaryRows();
906281
- } else {
906282
- pushSummaryRows();
906283
- rows.push({ text: "", kind: "empty" });
906284
- pushSelectedRows();
906285
- }
906286
- if (wizard.hydrationPending || wizard.hydrationError !== null || wizard.applyFeedback !== null) {
906287
- rows.push({ text: "", kind: "empty" });
906288
- }
906289
- if (wizard.hydrationPending) {
906290
- rows.push({ text: "Loading current settings...", fg: FULLSCREEN_PALETTE.info, bold: true });
906291
- }
906292
- if (wizard.hydrationError !== null) {
906293
- pushWrapped(rows, `Current settings could not load: ${wizard.hydrationError}`, width, { fg: FULLSCREEN_PALETTE.bad, bold: true });
906294
- }
906295
- if (wizard.applyFeedback !== null) {
906296
- const feedbackColor = wizard.applyFeedback.severity === "error" ? FULLSCREEN_PALETTE.bad : wizard.applyFeedback.severity === "warning" ? FULLSCREEN_PALETTE.warn : FULLSCREEN_PALETTE.info;
906297
- rows.push({ text: wizard.applyFeedback.title, fg: feedbackColor, bold: true });
906298
- pushWrapped(rows, wizard.applyFeedback.summary, width, { fg: FULLSCREEN_PALETTE.text });
906299
- for (const message of wizard.applyFeedback.messages) {
906300
- pushWrapped(rows, message, width, { fg: FULLSCREEN_PALETTE.muted });
906301
- }
906302
- }
906303
- return rows;
906304
- }
906305
- function formatFieldRowText(wizard, field, selected, width) {
906306
- const badge = `[${wizard.getFieldValueLabel(field)}]`;
906307
- const badgeWidth = getDisplayWidth(badge);
906308
- const labelWidth = Math.max(1, width - badgeWidth - 2);
906309
- const label = truncateDisplay(`${fieldRowPrefix(wizard, field, selected)}${field.label}`, labelWidth);
906310
- return `${label}${" ".repeat(Math.max(1, width - getDisplayWidth(label) - badgeWidth))}${badge}`;
906311
- }
906312
- function buildControlRows2(wizard, width, height) {
906313
- const visibleFields = Math.max(1, height);
906314
- const fieldRows = buildFieldRows(wizard, visibleFields, height);
906315
- const rows = fieldRows.map((row) => {
906316
- if (row.kind === "empty")
906317
- return { text: "", kind: "empty" };
906318
- if (row.kind === "moreAbove" || row.kind === "moreBelow") {
906319
- return { text: row.text, kind: "more", fg: FULLSCREEN_PALETTE.dim, dim: true };
906320
- }
906321
- const selected = row.absoluteIndex === wizard.getSelectedFieldIndex();
906322
- return {
906323
- text: formatFieldRowText(wizard, row.field, selected, width),
906324
- selected,
906325
- kind: "item",
906326
- fg: fieldColor(wizard, row.field, selected),
906327
- bold: selected || row.field.kind === "action"
906328
- };
906329
- });
906330
- while (rows.length < height)
906331
- rows.push({ text: "", kind: "empty" });
906332
- return rows.slice(0, height);
906333
- }
906334
- function renderOnboardingWizard(wizard, width, viewportHeight) {
906335
- const layoutOptions = {
906336
- width,
906337
- height: viewportHeight,
906338
- leftWidth: width < 90 ? undefined : 30,
906339
- contextRatio: 0.42,
906340
- minContextRows: 8
906341
- };
906342
- const metrics = getFullscreenWorkspaceMetrics(layoutOptions);
906343
- return renderFullscreenWorkspace({
906344
- width,
906345
- height: viewportHeight,
906346
- title: "GoodVibes Agent / Onboarding Wizard",
906347
- stateLabel: `${modeLabel(wizard.mode)} \xB7 ${wizard.stepIndex + 1}/${wizard.steps.length} \xB7 ${changedScreensLabel(wizard)}`,
906348
- leftHeader: "Steps",
906349
- mainHeader: wizard.currentStep.title,
906350
- leftRows: buildStepRows(wizard, metrics.bodyRows),
906351
- contextRows: buildContextRows2(wizard, metrics.contextWidth),
906352
- controlRows: buildControlRows2(wizard, metrics.contextWidth, metrics.controlRows),
906353
- footer: footerText4(wizard),
906354
- leftWidth: layoutOptions.leftWidth,
906355
- contextRatio: layoutOptions.contextRatio,
906356
- minContextRows: layoutOptions.minContextRows
906357
- });
906358
- }
906359
-
906360
906065
  // src/renderer/conversation-overlays.ts
906361
906066
  function applyConversationOverlays(viewport, context) {
906362
906067
  const { input, conversation, commandRegistry, keybindingsManager, conversationWidth, viewportHeight, contextWindow } = context;
906363
906068
  let next = viewport;
906364
906069
  const bottomDockInset = 1 + (input.searchManager.active || input.historySearch.active ? 1 : 0);
906365
906070
  if (input.onboardingWizard.active) {
906366
- const lines = renderOnboardingWizard(input.onboardingWizard, conversationWidth, viewportHeight);
906367
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
906368
- if (!input.modelPicker.active)
906369
- return next;
906071
+ return next;
906370
906072
  }
906371
906073
  if (input.filePicker.active) {
906372
906074
  const lines = renderFilePickerOverlay(input.filePicker, conversationWidth, viewportHeight);
@@ -906717,6 +906419,423 @@ function handleBlockingShellInput(options) {
906717
906419
  return { handled: false, pendingPermission, recoveryPending };
906718
906420
  }
906719
906421
 
906422
+ // src/renderer/onboarding/onboarding-workspace.ts
906423
+ var WORKSPACE_PALETTE2 = FULLSCREEN_PALETTE;
906424
+ function drawVertical3(line2, x2, bg = "") {
906425
+ if (x2 <= 0 || x2 >= line2.length - 1)
906426
+ return;
906427
+ drawVerticalRule(line2, x2, WORKSPACE_PALETTE2.border, bg);
906428
+ }
906429
+ function drawHorizontalRange2(line2, startX, endX, bg = "") {
906430
+ drawHorizontalRule(line2, Math.max(1, startX), Math.min(line2.length - 2, endX), WORKSPACE_PALETTE2.border, bg);
906431
+ }
906432
+ function leftWidthFor2(width, explicit) {
906433
+ if (explicit !== undefined)
906434
+ return clamp3(explicit, 14, Math.max(14, width - 24));
906435
+ return width < 80 ? clamp3(Math.round(width * 0.32), 14, Math.max(14, width - 24)) : clamp3(Math.round(width * 0.22), 24, 34);
906436
+ }
906437
+ function getFullscreenWorkspaceMetrics2(options) {
906438
+ const safeWidth = Math.max(1, options.width);
906439
+ const safeHeight = Math.max(12, options.height);
906440
+ const leftWidth = leftWidthFor2(safeWidth, options.leftWidth);
906441
+ const centerWidth = Math.max(20, safeWidth - leftWidth - 3);
906442
+ const bodyTop = 3;
906443
+ const footerY = safeHeight - 2;
906444
+ const bodyRows = Math.max(4, footerY - bodyTop);
906445
+ const contextWidth = Math.max(10, centerWidth - 2);
906446
+ const maxContextRows = Math.max(3, bodyRows - 4);
906447
+ const minContextRows = clamp3(options.minContextRows ?? 10, 3, maxContextRows);
906448
+ const contextRows = clamp3(Math.round(bodyRows * (options.contextRatio ?? 0.4)), Math.min(minContextRows, maxContextRows), maxContextRows);
906449
+ const controlRows = Math.max(3, bodyRows - contextRows - 1);
906450
+ return { safeWidth, safeHeight, leftWidth, centerWidth, bodyRows, contextWidth, contextRows, controlRows };
906451
+ }
906452
+ function rowFg2(row, fallback) {
906453
+ if (row.fg)
906454
+ return row.fg;
906455
+ if (row.kind === "group")
906456
+ return WORKSPACE_PALETTE2.subtitle;
906457
+ if (row.kind === "more")
906458
+ return WORKSPACE_PALETTE2.dim;
906459
+ return fallback;
906460
+ }
906461
+ function renderFullscreenWorkspace2(options) {
906462
+ const { safeWidth, safeHeight, leftWidth, centerWidth, bodyRows, contextWidth, contextRows } = getFullscreenWorkspaceMetrics2(options);
906463
+ const leftStart = 1;
906464
+ const dividerX = leftWidth + 1;
906465
+ const centerStart = dividerX + 1;
906466
+ const centerEnd = safeWidth - 2;
906467
+ const bodyTop = 3;
906468
+ const separatorY = bodyTop + contextRows;
906469
+ const lines = [];
906470
+ const top = borderLine(safeWidth, GLYPHS.frame.topLeft, GLYPHS.frame.horizontal, GLYPHS.frame.topRight);
906471
+ writeText2(top, 2, safeWidth - 4, ` ${options.title} `, { fg: WORKSPACE_PALETTE2.title, bold: true });
906472
+ if (options.stateLabel) {
906473
+ writeText2(top, Math.max(2, safeWidth - options.stateLabel.length - 4), options.stateLabel.length, options.stateLabel, {
906474
+ fg: WORKSPACE_PALETTE2.subtitle
906475
+ });
906476
+ }
906477
+ lines.push(top);
906478
+ const header = contentLine(safeWidth, WORKSPACE_PALETTE2.footerBg);
906479
+ drawVertical3(header, dividerX, WORKSPACE_PALETTE2.footerBg);
906480
+ writeText2(header, leftStart + 1, leftWidth - 2, options.leftHeader, {
906481
+ fg: WORKSPACE_PALETTE2.subtitle,
906482
+ bold: true,
906483
+ bg: WORKSPACE_PALETTE2.footerBg
906484
+ });
906485
+ writeText2(header, centerStart + 1, centerWidth - 2, options.mainHeader, {
906486
+ fg: WORKSPACE_PALETTE2.subtitle,
906487
+ bold: true,
906488
+ bg: WORKSPACE_PALETTE2.footerBg
906489
+ });
906490
+ lines.push(header);
906491
+ const headerSep = contentLine(safeWidth, "");
906492
+ drawVertical3(headerSep, dividerX);
906493
+ drawHorizontalRange2(headerSep, 1, safeWidth - 2);
906494
+ lines.push(headerSep);
906495
+ for (let row = 0;row < bodyRows; row += 1) {
906496
+ const y2 = bodyTop + row;
906497
+ const inContext = y2 < separatorY;
906498
+ const inSeparator = y2 === separatorY;
906499
+ const bg = inSeparator ? "" : inContext ? WORKSPACE_PALETTE2.contextBg : WORKSPACE_PALETTE2.controlsBg;
906500
+ const line2 = contentLine(safeWidth, bg);
906501
+ fillRange(line2, 1, dividerX - 1, WORKSPACE_PALETTE2.categoryBg);
906502
+ drawVertical3(line2, dividerX, bg);
906503
+ const leftRow = options.leftRows[row] ?? { text: "", kind: "empty" };
906504
+ if (leftRow.selected)
906505
+ fillRange(line2, leftStart, dividerX - 1, WORKSPACE_PALETTE2.selectedBg);
906506
+ writeText2(line2, leftStart + 1, leftWidth - 3, leftRow.text, {
906507
+ fg: leftRow.selected ? WORKSPACE_PALETTE2.text : rowFg2(leftRow, WORKSPACE_PALETTE2.muted),
906508
+ bg: leftRow.selected ? WORKSPACE_PALETTE2.selectedBg : WORKSPACE_PALETTE2.categoryBg,
906509
+ bold: leftRow.bold ?? (leftRow.selected || leftRow.kind === "group"),
906510
+ dim: leftRow.dim
906511
+ });
906512
+ if (inSeparator) {
906513
+ drawHorizontalRange2(line2, centerStart, centerEnd);
906514
+ } else if (inContext) {
906515
+ const contextRow = options.contextRows[row] ?? { text: "", kind: "empty" };
906516
+ writeText2(line2, centerStart + 1, contextWidth, contextRow.text, {
906517
+ fg: rowFg2(contextRow, WORKSPACE_PALETTE2.text),
906518
+ bg,
906519
+ bold: contextRow.bold,
906520
+ dim: contextRow.dim ?? contextRow.text.length === 0
906521
+ });
906522
+ } else {
906523
+ const controlRow = options.controlRows[row - contextRows - 1] ?? { text: "", kind: "empty" };
906524
+ if (controlRow.selected)
906525
+ fillRange(line2, centerStart, centerEnd, WORKSPACE_PALETTE2.selectedBg);
906526
+ writeText2(line2, centerStart + 1, contextWidth, controlRow.text, {
906527
+ fg: controlRow.selected ? WORKSPACE_PALETTE2.text : rowFg2(controlRow, WORKSPACE_PALETTE2.text),
906528
+ bg: controlRow.selected ? WORKSPACE_PALETTE2.selectedBg : bg,
906529
+ bold: controlRow.bold ?? controlRow.selected,
906530
+ dim: controlRow.dim ?? controlRow.text.length === 0
906531
+ });
906532
+ }
906533
+ lines.push(line2);
906534
+ }
906535
+ const footer = contentLine(safeWidth, WORKSPACE_PALETTE2.footerBg);
906536
+ writeText2(footer, 2, safeWidth - 4, options.footer, { fg: WORKSPACE_PALETTE2.muted, bg: WORKSPACE_PALETTE2.footerBg });
906537
+ lines.push(footer);
906538
+ lines.push(borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight));
906539
+ while (lines.length < safeHeight)
906540
+ lines.unshift(makeLine(safeWidth));
906541
+ return lines.slice(-safeHeight);
906542
+ }
906543
+
906544
+ // src/renderer/onboarding/onboarding-wizard.ts
906545
+ function modeLabel(mode) {
906546
+ if (mode === "edit")
906547
+ return "Edit existing";
906548
+ if (mode === "reopen")
906549
+ return "Reopen review";
906550
+ return "New setup";
906551
+ }
906552
+ function changedScreensLabel(wizard) {
906553
+ if (wizard.dirtyStepCount === 0)
906554
+ return "no changes";
906555
+ if (wizard.dirtyStepCount === 1)
906556
+ return "1 changed screen";
906557
+ return `${wizard.dirtyStepCount} changed screens`;
906558
+ }
906559
+ function stepGlyph(wizard, step, stepIndex) {
906560
+ if (stepIndex === wizard.stepIndex)
906561
+ return { glyph: GLYPHS.navigation.selected, fg: FULLSCREEN_PALETTE.info };
906562
+ const total = wizard.getStepFieldCount(stepIndex);
906563
+ const completed = wizard.getCompletedFieldCount(stepIndex);
906564
+ if (wizard.isStepDirty(stepIndex))
906565
+ return { glyph: GLYPHS.status.review, fg: FULLSCREEN_PALETTE.warn };
906566
+ if (total > 0 && completed === total)
906567
+ return { glyph: GLYPHS.status.success, fg: FULLSCREEN_PALETTE.good };
906568
+ return { glyph: GLYPHS.status.pending, fg: FULLSCREEN_PALETTE.muted };
906569
+ }
906570
+ function fieldRowPrefix(wizard, field, selected) {
906571
+ if (selected)
906572
+ return `${GLYPHS.navigation.selected} `;
906573
+ if (wizard.isFieldDirty(field.id))
906574
+ return `${GLYPHS.status.skipped} `;
906575
+ if (field.kind === "checklist")
906576
+ return wizard.getFieldValue(field) ? `${GLYPHS.status.success} ` : "\u25A1 ";
906577
+ if (field.kind === "acknowledgement")
906578
+ return wizard.getFieldValue(field) ? `${GLYPHS.status.success} ` : "\u25A1 ";
906579
+ if (field.kind === "action")
906580
+ return `${GLYPHS.navigation.next} `;
906581
+ if (field.kind === "radio")
906582
+ return `${GLYPHS.status.active} `;
906583
+ return " ";
906584
+ }
906585
+ function fieldColor(wizard, field, selected) {
906586
+ if (selected)
906587
+ return FULLSCREEN_PALETTE.text;
906588
+ if (field.kind === "status" || field.kind === "modelPicker")
906589
+ return FULLSCREEN_PALETTE.info;
906590
+ if (field.kind === "masked")
906591
+ return FULLSCREEN_PALETTE.warn;
906592
+ if (field.kind === "acknowledgement")
906593
+ return wizard.getFieldValue(field) ? FULLSCREEN_PALETTE.good : FULLSCREEN_PALETTE.warn;
906594
+ if (field.kind === "checklist")
906595
+ return wizard.getFieldValue(field) ? FULLSCREEN_PALETTE.good : FULLSCREEN_PALETTE.muted;
906596
+ if (wizard.getFieldValueLabel(field) === "Missing")
906597
+ return FULLSCREEN_PALETTE.warn;
906598
+ return FULLSCREEN_PALETTE.text;
906599
+ }
906600
+ function formatEditingValue(value, multiline) {
906601
+ if (!multiline)
906602
+ return value;
906603
+ const lines = value.split(/\r?\n/);
906604
+ if (lines.length <= 1)
906605
+ return value;
906606
+ const preview4 = lines[lines.length - 1] ?? "";
906607
+ return `${preview4} (${lines.length} lines)`;
906608
+ }
906609
+ function fieldHint(wizard, field, selected) {
906610
+ if (selected && wizard.isEditingTextField() && wizard.editingFieldId === field.id && (field.kind === "text" || field.kind === "masked")) {
906611
+ const rawValue = wizard.editBuffer.length > 0 ? wizard.editBuffer : field.placeholder;
906612
+ const editingValue = field.kind === "masked" && wizard.editBuffer.length > 0 ? "\u2022".repeat(Math.min(12, Math.max(4, wizard.editBuffer.length))) : formatEditingValue(rawValue, field.kind === "text" && field.multiline === true);
906613
+ return `Editing: ${editingValue}${GLYPHS.surface.cursor}`;
906614
+ }
906615
+ if (selected && field.kind === "modelPicker")
906616
+ return `${field.hint} Press Enter to open picker.`;
906617
+ if (selected && field.kind === "text") {
906618
+ return field.multiline === true ? `${field.hint} Press Enter to edit; Ctrl-J inserts a new line.` : `${field.hint} Press Enter to edit inline.`;
906619
+ }
906620
+ if (selected && field.kind === "masked")
906621
+ return `${field.hint} Press Enter to edit inline.`;
906622
+ return field.hint;
906623
+ }
906624
+ function selectedFieldText(wizard) {
906625
+ if (wizard.isEditingTextField() && wizard.editingFieldId !== null) {
906626
+ const editingField = wizard.getFieldById(wizard.editingFieldId);
906627
+ if (editingField) {
906628
+ return {
906629
+ title: `Editing: ${editingField.label}`,
906630
+ hint: fieldHint(wizard, editingField, true)
906631
+ };
906632
+ }
906633
+ }
906634
+ const field = wizard.getSelectedField();
906635
+ if (!field)
906636
+ return { title: "Selected: none", hint: "No selectable row is active on this screen." };
906637
+ return {
906638
+ title: `Selected: ${field.label} [${wizard.getFieldValueLabel(field)}]`,
906639
+ hint: fieldHint(wizard, field, true)
906640
+ };
906641
+ }
906642
+ function footerText4(wizard) {
906643
+ if (wizard.isEditingTextField()) {
906644
+ return wizard.isEditingMultilineTextField() ? "Controls: [Enter] save \xB7 [Ctrl-J] new line \xB7 [Esc] cancel \xB7 [Backspace] delete \xB7 [Del/Ctrl+U] clear" : "Controls: [Enter] save \xB7 [Esc] cancel \xB7 [Backspace] delete \xB7 [Del/Ctrl+U] clear";
906645
+ }
906646
+ return "Controls: [Enter] toggle/open \xB7 [Esc] close \xB7 [Tab/Shift+Tab] screen \xB7 [Up/Down] move \xB7 [Del/Ctrl+U] clear";
906647
+ }
906648
+ function controlsText(wizard) {
906649
+ if (wizard.isEditingTextField()) {
906650
+ return wizard.isEditingMultilineTextField() ? "Controls: Enter saves, Ctrl-J inserts a line, Esc cancels, Backspace deletes, Del clears." : "Controls: Enter saves, Esc cancels, Backspace deletes, Del clears.";
906651
+ }
906652
+ return "Controls: Enter selects, Del clears, Tab moves.";
906653
+ }
906654
+ function buildFieldRows(wizard, visibleFields, capacity) {
906655
+ wizard.ensureSelectionVisible(visibleFields);
906656
+ const fields = wizard.currentStep.fields;
906657
+ const rows = [];
906658
+ if (fields.length === 0 || capacity <= 0)
906659
+ return rows;
906660
+ const allRows = [];
906661
+ fields.forEach((field, absoluteIndex) => {
906662
+ const spacerRows = Math.max(0, field.spacerBeforeRows ?? 0);
906663
+ for (let index = 0;index < spacerRows; index += 1)
906664
+ allRows.push({ kind: "empty" });
906665
+ allRows.push({ kind: "field", field, absoluteIndex });
906666
+ });
906667
+ const selectedFieldIndex = wizard.getSelectedFieldIndex();
906668
+ const selectedRowIndex = Math.max(0, allRows.findIndex((row) => row.kind === "field" && row.absoluteIndex === selectedFieldIndex));
906669
+ const scrollFieldIndex = wizard.scrollOffsets[wizard.stepIndex] ?? 0;
906670
+ const scrollRowIndex = allRows.findIndex((row) => row.kind === "field" && row.absoluteIndex === scrollFieldIndex);
906671
+ const maxStart = Math.max(0, allRows.length - capacity);
906672
+ let start2 = Math.max(0, Math.min(scrollRowIndex >= 0 ? scrollRowIndex : 0, maxStart));
906673
+ if (selectedRowIndex < start2)
906674
+ start2 = selectedRowIndex;
906675
+ if (selectedRowIndex >= start2 + capacity)
906676
+ start2 = selectedRowIndex - capacity + 1;
906677
+ start2 = Math.max(0, Math.min(start2, maxStart));
906678
+ if (capacity > 1 && start2 > 0 && selectedRowIndex === start2)
906679
+ start2 = Math.max(0, start2 - 1);
906680
+ if (capacity > 1 && start2 + capacity < allRows.length && selectedRowIndex === start2 + capacity - 1) {
906681
+ start2 = Math.min(maxStart, start2 + 1);
906682
+ }
906683
+ rows.push(...allRows.slice(start2, start2 + capacity));
906684
+ const firstVisibleRow = rows[0];
906685
+ if (start2 > 0 && rows.length > 0 && !(firstVisibleRow?.kind === "field" && firstVisibleRow.absoluteIndex === selectedFieldIndex)) {
906686
+ rows[0] = { kind: "moreAbove", text: `${GLYPHS.navigation.moreAbove} ${start2} more above` };
906687
+ }
906688
+ const hiddenBelow = Math.max(0, allRows.length - (start2 + capacity));
906689
+ const lastVisibleRow = rows[rows.length - 1];
906690
+ if (hiddenBelow > 0 && rows.length > 0 && !(lastVisibleRow?.kind === "field" && lastVisibleRow.absoluteIndex === selectedFieldIndex)) {
906691
+ rows[rows.length - 1] = { kind: "moreBelow", text: `${GLYPHS.navigation.moreBelow} ${hiddenBelow} more below` };
906692
+ }
906693
+ while (rows.length < capacity)
906694
+ rows.push({ kind: "empty" });
906695
+ return rows.slice(0, capacity);
906696
+ }
906697
+ function pushWrapped(rows, text, width, options = {}) {
906698
+ if (text.length === 0) {
906699
+ rows.push({ text: "", kind: "empty" });
906700
+ return;
906701
+ }
906702
+ for (const line2 of wrapText(text, Math.max(1, width))) {
906703
+ rows.push({ text: line2, ...options });
906704
+ }
906705
+ }
906706
+ function buildStepRows(wizard, height) {
906707
+ const rendered = wizard.steps.map((step, stepIndex) => {
906708
+ const selected = stepIndex === wizard.stepIndex;
906709
+ const state4 = stepGlyph(wizard, step, stepIndex);
906710
+ const completion = `${wizard.getCompletedFieldCount(stepIndex)}/${wizard.getStepFieldCount(stepIndex)}`;
906711
+ return {
906712
+ text: `${state4.glyph} ${stepIndex + 1}. ${step.shortLabel} ${completion}`,
906713
+ selected,
906714
+ kind: "item",
906715
+ fg: state4.fg,
906716
+ bold: selected
906717
+ };
906718
+ });
906719
+ const visible = Math.max(1, height);
906720
+ const window3 = stableWindow(rendered.length, wizard.stepIndex, visible);
906721
+ const rows = rendered.slice(window3.start, window3.end);
906722
+ if (window3.start > 0 && rows.length > 0) {
906723
+ rows[0] = { text: `${GLYPHS.navigation.moreAbove} ${window3.start} more step(s) above`, kind: "more", fg: FULLSCREEN_PALETTE.dim, dim: true };
906724
+ }
906725
+ if (window3.end < rendered.length && rows.length > 0) {
906726
+ rows[rows.length - 1] = { text: `${GLYPHS.navigation.moreBelow} ${rendered.length - window3.end} more step(s) below`, kind: "more", fg: FULLSCREEN_PALETTE.dim, dim: true };
906727
+ }
906728
+ while (rows.length < height)
906729
+ rows.push({ text: "", kind: "empty" });
906730
+ return rows.slice(0, height);
906731
+ }
906732
+ function buildContextRows2(wizard, width) {
906733
+ const step = wizard.currentStep;
906734
+ const selected = selectedFieldText(wizard);
906735
+ const rows = [
906736
+ { text: step.title, fg: FULLSCREEN_PALETTE.title, bold: true }
906737
+ ];
906738
+ const pushSummaryRows = () => {
906739
+ rows.push({ text: "Summary", fg: FULLSCREEN_PALETTE.subtitle, bold: true });
906740
+ rows.push({ text: step.summaryTitle, fg: FULLSCREEN_PALETTE.info, bold: true });
906741
+ for (const line2 of step.summaryLines.slice(0, 4)) {
906742
+ pushWrapped(rows, line2, width, { fg: FULLSCREEN_PALETTE.text });
906743
+ }
906744
+ rows.push({
906745
+ text: `Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete \xB7 ${changedScreensLabel(wizard)}`,
906746
+ fg: wizard.dirtyStepCount > 0 ? FULLSCREEN_PALETTE.warn : FULLSCREEN_PALETTE.muted
906747
+ });
906748
+ };
906749
+ const pushSelectedRows = () => {
906750
+ rows.push({ text: selected.title, fg: FULLSCREEN_PALETTE.info, bold: true });
906751
+ pushWrapped(rows, selected.hint, width, { fg: FULLSCREEN_PALETTE.text });
906752
+ pushWrapped(rows, controlsText(wizard), width, { fg: FULLSCREEN_PALETTE.info });
906753
+ };
906754
+ pushWrapped(rows, step.description, width, { fg: FULLSCREEN_PALETTE.text });
906755
+ rows.push({ text: "", kind: "empty" });
906756
+ if (wizard.isEditingTextField()) {
906757
+ pushSelectedRows();
906758
+ rows.push({ text: "", kind: "empty" });
906759
+ pushSummaryRows();
906760
+ } else {
906761
+ pushSummaryRows();
906762
+ rows.push({ text: "", kind: "empty" });
906763
+ pushSelectedRows();
906764
+ }
906765
+ if (wizard.hydrationPending || wizard.hydrationError !== null || wizard.applyFeedback !== null) {
906766
+ rows.push({ text: "", kind: "empty" });
906767
+ }
906768
+ if (wizard.hydrationPending) {
906769
+ rows.push({ text: "Loading current settings...", fg: FULLSCREEN_PALETTE.info, bold: true });
906770
+ }
906771
+ if (wizard.hydrationError !== null) {
906772
+ pushWrapped(rows, `Current settings could not load: ${wizard.hydrationError}`, width, { fg: FULLSCREEN_PALETTE.bad, bold: true });
906773
+ }
906774
+ if (wizard.applyFeedback !== null) {
906775
+ const feedbackColor = wizard.applyFeedback.severity === "error" ? FULLSCREEN_PALETTE.bad : wizard.applyFeedback.severity === "warning" ? FULLSCREEN_PALETTE.warn : FULLSCREEN_PALETTE.info;
906776
+ rows.push({ text: wizard.applyFeedback.title, fg: feedbackColor, bold: true });
906777
+ pushWrapped(rows, wizard.applyFeedback.summary, width, { fg: FULLSCREEN_PALETTE.text });
906778
+ for (const message of wizard.applyFeedback.messages) {
906779
+ pushWrapped(rows, message, width, { fg: FULLSCREEN_PALETTE.muted });
906780
+ }
906781
+ }
906782
+ return rows;
906783
+ }
906784
+ function formatFieldRowText(wizard, field, selected, width) {
906785
+ const badge = `[${wizard.getFieldValueLabel(field)}]`;
906786
+ const badgeWidth = getDisplayWidth(badge);
906787
+ const labelWidth = Math.max(1, width - badgeWidth - 2);
906788
+ const label = truncateDisplay(`${fieldRowPrefix(wizard, field, selected)}${field.label}`, labelWidth);
906789
+ return `${label}${" ".repeat(Math.max(1, width - getDisplayWidth(label) - badgeWidth))}${badge}`;
906790
+ }
906791
+ function buildControlRows2(wizard, width, height) {
906792
+ const visibleFields = Math.max(1, height);
906793
+ const fieldRows = buildFieldRows(wizard, visibleFields, height);
906794
+ const rows = fieldRows.map((row) => {
906795
+ if (row.kind === "empty")
906796
+ return { text: "", kind: "empty" };
906797
+ if (row.kind === "moreAbove" || row.kind === "moreBelow") {
906798
+ return { text: row.text, kind: "more", fg: FULLSCREEN_PALETTE.dim, dim: true };
906799
+ }
906800
+ const selected = row.absoluteIndex === wizard.getSelectedFieldIndex();
906801
+ return {
906802
+ text: formatFieldRowText(wizard, row.field, selected, width),
906803
+ selected,
906804
+ kind: "item",
906805
+ fg: fieldColor(wizard, row.field, selected),
906806
+ bold: selected || row.field.kind === "action"
906807
+ };
906808
+ });
906809
+ while (rows.length < height)
906810
+ rows.push({ text: "", kind: "empty" });
906811
+ return rows.slice(0, height);
906812
+ }
906813
+ function renderOnboardingWizard(wizard, width, viewportHeight) {
906814
+ const layoutOptions = {
906815
+ width,
906816
+ height: viewportHeight,
906817
+ leftWidth: width < 90 ? undefined : 30,
906818
+ contextRatio: 0.42,
906819
+ minContextRows: 8
906820
+ };
906821
+ const metrics = getFullscreenWorkspaceMetrics2(layoutOptions);
906822
+ return renderFullscreenWorkspace2({
906823
+ width,
906824
+ height: viewportHeight,
906825
+ title: "GoodVibes Agent / Onboarding Wizard",
906826
+ stateLabel: `${modeLabel(wizard.mode)} \xB7 ${wizard.stepIndex + 1}/${wizard.steps.length} \xB7 ${changedScreensLabel(wizard)}`,
906827
+ leftHeader: "Steps",
906828
+ mainHeader: wizard.currentStep.title,
906829
+ leftRows: buildStepRows(wizard, metrics.bodyRows),
906830
+ contextRows: buildContextRows2(wizard, metrics.contextWidth),
906831
+ controlRows: buildControlRows2(wizard, metrics.contextWidth, metrics.controlRows),
906832
+ footer: footerText4(wizard),
906833
+ leftWidth: layoutOptions.leftWidth,
906834
+ contextRatio: layoutOptions.contextRatio,
906835
+ minContextRows: layoutOptions.minContextRows
906836
+ });
906837
+ }
906838
+
906720
906839
  // src/shell/onboarding-fullscreen.ts
906721
906840
  function normalizeFullscreenViewport(lines, width, height) {
906722
906841
  const viewport = lines.slice(0, height).map((line2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "private": false,
5
5
  "description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
6
6
  "type": "module",
@@ -19,7 +19,6 @@ import { renderProfilePickerModal } from './profile-picker-modal.ts';
19
19
  import { renderBookmarkModal } from './bookmark-modal.ts';
20
20
  import { renderHelpOverlay, renderShortcutsOverlay } from './help-overlay.ts';
21
21
  import { renderAutocompleteOverlay } from './autocomplete-overlay.ts';
22
- import { renderOnboardingWizard } from './onboarding/onboarding-wizard.ts';
23
22
  import { overlayViewportBottom, replaceViewportWithOverlay } from './conversation-layout.ts';
24
23
 
25
24
  export interface ConversationOverlayContext {
@@ -41,9 +40,7 @@ export function applyConversationOverlays(
41
40
  const bottomDockInset = 1 + (input.searchManager.active || input.historySearch.active ? 1 : 0);
42
41
 
43
42
  if (input.onboardingWizard.active) {
44
- const lines = renderOnboardingWizard(input.onboardingWizard, conversationWidth, viewportHeight);
45
- next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
46
- if (!input.modelPicker.active) return next;
43
+ return next;
47
44
  }
48
45
 
49
46
  if (input.filePicker.active) {
@@ -6,7 +6,7 @@ import {
6
6
  stableWindow,
7
7
  WORKSPACE_PALETTE as PALETTE,
8
8
  type WorkspaceRow,
9
- } from '../fullscreen-workspace.ts';
9
+ } from './onboarding-workspace.ts';
10
10
  import { GLYPHS } from '../ui-primitives.ts';
11
11
  import {
12
12
  type OnboardingWizardController,
@@ -0,0 +1,199 @@
1
+ import type { Line } from '../../types/grid.ts';
2
+ import { GLYPHS } from '../ui-primitives.ts';
3
+ import {
4
+ borderLine,
5
+ clamp,
6
+ contentLine,
7
+ drawHorizontalRule,
8
+ drawVerticalRule,
9
+ fillRange,
10
+ FULLSCREEN_PALETTE,
11
+ makeLine,
12
+ writeText,
13
+ } from '../fullscreen-primitives.ts';
14
+
15
+ export {
16
+ borderLine,
17
+ clamp,
18
+ contentLine,
19
+ fillRange,
20
+ makeLine,
21
+ padDisplay,
22
+ stableWindow,
23
+ writeText,
24
+ } from '../fullscreen-primitives.ts';
25
+ export { FULLSCREEN_PALETTE as WORKSPACE_PALETTE } from '../fullscreen-primitives.ts';
26
+
27
+ const WORKSPACE_PALETTE = FULLSCREEN_PALETTE;
28
+
29
+ export interface WorkspaceRow {
30
+ readonly text: string;
31
+ readonly selected?: boolean;
32
+ readonly bold?: boolean;
33
+ readonly dim?: boolean;
34
+ readonly fg?: string;
35
+ readonly bg?: string;
36
+ readonly kind?: 'group' | 'item' | 'more' | 'empty';
37
+ }
38
+
39
+ export interface FullscreenWorkspaceRenderOptions {
40
+ readonly width: number;
41
+ readonly height: number;
42
+ readonly title: string;
43
+ readonly stateLabel?: string;
44
+ readonly leftHeader: string;
45
+ readonly mainHeader: string;
46
+ readonly leftRows: readonly WorkspaceRow[];
47
+ readonly contextRows: readonly WorkspaceRow[];
48
+ readonly controlRows: readonly WorkspaceRow[];
49
+ readonly footer: string;
50
+ readonly leftWidth?: number;
51
+ readonly contextRatio?: number;
52
+ readonly minContextRows?: number;
53
+ }
54
+
55
+ export interface FullscreenWorkspaceMetrics {
56
+ readonly safeWidth: number;
57
+ readonly safeHeight: number;
58
+ readonly leftWidth: number;
59
+ readonly centerWidth: number;
60
+ readonly bodyRows: number;
61
+ readonly contextWidth: number;
62
+ readonly contextRows: number;
63
+ readonly controlRows: number;
64
+ }
65
+
66
+ export function drawVertical(line: Line, x: number, bg = ''): void {
67
+ if (x <= 0 || x >= line.length - 1) return;
68
+ drawVerticalRule(line, x, WORKSPACE_PALETTE.border, bg);
69
+ }
70
+
71
+ export function drawHorizontalRange(line: Line, startX: number, endX: number, bg = ''): void {
72
+ drawHorizontalRule(line, Math.max(1, startX), Math.min(line.length - 2, endX), WORKSPACE_PALETTE.border, bg);
73
+ }
74
+
75
+ function leftWidthFor(width: number, explicit?: number): number {
76
+ if (explicit !== undefined) return clamp(explicit, 14, Math.max(14, width - 24));
77
+ return width < 80
78
+ ? clamp(Math.round(width * 0.32), 14, Math.max(14, width - 24))
79
+ : clamp(Math.round(width * 0.22), 24, 34);
80
+ }
81
+
82
+ export function getFullscreenWorkspaceMetrics(options: Pick<
83
+ FullscreenWorkspaceRenderOptions,
84
+ 'width' | 'height' | 'leftWidth' | 'contextRatio' | 'minContextRows'
85
+ >): FullscreenWorkspaceMetrics {
86
+ const safeWidth = Math.max(1, options.width);
87
+ const safeHeight = Math.max(12, options.height);
88
+ const leftWidth = leftWidthFor(safeWidth, options.leftWidth);
89
+ const centerWidth = Math.max(20, safeWidth - leftWidth - 3);
90
+ const bodyTop = 3;
91
+ const footerY = safeHeight - 2;
92
+ const bodyRows = Math.max(4, footerY - bodyTop);
93
+ const contextWidth = Math.max(10, centerWidth - 2);
94
+ const maxContextRows = Math.max(3, bodyRows - 4);
95
+ const minContextRows = clamp(options.minContextRows ?? 10, 3, maxContextRows);
96
+ const contextRows = clamp(
97
+ Math.round(bodyRows * (options.contextRatio ?? 0.4)),
98
+ Math.min(minContextRows, maxContextRows),
99
+ maxContextRows,
100
+ );
101
+ const controlRows = Math.max(3, bodyRows - contextRows - 1);
102
+ return { safeWidth, safeHeight, leftWidth, centerWidth, bodyRows, contextWidth, contextRows, controlRows };
103
+ }
104
+
105
+ function rowFg(row: WorkspaceRow, fallback: string): string {
106
+ if (row.fg) return row.fg;
107
+ if (row.kind === 'group') return WORKSPACE_PALETTE.subtitle;
108
+ if (row.kind === 'more') return WORKSPACE_PALETTE.dim;
109
+ return fallback;
110
+ }
111
+
112
+ export function renderFullscreenWorkspace(options: FullscreenWorkspaceRenderOptions): Line[] {
113
+ const { safeWidth, safeHeight, leftWidth, centerWidth, bodyRows, contextWidth, contextRows } = getFullscreenWorkspaceMetrics(options);
114
+ const leftStart = 1;
115
+ const dividerX = leftWidth + 1;
116
+ const centerStart = dividerX + 1;
117
+ const centerEnd = safeWidth - 2;
118
+ const bodyTop = 3;
119
+ const separatorY = bodyTop + contextRows;
120
+ const lines: Line[] = [];
121
+
122
+ const top = borderLine(safeWidth, GLYPHS.frame.topLeft, GLYPHS.frame.horizontal, GLYPHS.frame.topRight);
123
+ writeText(top, 2, safeWidth - 4, ` ${options.title} `, { fg: WORKSPACE_PALETTE.title, bold: true });
124
+ if (options.stateLabel) {
125
+ writeText(top, Math.max(2, safeWidth - options.stateLabel.length - 4), options.stateLabel.length, options.stateLabel, {
126
+ fg: WORKSPACE_PALETTE.subtitle,
127
+ });
128
+ }
129
+ lines.push(top);
130
+
131
+ const header = contentLine(safeWidth, WORKSPACE_PALETTE.footerBg);
132
+ drawVertical(header, dividerX, WORKSPACE_PALETTE.footerBg);
133
+ writeText(header, leftStart + 1, leftWidth - 2, options.leftHeader, {
134
+ fg: WORKSPACE_PALETTE.subtitle,
135
+ bold: true,
136
+ bg: WORKSPACE_PALETTE.footerBg,
137
+ });
138
+ writeText(header, centerStart + 1, centerWidth - 2, options.mainHeader, {
139
+ fg: WORKSPACE_PALETTE.subtitle,
140
+ bold: true,
141
+ bg: WORKSPACE_PALETTE.footerBg,
142
+ });
143
+ lines.push(header);
144
+
145
+ const headerSep = contentLine(safeWidth, '');
146
+ drawVertical(headerSep, dividerX);
147
+ drawHorizontalRange(headerSep, 1, safeWidth - 2);
148
+ lines.push(headerSep);
149
+
150
+ for (let row = 0; row < bodyRows; row += 1) {
151
+ const y = bodyTop + row;
152
+ const inContext = y < separatorY;
153
+ const inSeparator = y === separatorY;
154
+ const bg = inSeparator ? '' : inContext ? WORKSPACE_PALETTE.contextBg : WORKSPACE_PALETTE.controlsBg;
155
+ const line = contentLine(safeWidth, bg);
156
+ fillRange(line, 1, dividerX - 1, WORKSPACE_PALETTE.categoryBg);
157
+ drawVertical(line, dividerX, bg);
158
+
159
+ const leftRow = options.leftRows[row] ?? { text: '', kind: 'empty' as const };
160
+ if (leftRow.selected) fillRange(line, leftStart, dividerX - 1, WORKSPACE_PALETTE.selectedBg);
161
+ writeText(line, leftStart + 1, leftWidth - 3, leftRow.text, {
162
+ fg: leftRow.selected ? WORKSPACE_PALETTE.text : rowFg(leftRow, WORKSPACE_PALETTE.muted),
163
+ bg: leftRow.selected ? WORKSPACE_PALETTE.selectedBg : WORKSPACE_PALETTE.categoryBg,
164
+ bold: leftRow.bold ?? (leftRow.selected || leftRow.kind === 'group'),
165
+ dim: leftRow.dim,
166
+ });
167
+
168
+ if (inSeparator) {
169
+ drawHorizontalRange(line, centerStart, centerEnd);
170
+ } else if (inContext) {
171
+ const contextRow = options.contextRows[row] ?? { text: '', kind: 'empty' as const };
172
+ writeText(line, centerStart + 1, contextWidth, contextRow.text, {
173
+ fg: rowFg(contextRow, WORKSPACE_PALETTE.text),
174
+ bg,
175
+ bold: contextRow.bold,
176
+ dim: contextRow.dim ?? contextRow.text.length === 0,
177
+ });
178
+ } else {
179
+ const controlRow = options.controlRows[row - contextRows - 1] ?? { text: '', kind: 'empty' as const };
180
+ if (controlRow.selected) fillRange(line, centerStart, centerEnd, WORKSPACE_PALETTE.selectedBg);
181
+ writeText(line, centerStart + 1, contextWidth, controlRow.text, {
182
+ fg: controlRow.selected ? WORKSPACE_PALETTE.text : rowFg(controlRow, WORKSPACE_PALETTE.text),
183
+ bg: controlRow.selected ? WORKSPACE_PALETTE.selectedBg : bg,
184
+ bold: controlRow.bold ?? controlRow.selected,
185
+ dim: controlRow.dim ?? controlRow.text.length === 0,
186
+ });
187
+ }
188
+
189
+ lines.push(line);
190
+ }
191
+
192
+ const footer = contentLine(safeWidth, WORKSPACE_PALETTE.footerBg);
193
+ writeText(footer, 2, safeWidth - 4, options.footer, { fg: WORKSPACE_PALETTE.muted, bg: WORKSPACE_PALETTE.footerBg });
194
+ lines.push(footer);
195
+ lines.push(borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight));
196
+
197
+ while (lines.length < safeHeight) lines.unshift(makeLine(safeWidth));
198
+ return lines.slice(-safeHeight);
199
+ }
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.0.38';
9
+ let _version = '1.0.40';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
12
12
  readonly version?: unknown;