agen-vektor 0.1.0 → 0.2.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.
- package/README.md +3 -3
- package/dist/tui/app.js +16 -6
- package/dist/tui/chat.js +15 -29
- package/dist/tui/components.js +28 -5
- package/dist/tui/diff.js +97 -22
- package/dist/tui/statusbar.js +32 -17
- package/dist/tui/theme.js +76 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,10 +57,10 @@ Then:
|
|
|
57
57
|
vector
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
### From npm
|
|
60
|
+
### From npm
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
npm install -g
|
|
63
|
+
npm install -g agen-vektor
|
|
64
64
|
vector
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -69,7 +69,7 @@ vector
|
|
|
69
69
|
```bash
|
|
70
70
|
pkg update && pkg upgrade
|
|
71
71
|
pkg install nodejs git
|
|
72
|
-
npm install -g
|
|
72
|
+
npm install -g agen-vektor
|
|
73
73
|
vector
|
|
74
74
|
```
|
|
75
75
|
|
package/dist/tui/app.js
CHANGED
|
@@ -53,6 +53,7 @@ const chat_1 = require("./chat");
|
|
|
53
53
|
const input_1 = require("./input");
|
|
54
54
|
const statusbar_1 = require("./statusbar");
|
|
55
55
|
const components_1 = require("./components");
|
|
56
|
+
const theme_1 = require("./theme");
|
|
56
57
|
const diff_1 = require("./diff");
|
|
57
58
|
const planner_1 = require("../agent/planner");
|
|
58
59
|
const session_1 = require("../agent/session");
|
|
@@ -76,6 +77,7 @@ class App {
|
|
|
76
77
|
pendingPrompt = '';
|
|
77
78
|
planToExecute = null;
|
|
78
79
|
abortController = null;
|
|
80
|
+
spinner = new theme_1.Spinner();
|
|
79
81
|
constructor(agent, opts) {
|
|
80
82
|
this.agent = agent;
|
|
81
83
|
this.opts = opts;
|
|
@@ -219,7 +221,8 @@ class App {
|
|
|
219
221
|
this.scroll = 0;
|
|
220
222
|
this.running = true;
|
|
221
223
|
this.status = 'Thinking';
|
|
222
|
-
this.statusColor =
|
|
224
|
+
this.statusColor = (0, theme_1.statusColor)('Thinking');
|
|
225
|
+
this.spinner.reset();
|
|
223
226
|
this.abortController = new AbortController();
|
|
224
227
|
// Show plan first if requested
|
|
225
228
|
if (this.opts.showPlan) {
|
|
@@ -259,6 +262,7 @@ class App {
|
|
|
259
262
|
kind: 'assistant',
|
|
260
263
|
content: '',
|
|
261
264
|
streaming: true,
|
|
265
|
+
meta: this.agent.config.model,
|
|
262
266
|
}) - 1;
|
|
263
267
|
this.agent.setCallbacks({
|
|
264
268
|
onDelta: (delta) => {
|
|
@@ -268,7 +272,7 @@ class App {
|
|
|
268
272
|
},
|
|
269
273
|
onStatus: (status) => {
|
|
270
274
|
this.status = status;
|
|
271
|
-
this.statusColor =
|
|
275
|
+
this.statusColor = (0, theme_1.statusColor)(status);
|
|
272
276
|
},
|
|
273
277
|
onToolCall: (tool, args) => {
|
|
274
278
|
let preview = '';
|
|
@@ -680,12 +684,14 @@ class App {
|
|
|
680
684
|
const { rows, cols } = (0, terminal_1.getTerminalSize)();
|
|
681
685
|
const info = {
|
|
682
686
|
status: this.status,
|
|
683
|
-
statusColor: this.
|
|
687
|
+
statusColor: (0, theme_1.statusColor)(this.status),
|
|
684
688
|
model: this.agent.config.model,
|
|
685
689
|
provider: this.agent.config.provider,
|
|
686
690
|
project: this.agent.cwd,
|
|
687
691
|
connected: this.connected,
|
|
688
692
|
mode: this.agent.config.permissionMode,
|
|
693
|
+
spinner: this.spinner.frame(),
|
|
694
|
+
running: this.running,
|
|
689
695
|
};
|
|
690
696
|
const parts = [];
|
|
691
697
|
parts.push((0, statusbar_1.renderHeader)(cols, info));
|
|
@@ -704,7 +710,7 @@ class App {
|
|
|
704
710
|
}
|
|
705
711
|
// Separator
|
|
706
712
|
const sepRow = chatTop + chatH;
|
|
707
|
-
parts.push((0, terminal_1.cursorTo)(sepRow, 1) + terminal_1.ANSI.clearLineEnd +
|
|
713
|
+
parts.push((0, terminal_1.cursorTo)(sepRow, 1) + terminal_1.ANSI.clearLineEnd + theme_1.THEME.border + terminal_1.BOX.h.repeat(Math.min(cols, 120)) + theme_1.THEME.reset);
|
|
708
714
|
// Input
|
|
709
715
|
const inputRow = sepRow + 1;
|
|
710
716
|
const rendered = this.input.render(cols);
|
|
@@ -712,7 +718,7 @@ class App {
|
|
|
712
718
|
const cursorCol = rendered.cursorCol + 1;
|
|
713
719
|
// Status bar
|
|
714
720
|
const statusRow = inputRow + 1;
|
|
715
|
-
parts.push((0, terminal_1.cursorTo)(statusRow, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderStatusBar)(
|
|
721
|
+
parts.push((0, terminal_1.cursorTo)(statusRow, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderStatusBar)(cols, info));
|
|
716
722
|
// Modal overlay
|
|
717
723
|
if (this.modal.type !== 'none') {
|
|
718
724
|
const overlay = this.renderModal(rows, cols);
|
|
@@ -867,7 +873,11 @@ class App {
|
|
|
867
873
|
],
|
|
868
874
|
}, rows, cols);
|
|
869
875
|
case 'confirm':
|
|
870
|
-
return (0, components_1.
|
|
876
|
+
return (0, components_1.renderConfirm)({
|
|
877
|
+
title: m.title,
|
|
878
|
+
body: m.body,
|
|
879
|
+
focused: 'confirm',
|
|
880
|
+
}, rows, cols);
|
|
871
881
|
case 'apikey':
|
|
872
882
|
return (0, components_1.renderModal)({
|
|
873
883
|
title: `API key for ${m.provider}`,
|
package/dist/tui/chat.js
CHANGED
|
@@ -7,15 +7,15 @@ exports.clampScroll = clampScroll;
|
|
|
7
7
|
* Chat view — renders conversation messages with scrolling.
|
|
8
8
|
* Message types: user, assistant (streamed), tool, system/status, plan.
|
|
9
9
|
*/
|
|
10
|
-
const
|
|
10
|
+
const theme_1 = require("./theme");
|
|
11
11
|
const KIND_STYLE = {
|
|
12
|
-
user: { prefix: 'YOU', color:
|
|
13
|
-
assistant: { prefix: 'VECTOR', color:
|
|
14
|
-
tool: { prefix: 'TOOL', color:
|
|
15
|
-
system: { prefix: '···', color:
|
|
16
|
-
error: { prefix: '✗', color:
|
|
17
|
-
success: { prefix: '✓', color:
|
|
18
|
-
warning: { prefix: '⚠', color:
|
|
12
|
+
user: { prefix: 'YOU', color: theme_1.THEME.info },
|
|
13
|
+
assistant: { prefix: 'VECTOR', color: theme_1.THEME.accent },
|
|
14
|
+
tool: { prefix: 'TOOL', color: theme_1.THEME.warn },
|
|
15
|
+
system: { prefix: '···', color: theme_1.THEME.muted },
|
|
16
|
+
error: { prefix: '✗', color: theme_1.THEME.error },
|
|
17
|
+
success: { prefix: '✓', color: theme_1.THEME.success },
|
|
18
|
+
warning: { prefix: '⚠', color: theme_1.THEME.warn },
|
|
19
19
|
};
|
|
20
20
|
function plainLines(text, width) {
|
|
21
21
|
const lines = text.split('\n');
|
|
@@ -37,29 +37,15 @@ function renderMessages(messages, width) {
|
|
|
37
37
|
for (const m of messages) {
|
|
38
38
|
const style = KIND_STYLE[m.kind];
|
|
39
39
|
const prefix = m.prefix || style.prefix;
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
40
|
+
const dot = m.streaming ? `${theme_1.THEME.warn}◐${theme_1.THEME.reset} ` : `${style.color}●${theme_1.THEME.reset} `;
|
|
41
|
+
const header = `${dot}${theme_1.THEME.bold}${style.color}${prefix}${theme_1.THEME.reset}`;
|
|
42
|
+
const meta = m.meta ? ` ${theme_1.THEME.dim}${m.meta}${theme_1.THEME.reset}` : '';
|
|
43
|
+
const toolName = m.tool ? ` ${theme_1.THEME.warn}${m.tool}${theme_1.THEME.reset}` : '';
|
|
44
|
+
out.push(header + toolName + meta);
|
|
43
45
|
const body = m.content || '(no content)';
|
|
46
|
+
const bar = `${style.color}│${theme_1.THEME.reset} `;
|
|
44
47
|
for (const line of plainLines(body, width)) {
|
|
45
|
-
|
|
46
|
-
out.push(`${terminal_1.ANSI.brightBlue}│ ${line}${terminal_1.ANSI.reset}`);
|
|
47
|
-
}
|
|
48
|
-
else if (m.kind === 'tool') {
|
|
49
|
-
out.push(`${terminal_1.ANSI.brightYellow}│ ${line}${terminal_1.ANSI.reset}`);
|
|
50
|
-
}
|
|
51
|
-
else if (m.kind === 'error') {
|
|
52
|
-
out.push(`${terminal_1.ANSI.brightRed}│ ${line}${terminal_1.ANSI.reset}`);
|
|
53
|
-
}
|
|
54
|
-
else if (m.kind === 'success') {
|
|
55
|
-
out.push(`${terminal_1.ANSI.brightGreen}│ ${line}${terminal_1.ANSI.reset}`);
|
|
56
|
-
}
|
|
57
|
-
else if (m.kind === 'warning') {
|
|
58
|
-
out.push(`${terminal_1.ANSI.brightYellow}│ ${line}${terminal_1.ANSI.reset}`);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
out.push(` ${line}`);
|
|
62
|
-
}
|
|
48
|
+
out.push(`${bar}${line}`);
|
|
63
49
|
}
|
|
64
50
|
out.push(''); // blank line between messages
|
|
65
51
|
}
|
package/dist/tui/components.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderModal = renderModal;
|
|
4
4
|
exports.renderSelector = renderSelector;
|
|
5
|
+
exports.renderConfirm = renderConfirm;
|
|
5
6
|
/**
|
|
6
7
|
* Reusable TUI components — modal overlay and list selector.
|
|
7
8
|
*/
|
|
8
9
|
const terminal_1 = require("../utils/terminal");
|
|
10
|
+
const theme_1 = require("./theme");
|
|
9
11
|
/** Render a centered modal box. Returns the box's top row (1-based). */
|
|
10
12
|
function renderModal(opts, rows, cols) {
|
|
11
13
|
const title = opts.title;
|
|
@@ -15,7 +17,7 @@ function renderModal(opts, rows, cols) {
|
|
|
15
17
|
const height = Math.min(rows - 2, bodyLines.length + footerLines.length + 4);
|
|
16
18
|
const top = Math.max(1, Math.floor((rows - height) / 2));
|
|
17
19
|
const left = Math.max(1, Math.floor((cols - innerWidth - 2) / 2));
|
|
18
|
-
const borderColor = opts.danger ?
|
|
20
|
+
const borderColor = opts.danger ? theme_1.THEME.error : theme_1.THEME.borderBright;
|
|
19
21
|
const lines = [];
|
|
20
22
|
const topBorder = borderColor + terminal_1.BOX.tl + terminal_1.BOX.h.repeat(innerWidth) + terminal_1.BOX.tr;
|
|
21
23
|
const bottomBorder = borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(innerWidth) + terminal_1.BOX.br;
|
|
@@ -23,19 +25,19 @@ function renderModal(opts, rows, cols) {
|
|
|
23
25
|
const t = (0, terminal_1.pad)(title.slice(0, innerWidth - 2), innerWidth);
|
|
24
26
|
lines.push((0, terminal_1.cursorTo)(top, left) +
|
|
25
27
|
topBorder.slice(0, 2) +
|
|
26
|
-
|
|
28
|
+
theme_1.THEME.bold + theme_1.THEME.textBright + t + theme_1.THEME.reset + borderColor +
|
|
27
29
|
topBorder.slice(2));
|
|
28
30
|
const maxBody = height - footerLines.length - 2;
|
|
29
31
|
for (let i = 0; i < maxBody; i++) {
|
|
30
32
|
const content = bodyLines[i] ?? '';
|
|
31
33
|
const lineStr = (0, terminal_1.pad)(content, innerWidth);
|
|
32
|
-
lines.push((0, terminal_1.cursorTo)(top + 1 + i, left) + borderColor + terminal_1.BOX.v + ' ' + lineStr +
|
|
34
|
+
lines.push((0, terminal_1.cursorTo)(top + 1 + i, left) + borderColor + terminal_1.BOX.v + ' ' + lineStr + theme_1.THEME.reset + borderColor + ' ' + terminal_1.BOX.v);
|
|
33
35
|
}
|
|
34
36
|
for (let i = 0; i < footerLines.length; i++) {
|
|
35
37
|
const row = top + 1 + maxBody + i;
|
|
36
38
|
const content = footerLines[i] ?? '';
|
|
37
39
|
const lineStr = (0, terminal_1.pad)(content, innerWidth);
|
|
38
|
-
lines.push((0, terminal_1.cursorTo)(row, left) + borderColor + terminal_1.BOX.v + ' ' +
|
|
40
|
+
lines.push((0, terminal_1.cursorTo)(row, left) + borderColor + terminal_1.BOX.v + ' ' + theme_1.THEME.dim + lineStr + theme_1.THEME.reset + borderColor + ' ' + terminal_1.BOX.v);
|
|
39
41
|
}
|
|
40
42
|
lines.push((0, terminal_1.cursorTo)(top + height - 1, left) + bottomBorder);
|
|
41
43
|
return { top, lines };
|
|
@@ -79,7 +81,28 @@ function renderSelector(opts) {
|
|
|
79
81
|
const footerText = opts.footer ? opts.footer : '↑↓ navigate · Enter select · Esc cancel';
|
|
80
82
|
const f = (0, terminal_1.pad)(footerText.slice(0, innerWidth - 2), innerWidth);
|
|
81
83
|
lines.push((0, terminal_1.cursorTo)(top + visible + 1, left) +
|
|
82
|
-
borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(2) +
|
|
84
|
+
borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(2) + theme_1.THEME.dim + f + theme_1.THEME.reset + borderColor +
|
|
83
85
|
terminal_1.BOX.h.repeat(Math.max(0, innerWidth - 2 - (0, terminal_1.visibleWidth)(f))) + terminal_1.BOX.br);
|
|
84
86
|
return { top, lines };
|
|
85
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Render a confirm dialog with a focused button ([OK] / [Cancel]).
|
|
90
|
+
* Body lines may contain ANSI codes; width is computed from content.
|
|
91
|
+
*/
|
|
92
|
+
function renderConfirm(opts, rows, cols) {
|
|
93
|
+
const confirmLabel = opts.confirmLabel || 'OK';
|
|
94
|
+
const cancelLabel = opts.cancelLabel || 'Cancel';
|
|
95
|
+
const focused = opts.focused || 'confirm';
|
|
96
|
+
const confirmBtn = focused === 'confirm'
|
|
97
|
+
? `${theme_1.THEME.bgPanel}${theme_1.THEME.bold}${opts.danger ? theme_1.THEME.error : theme_1.THEME.accent} ${confirmLabel} ${theme_1.THEME.reset}`
|
|
98
|
+
: `${theme_1.THEME.faint} ${confirmLabel} ${theme_1.THEME.reset}`;
|
|
99
|
+
const cancelBtn = focused === 'cancel'
|
|
100
|
+
? `${theme_1.THEME.bgPanel}${theme_1.THEME.bold}${theme_1.THEME.muted} ${cancelLabel} ${theme_1.THEME.reset}`
|
|
101
|
+
: `${theme_1.THEME.faint} ${cancelLabel} ${theme_1.THEME.reset}`;
|
|
102
|
+
const body = [...opts.body, '', `${confirmBtn} ${cancelBtn}`];
|
|
103
|
+
return renderModal({
|
|
104
|
+
title: opts.title,
|
|
105
|
+
body,
|
|
106
|
+
danger: opts.danger,
|
|
107
|
+
}, rows, cols);
|
|
108
|
+
}
|
package/dist/tui/diff.js
CHANGED
|
@@ -4,44 +4,105 @@ exports.diffLines = diffLines;
|
|
|
4
4
|
exports.computeDiff = computeDiff;
|
|
5
5
|
exports.renderDiffModal = renderDiffModal;
|
|
6
6
|
/**
|
|
7
|
-
* Diff viewer — renders unified diffs with color,
|
|
8
|
-
* modal with Accept / Reject actions.
|
|
7
|
+
* Diff viewer — renders unified diffs with hunk headers and color,
|
|
8
|
+
* plus a modal with Accept / Reject actions.
|
|
9
9
|
*/
|
|
10
|
-
const
|
|
10
|
+
const theme_1 = require("./theme");
|
|
11
11
|
const components_1 = require("./components");
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Compute a line-based unified diff with hunk headers (@@ -x,y +x,y @@).
|
|
14
|
+
* Uses LCS for small inputs; falls back to a full replace view for large ones.
|
|
15
|
+
*/
|
|
13
16
|
function diffLines(oldText, newText) {
|
|
14
|
-
return computeDiff(oldText, newText);
|
|
17
|
+
return computeDiff(oldText, newText).map((l) => l.text);
|
|
15
18
|
}
|
|
16
19
|
function computeDiff(oldText, newText) {
|
|
17
|
-
const oldLines = (oldText
|
|
18
|
-
const newLines = (newText
|
|
19
|
-
//
|
|
20
|
+
const oldLines = splitLines(oldText);
|
|
21
|
+
const newLines = splitLines(newText);
|
|
22
|
+
// Large inputs: LCS is O(n*m); fall back to a simple full-replace view.
|
|
20
23
|
if (oldLines.length * newLines.length > 250_000) {
|
|
21
|
-
const out = [
|
|
24
|
+
const out = [];
|
|
25
|
+
out.push({ type: 'file', text: `- old (${oldLines.length} lines)` });
|
|
22
26
|
for (const l of oldLines)
|
|
23
|
-
out.push(
|
|
24
|
-
out.push('
|
|
27
|
+
out.push({ type: 'del', text: `-${l}` });
|
|
28
|
+
out.push({ type: 'file', text: `+ new (${newLines.length} lines)` });
|
|
25
29
|
for (const l of newLines)
|
|
26
|
-
out.push(
|
|
30
|
+
out.push({ type: 'add', text: `+${l}` });
|
|
27
31
|
return out;
|
|
28
32
|
}
|
|
29
33
|
const ops = lcsDiff(oldLines, newLines);
|
|
30
34
|
const out = [];
|
|
35
|
+
// Rolling window of up to 3 context lines before a change.
|
|
36
|
+
const pendingContext = [];
|
|
37
|
+
// Current hunk buffer + its start positions.
|
|
38
|
+
let hunk = [];
|
|
39
|
+
let hunkOldStart = 0;
|
|
40
|
+
let hunkNewStart = 0;
|
|
41
|
+
// How many context lines currently trail the changes inside the hunk.
|
|
42
|
+
let trailingContext = 0;
|
|
43
|
+
let oldLine = 1;
|
|
44
|
+
let newLine = 1;
|
|
45
|
+
const flushHunk = () => {
|
|
46
|
+
if (hunk.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const oldCount = hunk.filter((l) => l.type !== 'add').length;
|
|
49
|
+
const newCount = hunk.filter((l) => l.type !== 'del').length;
|
|
50
|
+
out.push({ type: 'hunk', text: `@@ -${hunkOldStart},${oldCount} +${hunkNewStart},${newCount} @@` });
|
|
51
|
+
for (const l of hunk)
|
|
52
|
+
out.push(l);
|
|
53
|
+
hunk = [];
|
|
54
|
+
trailingContext = 0;
|
|
55
|
+
};
|
|
31
56
|
for (const op of ops) {
|
|
32
|
-
if (op.type === 'same')
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
if (op.type === 'same') {
|
|
58
|
+
if (hunk.length > 0 && trailingContext < 3) {
|
|
59
|
+
hunk.push({ type: 'same', text: ` ${op.line}` });
|
|
60
|
+
trailingContext++;
|
|
61
|
+
oldLine++;
|
|
62
|
+
newLine++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (hunk.length > 0)
|
|
66
|
+
flushHunk();
|
|
67
|
+
pendingContext.push({ type: 'same', text: ` ${op.line}` });
|
|
68
|
+
if (pendingContext.length > 3) {
|
|
69
|
+
const dropped = pendingContext.shift();
|
|
70
|
+
if (dropped)
|
|
71
|
+
out.push(dropped);
|
|
72
|
+
}
|
|
73
|
+
oldLine++;
|
|
74
|
+
newLine++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// A change: start or continue the hunk.
|
|
78
|
+
if (hunk.length === 0) {
|
|
79
|
+
hunkOldStart = oldLine - pendingContext.length;
|
|
80
|
+
hunkNewStart = newLine - pendingContext.length;
|
|
81
|
+
hunk = [...pendingContext];
|
|
82
|
+
pendingContext.length = 0;
|
|
83
|
+
}
|
|
84
|
+
hunk.push(op.type === 'del' ? { type: 'del', text: `-${op.line}` } : { type: 'add', text: `+${op.line}` });
|
|
85
|
+
trailingContext = 0;
|
|
86
|
+
if (op.type === 'del')
|
|
87
|
+
oldLine++;
|
|
36
88
|
else
|
|
37
|
-
|
|
89
|
+
newLine++;
|
|
90
|
+
}
|
|
91
|
+
flushHunk();
|
|
92
|
+
if (out.length === 0) {
|
|
93
|
+
out.push({ type: 'file', text: '(no changes)' });
|
|
38
94
|
}
|
|
39
95
|
return out;
|
|
40
96
|
}
|
|
97
|
+
function splitLines(text) {
|
|
98
|
+
const lines = (text || '').split('\n');
|
|
99
|
+
if (text.endsWith('\n'))
|
|
100
|
+
lines.pop();
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
41
103
|
function lcsDiff(a, b) {
|
|
42
104
|
const n = a.length;
|
|
43
105
|
const m = b.length;
|
|
44
|
-
// DP table
|
|
45
106
|
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
46
107
|
for (let i = n - 1; i >= 0; i--) {
|
|
47
108
|
for (let j = m - 1; j >= 0; j--) {
|
|
@@ -79,14 +140,26 @@ function lcsDiff(a, b) {
|
|
|
79
140
|
}
|
|
80
141
|
return out;
|
|
81
142
|
}
|
|
143
|
+
function renderDiffLine(l) {
|
|
144
|
+
if (l.type === 'hunk')
|
|
145
|
+
return `${theme_1.THEME.accentDim}${theme_1.THEME.bold}${l.text}${theme_1.THEME.reset}`;
|
|
146
|
+
if (l.type === 'file')
|
|
147
|
+
return `${theme_1.THEME.gold}${theme_1.THEME.bold}${l.text}${theme_1.THEME.reset}`;
|
|
148
|
+
if (l.type === 'del')
|
|
149
|
+
return `${theme_1.THEME.error}${l.text}${theme_1.THEME.reset}`;
|
|
150
|
+
if (l.type === 'add')
|
|
151
|
+
return `${theme_1.THEME.success}${l.text}${theme_1.THEME.reset}`;
|
|
152
|
+
return `${theme_1.THEME.faint}${l.text}${theme_1.THEME.reset}`;
|
|
153
|
+
}
|
|
82
154
|
/** Render the diff modal with a header per file. */
|
|
83
155
|
function renderDiffModal(entries, rows, cols, scroll, selectedAccept) {
|
|
84
156
|
const innerWidth = Math.max(20, cols - 4);
|
|
85
157
|
const body = [];
|
|
86
158
|
for (const e of entries) {
|
|
87
|
-
body.push(`${
|
|
88
|
-
const
|
|
89
|
-
|
|
159
|
+
body.push(`${theme_1.THEME.bold}${theme_1.THEME.accent}── ${e.path}${theme_1.THEME.reset}`);
|
|
160
|
+
for (const l of computeDiff(e.oldContent || '', e.newContent)) {
|
|
161
|
+
body.push(renderDiffLine(l));
|
|
162
|
+
}
|
|
90
163
|
body.push('');
|
|
91
164
|
}
|
|
92
165
|
// Scroll window
|
|
@@ -94,7 +167,9 @@ function renderDiffModal(entries, rows, cols, scroll, selectedAccept) {
|
|
|
94
167
|
const maxScroll = Math.max(0, body.length - visibleLines);
|
|
95
168
|
const s = Math.min(scroll, maxScroll);
|
|
96
169
|
const windowLines = body.slice(s, s + visibleLines);
|
|
97
|
-
const
|
|
170
|
+
const accept = selectedAccept === 'accept' ? `${theme_1.THEME.bold}${theme_1.THEME.success}[Accept]${theme_1.THEME.reset}` : `[Accept]`;
|
|
171
|
+
const reject = selectedAccept === 'reject' ? `${theme_1.THEME.bold}${theme_1.THEME.error}[Reject]${theme_1.THEME.reset}` : `[Reject]`;
|
|
172
|
+
const footer = `${accept} ${reject} ↑↓ scroll · ←→ choose · Enter confirm · Esc cancel`;
|
|
98
173
|
const modal = (0, components_1.renderModal)({
|
|
99
174
|
title: `Diff — ${entries.length} file(s)`,
|
|
100
175
|
body: windowLines,
|
package/dist/tui/statusbar.js
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.renderStatusBar = renderStatusBar;
|
|
4
3
|
exports.renderHeader = renderHeader;
|
|
5
4
|
exports.renderProjectBar = renderProjectBar;
|
|
5
|
+
exports.renderStatusBar = renderStatusBar;
|
|
6
6
|
/**
|
|
7
|
-
* Status bar —
|
|
7
|
+
* Status bar — top header + project bar + bottom hint bar.
|
|
8
|
+
* Uses the VECTOR theme with a live spinner while busy.
|
|
8
9
|
*/
|
|
9
10
|
const terminal_1 = require("../utils/terminal");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const right = `${info.connected ? terminal_1.ANSI.green + '●' : terminal_1.ANSI.red + '○'}${terminal_1.ANSI.reset} ${info.mode === 'yolo' ? terminal_1.ANSI.yellow + 'YOLO' + terminal_1.ANSI.reset : 'ask'}`;
|
|
13
|
-
const leftWidth = width - right.length - 1;
|
|
14
|
-
return terminal_1.ANSI.bgBlack + (0, terminal_1.pad)(left.slice(0, Math.max(0, leftWidth)), Math.max(0, leftWidth)) + right + terminal_1.ANSI.reset;
|
|
15
|
-
}
|
|
11
|
+
const theme_1 = require("./theme");
|
|
12
|
+
/** Render the top header line: VECTOR ● status .... provider · model */
|
|
16
13
|
function renderHeader(width, info) {
|
|
17
|
-
const brand = `${
|
|
18
|
-
const
|
|
14
|
+
const brand = `${theme_1.THEME.bold}${theme_1.THEME.accent}VECTOR${theme_1.THEME.reset}`;
|
|
15
|
+
const dot = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
16
|
+
const status = `${dot} ${(0, theme_1.statusColor)(info.status)}${info.status}${theme_1.THEME.reset}`;
|
|
19
17
|
const left = `${brand} ${status}`;
|
|
20
|
-
const
|
|
21
|
-
const
|
|
18
|
+
const provider = `${theme_1.THEME.dim}${info.provider}${theme_1.THEME.reset}`;
|
|
19
|
+
const model = `${theme_1.THEME.accentDim}${info.model}${theme_1.THEME.reset}`;
|
|
20
|
+
const right = `${provider} · ${model}`;
|
|
21
|
+
const rightWidth = (0, terminal_1.stripAnsi)(right).length;
|
|
22
22
|
const leftWidth = Math.max(0, width - rightWidth - 2);
|
|
23
23
|
const line = (0, terminal_1.pad)(left, leftWidth) + ' ' + right;
|
|
24
|
-
return
|
|
24
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(line, width) + theme_1.THEME.reset;
|
|
25
25
|
}
|
|
26
|
+
/** Render the project line: Project: <path> .... Mode: ask|yolo */
|
|
26
27
|
function renderProjectBar(width, info) {
|
|
27
|
-
const
|
|
28
|
-
|
|
28
|
+
const label = `${theme_1.THEME.dim}Project:${theme_1.THEME.reset} ${theme_1.THEME.text}${info.project}${theme_1.THEME.reset}`;
|
|
29
|
+
const modeColor = info.mode === 'yolo' ? theme_1.THEME.warn : theme_1.THEME.muted;
|
|
30
|
+
const mode = `${theme_1.THEME.dim}Mode:${theme_1.THEME.reset} ${modeColor}${info.mode}${theme_1.THEME.reset}`;
|
|
31
|
+
const line = `${label} ${mode}`;
|
|
32
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(line, width) + theme_1.THEME.reset;
|
|
29
33
|
}
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
/** Render the bottom hint/status bar with live spinner. */
|
|
35
|
+
function renderStatusBar(width, info) {
|
|
36
|
+
const left = `ENTER ${theme_1.THEME.accent}Send${theme_1.THEME.reset} TAB ${theme_1.THEME.accent}Commands${theme_1.THEME.reset} CTRL+C ${theme_1.THEME.accent}Stop${theme_1.THEME.reset} ? ${theme_1.THEME.accent}Help${theme_1.THEME.reset}`;
|
|
37
|
+
let right = '';
|
|
38
|
+
if (info.running && info.spinner) {
|
|
39
|
+
right = `${(0, theme_1.statusColor)(info.status)}${info.spinner}${theme_1.THEME.reset} `;
|
|
40
|
+
}
|
|
41
|
+
const conn = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
42
|
+
const mode = info.mode === 'yolo' ? `${theme_1.THEME.warn}YOLO${theme_1.THEME.reset}` : `${theme_1.THEME.muted}ask${theme_1.THEME.reset}`;
|
|
43
|
+
right += `${conn} ${mode}`;
|
|
44
|
+
const rightWidth = (0, terminal_1.stripAnsi)(right).length;
|
|
45
|
+
const leftWidth = Math.max(0, width - rightWidth - 1);
|
|
46
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(left, leftWidth) + ' ' + right + theme_1.THEME.reset;
|
|
32
47
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Spinner = exports.SPINNERS = exports.THEME = void 0;
|
|
4
|
+
exports.statusColor = statusColor;
|
|
5
|
+
/**
|
|
6
|
+
* VECTOR theme — consistent premium palette for the TUI.
|
|
7
|
+
* Dark cyber palette with cyan accent + gold highlights.
|
|
8
|
+
*/
|
|
9
|
+
exports.THEME = {
|
|
10
|
+
accent: '\x1b[38;5;51m', // bright cyan
|
|
11
|
+
accentDim: '\x1b[38;5;37m',
|
|
12
|
+
gold: '\x1b[38;5;220m',
|
|
13
|
+
text: '\x1b[38;5;252m',
|
|
14
|
+
textBright: '\x1b[38;5;255m',
|
|
15
|
+
muted: '\x1b[38;5;244m',
|
|
16
|
+
faint: '\x1b[38;5;240m',
|
|
17
|
+
border: '\x1b[38;5;66m',
|
|
18
|
+
borderBright: '\x1b[38;5;51m',
|
|
19
|
+
success: '\x1b[38;5;114m',
|
|
20
|
+
warn: '\x1b[38;5;215m',
|
|
21
|
+
error: '\x1b[38;5;203m',
|
|
22
|
+
info: '\x1b[38;5;111m',
|
|
23
|
+
bgBar: '\x1b[48;5;235m',
|
|
24
|
+
bgPanel: '\x1b[48;5;236m',
|
|
25
|
+
bold: '\x1b[1m',
|
|
26
|
+
dim: '\x1b[2m',
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
};
|
|
29
|
+
/** Status → color mapping used by the header/statusbar. */
|
|
30
|
+
function statusColor(status) {
|
|
31
|
+
const s = status.toLowerCase();
|
|
32
|
+
if (s.includes('error') || s.includes('fail') || s.includes('✗'))
|
|
33
|
+
return exports.THEME.error;
|
|
34
|
+
if (s.includes('warn') || s.includes('permission') || s.includes('⚠'))
|
|
35
|
+
return exports.THEME.warn;
|
|
36
|
+
if (s.includes('completed') || s.includes('success') || s.includes('✓'))
|
|
37
|
+
return exports.THEME.success;
|
|
38
|
+
if (s.includes('ready'))
|
|
39
|
+
return exports.THEME.success;
|
|
40
|
+
if (s.includes('stopped') || s.includes('cancel'))
|
|
41
|
+
return exports.THEME.muted;
|
|
42
|
+
if (s.includes('thinking') || s.includes('plan'))
|
|
43
|
+
return exports.THEME.info;
|
|
44
|
+
if (s.includes('run') || s.includes('exec'))
|
|
45
|
+
return exports.THEME.accent;
|
|
46
|
+
return exports.THEME.info;
|
|
47
|
+
}
|
|
48
|
+
/** Predefined spinner frame sets. */
|
|
49
|
+
exports.SPINNERS = {
|
|
50
|
+
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
|
51
|
+
line: ['─', '╌', '╍', '═', '╍', '╌'],
|
|
52
|
+
arrows: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
|
|
53
|
+
pulse: ['●', '◐', '◑', '○'],
|
|
54
|
+
};
|
|
55
|
+
/** A frame-advancing spinner (call frame() on each render tick). */
|
|
56
|
+
class Spinner {
|
|
57
|
+
frames;
|
|
58
|
+
index = 0;
|
|
59
|
+
ticks = 0;
|
|
60
|
+
constructor(frames = exports.SPINNERS.dots) {
|
|
61
|
+
this.frames = frames;
|
|
62
|
+
}
|
|
63
|
+
/** Advance and return the current frame. */
|
|
64
|
+
frame() {
|
|
65
|
+
// Advance roughly every 3rd call for a calm ~1.5s cycle at 50ms ticks
|
|
66
|
+
this.ticks++;
|
|
67
|
+
if (this.ticks % 3 === 0)
|
|
68
|
+
this.index = (this.index + 1) % this.frames.length;
|
|
69
|
+
return this.frames[this.index];
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.index = 0;
|
|
73
|
+
this.ticks = 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.Spinner = Spinner;
|
package/package.json
CHANGED