@pellux/goodvibes-tui 0.20.0 → 0.20.1
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 +7 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/renderer/fullscreen-primitives.ts +130 -0
- package/src/renderer/fullscreen-workspace.ts +199 -0
- package/src/renderer/mcp-workspace.ts +176 -236
- package/src/renderer/onboarding/onboarding-wizard.ts +16 -42
- package/src/renderer/overlay-box.ts +12 -31
- package/src/renderer/settings-modal.ts +53 -210
- package/src/version.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
1
|
+
import { createStyledCell, type Line } from '../types/grid.ts';
|
|
3
2
|
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
4
3
|
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
4
|
+
import { fillWidth, makeLine, writeText } from './fullscreen-primitives.ts';
|
|
5
5
|
|
|
6
6
|
export interface OverlayBoxPalette {
|
|
7
7
|
readonly borderFg: string;
|
|
@@ -60,29 +60,12 @@ export function putOverlayText(
|
|
|
60
60
|
text: string,
|
|
61
61
|
style: OverlayTextStyle,
|
|
62
62
|
): void {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
line[x] = createStyledCell(ch, {
|
|
70
|
-
fg: style.fg,
|
|
71
|
-
bg: style.bg ?? '',
|
|
72
|
-
bold: style.bold ?? false,
|
|
73
|
-
dim: style.dim ?? false,
|
|
74
|
-
});
|
|
75
|
-
if (cellWidth > 1 && x + 1 < line.length) {
|
|
76
|
-
line[x + 1] = createStyledCell(' ', {
|
|
77
|
-
fg: style.fg,
|
|
78
|
-
bg: style.bg ?? '',
|
|
79
|
-
bold: style.bold ?? false,
|
|
80
|
-
dim: style.dim ?? false,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
x += cellWidth;
|
|
84
|
-
used += cellWidth;
|
|
85
|
-
}
|
|
63
|
+
writeText(line, startX, maxWidth, text, {
|
|
64
|
+
fg: style.fg,
|
|
65
|
+
bg: style.bg ?? '',
|
|
66
|
+
bold: style.bold ?? false,
|
|
67
|
+
dim: style.dim ?? false,
|
|
68
|
+
});
|
|
86
69
|
}
|
|
87
70
|
|
|
88
71
|
export function createOverlayBorderLine(
|
|
@@ -93,7 +76,7 @@ export function createOverlayBorderLine(
|
|
|
93
76
|
right: string,
|
|
94
77
|
fg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
|
|
95
78
|
): Line {
|
|
96
|
-
const line =
|
|
79
|
+
const line = makeLine(terminalWidth);
|
|
97
80
|
const leftX = layout.margin;
|
|
98
81
|
const rightX = layout.margin + layout.width - 1;
|
|
99
82
|
line[leftX] = createStyledCell(left, { fg });
|
|
@@ -110,13 +93,11 @@ export function createOverlayContentLine(
|
|
|
110
93
|
borderFg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
|
|
111
94
|
bg = '',
|
|
112
95
|
): Line {
|
|
113
|
-
const line =
|
|
96
|
+
const line = makeLine(terminalWidth);
|
|
114
97
|
const leftX = layout.margin;
|
|
115
98
|
const rightX = layout.margin + layout.width - 1;
|
|
116
99
|
line[leftX] = createStyledCell('│', { fg: borderFg });
|
|
117
|
-
|
|
118
|
-
line[x] = createStyledCell(' ', { bg });
|
|
119
|
-
}
|
|
100
|
+
fillWidth(line, leftX + 1, rightX - leftX - 1, bg);
|
|
120
101
|
line[rightX] = createStyledCell('│', { fg: borderFg });
|
|
121
102
|
return line;
|
|
122
103
|
}
|
|
@@ -130,7 +111,7 @@ export function createOverlayFilledBorderLine(
|
|
|
130
111
|
fg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
|
|
131
112
|
bg = '',
|
|
132
113
|
): Line {
|
|
133
|
-
const line =
|
|
114
|
+
const line = makeLine(terminalWidth);
|
|
134
115
|
const leftX = layout.margin;
|
|
135
116
|
const rightX = layout.margin + layout.width - 1;
|
|
136
117
|
line[leftX] = createStyledCell(left, { fg, bg });
|
|
@@ -6,31 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Line } from '../types/grid.ts';
|
|
9
|
-
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
10
9
|
import type { SettingsModal, SettingEntry, FlagEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
|
|
11
10
|
import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
|
|
12
11
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
13
12
|
import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
14
13
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
15
|
-
import { GLYPHS
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
categoryBg: '#141b25',
|
|
26
|
-
contextBg: '#121923',
|
|
27
|
-
controlsBg: '#0f141d',
|
|
28
|
-
footerBg: '#111827',
|
|
29
|
-
good: UI_TONES.state.good,
|
|
30
|
-
warn: UI_TONES.state.warn,
|
|
31
|
-
bad: UI_TONES.state.bad,
|
|
32
|
-
info: UI_TONES.state.info,
|
|
33
|
-
};
|
|
14
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
15
|
+
import {
|
|
16
|
+
clamp,
|
|
17
|
+
getFullscreenWorkspaceMetrics,
|
|
18
|
+
padDisplay,
|
|
19
|
+
renderFullscreenWorkspace,
|
|
20
|
+
stableWindow,
|
|
21
|
+
WORKSPACE_PALETTE as PALETTE,
|
|
22
|
+
type WorkspaceRow,
|
|
23
|
+
} from './fullscreen-workspace.ts';
|
|
34
24
|
|
|
35
25
|
const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
36
26
|
display: 'Presentation settings for the terminal transcript: streaming, line numbers, thinking visibility, reasoning summaries, token speed, and tool previews.',
|
|
@@ -131,77 +121,6 @@ const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
|
|
|
131
121
|
},
|
|
132
122
|
};
|
|
133
123
|
|
|
134
|
-
function clamp(value: number, min: number, max: number): number {
|
|
135
|
-
return Math.max(min, Math.min(max, value));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function fillRange(line: Line, startX: number, endX: number, bg: string): void {
|
|
139
|
-
for (let x = Math.max(0, startX); x <= Math.min(line.length - 1, endX); x += 1) {
|
|
140
|
-
const cell = line[x] ?? createStyledCell(' ');
|
|
141
|
-
line[x] = createStyledCell(cell.char, {
|
|
142
|
-
fg: cell.fg,
|
|
143
|
-
bg,
|
|
144
|
-
bold: cell.bold,
|
|
145
|
-
dim: cell.dim,
|
|
146
|
-
underline: cell.underline,
|
|
147
|
-
italic: cell.italic,
|
|
148
|
-
strikethrough: cell.strikethrough,
|
|
149
|
-
link: cell.link,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function writeText(line: Line, startX: number, maxWidth: number, text: string, style: Partial<Omit<Line[number], 'char'>> = {}): void {
|
|
155
|
-
let x = startX;
|
|
156
|
-
let used = 0;
|
|
157
|
-
for (const ch of text) {
|
|
158
|
-
const width = getDisplayWidth(ch);
|
|
159
|
-
if (width <= 0) continue;
|
|
160
|
-
if (used + width > maxWidth || x >= line.length) break;
|
|
161
|
-
line[x] = createStyledCell(ch, style);
|
|
162
|
-
if (width > 1 && x + 1 < line.length) {
|
|
163
|
-
line[x + 1] = createStyledCell(' ', style);
|
|
164
|
-
}
|
|
165
|
-
x += width;
|
|
166
|
-
used += width;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function makeLine(width: number, bg = ''): Line {
|
|
171
|
-
const line = createEmptyLine(width);
|
|
172
|
-
if (bg) fillRange(line, 0, width - 1, bg);
|
|
173
|
-
return line;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function borderLine(width: number, left: string, fill: string, right: string): Line {
|
|
177
|
-
const line = makeLine(width);
|
|
178
|
-
if (width <= 0) return line;
|
|
179
|
-
line[0] = createStyledCell(left, { fg: PALETTE.border });
|
|
180
|
-
for (let x = 1; x < width - 1; x += 1) {
|
|
181
|
-
line[x] = createStyledCell(fill, { fg: PALETTE.border });
|
|
182
|
-
}
|
|
183
|
-
if (width > 1) line[width - 1] = createStyledCell(right, { fg: PALETTE.border });
|
|
184
|
-
return line;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function contentLine(width: number, bg: string): Line {
|
|
188
|
-
const line = makeLine(width, bg);
|
|
189
|
-
if (width > 0) line[0] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
|
|
190
|
-
if (width > 1) line[width - 1] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
|
|
191
|
-
return line;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function drawVertical(line: Line, x: number, bg = ''): void {
|
|
195
|
-
if (x <= 0 || x >= line.length - 1) return;
|
|
196
|
-
line[x] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border, bg });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function drawHorizontalRange(line: Line, startX: number, endX: number, bg = ''): void {
|
|
200
|
-
for (let x = Math.max(1, startX); x <= Math.min(line.length - 2, endX); x += 1) {
|
|
201
|
-
line[x] = createStyledCell(GLYPHS.frame.horizontal, { fg: PALETTE.border, bg });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
124
|
function paddedWrapped(text: string, width: number, prefix = ''): string[] {
|
|
206
125
|
const safeWidth = Math.max(1, width - getDisplayWidth(prefix));
|
|
207
126
|
const wrapped = wrapText(text, safeWidth);
|
|
@@ -209,34 +128,6 @@ function paddedWrapped(text: string, width: number, prefix = ''): string[] {
|
|
|
209
128
|
return wrapped.map((line, index) => `${index === 0 ? prefix : ' '.repeat(getDisplayWidth(prefix))}${line}`);
|
|
210
129
|
}
|
|
211
130
|
|
|
212
|
-
function clipDisplay(text: string, width: number): string {
|
|
213
|
-
if (width <= 0) return '';
|
|
214
|
-
let used = 0;
|
|
215
|
-
let output = '';
|
|
216
|
-
for (const ch of text) {
|
|
217
|
-
const chWidth = getDisplayWidth(ch);
|
|
218
|
-
if (chWidth <= 0) continue;
|
|
219
|
-
if (used + chWidth > width) break;
|
|
220
|
-
output += ch;
|
|
221
|
-
used += chWidth;
|
|
222
|
-
}
|
|
223
|
-
return output;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function padDisplay(text: string, width: number): string {
|
|
227
|
-
const clipped = clipDisplay(text, width);
|
|
228
|
-
return `${clipped}${' '.repeat(Math.max(0, width - getDisplayWidth(clipped)))}`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function stableWindow(total: number, selectedIndex: number, visibleCount: number): { start: number; end: number } {
|
|
232
|
-
if (total <= 0 || visibleCount <= 0) return { start: 0, end: 0 };
|
|
233
|
-
if (total <= visibleCount) return { start: 0, end: total };
|
|
234
|
-
const selected = clamp(selectedIndex, 0, total - 1);
|
|
235
|
-
const half = Math.floor(visibleCount / 2);
|
|
236
|
-
const start = clamp(selected - half, 0, total - visibleCount);
|
|
237
|
-
return { start, end: start + visibleCount };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
131
|
function formatDefaultValue(value: unknown): string {
|
|
241
132
|
if (value === '') return '(empty)';
|
|
242
133
|
if (value === null || value === undefined) return '(unset)';
|
|
@@ -596,99 +487,51 @@ export function renderSettingsModal(
|
|
|
596
487
|
width: number,
|
|
597
488
|
viewportHeight = 24,
|
|
598
489
|
): Line[] {
|
|
599
|
-
const safeWidth = Math.max(1, width);
|
|
600
|
-
const safeHeight = Math.max(12, viewportHeight);
|
|
601
|
-
const lines: Line[] = [];
|
|
602
|
-
const leftWidth = safeWidth < 80
|
|
603
|
-
? clamp(Math.round(safeWidth * 0.32), 14, Math.max(14, safeWidth - 24))
|
|
604
|
-
: clamp(Math.round(safeWidth * 0.22), 24, 34);
|
|
605
|
-
const centerWidth = Math.max(20, safeWidth - leftWidth - 3);
|
|
606
|
-
const leftStart = 1;
|
|
607
|
-
const dividerX = leftWidth + 1;
|
|
608
|
-
const centerStart = dividerX + 1;
|
|
609
|
-
const centerEnd = safeWidth - 2;
|
|
610
|
-
const bodyTop = 3;
|
|
611
|
-
const footerY = safeHeight - 2;
|
|
612
|
-
const bodyRows = Math.max(4, footerY - bodyTop);
|
|
613
|
-
const contextWidth = Math.max(10, centerWidth - 2);
|
|
614
|
-
const contextLines = buildContextLines(modal, contextWidth);
|
|
615
|
-
const maxContextRows = Math.max(3, bodyRows - 4);
|
|
616
|
-
const contextRows = clamp(Math.round(bodyRows * 0.4), Math.min(10, maxContextRows), maxContextRows);
|
|
617
|
-
const controlsRows = Math.max(3, bodyRows - contextRows - 1);
|
|
618
|
-
const separatorY = bodyTop + contextRows;
|
|
619
|
-
|
|
620
|
-
const top = borderLine(safeWidth, GLYPHS.frame.topLeft, GLYPHS.frame.horizontal, GLYPHS.frame.topRight);
|
|
621
|
-
writeText(top, 2, safeWidth - 4, ` Configuration Workspace / Settings `, { fg: PALETTE.title, bold: true });
|
|
622
|
-
lines.push(top);
|
|
623
|
-
|
|
624
|
-
const header = contentLine(safeWidth, PALETTE.footerBg);
|
|
625
|
-
drawVertical(header, dividerX, PALETTE.footerBg);
|
|
626
|
-
writeText(header, leftStart + 1, leftWidth - 2, 'Categories', { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
|
|
627
490
|
const notices = [
|
|
628
491
|
...(modal.lastSaveTriggeredRestart ? [`Restarting ${modal.lastSaveTriggeredRestart}`] : []),
|
|
629
492
|
...(modal.lastSettingEffectMessage ? [modal.lastSettingEffectMessage] : []),
|
|
630
493
|
];
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const selected = controlText.startsWith(GLYPHS.navigation.selected);
|
|
675
|
-
if (selected) fillRange(line, centerStart, centerEnd, PALETTE.selectedBg);
|
|
676
|
-
writeText(line, centerStart + 1, contextWidth, controlText, {
|
|
677
|
-
fg: selected ? PALETTE.text : controlText.startsWith('value:') || controlText.trimStart().startsWith('value:') ? PALETTE.info : rowColorForSetting(modal, controlText),
|
|
678
|
-
bg: selected ? PALETTE.selectedBg : bg,
|
|
679
|
-
bold: selected,
|
|
680
|
-
dim: controlText.length === 0,
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
lines.push(line);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
const footer = contentLine(safeWidth, PALETTE.footerBg);
|
|
687
|
-
writeText(footer, 2, safeWidth - 4, footerText(modal), { fg: PALETTE.muted, bg: PALETTE.footerBg });
|
|
688
|
-
lines.push(footer);
|
|
689
|
-
const bottom = borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight);
|
|
690
|
-
lines.push(bottom);
|
|
691
|
-
|
|
692
|
-
while (lines.length < safeHeight) lines.unshift(makeLine(safeWidth));
|
|
693
|
-
return lines.slice(-safeHeight);
|
|
494
|
+
const metrics = getFullscreenWorkspaceMetrics({ width, height: viewportHeight });
|
|
495
|
+
const categoryRows = renderCategories(modal, metrics.leftWidth - 2, metrics.bodyRows);
|
|
496
|
+
const contextRows = buildContextLines(modal, metrics.contextWidth).map((text, row): WorkspaceRow => {
|
|
497
|
+
const selectedSetting = modal.getSelected();
|
|
498
|
+
const isTitle = row === 0 || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
|
|
499
|
+
return {
|
|
500
|
+
text,
|
|
501
|
+
fg: row === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
|
|
502
|
+
bold: isTitle,
|
|
503
|
+
dim: text.length === 0,
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
const controlRows = renderControlRows(modal, metrics.contextWidth, metrics.controlRows).map((text): WorkspaceRow => {
|
|
507
|
+
const selected = text.startsWith(GLYPHS.navigation.selected);
|
|
508
|
+
return {
|
|
509
|
+
text,
|
|
510
|
+
selected,
|
|
511
|
+
fg: selected
|
|
512
|
+
? PALETTE.text
|
|
513
|
+
: text.startsWith('value:') || text.trimStart().startsWith('value:')
|
|
514
|
+
? PALETTE.info
|
|
515
|
+
: rowColorForSetting(modal, text),
|
|
516
|
+
bold: selected,
|
|
517
|
+
dim: text.length === 0,
|
|
518
|
+
};
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return renderFullscreenWorkspace({
|
|
522
|
+
width,
|
|
523
|
+
height: viewportHeight,
|
|
524
|
+
title: 'Configuration Workspace / Settings',
|
|
525
|
+
leftHeader: 'Categories',
|
|
526
|
+
mainHeader: `${CATEGORY_LABELS[modal.currentCategory]} (${categoryItemCount(modal, modal.currentCategory)})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`,
|
|
527
|
+
leftRows: categoryRows.map((row): WorkspaceRow => ({
|
|
528
|
+
text: row.text,
|
|
529
|
+
selected: row.selected,
|
|
530
|
+
kind: row.type === 'group' ? 'group' : row.type === 'more' ? 'more' : row.type === 'empty' ? 'empty' : 'item',
|
|
531
|
+
bold: row.selected || row.type === 'group',
|
|
532
|
+
})),
|
|
533
|
+
contextRows,
|
|
534
|
+
controlRows,
|
|
535
|
+
footer: footerText(modal),
|
|
536
|
+
});
|
|
694
537
|
}
|
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 = '0.20.
|
|
9
|
+
let _version = '0.20.1';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|