@pellux/goodvibes-tui 0.19.99 → 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 +16 -0
- package/README.md +10 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/input/commands/local-setup-review.ts +4 -1
- package/src/input/commands/platform-sandbox-qemu.ts +17 -35
- package/src/input/commands/platform-sandbox-runtime.ts +4 -2
- 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-helpers.ts +1 -0
- package/src/renderer/settings-modal.ts +53 -210
- package/src/runtime/sandbox-public-gaps.ts +158 -38
- package/src/runtime/sandbox-qemu-templates.ts +340 -0
- package/src/version.ts +1 -1
|
@@ -1,297 +1,237 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
3
1
|
import type { McpWorkspace, McpWorkspaceRow } from '../input/mcp-workspace.ts';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
detailBg: '#111827',
|
|
16
|
-
good: '#22c55e',
|
|
17
|
-
warn: '#f59e0b',
|
|
18
|
-
bad: '#ef4444',
|
|
19
|
-
info: '#38bdf8',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type Style = Partial<Omit<Line[number], 'char'>>;
|
|
23
|
-
|
|
24
|
-
function line(width: number, bg?: string): Line {
|
|
25
|
-
const out = createEmptyLine(width);
|
|
26
|
-
if (bg) {
|
|
27
|
-
for (let i = 0; i < out.length; i += 1) out[i] = createStyledCell(' ', { bg });
|
|
28
|
-
}
|
|
29
|
-
return out;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function write(out: Line, x: number, maxWidth: number, text: string, style: Style = {}): void {
|
|
33
|
-
let col = x;
|
|
34
|
-
let used = 0;
|
|
35
|
-
for (const ch of text) {
|
|
36
|
-
const w = getDisplayWidth(ch);
|
|
37
|
-
if (w <= 0) continue;
|
|
38
|
-
if (used + w > maxWidth || col >= out.length) break;
|
|
39
|
-
out[col] = createStyledCell(ch, style);
|
|
40
|
-
if (w > 1 && col + 1 < out.length) out[col + 1] = createStyledCell(' ', style);
|
|
41
|
-
col += w;
|
|
42
|
-
used += w;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function hline(width: number, left: string, fill: string, right: string): Line {
|
|
47
|
-
const out = line(width);
|
|
48
|
-
if (width <= 0) return out;
|
|
49
|
-
out[0] = createStyledCell(left, { fg: C.border });
|
|
50
|
-
for (let i = 1; i < width - 1; i += 1) out[i] = createStyledCell(fill, { fg: C.border });
|
|
51
|
-
if (width > 1) out[width - 1] = createStyledCell(right, { fg: C.border });
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function content(width: number, bg = C.panelBg): Line {
|
|
56
|
-
const out = line(width, bg);
|
|
57
|
-
if (width > 0) out[0] = createStyledCell('|', { fg: C.border, bg });
|
|
58
|
-
if (width > 1) out[width - 1] = createStyledCell('|', { fg: C.border, bg });
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function putVertical(out: Line, x: number, bg = C.panelBg): void {
|
|
63
|
-
if (x <= 0 || x >= out.length - 1) return;
|
|
64
|
-
out[x] = createStyledCell('|', { fg: C.border, bg });
|
|
65
|
-
}
|
|
2
|
+
import type { Line } from '../types/grid.ts';
|
|
3
|
+
import { wrapText } from '../utils/terminal-width.ts';
|
|
4
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
5
|
+
import {
|
|
6
|
+
getFullscreenWorkspaceMetrics,
|
|
7
|
+
padDisplay,
|
|
8
|
+
renderFullscreenWorkspace,
|
|
9
|
+
stableWindow,
|
|
10
|
+
WORKSPACE_PALETTE as PALETTE,
|
|
11
|
+
type WorkspaceRow,
|
|
12
|
+
} from './fullscreen-workspace.ts';
|
|
66
13
|
|
|
67
14
|
function statusColor(text: string): string {
|
|
68
|
-
if (text.includes('failed') || text.includes('Save failed') || text.includes('Remove failed')) return
|
|
69
|
-
if (text.includes('attention') || text.includes('quarantine')) return
|
|
70
|
-
return
|
|
15
|
+
if (text.includes('failed') || text.includes('Save failed') || text.includes('Remove failed')) return PALETTE.bad;
|
|
16
|
+
if (text.includes('attention') || text.includes('quarantine')) return PALETTE.warn;
|
|
17
|
+
return PALETTE.muted;
|
|
71
18
|
}
|
|
72
19
|
|
|
73
20
|
function connectedColor(connected: boolean): string {
|
|
74
|
-
return connected ?
|
|
21
|
+
return connected ? PALETTE.good : PALETTE.warn;
|
|
75
22
|
}
|
|
76
23
|
|
|
77
24
|
function rowLabel(row: McpWorkspaceRow): string {
|
|
78
|
-
if (row.type === 'server') return `${row.server.
|
|
79
|
-
return
|
|
25
|
+
if (row.type === 'server') return `${row.server.name} (${row.server.source})`;
|
|
26
|
+
return row.label;
|
|
80
27
|
}
|
|
81
28
|
|
|
82
29
|
function rowDetail(row: McpWorkspaceRow): string {
|
|
83
30
|
if (row.type === 'server') {
|
|
84
31
|
const server = row.server;
|
|
85
|
-
return `${server.
|
|
32
|
+
return `${server.connected ? 'connected' : 'offline'} · ${server.role} · ${server.trustMode} · schema ${server.freshness}`;
|
|
86
33
|
}
|
|
87
34
|
return row.detail;
|
|
88
35
|
}
|
|
89
36
|
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return [top];
|
|
96
|
-
}
|
|
37
|
+
function buildLeftRows(workspace: McpWorkspace, height: number): WorkspaceRow[] {
|
|
38
|
+
const rendered: WorkspaceRow[] = [];
|
|
39
|
+
let selectedRenderedIndex = 0;
|
|
40
|
+
let sawServerGroup = false;
|
|
41
|
+
let sawActionGroup = false;
|
|
97
42
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const visible = Math.max(1, height - 2);
|
|
103
|
-
const start = rows.length <= visible ? 0 : Math.max(0, Math.min(selected - Math.floor(visible / 2), rows.length - visible));
|
|
104
|
-
const end = Math.min(rows.length, start + visible);
|
|
105
|
-
const title = line(width, C.detailBg);
|
|
106
|
-
write(title, 1, width - 2, 'Servers and actions', { fg: C.section, bold: true, bg: C.detailBg });
|
|
107
|
-
out.push(title);
|
|
108
|
-
if (start > 0) {
|
|
109
|
-
const above = line(width, C.panelBg);
|
|
110
|
-
write(above, 1, width - 2, `^ ${start} item(s) above`, { fg: C.dim, bg: C.panelBg });
|
|
111
|
-
out.push(above);
|
|
112
|
-
}
|
|
113
|
-
for (let index = start; index < end && out.length < height; index += 1) {
|
|
114
|
-
const row = rows[index]!;
|
|
115
|
-
const selectedRow = index === selected && workspace.mode === 'browse';
|
|
116
|
-
const bg = selectedRow ? C.selectedBg : C.panelBg;
|
|
117
|
-
const item = line(width, bg);
|
|
118
|
-
write(item, 1, 2, selectedRow ? '>' : ' ', { fg: C.text, bg, bold: selectedRow });
|
|
119
|
-
write(item, 3, width - 4, rowLabel(row), {
|
|
120
|
-
fg: row.type === 'server' ? connectedColor(row.server.connected) : C.info,
|
|
121
|
-
bg,
|
|
122
|
-
bold: selectedRow || row.type === 'action',
|
|
123
|
-
});
|
|
124
|
-
out.push(item);
|
|
125
|
-
if (row.type === 'server' && out.length < height) {
|
|
126
|
-
const detail = line(width, bg);
|
|
127
|
-
write(detail, 5, width - 6, rowDetail(row), { fg: C.muted, bg });
|
|
128
|
-
out.push(detail);
|
|
43
|
+
workspace.rows.forEach((row, rowIndex) => {
|
|
44
|
+
if (row.type === 'server' && !sawServerGroup) {
|
|
45
|
+
rendered.push({ text: 'SERVERS', kind: 'group', bold: true });
|
|
46
|
+
sawServerGroup = true;
|
|
129
47
|
}
|
|
48
|
+
if (row.type === 'action' && !sawActionGroup) {
|
|
49
|
+
if (!sawServerGroup) rendered.push({ text: 'SERVERS', kind: 'group', bold: true });
|
|
50
|
+
if (workspace.servers.length === 0) rendered.push({ text: ' No configured servers', kind: 'item', fg: PALETTE.dim, dim: true });
|
|
51
|
+
rendered.push({ text: 'ACTIONS', kind: 'group', bold: true });
|
|
52
|
+
sawActionGroup = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const selected = workspace.mode === 'browse' && rowIndex === workspace.selectedIndex;
|
|
56
|
+
if (selected) selectedRenderedIndex = rendered.length;
|
|
57
|
+
const marker = selected ? GLYPHS.navigation.selected : row.type === 'server' ? (row.server.connected ? '✓' : '•') : '+';
|
|
58
|
+
rendered.push({
|
|
59
|
+
text: ` ${marker} ${rowLabel(row)}`,
|
|
60
|
+
selected,
|
|
61
|
+
kind: 'item',
|
|
62
|
+
fg: row.type === 'server' ? connectedColor(row.server.connected) : PALETTE.info,
|
|
63
|
+
bold: selected || row.type === 'action',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const visible = Math.max(1, height);
|
|
68
|
+
const window = stableWindow(rendered.length, selectedRenderedIndex, visible);
|
|
69
|
+
const rows = rendered.slice(window.start, window.end);
|
|
70
|
+
if (window.start > 0 && rows.length > 0) {
|
|
71
|
+
rows[0] = { text: `${GLYPHS.navigation.moreAbove} ${window.start} more row(s) above`, kind: 'more', fg: PALETTE.dim, dim: true };
|
|
130
72
|
}
|
|
131
|
-
if (end <
|
|
132
|
-
|
|
133
|
-
write(below, 1, width - 2, `v ${rows.length - end} item(s) below`, { fg: C.dim, bg: C.panelBg });
|
|
134
|
-
out.push(below);
|
|
73
|
+
if (window.end < rendered.length && rows.length > 0) {
|
|
74
|
+
rows[rows.length - 1] = { text: `${GLYPHS.navigation.moreBelow} ${rendered.length - window.end} more row(s) below`, kind: 'more', fg: PALETTE.dim, dim: true };
|
|
135
75
|
}
|
|
136
|
-
while (
|
|
137
|
-
return
|
|
76
|
+
while (rows.length < height) rows.push({ text: '', kind: 'empty' });
|
|
77
|
+
return rows.slice(0, height);
|
|
138
78
|
}
|
|
139
79
|
|
|
140
|
-
function selectedDetailLines(workspace: McpWorkspace, width: number):
|
|
80
|
+
function selectedDetailLines(workspace: McpWorkspace, width: number): WorkspaceRow[] {
|
|
81
|
+
const lines: string[] = [];
|
|
141
82
|
if (workspace.mode === 'form') {
|
|
142
83
|
const field = workspace.formFields[workspace.formIndex];
|
|
143
|
-
|
|
84
|
+
lines.push(
|
|
144
85
|
workspace.editingServerName ? `Editing server: ${workspace.editingServerName}` : 'Adding an MCP server',
|
|
145
|
-
'
|
|
86
|
+
'Write a server through the SDK MCP config manager, then reload the live runtime without restarting the TUI.',
|
|
146
87
|
field ? `${field.label}: ${field.help}` : '',
|
|
147
|
-
'
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return [
|
|
88
|
+
'Project scope writes to this workspace. Global scope writes to your user MCP config. External Claude/Desktop config files are shown but not edited here.',
|
|
89
|
+
);
|
|
90
|
+
} else if (workspace.mode === 'delete-confirm') {
|
|
91
|
+
lines.push(
|
|
152
92
|
`Remove configured server: ${workspace.editingServerName ?? '(unknown)'}`,
|
|
153
93
|
'This removes the selected writable project/global config entry and reloads MCP runtime state.',
|
|
154
94
|
'Press y to remove, n or Esc to cancel.',
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
const selected = workspace.selectedRow;
|
|
98
|
+
if (!selected) lines.push('No MCP rows available.');
|
|
99
|
+
else if (selected.type === 'action') lines.push(selected.label, selected.detail);
|
|
100
|
+
else {
|
|
101
|
+
const server = selected.server;
|
|
102
|
+
lines.push(
|
|
103
|
+
server.name,
|
|
104
|
+
`Connected: ${server.connected ? 'yes' : 'no'} Source: ${server.source} Schema: ${server.freshness}`,
|
|
105
|
+
`Role: ${server.role} Trust: ${server.trustMode}`,
|
|
106
|
+
`Command: ${server.command ? `${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''}` : '(runtime only; no launch config found)'}`,
|
|
107
|
+
`Allowed paths: ${server.allowedPaths.length > 0 ? server.allowedPaths.join(', ') : '(none)'}`,
|
|
108
|
+
`Allowed hosts: ${server.allowedHosts.length > 0 ? server.allowedHosts.join(', ') : '(none)'}`,
|
|
109
|
+
...(server.quarantineReason ? [`Quarantine: ${server.quarantineReason}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`] : []),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
161
112
|
}
|
|
162
|
-
const server = selected.server;
|
|
163
|
-
return [
|
|
164
|
-
server.name,
|
|
165
|
-
`Connected: ${server.connected ? 'yes' : 'no'} Source: ${server.source} Schema: ${server.freshness}`,
|
|
166
|
-
`Role: ${server.role} Trust: ${server.trustMode}`,
|
|
167
|
-
`Command: ${server.command ? `${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''}` : '(runtime only; no launch config found)'}`,
|
|
168
|
-
`Allowed paths: ${server.allowedPaths.length > 0 ? server.allowedPaths.join(', ') : '(none)'}`,
|
|
169
|
-
`Allowed hosts: ${server.allowedHosts.length > 0 ? server.allowedHosts.join(', ') : '(none)'}`,
|
|
170
|
-
...(server.quarantineReason ? [`Quarantine: ${server.quarantineReason}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`] : []),
|
|
171
|
-
];
|
|
172
|
-
}
|
|
173
113
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
114
|
+
lines.push('', `Status: ${workspace.status}`);
|
|
115
|
+
return lines.flatMap((text, index): WorkspaceRow[] => {
|
|
116
|
+
if (text === '') return [{ text: '', dim: true }];
|
|
117
|
+
return wrapText(text, Math.max(1, width)).map((wrapped, wrapIndex): WorkspaceRow => ({
|
|
118
|
+
text: wrapped,
|
|
119
|
+
fg: index === 0 ? PALETTE.title : text.startsWith('Status:') ? statusColor(workspace.status) : PALETTE.text,
|
|
120
|
+
bold: index === 0 && wrapIndex === 0,
|
|
121
|
+
dim: text.length === 0,
|
|
122
|
+
}));
|
|
181
123
|
});
|
|
182
|
-
out.push(header);
|
|
183
|
-
const wrapped = selectedDetailLines(workspace, width - 4).flatMap((entry) => wrapText(entry, Math.max(1, width - 4)));
|
|
184
|
-
for (const text of wrapped) {
|
|
185
|
-
const next = line(width, C.detailBg);
|
|
186
|
-
write(next, 1, width - 2, text, { fg: C.text, bg: C.detailBg });
|
|
187
|
-
out.push(next);
|
|
188
|
-
if (out.length >= height) break;
|
|
189
|
-
}
|
|
190
|
-
while (out.length < height) out.push(line(width, C.detailBg));
|
|
191
|
-
return out.slice(0, height);
|
|
192
124
|
}
|
|
193
125
|
|
|
194
|
-
function
|
|
195
|
-
const
|
|
126
|
+
function buildFormRows(workspace: McpWorkspace, width: number, height: number): WorkspaceRow[] {
|
|
127
|
+
const rows: WorkspaceRow[] = [];
|
|
196
128
|
const fields = workspace.formFields;
|
|
197
|
-
const labelWidth = Math.min(
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
129
|
+
const labelWidth = Math.min(24, Math.max(14, Math.floor(width * 0.24)));
|
|
130
|
+
const valueWidth = Math.max(12, width - labelWidth - 20);
|
|
131
|
+
rows.push({
|
|
132
|
+
text: ` ${padDisplay('Field', labelWidth)} ${padDisplay('Value', valueWidth)} ${padDisplay('Edit', 10)}`,
|
|
133
|
+
fg: PALETTE.muted,
|
|
134
|
+
bold: true,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const visible = Math.max(1, height - 2);
|
|
138
|
+
const window = stableWindow(fields.length, workspace.formIndex, visible);
|
|
139
|
+
if (window.start > 0) rows.push({ text: `${GLYPHS.navigation.moreAbove} ${window.start} more field(s) above`, kind: 'more', fg: PALETTE.dim, dim: true });
|
|
140
|
+
for (let index = window.start; index < window.end; index += 1) {
|
|
201
141
|
const field = fields[index]!;
|
|
202
142
|
const selected = index === workspace.formIndex;
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
out.push(row);
|
|
143
|
+
const marker = selected ? GLYPHS.navigation.selected : ' ';
|
|
144
|
+
const value = field.id === 'save' || field.id === 'cancel'
|
|
145
|
+
? field.help
|
|
146
|
+
: field.value.length > 0 ? field.value : '(empty)';
|
|
147
|
+
rows.push({
|
|
148
|
+
text: `${marker} ${padDisplay(field.label, labelWidth)} ${padDisplay(value, valueWidth)} ${padDisplay(field.editable ? 'text' : 'cycle/action', 12)}`,
|
|
149
|
+
selected,
|
|
150
|
+
fg: field.id === 'save' ? PALETTE.good : field.id === 'cancel' ? PALETTE.warn : field.editable ? PALETTE.text : PALETTE.info,
|
|
151
|
+
bold: selected,
|
|
152
|
+
});
|
|
214
153
|
}
|
|
215
|
-
|
|
216
|
-
|
|
154
|
+
if (window.end < fields.length) rows.push({ text: `${GLYPHS.navigation.moreBelow} ${fields.length - window.end} more field(s) below`, kind: 'more', fg: PALETTE.dim, dim: true });
|
|
155
|
+
while (rows.length < height) rows.push({ text: '', kind: 'empty' });
|
|
156
|
+
return rows.slice(0, height);
|
|
217
157
|
}
|
|
218
158
|
|
|
219
|
-
function
|
|
220
|
-
const out: Line[] = [];
|
|
159
|
+
function buildToolRows(workspace: McpWorkspace, width: number, height: number): WorkspaceRow[] {
|
|
221
160
|
const server = workspace.selectedServer?.name;
|
|
222
161
|
const tools = server ? workspace.tools.filter((tool) => tool.serverName === server) : workspace.tools;
|
|
223
|
-
const
|
|
162
|
+
const toolWidth = Math.min(36, Math.max(18, Math.floor(width * 0.34)));
|
|
163
|
+
const serverWidth = Math.min(24, Math.max(12, Math.floor(width * 0.20)));
|
|
164
|
+
const descriptionWidth = Math.max(12, width - toolWidth - serverWidth - 8);
|
|
224
165
|
const label = workspace.loadingTools
|
|
225
166
|
? 'Tools: loading...'
|
|
226
167
|
: server
|
|
227
168
|
? `Tools for ${server}: ${tools.length}`
|
|
228
169
|
: `Tools: ${tools.length}`;
|
|
229
|
-
|
|
230
|
-
|
|
170
|
+
const rows: WorkspaceRow[] = [
|
|
171
|
+
{ text: label, fg: PALETTE.subtitle, bold: true },
|
|
172
|
+
{ text: ` ${padDisplay('Tool', toolWidth)} ${padDisplay('Server', serverWidth)} ${padDisplay('Description', descriptionWidth)}`, fg: PALETTE.muted, bold: true },
|
|
173
|
+
];
|
|
174
|
+
|
|
231
175
|
if (tools.length === 0) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
176
|
+
rows.push({
|
|
177
|
+
text: workspace.loadingTools ? 'Loading tool list from connected MCP servers.' : 'No tools cached for the selected server. Press t to refresh.',
|
|
178
|
+
fg: PALETTE.muted,
|
|
179
|
+
dim: true,
|
|
180
|
+
});
|
|
235
181
|
} else {
|
|
236
|
-
for (const tool of tools.slice(0, Math.max(0, height -
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
182
|
+
for (const tool of tools.slice(0, Math.max(0, height - rows.length))) {
|
|
183
|
+
rows.push({
|
|
184
|
+
text: ` ${padDisplay(tool.toolName, toolWidth)} ${padDisplay(tool.serverName, serverWidth)} ${padDisplay(tool.description ?? '', descriptionWidth)}`,
|
|
185
|
+
fg: PALETTE.text,
|
|
186
|
+
});
|
|
241
187
|
}
|
|
242
188
|
}
|
|
243
|
-
|
|
244
|
-
|
|
189
|
+
|
|
190
|
+
while (rows.length < height) rows.push({ text: '', kind: 'empty' });
|
|
191
|
+
return rows.slice(0, height);
|
|
245
192
|
}
|
|
246
193
|
|
|
247
|
-
function
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const bottom = workspace.mode === 'form'
|
|
255
|
-
? renderForm(workspace, rightWidth, bottomHeight)
|
|
256
|
-
: renderTools(workspace, rightWidth, bottomHeight);
|
|
257
|
-
const right = [...details, line(rightWidth, C.panelBg), ...bottom].slice(0, height);
|
|
258
|
-
const out: Line[] = [];
|
|
259
|
-
for (let i = 0; i < height; i += 1) {
|
|
260
|
-
const row = content(width);
|
|
261
|
-
const leftRow = left[i] ?? line(leftWidth, C.panelBg);
|
|
262
|
-
const rightRow = right[i] ?? line(rightWidth, C.panelBg);
|
|
263
|
-
for (let x = 0; x < leftWidth && x + 1 < row.length; x += 1) row[x + 1] = leftRow[x] ?? createStyledCell(' ');
|
|
264
|
-
putVertical(row, leftWidth + 1);
|
|
265
|
-
for (let x = 0; x < rightWidth && leftWidth + 3 + x < row.length - 1; x += 1) {
|
|
266
|
-
row[leftWidth + 3 + x] = rightRow[x] ?? createStyledCell(' ');
|
|
267
|
-
}
|
|
268
|
-
out.push(row);
|
|
269
|
-
}
|
|
270
|
-
return out;
|
|
194
|
+
function buildDeleteRows(workspace: McpWorkspace, height: number): WorkspaceRow[] {
|
|
195
|
+
const rows: WorkspaceRow[] = [
|
|
196
|
+
{ text: `${GLYPHS.navigation.selected} Confirm remove ${workspace.editingServerName ?? '(unknown)'}`, selected: true, fg: PALETTE.bad, bold: true },
|
|
197
|
+
{ text: ' Cancel and return to MCP server browser', fg: PALETTE.muted },
|
|
198
|
+
];
|
|
199
|
+
while (rows.length < height) rows.push({ text: '', kind: 'empty' });
|
|
200
|
+
return rows.slice(0, height);
|
|
271
201
|
}
|
|
272
202
|
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
write(controls, 2, width - 4, text, { fg: C.muted, bg: C.detailBg });
|
|
284
|
-
return [status, controls, hline(width, '+', '-', '+')];
|
|
203
|
+
function buildControlRows(workspace: McpWorkspace, width: number, height: number): WorkspaceRow[] {
|
|
204
|
+
if (workspace.mode === 'form') return buildFormRows(workspace, width, height);
|
|
205
|
+
if (workspace.mode === 'delete-confirm') return buildDeleteRows(workspace, height);
|
|
206
|
+
return buildToolRows(workspace, width, height);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function footerText(workspace: McpWorkspace): string {
|
|
210
|
+
if (workspace.mode === 'form') return 'Focus server form · Up/Down field · Left/Right cycle · Type edit · Enter save/cancel row · Esc back';
|
|
211
|
+
if (workspace.mode === 'delete-confirm') return 'Focus remove confirmation · y confirm · n/Esc cancel';
|
|
212
|
+
return 'Focus MCP workspace · Up/Down choose · Enter edit/action · a add · d remove · r reload · t tools · Esc close';
|
|
285
213
|
}
|
|
286
214
|
|
|
287
215
|
export function renderMcpWorkspace(workspace: McpWorkspace, width: number, height: number): Line[] {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
216
|
+
const metrics = getFullscreenWorkspaceMetrics({ width, height });
|
|
217
|
+
const connected = workspace.servers.filter((server) => server.connected).length;
|
|
218
|
+
const stateLabel = workspace.mode === 'browse' ? 'Browse' : workspace.mode === 'form' ? 'Edit Server' : 'Confirm Remove';
|
|
219
|
+
const mainHeader = workspace.mode === 'form'
|
|
220
|
+
? 'MCP server form'
|
|
221
|
+
: workspace.mode === 'delete-confirm'
|
|
222
|
+
? 'Remove MCP server'
|
|
223
|
+
: `Servers ${connected}/${workspace.servers.length} connected · Tools ${workspace.tools.length}`;
|
|
224
|
+
|
|
225
|
+
return renderFullscreenWorkspace({
|
|
226
|
+
width,
|
|
227
|
+
height,
|
|
228
|
+
title: 'MCP Workspace / Servers',
|
|
229
|
+
stateLabel,
|
|
230
|
+
leftHeader: 'Servers',
|
|
231
|
+
mainHeader,
|
|
232
|
+
leftRows: buildLeftRows(workspace, metrics.bodyRows),
|
|
233
|
+
contextRows: selectedDetailLines(workspace, metrics.contextWidth),
|
|
234
|
+
controlRows: buildControlRows(workspace, metrics.contextWidth, metrics.controlRows),
|
|
235
|
+
footer: footerText(workspace),
|
|
236
|
+
});
|
|
297
237
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Line } from '../../types/grid.ts';
|
|
2
|
-
import { createStyledCell } from '../../types/grid.ts';
|
|
3
2
|
import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText } from '../../utils/terminal-width.ts';
|
|
4
3
|
import {
|
|
5
4
|
createOverlayBoxLayout,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
OVERLAY_GLYPHS,
|
|
10
9
|
putOverlayText,
|
|
11
10
|
} from '../overlay-box.ts';
|
|
11
|
+
import { clamp, drawVerticalRule, fillWidth } from '../fullscreen-primitives.ts';
|
|
12
12
|
import { UI_TONES } from '../ui-primitives.ts';
|
|
13
13
|
import {
|
|
14
14
|
getOnboardingWizardBodyRows,
|
|
@@ -28,32 +28,6 @@ type RenderedFieldRow =
|
|
|
28
28
|
readonly absoluteIndex: number;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
function clamp(value: number, min: number, max: number): number {
|
|
32
|
-
return Math.max(min, Math.min(max, value));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function fillRange(line: Line, startX: number, width: number, bg: string): void {
|
|
36
|
-
for (let x = startX; x < Math.min(line.length, startX + width); x += 1) {
|
|
37
|
-
const cell = line[x];
|
|
38
|
-
if (!cell) continue;
|
|
39
|
-
line[x] = createStyledCell(cell.char, {
|
|
40
|
-
fg: cell.fg,
|
|
41
|
-
bg,
|
|
42
|
-
bold: cell.bold,
|
|
43
|
-
dim: cell.dim,
|
|
44
|
-
underline: cell.underline,
|
|
45
|
-
italic: cell.italic,
|
|
46
|
-
strikethrough: cell.strikethrough,
|
|
47
|
-
link: cell.link,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function drawVerticalRule(line: Line, x: number, fg: string, bg = ''): void {
|
|
53
|
-
if (x < 0 || x >= line.length) return;
|
|
54
|
-
line[x] = createStyledCell('│', { fg, bg });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
31
|
function modeLabel(mode: OnboardingWizardController['mode']): string {
|
|
58
32
|
if (mode === 'edit') return 'Edit existing';
|
|
59
33
|
if (mode === 'reopen') return 'Reopen review';
|
|
@@ -250,7 +224,7 @@ function renderFieldRow(
|
|
|
250
224
|
const selected = fieldRow.absoluteIndex === wizard.getSelectedFieldIndex();
|
|
251
225
|
const field = fieldRow.field;
|
|
252
226
|
const fieldBg = selected ? DEFAULT_OVERLAY_PALETTE.selectedBg : UI_TONES.bg.base;
|
|
253
|
-
|
|
227
|
+
fillWidth(line, startX, width, fieldBg);
|
|
254
228
|
|
|
255
229
|
const badge = truncateDisplay(`[${wizard.getFieldValueLabel(field)}]`, Math.max(8, Math.floor(width * 0.34)));
|
|
256
230
|
const badgeWidth = getDisplayWidth(badge);
|
|
@@ -354,9 +328,9 @@ function renderWideLayout(
|
|
|
354
328
|
lines.push(topLine);
|
|
355
329
|
|
|
356
330
|
const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
331
|
+
fillWidth(headerLine, leftStart, leftWidth, railBg);
|
|
332
|
+
fillWidth(headerLine, centerStart, centerWidth, headerBg);
|
|
333
|
+
fillWidth(headerLine, rightStart, rightWidth, summaryBg);
|
|
360
334
|
drawVerticalRule(headerLine, leftSeparatorX, borderFg, headerBg);
|
|
361
335
|
drawVerticalRule(headerLine, rightSeparatorX, borderFg, headerBg);
|
|
362
336
|
putOverlayText(headerLine, leftStart + 1, leftWidth - 2, 'Steps', {
|
|
@@ -388,9 +362,9 @@ function renderWideLayout(
|
|
|
388
362
|
|
|
389
363
|
for (let row = 0; row < bodyRows; row += 1) {
|
|
390
364
|
const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
365
|
+
fillWidth(line, leftStart, leftWidth, railBg);
|
|
366
|
+
fillWidth(line, centerStart, centerWidth, bodyBg);
|
|
367
|
+
fillWidth(line, rightStart, rightWidth, summaryBg);
|
|
394
368
|
drawVerticalRule(line, leftSeparatorX, borderFg);
|
|
395
369
|
drawVerticalRule(line, rightSeparatorX, borderFg);
|
|
396
370
|
|
|
@@ -411,20 +385,20 @@ function renderWideLayout(
|
|
|
411
385
|
bg: bodyBg,
|
|
412
386
|
});
|
|
413
387
|
} else if (row === 3) {
|
|
414
|
-
|
|
388
|
+
fillWidth(line, centerStart, centerWidth, railBg);
|
|
415
389
|
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(controlsText(wizard), centerWidth - 2), {
|
|
416
390
|
fg: UI_TONES.state.info,
|
|
417
391
|
bg: railBg,
|
|
418
392
|
});
|
|
419
393
|
} else if (row === 4) {
|
|
420
|
-
|
|
394
|
+
fillWidth(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
421
395
|
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, centerWidth - 2), {
|
|
422
396
|
fg: UI_TONES.fg.primary,
|
|
423
397
|
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
424
398
|
bold: true,
|
|
425
399
|
});
|
|
426
400
|
} else if (row === 5) {
|
|
427
|
-
|
|
401
|
+
fillWidth(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
428
402
|
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(selectedText.hint, centerWidth - 2), {
|
|
429
403
|
fg: UI_TONES.fg.secondary,
|
|
430
404
|
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
@@ -534,7 +508,7 @@ function renderCollapsedLayout(
|
|
|
534
508
|
lines.push(topLine);
|
|
535
509
|
|
|
536
510
|
const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
|
|
537
|
-
|
|
511
|
+
fillWidth(headerLine, innerStart, innerWidth, headerBg);
|
|
538
512
|
putOverlayText(headerLine, innerStart + 1, innerWidth - 2, fitDisplay(`${modeLabel(wizard.mode)} • ${currentStep.shortLabel}`, innerWidth - 2), {
|
|
539
513
|
fg: UI_TONES.state.active,
|
|
540
514
|
bg: headerBg,
|
|
@@ -554,7 +528,7 @@ function renderCollapsedLayout(
|
|
|
554
528
|
|
|
555
529
|
for (let row = 0; row < bodyRows; row += 1) {
|
|
556
530
|
const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
|
|
557
|
-
|
|
531
|
+
fillWidth(line, innerStart, innerWidth, bodyBg);
|
|
558
532
|
|
|
559
533
|
if (row === 0) {
|
|
560
534
|
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(currentStep.title, innerWidth - 2), {
|
|
@@ -573,20 +547,20 @@ function renderCollapsedLayout(
|
|
|
573
547
|
bg: bodyBg,
|
|
574
548
|
});
|
|
575
549
|
} else if (row === 3) {
|
|
576
|
-
|
|
550
|
+
fillWidth(line, innerStart, innerWidth, UI_TONES.bg.section);
|
|
577
551
|
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(controlsText(wizard), innerWidth - 2), {
|
|
578
552
|
fg: UI_TONES.state.info,
|
|
579
553
|
bg: UI_TONES.bg.section,
|
|
580
554
|
});
|
|
581
555
|
} else if (row === 4) {
|
|
582
|
-
|
|
556
|
+
fillWidth(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
583
557
|
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, innerWidth - 2), {
|
|
584
558
|
fg: UI_TONES.fg.primary,
|
|
585
559
|
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
586
560
|
bold: true,
|
|
587
561
|
});
|
|
588
562
|
} else if (row === 5) {
|
|
589
|
-
|
|
563
|
+
fillWidth(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
590
564
|
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(selectedText.hint, innerWidth - 2), {
|
|
591
565
|
fg: UI_TONES.fg.secondary,
|
|
592
566
|
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|