@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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.20.1] — 2026-05-12
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
- Added shared fullscreen rendering primitives for frame drawing, text writing, clipping, fills, rules, stable windows, and the workspace palette.
|
|
11
|
+
- Refactored `/config` and `/mcp` to use the same reusable fullscreen workspace base instead of maintaining separate one-off renderers.
|
|
12
|
+
- Updated onboarding to consume the shared primitive layer while preserving its wizard-specific layout and behavior.
|
|
13
|
+
|
|
7
14
|
## [0.20.0] — 2026-05-11
|
|
8
15
|
|
|
9
16
|
### Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
3
|
+
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
|
+
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
5
|
+
|
|
6
|
+
export const FULLSCREEN_PALETTE = {
|
|
7
|
+
border: '#64748b',
|
|
8
|
+
title: '#67e8f9',
|
|
9
|
+
subtitle: '#93c5fd',
|
|
10
|
+
text: '#e2e8f0',
|
|
11
|
+
muted: '#94a3b8',
|
|
12
|
+
dim: '#64748b',
|
|
13
|
+
selectedBg: '#223049',
|
|
14
|
+
categoryBg: '#141b25',
|
|
15
|
+
contextBg: '#121923',
|
|
16
|
+
controlsBg: '#0f141d',
|
|
17
|
+
footerBg: '#111827',
|
|
18
|
+
good: UI_TONES.state.good,
|
|
19
|
+
warn: UI_TONES.state.warn,
|
|
20
|
+
bad: UI_TONES.state.bad,
|
|
21
|
+
info: UI_TONES.state.info,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export type FullscreenTextStyle = Partial<Omit<Line[number], 'char'>>;
|
|
25
|
+
|
|
26
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
27
|
+
return Math.max(min, Math.min(max, value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fillRange(line: Line, startX: number, endX: number, bg: string): void {
|
|
31
|
+
for (let x = Math.max(0, startX); x <= Math.min(line.length - 1, endX); x += 1) {
|
|
32
|
+
const cell = line[x] ?? createStyledCell(' ');
|
|
33
|
+
line[x] = createStyledCell(cell.char, {
|
|
34
|
+
fg: cell.fg,
|
|
35
|
+
bg,
|
|
36
|
+
bold: cell.bold,
|
|
37
|
+
dim: cell.dim,
|
|
38
|
+
underline: cell.underline,
|
|
39
|
+
italic: cell.italic,
|
|
40
|
+
strikethrough: cell.strikethrough,
|
|
41
|
+
link: cell.link,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function fillWidth(line: Line, startX: number, width: number, bg: string): void {
|
|
47
|
+
if (width <= 0) return;
|
|
48
|
+
fillRange(line, startX, startX + width - 1, bg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeText(
|
|
52
|
+
line: Line,
|
|
53
|
+
startX: number,
|
|
54
|
+
maxWidth: number,
|
|
55
|
+
text: string,
|
|
56
|
+
style: FullscreenTextStyle = {},
|
|
57
|
+
): void {
|
|
58
|
+
let x = startX;
|
|
59
|
+
let used = 0;
|
|
60
|
+
for (const ch of text) {
|
|
61
|
+
const width = getDisplayWidth(ch);
|
|
62
|
+
if (width <= 0) continue;
|
|
63
|
+
if (used + width > maxWidth || x >= line.length) break;
|
|
64
|
+
line[x] = createStyledCell(ch, style);
|
|
65
|
+
if (width > 1 && x + 1 < line.length) line[x + 1] = createStyledCell(' ', style);
|
|
66
|
+
x += width;
|
|
67
|
+
used += width;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function makeLine(width: number, bg = ''): Line {
|
|
72
|
+
const line = createEmptyLine(width);
|
|
73
|
+
if (bg) fillRange(line, 0, width - 1, bg);
|
|
74
|
+
return line;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function borderLine(width: number, left: string, fill: string, right: string, fg: string = FULLSCREEN_PALETTE.border): Line {
|
|
78
|
+
const line = makeLine(width);
|
|
79
|
+
if (width <= 0) return line;
|
|
80
|
+
line[0] = createStyledCell(left, { fg });
|
|
81
|
+
for (let x = 1; x < width - 1; x += 1) line[x] = createStyledCell(fill, { fg });
|
|
82
|
+
if (width > 1) line[width - 1] = createStyledCell(right, { fg });
|
|
83
|
+
return line;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function contentLine(width: number, bg: string, borderFg: string = FULLSCREEN_PALETTE.border): Line {
|
|
87
|
+
const line = makeLine(width, bg);
|
|
88
|
+
if (width > 0) line[0] = createStyledCell(GLYPHS.frame.vertical, { fg: borderFg });
|
|
89
|
+
if (width > 1) line[width - 1] = createStyledCell(GLYPHS.frame.vertical, { fg: borderFg });
|
|
90
|
+
return line;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function drawVerticalRule(line: Line, x: number, fg: string = FULLSCREEN_PALETTE.border, bg = ''): void {
|
|
94
|
+
if (x < 0 || x >= line.length) return;
|
|
95
|
+
line[x] = createStyledCell(GLYPHS.frame.vertical, { fg, bg });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function drawHorizontalRule(line: Line, startX: number, endX: number, fg: string = FULLSCREEN_PALETTE.border, bg = ''): void {
|
|
99
|
+
for (let x = Math.max(0, startX); x <= Math.min(line.length - 1, endX); x += 1) {
|
|
100
|
+
line[x] = createStyledCell(GLYPHS.frame.horizontal, { fg, bg });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function clipDisplay(text: string, width: number): string {
|
|
105
|
+
if (width <= 0) return '';
|
|
106
|
+
let used = 0;
|
|
107
|
+
let output = '';
|
|
108
|
+
for (const ch of text) {
|
|
109
|
+
const chWidth = getDisplayWidth(ch);
|
|
110
|
+
if (chWidth <= 0) continue;
|
|
111
|
+
if (used + chWidth > width) break;
|
|
112
|
+
output += ch;
|
|
113
|
+
used += chWidth;
|
|
114
|
+
}
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function padDisplay(text: string, width: number): string {
|
|
119
|
+
const clipped = clipDisplay(text, width);
|
|
120
|
+
return `${clipped}${' '.repeat(Math.max(0, width - getDisplayWidth(clipped)))}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function stableWindow(total: number, selectedIndex: number, visibleCount: number): { start: number; end: number } {
|
|
124
|
+
if (total <= 0 || visibleCount <= 0) return { start: 0, end: 0 };
|
|
125
|
+
if (total <= visibleCount) return { start: 0, end: total };
|
|
126
|
+
const selected = clamp(selectedIndex, 0, total - 1);
|
|
127
|
+
const half = Math.floor(visibleCount / 2);
|
|
128
|
+
const start = clamp(selected - half, 0, total - visibleCount);
|
|
129
|
+
return { start, end: start + visibleCount };
|
|
130
|
+
}
|
|
@@ -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
|
+
}
|