@pellux/goodvibes-tui 0.29.0 → 1.0.0

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 (43) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/handler-content-actions.ts +8 -3
  8. package/src/input/input-history.ts +17 -4
  9. package/src/main.ts +14 -10
  10. package/src/panels/base-panel.ts +1 -1
  11. package/src/panels/diff-panel.ts +10 -4
  12. package/src/panels/git-panel.ts +2 -2
  13. package/src/renderer/agent-detail-modal.ts +37 -8
  14. package/src/renderer/code-block.ts +58 -28
  15. package/src/renderer/context-inspector.ts +3 -6
  16. package/src/renderer/conversation-surface.ts +1 -21
  17. package/src/renderer/diff-view.ts +6 -4
  18. package/src/renderer/fullscreen-primitives.ts +1 -1
  19. package/src/renderer/fullscreen-workspace.ts +2 -7
  20. package/src/renderer/hint-grammar.ts +1 -1
  21. package/src/renderer/history-search-overlay.ts +10 -16
  22. package/src/renderer/markdown.ts +9 -1
  23. package/src/renderer/model-picker-overlay.ts +8 -499
  24. package/src/renderer/model-workspace.ts +9 -24
  25. package/src/renderer/overlay-box.ts +7 -9
  26. package/src/renderer/process-indicator.ts +13 -25
  27. package/src/renderer/profile-picker-modal.ts +13 -14
  28. package/src/renderer/prompt-content-width.ts +16 -0
  29. package/src/renderer/selection-modal-overlay.ts +3 -1
  30. package/src/renderer/semantic-diff.ts +1 -1
  31. package/src/renderer/settings-modal-helpers.ts +10 -34
  32. package/src/renderer/shell-surface.ts +21 -8
  33. package/src/renderer/surface-layout.ts +0 -12
  34. package/src/renderer/term-caps.ts +1 -1
  35. package/src/renderer/tool-call.ts +6 -20
  36. package/src/renderer/ui-factory.ts +7 -7
  37. package/src/renderer/ui-primitives.ts +18 -1
  38. package/src/runtime/render-scheduler.ts +80 -0
  39. package/src/utils/splash-lines.ts +10 -2
  40. package/src/version.ts +1 -1
  41. package/src/renderer/file-tree.ts +0 -153
  42. package/src/renderer/progress.ts +0 -100
  43. package/src/renderer/status-token.ts +0 -67
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.29.0';
9
+ let _version = '1.0.0';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;
@@ -1,153 +0,0 @@
1
- import { type Line, createStyledCell } from '../types/grid.ts';
2
- import { UIFactory } from './ui-factory.ts';
3
- import { getDisplayWidth } from '../utils/terminal-width.ts';
4
-
5
- /** Color by file extension category. */
6
- function getFileColor(name: string): string {
7
- if (name.endsWith('/')) return '#00ffff'; // directory
8
- const ext = name.split('.').pop()?.toLowerCase() ?? '';
9
- if (['ts', 'tsx', 'js', 'jsx', 'mjs'].includes(ext)) return '#dcdcaa';
10
- if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return '#ce9178';
11
- if (['md', 'txt', 'rst'].includes(ext)) return '252';
12
- if (['sh', 'bash', 'zsh'].includes(ext)) return '#22c55e';
13
- if (['css', 'scss', 'less'].includes(ext)) return '#569cd6';
14
- if (['html', 'htm', 'xml', 'svg'].includes(ext)) return '#f97316';
15
- if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'].includes(ext)) return '#a855f7';
16
- if (['lock', 'env', 'gitignore'].includes(name)) return '238';
17
- return '252';
18
- }
19
-
20
- export interface FileTreeEntry {
21
- name: string;
22
- isDir: boolean;
23
- size?: number;
24
- depth: number;
25
- isLast: boolean;
26
- isLastAtDepth: boolean[]; // tracks if each ancestor was last child
27
- }
28
-
29
- /**
30
- * renderFileTree - Render a directory listing as a tree with colored file types.
31
- * Returns Line[] for the cell-based pipeline.
32
- */
33
- export function renderFileTree(
34
- entries: FileTreeEntry[],
35
- width: number,
36
- title?: string
37
- ): Line[] {
38
- const lines: Line[] = [];
39
-
40
- // Optional header
41
- if (title) {
42
- lines.push(UIFactory.stringToLine(` [dir] ${title}`, width, { fg: '#00ffff', bold: true }));
43
- lines.push(UIFactory.stringToLine(' ' + '-'.repeat(width - 2), width, { fg: '240' }));
44
- }
45
-
46
- for (const entry of entries) {
47
- const { name, isDir, size, depth, isLast, isLastAtDepth } = entry;
48
-
49
- // Build tree prefix
50
- let prefix = '';
51
- for (let d = 0; d < depth; d++) {
52
- if (d < depth - 1) {
53
- prefix += isLastAtDepth[d] ? ' ' : '| ';
54
- }
55
- }
56
- if (depth > 0) {
57
- prefix += isLast ? '`-- ' : '|-- ';
58
- } else {
59
- prefix += '';
60
- }
61
-
62
- const displayName = isDir ? name + '/' : name;
63
- const fg = getFileColor(isDir ? displayName : name);
64
-
65
- // Size info (dimmed)
66
- let sizeStr = '';
67
- if (size !== undefined && !isDir) {
68
- if (size < 1024) sizeStr = ` ${size}B`;
69
- else if (size < 1024 * 1024) sizeStr = ` ${(size / 1024).toFixed(1)}K`;
70
- else sizeStr = ` ${(size / (1024 * 1024)).toFixed(1)}M`;
71
- }
72
-
73
- const fullText = prefix + displayName;
74
- const sizeW = getDisplayWidth(sizeStr);
75
- const nameW = getDisplayWidth(fullText);
76
- const maxNameW = width - sizeW - 1;
77
-
78
- const truncated = nameW > maxNameW ? fullText.slice(0, Math.max(0, maxNameW - 3)) + '...' : fullText;
79
-
80
- // Compose: name + padding + size
81
- const paddingW = Math.max(0, width - getDisplayWidth(truncated) - sizeW);
82
- const fullLine = truncated + ' '.repeat(paddingW);
83
-
84
- // Build the line with per-character styles
85
- const line = UIFactory.stringToLine(fullLine, width, { fg });
86
-
87
- // Dim the size info
88
- if (sizeStr && sizeW > 0) {
89
- const sizeStartX = width - sizeW;
90
- let cx = sizeStartX;
91
- for (const ch of sizeStr) {
92
- if (cx >= width) break;
93
- const cw = getDisplayWidth(ch);
94
- line[cx] = createStyledCell(ch, { fg: '240', dim: true });
95
- if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
96
- cx += cw;
97
- }
98
- }
99
-
100
- lines.push(line);
101
- }
102
-
103
- return lines;
104
- }
105
-
106
- /**
107
- * parseListDirOutput - Convert a simple list directory output string to FileTreeEntry[].
108
- * Expects one path per line, relative paths with '/' for directories.
109
- */
110
- export function parseListDirOutput(output: string, rootDir: string): FileTreeEntry[] {
111
- const rawLines = output.trim().split('\n').filter(Boolean);
112
- const entries: FileTreeEntry[] = [];
113
-
114
- for (const line of rawLines) {
115
- const trimmed = line.trim();
116
- if (!trimmed) continue;
117
- const isDir = trimmed.endsWith('/');
118
- const name = isDir ? trimmed.slice(0, -1).split('/').pop() + '/' : trimmed.split('/').pop() ?? trimmed;
119
- const depth = (trimmed.match(/\//g) ?? []).length - (isDir ? 1 : 0);
120
-
121
- entries.push({
122
- name: name || trimmed,
123
- isDir,
124
- depth: Math.max(0, depth),
125
- isLast: false,
126
- isLastAtDepth: [],
127
- });
128
- }
129
-
130
- // Mark isLast for each entry (last sibling at same depth under same parent)
131
- for (let i = 0; i < entries.length; i++) {
132
- const nextSameOrLower = entries.slice(i + 1).findIndex(
133
- e => e.depth <= entries[i].depth
134
- );
135
- entries[i].isLast = nextSameOrLower === 0 || nextSameOrLower === -1;
136
-
137
- // Build isLastAtDepth[d] = true if the ancestor at depth d was the last child
138
- // of its parent. This governs whether to draw '| ' or ' ' vertical bars.
139
- const isLastAtDepth: boolean[] = new Array(entries[i].depth).fill(false);
140
- for (let d = 0; d < entries[i].depth; d++) {
141
- // Walk backwards to find the most recent ancestor at depth d
142
- for (let j = i - 1; j >= 0; j--) {
143
- if (entries[j].depth === d) {
144
- isLastAtDepth[d] = entries[j].isLast;
145
- break;
146
- }
147
- }
148
- }
149
- entries[i].isLastAtDepth = isLastAtDepth;
150
- }
151
-
152
- return entries;
153
- }
@@ -1,100 +0,0 @@
1
- import { type Line } from '../types/grid.ts';
2
- import { UIFactory } from './ui-factory.ts';
3
- import { getDisplayWidth, padDisplayEnd } from '../utils/terminal-width.ts';
4
- import { abbreviateCount } from '../utils/format-number.ts';
5
- import { SPINNER_FRAMES } from './ui-primitives.ts';
6
-
7
- // Rich spinner frames (used by progress indicators) — single source in ui-primitives.ts.
8
- export { SPINNER_FRAMES };
9
- // Braille thinking spinner frames (used by the orchestrator thinking animation) — same frame set.
10
- export const THINKING_SPINNER_FRAMES = SPINNER_FRAMES;
11
-
12
- /**
13
- * renderSpinner - Render a spinner with label as a single Line.
14
- */
15
- export function renderSpinner(
16
- frame: string,
17
- label: string,
18
- width: number,
19
- fg: string = '135'
20
- ): Line {
21
- const text = ` ${frame} ${label}`;
22
- return UIFactory.stringToLine(padDisplayEnd(text, width), width, { fg, bold: true });
23
- }
24
-
25
- /**
26
- * renderToolProgress - Render tool execution progress.
27
- * E.g. "[2/5] Editing src/config.ts..."
28
- */
29
- export function renderToolProgress(
30
- current: number,
31
- total: number,
32
- label: string,
33
- width: number
34
- ): Line[] {
35
- const counter = `[${current}/${total}]`;
36
- const text = ` ${counter} ${label}`;
37
- return [
38
- UIFactory.stringToLine(padDisplayEnd(text, width), width, { fg: '#ffcc00', bold: true }),
39
- ];
40
- }
41
-
42
- /**
43
- * renderTokenBar - Render a token usage bar for the footer.
44
- * Shows used/max tokens as a visual bar + numbers.
45
- */
46
- export function renderTokenBar(
47
- used: number,
48
- max: number,
49
- width: number,
50
- model: string,
51
- toolCount: number
52
- ): Line[] {
53
- const lines: Line[] = [];
54
-
55
- // Stats row
56
- const usedK = abbreviateCount(used, { noM: true });
57
- const maxK = abbreviateCount(max, { noM: true });
58
- const pct = max > 0 ? Math.min(100, Math.round((used / max) * 100)) : 0;
59
- const toolStr = toolCount > 0 ? ` tools:${toolCount}` : '';
60
- const statsText = ` ${model} in:${usedK}/${maxK} (${pct}%)${toolStr}`;
61
-
62
- // Progress bar
63
- const barLabel = ' ctx ';
64
- const barLabelW = getDisplayWidth(barLabel);
65
- const barW = Math.max(10, Math.floor(width * 0.3));
66
- const filled = Math.round((pct / 100) * barW);
67
- const empty = barW - filled;
68
-
69
- // Color based on usage
70
- const barFg = pct > 85 ? '#ef4444' : pct > 60 ? '#ffcc00' : '#22c55e';
71
-
72
- const bar = '#'.repeat(filled) + '-'.repeat(empty);
73
- const barText = barLabel + bar;
74
-
75
- const statsW = getDisplayWidth(statsText);
76
- const barTextW = getDisplayWidth(barText);
77
- const spacingW = Math.max(1, width - statsW - barTextW - 2);
78
-
79
- // Build with mixed colors
80
- const line = UIFactory.stringToLine(statsText + ' '.repeat(spacingW), width, { fg: '244', dim: true });
81
-
82
- // Overlay the bar with color
83
- let barStartX = getDisplayWidth(statsText) + spacingW;
84
- for (const ch of barLabel) {
85
- if (barStartX >= width) break;
86
- const cw = getDisplayWidth(ch);
87
- line[barStartX] = { char: ch, fg: '244', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
88
- if (cw === 2 && barStartX + 1 < width) line[barStartX + 1] = { ...line[barStartX], char: '' };
89
- barStartX += cw;
90
- }
91
- for (let i = 0; i < filled && barStartX + i < width; i++) {
92
- line[barStartX + i] = { char: '#', fg: barFg, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
93
- }
94
- for (let i = 0; i < empty && barStartX + filled + i < width; i++) {
95
- line[barStartX + filled + i] = { char: '-', fg: '238', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
96
- }
97
-
98
- lines.push(line);
99
- return lines;
100
- }
@@ -1,67 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // buildStatusToken — always glyph + color, never color-only.
3
- //
4
- // Maps a semantic state to a Unicode glyph AND a palette color so that
5
- // colorblind users and screen readers can distinguish states without color.
6
- //
7
- // Glyphs:
8
- // good ✓ (CHECK MARK)
9
- // warn ⚠ (WARNING SIGN)
10
- // bad ✕ (MULTIPLICATION X)
11
- // info ○ (WHITE CIRCLE)
12
- // ---------------------------------------------------------------------------
13
-
14
- import type { Cell } from '../types/grid.ts';
15
- import { DEFAULT_PANEL_PALETTE } from '../panels/polish.ts';
16
- import { type StatusState, STATE_GLYPHS } from './status-glyphs.ts';
17
-
18
- // Re-export for downstream consumers that import from this module.
19
- export type { StatusState } from './status-glyphs.ts';
20
- export { STATE_GLYPHS } from './status-glyphs.ts';
21
-
22
- const STATE_COLORS: Record<StatusState, string> = {
23
- good: DEFAULT_PANEL_PALETTE.good,
24
- warn: DEFAULT_PANEL_PALETTE.warn,
25
- bad: DEFAULT_PANEL_PALETTE.bad,
26
- info: DEFAULT_PANEL_PALETTE.info,
27
- };
28
-
29
- export interface StatusTokenOpts {
30
- /** Append a numeric count after the label, e.g. "label (3)". */
31
- count?: number;
32
- /** Override the default glyph for this state. */
33
- glyph?: string;
34
- }
35
-
36
- /**
37
- * Build a small sequence of styled cells: [glyph, space, label, optional count]
38
- *
39
- * Always prepends the glyph so colorblind users can parse the state.
40
- * The color is applied to both glyph and label as a redundant cue.
41
- *
42
- * @param state Semantic state — controls default glyph and color.
43
- * @param label Human-readable label string.
44
- * @param opts Optional count suffix or glyph override.
45
- * @returns Array of Cell objects ready to embed in a Line.
46
- */
47
- export function buildStatusToken(
48
- state: StatusState,
49
- label: string,
50
- opts?: StatusTokenOpts,
51
- ): Cell[] {
52
- const glyph = opts?.glyph ?? STATE_GLYPHS[state];
53
- const color = STATE_COLORS[state];
54
- const suffix = opts?.count !== undefined ? ` (${opts.count})` : '';
55
- const text = `${glyph} ${label}${suffix}`;
56
-
57
- return text.split('').map((char): Cell => ({
58
- char,
59
- fg: color,
60
- bg: '',
61
- bold: false,
62
- dim: false,
63
- underline: false,
64
- italic: false,
65
- strikethrough: false,
66
- }));
67
- }