@pellux/goodvibes-tui 0.18.20 → 0.18.23
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 +120 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -82
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +45 -14
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/subscription-panel.ts +63 -86
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/version.ts +1 -1
package/src/renderer/buffer.ts
CHANGED
|
@@ -2,17 +2,23 @@ import { type Line, type Cell, createEmptyLine, createEmptyCell } from '../types
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* TerminalBuffer - Represents a 2D grid of styled cells.
|
|
5
|
+
* Tracks a per-row dirty bitmap so the diff engine can skip rows that were
|
|
6
|
+
* never written in the current frame.
|
|
5
7
|
*/
|
|
6
8
|
export class TerminalBuffer {
|
|
7
9
|
public cells: Line[];
|
|
10
|
+
/** dirtyRows[y] is true if row y was written since the last reset(). */
|
|
11
|
+
public dirtyRows: boolean[];
|
|
8
12
|
|
|
9
13
|
constructor(public width: number, public height: number) {
|
|
10
14
|
this.cells = Array.from({ length: height }, () => createEmptyLine(width));
|
|
15
|
+
this.dirtyRows = new Array(height).fill(false);
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
public setCell(x: number, y: number, cell: Partial<Cell>): void {
|
|
14
19
|
if (y >= 0 && y < this.height && x >= 0 && x < this.width) {
|
|
15
20
|
this.cells[y][x] = { ...this.cells[y][x], ...cell };
|
|
21
|
+
this.dirtyRows[y] = true;
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -23,30 +29,35 @@ export class TerminalBuffer {
|
|
|
23
29
|
public blitLine(row: number, line: Line): void {
|
|
24
30
|
if (row >= 0 && row < this.height) {
|
|
25
31
|
this.cells[row] = [...line];
|
|
32
|
+
this.dirtyRows[row] = true;
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
public clone(): TerminalBuffer {
|
|
30
37
|
const newBuf = new TerminalBuffer(this.width, this.height);
|
|
31
38
|
newBuf.cells = this.cells.map(line => line.map(cell => ({ ...cell })));
|
|
39
|
+
newBuf.dirtyRows = [...this.dirtyRows];
|
|
32
40
|
return newBuf;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/**
|
|
36
44
|
* Reset all cells in-place to empty, reusing this buffer instance.
|
|
37
45
|
* If dimensions changed, reallocates cells array.
|
|
46
|
+
* Always clears the dirty bitmap.
|
|
38
47
|
*/
|
|
39
48
|
public reset(width: number, height: number): void {
|
|
40
49
|
if (width !== this.width || height !== this.height) {
|
|
41
50
|
this.width = width;
|
|
42
51
|
this.height = height;
|
|
43
52
|
this.cells = Array.from({ length: height }, () => createEmptyLine(width));
|
|
53
|
+
this.dirtyRows = new Array(height).fill(false);
|
|
44
54
|
} else {
|
|
45
55
|
for (let y = 0; y < this.height; y++) {
|
|
46
56
|
const row = this.cells[y]!;
|
|
47
57
|
for (let x = 0; x < this.width; x++) {
|
|
48
58
|
row[x] = createEmptyCell();
|
|
49
59
|
}
|
|
60
|
+
this.dirtyRows[y] = false;
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
}
|
package/src/renderer/diff.ts
CHANGED
|
@@ -29,6 +29,14 @@ export class DiffEngine {
|
|
|
29
29
|
let output = '';
|
|
30
30
|
|
|
31
31
|
for (let y = 0; y < newBuffer.height; y++) {
|
|
32
|
+
// Skip rows that were not written in either the old or new buffer.
|
|
33
|
+
// If neither side touched the row, both must match the prior frame:
|
|
34
|
+
// old row was never written this frame (clean) and new row is also
|
|
35
|
+
// clean, so the on-screen content is still correct. No diff needed.
|
|
36
|
+
const newDirty = newBuffer.dirtyRows[y] ?? false;
|
|
37
|
+
const oldDirty = oldBuffer ? (oldBuffer.dirtyRows[y] ?? false) : true;
|
|
38
|
+
if (!newDirty && !oldDirty) continue;
|
|
39
|
+
|
|
32
40
|
for (let x = 0; x < newBuffer.width; x++) {
|
|
33
41
|
const oldCell = oldBuffer?.getCell(x, y);
|
|
34
42
|
const newCell = newBuffer.cells[y]?.[x];
|
|
@@ -64,36 +64,45 @@ export function renderHelpOverlay(
|
|
|
64
64
|
'',
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'',
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'',
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
' /knowledge Durable knowledge and review queue',
|
|
88
|
-
'',
|
|
89
|
-
' Power Surfaces',
|
|
90
|
-
' ' + '\u2500'.repeat(40),
|
|
91
|
-
' /hooks Hook workbench and runtime activity',
|
|
92
|
-
' /orchestration Graph and recursive-agent control room',
|
|
93
|
-
' /communication Structured agent communication workspace',
|
|
94
|
-
' /tasks Task surface for list/show/pause/resume/output',
|
|
67
|
+
// Featured commands shown in the Quick Start section.
|
|
68
|
+
// Each entry is [commandName, subcommandOrArgHint, description].
|
|
69
|
+
// Commands not registered in the live registry are omitted at render time.
|
|
70
|
+
const FEATURED_COMMANDS: Array<[name: string, argHint: string, desc: string]> = [
|
|
71
|
+
['setup', 'onboarding', 'Guided first-run review and environment posture'],
|
|
72
|
+
['cockpit', '', 'Unified runtime control room'],
|
|
73
|
+
['settings', '', 'Settings and config browser'],
|
|
74
|
+
['provider', '', 'Choose provider or model family'],
|
|
75
|
+
['subscription', '', 'Review provider logins and subscriptions'],
|
|
76
|
+
['marketplace', 'open', 'Browse plugins, skills, and packs'],
|
|
77
|
+
['remote', 'setup', 'Review remote, bridge, and tunnel flows'],
|
|
78
|
+
['sandbox', 'review', 'Inspect secure execution posture'],
|
|
79
|
+
['security', '', 'Security review workspace'],
|
|
80
|
+
['policy', '', 'Simulation, lint, and preflight review'],
|
|
81
|
+
['incident', '', 'Incident workspace and export flows'],
|
|
82
|
+
['knowledge', '', 'Durable knowledge and review queue'],
|
|
83
|
+
['hooks', '', 'Hook workbench and runtime activity'],
|
|
84
|
+
['orchestration','', 'Graph and recursive-agent control room'],
|
|
85
|
+
['communication','', 'Structured agent communication workspace'],
|
|
86
|
+
['tasks', '', 'Task surface for list/show/pause/resume/output'],
|
|
95
87
|
];
|
|
96
88
|
|
|
89
|
+
// Build command rows from featured list, filtering out unregistered commands.
|
|
90
|
+
function featuredRow(name: string, argHint: string, desc: string): string {
|
|
91
|
+
const invocation = argHint ? `/${name} ${argHint}` : `/${name}`;
|
|
92
|
+
return ` ${invocation.padEnd(23)} ${desc}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const quickStartRows: string[] = [];
|
|
96
|
+
for (const [name, argHint, desc] of FEATURED_COMMANDS) {
|
|
97
|
+
if (!hasCommand(name)) continue; // omit if not in live registry
|
|
98
|
+
quickStartRows.push(featuredRow(name, argHint, desc));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const commandRows: string[] = [];
|
|
102
|
+
if (quickStartRows.length > 0) {
|
|
103
|
+
commandRows.push(' Quick Start', ' ' + '\u2500'.repeat(40), ...quickStartRows, '');
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
if (commands && commands.length > 0) {
|
|
98
107
|
commandRows.push('', ' Available Slash Commands', ' ' + '\u2500'.repeat(40));
|
|
99
108
|
const preferred = ['setup', 'cockpit', 'settings', 'provider', 'subscription', 'marketplace', 'remote', 'sandbox', 'security', 'policy', 'incident', 'knowledge', 'hooks', 'orchestration', 'communication', 'tasks'];
|
package/src/renderer/markdown.ts
CHANGED
|
@@ -30,153 +30,11 @@ function isLikelyTableHeaderRow(row: string): boolean {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* renderMarkdown - Parse markdown text into styled Line[]
|
|
34
|
-
*
|
|
33
|
+
* renderMarkdown - Parse markdown text into styled Line[].
|
|
34
|
+
* Thin wrapper over renderMarkdownTracked for callers that don't need code-block metadata.
|
|
35
35
|
*/
|
|
36
36
|
export function renderMarkdown(text: string, width: number, options: MarkdownRenderOptions = {}): Line[] {
|
|
37
|
-
|
|
38
|
-
const rawLines = text.split('\n');
|
|
39
|
-
|
|
40
|
-
let inCodeBlock = false;
|
|
41
|
-
let codeBlockLang = '';
|
|
42
|
-
let codeBlockLines: string[] = [];
|
|
43
|
-
const indent = LAYOUT.LEFT_MARGIN;
|
|
44
|
-
const contentWidth = LAYOUT.contentWidth(width);
|
|
45
|
-
|
|
46
|
-
for (let i = 0; i < rawLines.length; i++) {
|
|
47
|
-
const raw = rawLines[i];
|
|
48
|
-
|
|
49
|
-
// --- Code block fence ---
|
|
50
|
-
const fenceMatch = raw.match(/^```(\w*)/);
|
|
51
|
-
if (fenceMatch && !inCodeBlock) {
|
|
52
|
-
inCodeBlock = true;
|
|
53
|
-
codeBlockLang = fenceMatch[1] || '';
|
|
54
|
-
codeBlockLines = [];
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (inCodeBlock) {
|
|
58
|
-
if (raw.trimStart().startsWith('```')) {
|
|
59
|
-
// End of code block - delegate to code block renderer
|
|
60
|
-
const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
|
|
61
|
-
showLineNumbers: options.codeBlockLineNumbers ?? true,
|
|
62
|
-
});
|
|
63
|
-
lines.push(...rendered);
|
|
64
|
-
inCodeBlock = false;
|
|
65
|
-
codeBlockLang = '';
|
|
66
|
-
codeBlockLines = [];
|
|
67
|
-
} else {
|
|
68
|
-
codeBlockLines.push(raw);
|
|
69
|
-
}
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// --- Empty line ---
|
|
74
|
-
if (raw.trim() === '') {
|
|
75
|
-
lines.push(UIFactory.stringToLine('', width));
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// --- Heading ---
|
|
80
|
-
const h3 = raw.match(/^### (.+)/);
|
|
81
|
-
const h2 = raw.match(/^## (.+)/);
|
|
82
|
-
const h1 = raw.match(/^# (.+)/);
|
|
83
|
-
if (h1) {
|
|
84
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h1[1].toUpperCase(), width, { fg: '#00ffff', bold: true }));
|
|
85
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '━'.repeat(Math.min(getDisplayWidth(h1[1]), contentWidth)), width, { fg: '244' }));
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (h2) {
|
|
89
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h2[1], width, { fg: '#00ffff', bold: true }));
|
|
90
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(Math.min(getDisplayWidth(h2[1]), contentWidth)), width, { fg: '240' }));
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (h3) {
|
|
94
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h3[1], width, { fg: '111', bold: true }));
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// --- Task list ---
|
|
99
|
-
const taskMatch = raw.match(/^(\s*)[-*] \[([ xX])\] (.+)/);
|
|
100
|
-
if (taskMatch) {
|
|
101
|
-
const listIndent = Math.floor(taskMatch[1].length / 2);
|
|
102
|
-
const checked = taskMatch[2] !== ' ';
|
|
103
|
-
const bulletX = indent + listIndent * 2;
|
|
104
|
-
const textStartX = bulletX + 4;
|
|
105
|
-
const checkbox = checked ? '\u2611 ' : '\u2610 '; // ☑ or ☐
|
|
106
|
-
const rendered = renderInlineMarkdown(taskMatch[3]);
|
|
107
|
-
const prefix = ' '.repeat(bulletX) + checkbox;
|
|
108
|
-
const style = checked ? { fg: '244', strikethrough: true } : {};
|
|
109
|
-
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: checked ? '#22c55e' : '252', ...style }, textStartX));
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// --- Unordered list ---
|
|
114
|
-
const ulMatch = raw.match(/^(\s*)[-*] (.+)/);
|
|
115
|
-
if (ulMatch) {
|
|
116
|
-
const listIndent = Math.floor(ulMatch[1].length / 2);
|
|
117
|
-
const bulletX = indent + listIndent * 2;
|
|
118
|
-
const textStartX = bulletX + 2;
|
|
119
|
-
const rendered = renderInlineMarkdown(ulMatch[2]);
|
|
120
|
-
const prefix = ' '.repeat(bulletX) + '• ';
|
|
121
|
-
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// --- Ordered list ---
|
|
126
|
-
const olMatch = raw.match(/^(\s*)(\d+)\. (.+)/);
|
|
127
|
-
if (olMatch) {
|
|
128
|
-
const listIndent = Math.floor(olMatch[1].length / 2);
|
|
129
|
-
const numStr = olMatch[2] + '. ';
|
|
130
|
-
const bulletX = indent + listIndent * 2;
|
|
131
|
-
const textStartX = bulletX + numStr.length;
|
|
132
|
-
const rendered = renderInlineMarkdown(olMatch[3]);
|
|
133
|
-
const prefix = ' '.repeat(bulletX) + numStr;
|
|
134
|
-
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// --- Horizontal rule ---
|
|
139
|
-
if (/^[-*_]{3,}$/.test(raw.trim())) {
|
|
140
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(contentWidth), width, { fg: '240' }));
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// --- Blockquote ---
|
|
145
|
-
const bqMatch = raw.match(/^> (.*)/);
|
|
146
|
-
if (bqMatch) {
|
|
147
|
-
const rendered = renderInlineMarkdown(bqMatch[1]);
|
|
148
|
-
const prefix = ' '.repeat(indent) + '┃ ';
|
|
149
|
-
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '244', italic: true }, indent + 3));
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- Table ---
|
|
154
|
-
if (raw.includes('|') && i + 1 < rawLines.length && isLikelyTableHeaderRow(raw) && isLikelyTableSeparatorRow(rawLines[i + 1])) {
|
|
155
|
-
const tableRows: string[] = [];
|
|
156
|
-
let j = i;
|
|
157
|
-
while (j < rawLines.length && rawLines[j].includes('|')) {
|
|
158
|
-
tableRows.push(rawLines[j]);
|
|
159
|
-
j++;
|
|
160
|
-
}
|
|
161
|
-
i = j - 1;
|
|
162
|
-
lines.push(...renderTable(tableRows, width, indent));
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// --- Normal paragraph ---
|
|
167
|
-
const rendered = renderInlineMarkdown(raw);
|
|
168
|
-
lines.push(...compositeInlineLine(' '.repeat(indent), rendered, width, {}, indent));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Handle unclosed code block
|
|
172
|
-
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
173
|
-
const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
|
|
174
|
-
showLineNumbers: options.codeBlockLineNumbers ?? true,
|
|
175
|
-
});
|
|
176
|
-
lines.push(...rendered);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return lines;
|
|
37
|
+
return renderMarkdownTracked(text, width, options).lines;
|
|
180
38
|
}
|
|
181
39
|
|
|
182
40
|
export interface CodeBlockSpan {
|
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.18.
|
|
9
|
+
let _version = '0.18.23';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|