@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-rendering.ts +20 -6
  5. package/src/input/commands/session.ts +0 -1
  6. package/src/input/feed-context-factory.ts +236 -0
  7. package/src/input/handler-feed.ts +44 -6
  8. package/src/input/handler-shortcuts.ts +138 -125
  9. package/src/input/handler.ts +121 -119
  10. package/src/input/keybindings.ts +30 -0
  11. package/src/panels/approval-panel.ts +54 -82
  12. package/src/panels/automation-control-panel.ts +119 -161
  13. package/src/panels/communication-panel.ts +68 -107
  14. package/src/panels/control-plane-panel.ts +116 -172
  15. package/src/panels/hooks-panel.ts +101 -138
  16. package/src/panels/incident-review-panel.ts +55 -107
  17. package/src/panels/local-auth-panel.ts +76 -93
  18. package/src/panels/mcp-panel.ts +108 -155
  19. package/src/panels/ops-control-panel.ts +50 -85
  20. package/src/panels/panel-manager.ts +22 -2
  21. package/src/panels/plugins-panel.ts +36 -60
  22. package/src/panels/routes-panel.ts +89 -141
  23. package/src/panels/scrollable-list-panel.ts +45 -14
  24. package/src/panels/security-panel.ts +101 -137
  25. package/src/panels/services-panel.ts +58 -102
  26. package/src/panels/settings-sync-panel.ts +76 -122
  27. package/src/panels/subscription-panel.ts +63 -86
  28. package/src/panels/tasks-panel.ts +129 -179
  29. package/src/panels/watchers-panel.ts +88 -137
  30. package/src/renderer/buffer.ts +11 -0
  31. package/src/renderer/diff.ts +8 -0
  32. package/src/renderer/help-overlay.ts +37 -28
  33. package/src/renderer/markdown.ts +3 -145
  34. package/src/version.ts +1 -1
@@ -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
  }
@@ -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
- const commandRows: string[] = [
68
- ' Quick Start',
69
- ' ' + '\u2500'.repeat(40),
70
- ' /setup onboarding Guided first-run review and environment posture',
71
- ' /cockpit Unified runtime control room',
72
- ' /settings Settings and config browser',
73
- '',
74
- ' Build And Operate',
75
- ' ' + '\u2500'.repeat(40),
76
- ' /provider Choose provider or model family',
77
- ' /subscription Review provider logins and subscriptions',
78
- ' /marketplace open Browse plugins, skills, and packs',
79
- ' /remote setup Review remote, bridge, and tunnel flows',
80
- ' /sandbox review Inspect secure execution posture',
81
- '',
82
- ' Review And Govern',
83
- ' ' + '\u2500'.repeat(40),
84
- ' /security Security review workspace',
85
- ' /policy Simulation, lint, and preflight review',
86
- ' /incident Incident workspace and export flows',
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'];
@@ -30,153 +30,11 @@ function isLikelyTableHeaderRow(row: string): boolean {
30
30
  }
31
31
 
32
32
  /**
33
- * renderMarkdown - Parse markdown text into styled Line[] using a line-by-line state machine.
34
- * Supports headers, bold, italic, inline code, code blocks, lists, and links.
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
- const lines: Line[] = [];
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.20';
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;