agen-vektor 0.3.4 → 0.3.5
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/dist/cli/keyboard.js +14 -4
- package/dist/tui/app.js +13 -27
- package/dist/tui/chat.js +29 -33
- package/dist/tui/components.js +15 -13
- package/dist/tui/input.js +3 -1
- package/dist/tui/statusbar.js +16 -34
- package/dist/utils/terminal.js +42 -11
- package/package.json +2 -2
package/dist/cli/keyboard.js
CHANGED
|
@@ -90,8 +90,7 @@ function parseKey(data) {
|
|
|
90
90
|
const parts = s.slice(i + 3, e).split(';').map(Number);
|
|
91
91
|
if (parts.length >= 3) {
|
|
92
92
|
const [b, x, y] = parts;
|
|
93
|
-
|
|
94
|
-
events.push({ name: 'mouse', kind, x, y });
|
|
93
|
+
events.push({ name: 'mouse', kind: mouseKind(b ?? 0), x, y });
|
|
95
94
|
}
|
|
96
95
|
i = e + 1;
|
|
97
96
|
continue;
|
|
@@ -113,10 +112,9 @@ function parseKey(data) {
|
|
|
113
112
|
// X10-style mouse
|
|
114
113
|
const m = seq.match(/^<(\d+);(\d+);(\d+)([Mm])$/);
|
|
115
114
|
if (m) {
|
|
116
|
-
const b = Number(m[1]);
|
|
117
115
|
events.push({
|
|
118
116
|
name: 'mouse',
|
|
119
|
-
kind:
|
|
117
|
+
kind: mouseKind(Number(m[1])),
|
|
120
118
|
x: Number(m[2]),
|
|
121
119
|
y: Number(m[3]),
|
|
122
120
|
});
|
|
@@ -251,6 +249,18 @@ function parseKey(data) {
|
|
|
251
249
|
}
|
|
252
250
|
return events;
|
|
253
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Map an SGR/X10 mouse button code to a semantic kind.
|
|
254
|
+
* Wheel-up is button 64, wheel-down is 65; modifier bits (4 shift, 8 meta,
|
|
255
|
+
* 16 ctrl) may be OR-ed in, so bit tests must not be exact equality.
|
|
256
|
+
*/
|
|
257
|
+
function mouseKind(b) {
|
|
258
|
+
if ((b & 64) === 64 && (b & 1) === 0)
|
|
259
|
+
return 'scroll_up';
|
|
260
|
+
if ((b & 65) === 65)
|
|
261
|
+
return 'scroll_down';
|
|
262
|
+
return 'click';
|
|
263
|
+
}
|
|
254
264
|
/**
|
|
255
265
|
* Enter raw mode. Returns a cleanup function.
|
|
256
266
|
* Works with process.stdin on Linux, macOS, and Termux.
|
package/dist/tui/app.js
CHANGED
|
@@ -123,7 +123,8 @@ class App {
|
|
|
123
123
|
}
|
|
124
124
|
switch (ev.name) {
|
|
125
125
|
case 'char':
|
|
126
|
-
|
|
126
|
+
// '?' opens help only on an empty input, so questions can be typed
|
|
127
|
+
if (ev.char === '?' && this.input.value.length === 0)
|
|
127
128
|
this.modal = { type: 'help' };
|
|
128
129
|
else if (ev.char)
|
|
129
130
|
this.input.insert(ev.char);
|
|
@@ -710,7 +711,7 @@ class App {
|
|
|
710
711
|
return (0, terminal_1.getTerminalSize)().cols;
|
|
711
712
|
}
|
|
712
713
|
chatHeight() {
|
|
713
|
-
return Math.max(1, (0, terminal_1.getTerminalSize)().rows -
|
|
714
|
+
return Math.max(1, (0, terminal_1.getTerminalSize)().rows - 6);
|
|
714
715
|
}
|
|
715
716
|
render() {
|
|
716
717
|
const { rows, cols } = (0, terminal_1.getTerminalSize)();
|
|
@@ -725,49 +726,34 @@ class App {
|
|
|
725
726
|
spinner: this.spinner.frame(),
|
|
726
727
|
running: this.running,
|
|
727
728
|
};
|
|
728
|
-
const frame = theme_1.THEME.borderBright;
|
|
729
|
-
const innerW = Math.max(4, cols - 2);
|
|
730
|
-
// Narrow terminals (mobile portrait) fall back to ASCII borders.
|
|
731
|
-
const wide = cols >= 60;
|
|
732
|
-
const bl = wide ? terminal_1.BOX.bl : '+';
|
|
733
|
-
const br = wide ? terminal_1.BOX.br : '+';
|
|
734
|
-
const hz = wide ? terminal_1.BOX.h : '-';
|
|
735
|
-
const vr = wide ? terminal_1.BOX.v : '|';
|
|
736
|
-
const trtee = wide ? terminal_1.BOX.teeRight : '+';
|
|
737
|
-
const tltee = wide ? terminal_1.BOX.teeLeft : '+';
|
|
738
729
|
const parts = [];
|
|
739
|
-
// Row 1-2:
|
|
730
|
+
// Row 1-2: header + project line
|
|
740
731
|
parts.push((0, terminal_1.cursorTo)(1, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderHeader)(cols, info));
|
|
741
732
|
parts.push((0, terminal_1.cursorTo)(2, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderProjectBar)(cols, info));
|
|
742
733
|
// Row 3: separator
|
|
743
|
-
parts.push((0, terminal_1.cursorTo)(3, 1) + terminal_1.ANSI.clearLineEnd +
|
|
744
|
-
// Chat area (rows 4..) —
|
|
734
|
+
parts.push((0, terminal_1.cursorTo)(3, 1) + terminal_1.ANSI.clearLineEnd + theme_1.THEME.border + terminal_1.BOX.h.repeat(Math.min(cols, 120)) + theme_1.THEME.reset);
|
|
735
|
+
// Chat area (rows 4..) — clean Freebuff-style message blocks
|
|
745
736
|
const chatH = this.chatHeight();
|
|
746
|
-
const
|
|
747
|
-
const all = (0, chat_1.renderMessages)(this.messages, chatW);
|
|
737
|
+
const all = (0, chat_1.renderMessages)(this.messages, cols);
|
|
748
738
|
const maxScroll = Math.max(0, all.length - chatH);
|
|
749
739
|
this.scroll = Math.min(this.scroll, maxScroll);
|
|
750
740
|
const visible = all.slice(this.scroll, this.scroll + chatH);
|
|
751
741
|
const chatTop = 4;
|
|
752
|
-
const empty = ' '.repeat(innerW);
|
|
753
742
|
for (let i = 0; i < chatH; i++) {
|
|
754
|
-
const content = visible[i] !== undefined ? visible[i] :
|
|
755
|
-
parts.push((0, terminal_1.cursorTo)(chatTop + i, 1) + terminal_1.ANSI.clearLineEnd +
|
|
743
|
+
const content = visible[i] !== undefined ? visible[i] : '';
|
|
744
|
+
parts.push((0, terminal_1.cursorTo)(chatTop + i, 1) + terminal_1.ANSI.clearLineEnd + content);
|
|
756
745
|
}
|
|
757
746
|
// Separator between chat and input
|
|
758
747
|
const sepRow = chatTop + chatH;
|
|
759
|
-
parts.push((0, terminal_1.cursorTo)(sepRow, 1) + terminal_1.ANSI.clearLineEnd +
|
|
748
|
+
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);
|
|
760
749
|
// Input row
|
|
761
750
|
const inputRow = sepRow + 1;
|
|
762
|
-
const rendered = this.input.render(
|
|
763
|
-
parts.push((0, terminal_1.cursorTo)(inputRow, 1) + terminal_1.ANSI.clearLineEnd +
|
|
764
|
-
const cursorCol = rendered.cursorCol +
|
|
751
|
+
const rendered = this.input.render(cols);
|
|
752
|
+
parts.push((0, terminal_1.cursorTo)(inputRow, 1) + terminal_1.ANSI.clearLineEnd + rendered.line);
|
|
753
|
+
const cursorCol = rendered.cursorCol + 1;
|
|
765
754
|
// Status bar row
|
|
766
755
|
const statusRow = inputRow + 1;
|
|
767
756
|
parts.push((0, terminal_1.cursorTo)(statusRow, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderStatusBar)(cols, info));
|
|
768
|
-
// Bottom border
|
|
769
|
-
const bottomRow = statusRow + 1;
|
|
770
|
-
parts.push((0, terminal_1.cursorTo)(bottomRow, 1) + terminal_1.ANSI.clearLineEnd + frame + bl + hz.repeat(innerW) + br + theme_1.THEME.reset);
|
|
771
757
|
// Modal overlay
|
|
772
758
|
if (this.modal.type !== 'none') {
|
|
773
759
|
const overlay = this.renderModal(rows, cols);
|
package/dist/tui/chat.js
CHANGED
|
@@ -4,16 +4,25 @@ exports.renderMessages = renderMessages;
|
|
|
4
4
|
exports.defaultScroll = defaultScroll;
|
|
5
5
|
exports.clampScroll = clampScroll;
|
|
6
6
|
/**
|
|
7
|
-
* Chat view — renders conversation messages
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* Chat view — renders conversation messages the Freebuff way:
|
|
8
|
+
* clean plain-text blocks with a small colored label row. User messages
|
|
9
|
+
* are right-aligned (chat-style bubble), everything else left-aligned.
|
|
10
|
+
* No boxes or frames — stays aligned on narrow/mobile terminals.
|
|
11
|
+
*
|
|
12
|
+
* ● VectorHead · model
|
|
13
|
+
* Inspecting project...
|
|
14
|
+
*
|
|
15
|
+
* ● YOU
|
|
16
|
+
* perbaiki error auth
|
|
17
|
+
*
|
|
18
|
+
* ⚙ shell · $ npm test
|
|
14
19
|
*/
|
|
15
20
|
const theme_1 = require("./theme");
|
|
16
21
|
const terminal_1 = require("../utils/terminal");
|
|
22
|
+
/** Right-align a string to a fixed visible width (leading padding). */
|
|
23
|
+
function padLeft(str, width) {
|
|
24
|
+
return ' '.repeat(Math.max(0, width - (0, terminal_1.visibleWidth)(str))) + str;
|
|
25
|
+
}
|
|
17
26
|
const KIND_STYLE = {
|
|
18
27
|
user: { prefix: 'YOU', color: theme_1.THEME.gold },
|
|
19
28
|
assistant: { prefix: 'VectorHead', color: theme_1.THEME.accent },
|
|
@@ -23,44 +32,31 @@ const KIND_STYLE = {
|
|
|
23
32
|
success: { prefix: '✓', color: theme_1.THEME.success },
|
|
24
33
|
warning: { prefix: '⚠', color: theme_1.THEME.warn },
|
|
25
34
|
};
|
|
26
|
-
/** Render one message as a
|
|
27
|
-
function
|
|
35
|
+
/** Render one message as a label row + wrapped content (user right-aligned). */
|
|
36
|
+
function renderMessage(m, width) {
|
|
28
37
|
const style = KIND_STYLE[m.kind];
|
|
29
38
|
const prefix = m.prefix || style.prefix;
|
|
30
|
-
const dot = m.streaming ? `${theme_1.THEME.warn}◐${theme_1.THEME.reset}
|
|
31
|
-
const title = `${dot}${theme_1.THEME.bold}${style.color}${prefix}${theme_1.THEME.reset}`;
|
|
39
|
+
const dot = m.streaming ? `${theme_1.THEME.warn}◐${theme_1.THEME.reset}` : `${style.color}●${theme_1.THEME.reset}`;
|
|
32
40
|
const meta = m.meta ? ` ${theme_1.THEME.dim}${m.meta}${theme_1.THEME.reset}` : '';
|
|
33
41
|
const toolName = m.tool ? ` ${theme_1.THEME.warn}${m.tool}${theme_1.THEME.reset}` : '';
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const tr = ascii ? '+' : terminal_1.BOX.tr;
|
|
40
|
-
const bl = ascii ? '+' : terminal_1.BOX.bl;
|
|
41
|
-
const br = ascii ? '+' : terminal_1.BOX.br;
|
|
42
|
-
const hz = ascii ? '-' : terminal_1.BOX.h;
|
|
43
|
-
const vr = ascii ? '|' : terminal_1.BOX.v;
|
|
44
|
-
const out = [];
|
|
45
|
-
// Top border
|
|
46
|
-
out.push(border + tl + hz.repeat(inner + 2) + tr + theme_1.THEME.reset);
|
|
47
|
-
// Title row: ● prefix · model / tool badge (truncated to fit narrow widths)
|
|
48
|
-
out.push(`${border}${vr}${theme_1.THEME.reset} ` + (0, terminal_1.pad)((0, terminal_1.truncate)(title + toolName + meta, inner), inner) + ` ${border}${vr}${theme_1.THEME.reset}`);
|
|
49
|
-
// Content rows
|
|
42
|
+
const rightAlign = m.kind === 'user';
|
|
43
|
+
// Label row: ● Label · tool · model (right-aligned for user, truncated to fit narrow widths)
|
|
44
|
+
const label = `${dot} ${theme_1.THEME.bold}${style.color}${prefix}${theme_1.THEME.reset}${toolName}${meta}`;
|
|
45
|
+
const out = [rightAlign ? padLeft((0, terminal_1.truncate)(label, width), width) : (0, terminal_1.truncate)(label, width)];
|
|
46
|
+
// Content rows — wrapped to fit; user wraps the full width for a flush right edge
|
|
50
47
|
const body = m.content || '(no content)';
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
const wrapW = rightAlign ? width : Math.max(4, width - 2);
|
|
49
|
+
for (const line of (0, terminal_1.wrapText)(body, wrapW)) {
|
|
50
|
+
out.push(rightAlign ? padLeft(line, width) : ` ${line}`);
|
|
53
51
|
}
|
|
54
|
-
//
|
|
55
|
-
out.push(border + bl + hz.repeat(inner + 2) + br + theme_1.THEME.reset);
|
|
56
|
-
out.push('');
|
|
52
|
+
out.push(''); // blank separator between messages
|
|
57
53
|
return out;
|
|
58
54
|
}
|
|
59
55
|
/** Precompute the full list of rendered lines for all messages. */
|
|
60
56
|
function renderMessages(messages, width) {
|
|
61
57
|
const out = [];
|
|
62
58
|
for (const m of messages) {
|
|
63
|
-
out.push(...
|
|
59
|
+
out.push(...renderMessage(m, width));
|
|
64
60
|
}
|
|
65
61
|
return out;
|
|
66
62
|
}
|
package/dist/tui/components.js
CHANGED
|
@@ -19,24 +19,24 @@ function renderModal(opts, rows, cols) {
|
|
|
19
19
|
const left = Math.max(1, Math.floor((cols - innerWidth - 2) / 2));
|
|
20
20
|
const borderColor = opts.danger ? theme_1.THEME.error : theme_1.THEME.borderBright;
|
|
21
21
|
const lines = [];
|
|
22
|
-
const topBorder = borderColor + terminal_1.BOX.tl + terminal_1.BOX.h.repeat(innerWidth) + terminal_1.BOX.tr;
|
|
23
22
|
const bottomBorder = borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(innerWidth) + terminal_1.BOX.br;
|
|
24
|
-
//
|
|
25
|
-
|
|
23
|
+
const contentW = innerWidth - 2; // visible area between the side borders
|
|
24
|
+
// Title row: ╭─ Title ────────────────╮ (exactly innerWidth+2 wide)
|
|
25
|
+
const t = (0, terminal_1.truncate)(title, contentW);
|
|
26
26
|
lines.push((0, terminal_1.cursorTo)(top, left) +
|
|
27
|
-
|
|
27
|
+
borderColor + terminal_1.BOX.tl + terminal_1.BOX.h +
|
|
28
28
|
theme_1.THEME.bold + theme_1.THEME.textBright + t + theme_1.THEME.reset + borderColor +
|
|
29
|
-
|
|
29
|
+
terminal_1.BOX.h.repeat(Math.max(0, innerWidth - 1 - (0, terminal_1.visibleWidth)(t))) + terminal_1.BOX.tr);
|
|
30
30
|
const maxBody = height - footerLines.length - 2;
|
|
31
31
|
for (let i = 0; i < maxBody; i++) {
|
|
32
32
|
const content = bodyLines[i] ?? '';
|
|
33
|
-
const lineStr = (0, terminal_1.pad)(content,
|
|
33
|
+
const lineStr = (0, terminal_1.pad)((0, terminal_1.truncate)(content, contentW), contentW);
|
|
34
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);
|
|
35
35
|
}
|
|
36
36
|
for (let i = 0; i < footerLines.length; i++) {
|
|
37
37
|
const row = top + 1 + maxBody + i;
|
|
38
38
|
const content = footerLines[i] ?? '';
|
|
39
|
-
const lineStr = (0, terminal_1.pad)(content,
|
|
39
|
+
const lineStr = (0, terminal_1.pad)((0, terminal_1.truncate)(content, contentW), contentW);
|
|
40
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);
|
|
41
41
|
}
|
|
42
42
|
lines.push((0, terminal_1.cursorTo)(top + height - 1, left) + bottomBorder);
|
|
@@ -52,7 +52,9 @@ function renderSelector(opts) {
|
|
|
52
52
|
const left = Math.max(1, Math.floor((opts.cols - innerWidth - 2) / 2));
|
|
53
53
|
const lines = [];
|
|
54
54
|
const borderColor = terminal_1.ANSI.cyan;
|
|
55
|
-
const
|
|
55
|
+
const contentW = innerWidth - 2;
|
|
56
|
+
// Title row: ╭── Title ────────────╮ (exactly innerWidth+2 wide)
|
|
57
|
+
const t = (0, terminal_1.truncate)(opts.title, contentW);
|
|
56
58
|
lines.push((0, terminal_1.cursorTo)(top, left) +
|
|
57
59
|
borderColor + terminal_1.BOX.tl + terminal_1.BOX.h.repeat(2) +
|
|
58
60
|
terminal_1.ANSI.bold + terminal_1.ANSI.white + t + terminal_1.ANSI.reset + borderColor +
|
|
@@ -67,19 +69,19 @@ function renderSelector(opts) {
|
|
|
67
69
|
const label = marker + option.label;
|
|
68
70
|
const hint = option.hint ? terminal_1.ANSI.dim + ' — ' + option.hint + terminal_1.ANSI.reset : '';
|
|
69
71
|
if (idx === opts.selected) {
|
|
70
|
-
content = terminal_1.ANSI.bold + terminal_1.ANSI.cyan + (0, terminal_1.
|
|
72
|
+
content = terminal_1.ANSI.bold + terminal_1.ANSI.cyan + (0, terminal_1.truncate)(label, contentW - (0, terminal_1.visibleWidth)(hint)) + terminal_1.ANSI.reset + hint;
|
|
71
73
|
}
|
|
72
74
|
else {
|
|
73
|
-
content = (0, terminal_1.
|
|
75
|
+
content = (0, terminal_1.truncate)(label, contentW - (0, terminal_1.visibleWidth)(hint)) + hint;
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
77
|
-
content = '
|
|
79
|
+
content = '';
|
|
78
80
|
}
|
|
79
|
-
lines.push((0, terminal_1.cursorTo)(row, left) + borderColor + terminal_1.BOX.v + ' ' + content +
|
|
81
|
+
lines.push((0, terminal_1.cursorTo)(row, left) + borderColor + terminal_1.BOX.v + ' ' + (0, terminal_1.pad)(content, contentW) + theme_1.THEME.reset + ' ' + terminal_1.BOX.v);
|
|
80
82
|
}
|
|
81
83
|
const footerText = opts.footer ? opts.footer : '↑↓ navigate · Enter select · Esc cancel';
|
|
82
|
-
const f = (0, terminal_1.
|
|
84
|
+
const f = (0, terminal_1.truncate)(footerText, contentW);
|
|
83
85
|
lines.push((0, terminal_1.cursorTo)(top + visible + 1, left) +
|
|
84
86
|
borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(2) + theme_1.THEME.dim + f + theme_1.THEME.reset + borderColor +
|
|
85
87
|
terminal_1.BOX.h.repeat(Math.max(0, innerWidth - 2 - (0, terminal_1.visibleWidth)(f))) + terminal_1.BOX.br);
|
package/dist/tui/input.js
CHANGED
|
@@ -107,7 +107,9 @@ class InputBox {
|
|
|
107
107
|
let start = this.cursor - Math.floor(available / 2);
|
|
108
108
|
start = Math.max(0, Math.min(start, this.text.length - available));
|
|
109
109
|
display = this.text.slice(start, start + available);
|
|
110
|
-
|
|
110
|
+
// Visible column: prefix width (2) + offset inside the window.
|
|
111
|
+
// (prefix.length is the raw ANSI byte length — must not be used here.)
|
|
112
|
+
cursorCol = prefixWidth + (this.cursor - start);
|
|
111
113
|
}
|
|
112
114
|
let suffix = '';
|
|
113
115
|
if (!this.text && this.hint) {
|
package/dist/tui/statusbar.js
CHANGED
|
@@ -4,27 +4,15 @@ exports.renderHeader = renderHeader;
|
|
|
4
4
|
exports.renderProjectBar = renderProjectBar;
|
|
5
5
|
exports.renderStatusBar = renderStatusBar;
|
|
6
6
|
/**
|
|
7
|
-
* Status bar — top header + project
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* │ Project: ~/x Mode: ask │
|
|
11
|
-
* │ ENTER Send · TAB Commands · ? Help ● ask │
|
|
7
|
+
* Status bar — top header + project line + bottom hint bar.
|
|
8
|
+
* Clean Freebuff-style lines (no frames), truncated to fit narrow/mobile
|
|
9
|
+
* terminals.
|
|
12
10
|
*/
|
|
13
11
|
const terminal_1 = require("../utils/terminal");
|
|
14
12
|
const theme_1 = require("./theme");
|
|
15
|
-
const frame = theme_1.THEME.borderBright;
|
|
16
|
-
/**
|
|
17
|
-
* Frame characters. Narrow terminals (mobile portrait, ~<60 cols) fall back
|
|
18
|
-
* to ASCII so borders never misalign on fonts without box-drawing glyphs.
|
|
19
|
-
*/
|
|
20
|
-
function frameChars(width) {
|
|
21
|
-
if (width < 60)
|
|
22
|
-
return { tl: '+', tr: '+', bl: '+', br: '+', v: '|' };
|
|
23
|
-
return { tl: terminal_1.BOX.tl, tr: terminal_1.BOX.tr, bl: terminal_1.BOX.bl, br: terminal_1.BOX.br, v: terminal_1.BOX.v };
|
|
24
|
-
}
|
|
25
13
|
/** Split two ANSI strings across `inner` columns, truncating with … if needed. */
|
|
26
14
|
function fitTwo(left, right, inner) {
|
|
27
|
-
const avail = inner
|
|
15
|
+
const avail = Math.max(4, inner);
|
|
28
16
|
const l = (0, terminal_1.stripAnsi)(left).length;
|
|
29
17
|
const r = (0, terminal_1.stripAnsi)(right).length;
|
|
30
18
|
if (l + r <= avail)
|
|
@@ -33,7 +21,7 @@ function fitTwo(left, right, inner) {
|
|
|
33
21
|
const maxR = Math.max(0, avail - maxL);
|
|
34
22
|
return { left: (0, terminal_1.truncate)(left, maxL), right: (0, terminal_1.truncate)(right, maxR) };
|
|
35
23
|
}
|
|
36
|
-
/** Render the top
|
|
24
|
+
/** Render the top header line: VectorHead ● status ... provider · model */
|
|
37
25
|
function renderHeader(width, info) {
|
|
38
26
|
const brand = `${theme_1.THEME.bold}${theme_1.THEME.accent}VectorHead${theme_1.THEME.reset}`;
|
|
39
27
|
const dot = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
@@ -42,24 +30,20 @@ function renderHeader(width, info) {
|
|
|
42
30
|
const provider = `${theme_1.THEME.dim}${info.provider}${theme_1.THEME.reset}`;
|
|
43
31
|
const model = `${theme_1.THEME.accentDim}${info.model}${theme_1.THEME.reset}`;
|
|
44
32
|
const right = `${provider} · ${model}`;
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const c = frameChars(width);
|
|
49
|
-
return `${frame}${c.tl}${theme_1.THEME.reset} ${fit.left}${' '.repeat(mid)}${fit.right} ${frame}${c.tr}${theme_1.THEME.reset}`;
|
|
33
|
+
const fit = fitTwo(left, right, width);
|
|
34
|
+
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
35
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
50
36
|
}
|
|
51
|
-
/** Render the
|
|
37
|
+
/** Render the project line: Project: <path> ... Mode: ask */
|
|
52
38
|
function renderProjectBar(width, info) {
|
|
53
39
|
const label = `${theme_1.THEME.dim}Project:${theme_1.THEME.reset} ${theme_1.THEME.text}${info.project}${theme_1.THEME.reset}`;
|
|
54
40
|
const modeColor = info.mode === 'yolo' ? theme_1.THEME.warn : theme_1.THEME.muted;
|
|
55
41
|
const mode = `${theme_1.THEME.dim}Mode:${theme_1.THEME.reset} ${modeColor}${info.mode}${theme_1.THEME.reset}`;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const c = frameChars(width);
|
|
60
|
-
return `${frame}${c.v}${theme_1.THEME.reset} ${fit.left}${' '.repeat(mid)}${fit.right} ${frame}${c.v}${theme_1.THEME.reset}`;
|
|
42
|
+
const fit = fitTwo(label, mode, width);
|
|
43
|
+
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
44
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
61
45
|
}
|
|
62
|
-
/** Render the
|
|
46
|
+
/** Render the bottom hint/status bar with live spinner. */
|
|
63
47
|
function renderStatusBar(width, info) {
|
|
64
48
|
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}`;
|
|
65
49
|
let right = '';
|
|
@@ -69,9 +53,7 @@ function renderStatusBar(width, info) {
|
|
|
69
53
|
const conn = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
70
54
|
const mode = info.mode === 'yolo' ? `${theme_1.THEME.warn}YOLO${theme_1.THEME.reset}` : `${theme_1.THEME.muted}ask${theme_1.THEME.reset}`;
|
|
71
55
|
right += `${conn} ${mode}`;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const c = frameChars(width);
|
|
76
|
-
return `${frame}${c.v}${theme_1.THEME.reset} ${fit.left}${' '.repeat(mid)}${fit.right} ${frame}${c.v}${theme_1.THEME.reset}`;
|
|
56
|
+
const fit = fitTwo(left, right, width);
|
|
57
|
+
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
58
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
77
59
|
}
|
package/dist/utils/terminal.js
CHANGED
|
@@ -91,7 +91,8 @@ function getTerminalSize() {
|
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
93
|
* Wrap text to a given width, respecting ANSI escape sequences so they
|
|
94
|
-
* are not counted toward the visible length.
|
|
94
|
+
* are not counted toward the visible length. Surrogate pairs (emoji) are
|
|
95
|
+
* counted as a single 2-column character.
|
|
95
96
|
*/
|
|
96
97
|
function wrapText(text, width) {
|
|
97
98
|
if (width <= 0)
|
|
@@ -129,14 +130,16 @@ function wrapText(text, width) {
|
|
|
129
130
|
i++;
|
|
130
131
|
continue;
|
|
131
132
|
}
|
|
132
|
-
//
|
|
133
|
-
const
|
|
133
|
+
// One full code point — surrogate pairs count as a single 2-column char.
|
|
134
|
+
const cp = text.codePointAt(i);
|
|
135
|
+
const unit = String.fromCodePoint(cp);
|
|
136
|
+
const w = cp > 0x2fff ? 2 : 1;
|
|
134
137
|
if (visibleLen + w > width && visibleLen > 0) {
|
|
135
138
|
flushLine();
|
|
136
139
|
}
|
|
137
|
-
current +=
|
|
140
|
+
current += unit;
|
|
138
141
|
visibleLen += w;
|
|
139
|
-
i
|
|
142
|
+
i += unit.length;
|
|
140
143
|
}
|
|
141
144
|
flushLine();
|
|
142
145
|
return lines;
|
|
@@ -145,11 +148,11 @@ function wrapText(text, width) {
|
|
|
145
148
|
function stripAnsi(text) {
|
|
146
149
|
return text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
147
150
|
}
|
|
148
|
-
/** Visible width of a string without ANSI codes. */
|
|
151
|
+
/** Visible width of a string without ANSI codes (emoji counted as 2). */
|
|
149
152
|
function visibleWidth(text) {
|
|
150
153
|
let w = 0;
|
|
151
154
|
for (const ch of stripAnsi(text)) {
|
|
152
|
-
w += ch.
|
|
155
|
+
w += ch.codePointAt(0) > 0x2fff ? 2 : 1;
|
|
153
156
|
}
|
|
154
157
|
return w;
|
|
155
158
|
}
|
|
@@ -160,12 +163,40 @@ function pad(str, width) {
|
|
|
160
163
|
return str;
|
|
161
164
|
return str + ' '.repeat(width - len);
|
|
162
165
|
}
|
|
163
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Truncate a string (with ANSI) to a max visible width. ANSI sequences are
|
|
168
|
+
* copied verbatim so colors survive; when truncated, one column is reserved
|
|
169
|
+
* for '…' and a closing reset is appended so styling never bleeds out.
|
|
170
|
+
*/
|
|
164
171
|
function truncate(str, width) {
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
if (width <= 0)
|
|
173
|
+
return '';
|
|
174
|
+
if (visibleWidth(str) <= width)
|
|
167
175
|
return str;
|
|
168
|
-
|
|
176
|
+
const ESC_RE = /^\x1b\[[0-9;]*[A-Za-z]/;
|
|
177
|
+
let out = '';
|
|
178
|
+
let vis = 0;
|
|
179
|
+
let i = 0;
|
|
180
|
+
while (i < str.length && vis < width - 1) {
|
|
181
|
+
const ch = str[i];
|
|
182
|
+
if (ch === '\x1b') {
|
|
183
|
+
const m = str.slice(i).match(ESC_RE);
|
|
184
|
+
if (m) {
|
|
185
|
+
out += m[0];
|
|
186
|
+
i += m[0].length;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const cp = str.codePointAt(i);
|
|
191
|
+
const unit = String.fromCodePoint(cp);
|
|
192
|
+
const w = cp > 0x2fff ? 2 : 1;
|
|
193
|
+
if (vis + w > width - 1)
|
|
194
|
+
break; // wide char leaves no room for '…'
|
|
195
|
+
out += unit;
|
|
196
|
+
vis += w;
|
|
197
|
+
i += unit.length;
|
|
198
|
+
}
|
|
199
|
+
return out + '…' + exports.ANSI.reset;
|
|
169
200
|
}
|
|
170
201
|
/** Render a line with a trailing newline reset applied. */
|
|
171
202
|
function line(text = '') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agen-vektor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "VectorHead (agen-vektor) — AI Coding Agent CLI/TUI for Linux & Termux. Multi-provider, tool calling, session, permission system.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "tsc",
|
|
16
|
+
"build": "tsc && chmod +x dist/cli/index.js",
|
|
17
17
|
"dev": "tsx src/cli/index.ts",
|
|
18
18
|
"start": "node dist/cli/index.js",
|
|
19
19
|
"test": "tsx --test tests/*.test.ts",
|