agen-vektor 0.3.3 → 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 +17 -19
- package/dist/tui/chat.js +29 -25
- package/dist/tui/components.js +15 -13
- package/dist/tui/input.js +3 -1
- package/dist/tui/statusbar.js +26 -18
- 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);
|
|
@@ -184,6 +185,10 @@ class App {
|
|
|
184
185
|
this.scroll = 0;
|
|
185
186
|
this.addSystem('Conversation cleared.');
|
|
186
187
|
break;
|
|
188
|
+
case 'ctrl_d':
|
|
189
|
+
// EOF / Ctrl+D — another way to quit (handy on mobile where Ctrl is awkward)
|
|
190
|
+
this.exitRequested = true;
|
|
191
|
+
break;
|
|
187
192
|
case 'tab':
|
|
188
193
|
this.modal = { type: 'commands' };
|
|
189
194
|
break;
|
|
@@ -706,7 +711,7 @@ class App {
|
|
|
706
711
|
return (0, terminal_1.getTerminalSize)().cols;
|
|
707
712
|
}
|
|
708
713
|
chatHeight() {
|
|
709
|
-
return Math.max(1, (0, terminal_1.getTerminalSize)().rows -
|
|
714
|
+
return Math.max(1, (0, terminal_1.getTerminalSize)().rows - 6);
|
|
710
715
|
}
|
|
711
716
|
render() {
|
|
712
717
|
const { rows, cols } = (0, terminal_1.getTerminalSize)();
|
|
@@ -721,41 +726,34 @@ class App {
|
|
|
721
726
|
spinner: this.spinner.frame(),
|
|
722
727
|
running: this.running,
|
|
723
728
|
};
|
|
724
|
-
const frame = theme_1.THEME.borderBright;
|
|
725
|
-
const innerW = Math.max(4, cols - 2);
|
|
726
729
|
const parts = [];
|
|
727
|
-
// Row 1-2:
|
|
730
|
+
// Row 1-2: header + project line
|
|
728
731
|
parts.push((0, terminal_1.cursorTo)(1, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderHeader)(cols, info));
|
|
729
732
|
parts.push((0, terminal_1.cursorTo)(2, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderProjectBar)(cols, info));
|
|
730
733
|
// Row 3: separator
|
|
731
|
-
parts.push((0, terminal_1.cursorTo)(3, 1) + terminal_1.ANSI.clearLineEnd +
|
|
732
|
-
// 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
|
|
733
736
|
const chatH = this.chatHeight();
|
|
734
|
-
const
|
|
735
|
-
const all = (0, chat_1.renderMessages)(this.messages, chatW);
|
|
737
|
+
const all = (0, chat_1.renderMessages)(this.messages, cols);
|
|
736
738
|
const maxScroll = Math.max(0, all.length - chatH);
|
|
737
739
|
this.scroll = Math.min(this.scroll, maxScroll);
|
|
738
740
|
const visible = all.slice(this.scroll, this.scroll + chatH);
|
|
739
741
|
const chatTop = 4;
|
|
740
|
-
const empty = ' '.repeat(innerW);
|
|
741
742
|
for (let i = 0; i < chatH; i++) {
|
|
742
|
-
const content = visible[i] !== undefined ? visible[i] :
|
|
743
|
-
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);
|
|
744
745
|
}
|
|
745
746
|
// Separator between chat and input
|
|
746
747
|
const sepRow = chatTop + chatH;
|
|
747
|
-
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);
|
|
748
749
|
// Input row
|
|
749
750
|
const inputRow = sepRow + 1;
|
|
750
|
-
const rendered = this.input.render(
|
|
751
|
-
parts.push((0, terminal_1.cursorTo)(inputRow, 1) + terminal_1.ANSI.clearLineEnd +
|
|
752
|
-
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;
|
|
753
754
|
// Status bar row
|
|
754
755
|
const statusRow = inputRow + 1;
|
|
755
756
|
parts.push((0, terminal_1.cursorTo)(statusRow, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderStatusBar)(cols, info));
|
|
756
|
-
// Bottom border
|
|
757
|
-
const bottomRow = statusRow + 1;
|
|
758
|
-
parts.push((0, terminal_1.cursorTo)(bottomRow, 1) + terminal_1.ANSI.clearLineEnd + frame + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(innerW) + terminal_1.BOX.br + theme_1.THEME.reset);
|
|
759
757
|
// Modal overlay
|
|
760
758
|
if (this.modal.type !== 'none') {
|
|
761
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,36 +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
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Title row: ● prefix · model / tool badge
|
|
40
|
-
out.push(`${border}${terminal_1.BOX.v}${theme_1.THEME.reset} ` + (0, terminal_1.pad)(title + toolName + meta, inner) + ` ${border}${terminal_1.BOX.v}${theme_1.THEME.reset}`);
|
|
41
|
-
// 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
|
|
42
47
|
const body = m.content || '(no content)';
|
|
43
|
-
|
|
44
|
-
|
|
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}`);
|
|
45
51
|
}
|
|
46
|
-
//
|
|
47
|
-
out.push(border + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(inner + 2) + terminal_1.BOX.br + theme_1.THEME.reset);
|
|
48
|
-
out.push('');
|
|
52
|
+
out.push(''); // blank separator between messages
|
|
49
53
|
return out;
|
|
50
54
|
}
|
|
51
55
|
/** Precompute the full list of rendered lines for all messages. */
|
|
52
56
|
function renderMessages(messages, width) {
|
|
53
57
|
const out = [];
|
|
54
58
|
for (const m of messages) {
|
|
55
|
-
out.push(...
|
|
59
|
+
out.push(...renderMessage(m, width));
|
|
56
60
|
}
|
|
57
61
|
return out;
|
|
58
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,16 +4,24 @@ 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
|
-
|
|
16
|
-
|
|
13
|
+
/** Split two ANSI strings across `inner` columns, truncating with … if needed. */
|
|
14
|
+
function fitTwo(left, right, inner) {
|
|
15
|
+
const avail = Math.max(4, inner);
|
|
16
|
+
const l = (0, terminal_1.stripAnsi)(left).length;
|
|
17
|
+
const r = (0, terminal_1.stripAnsi)(right).length;
|
|
18
|
+
if (l + r <= avail)
|
|
19
|
+
return { left, right };
|
|
20
|
+
const maxL = Math.max(0, Math.floor((avail * l) / (l + r)));
|
|
21
|
+
const maxR = Math.max(0, avail - maxL);
|
|
22
|
+
return { left: (0, terminal_1.truncate)(left, maxL), right: (0, terminal_1.truncate)(right, maxR) };
|
|
23
|
+
}
|
|
24
|
+
/** Render the top header line: VectorHead ● status ... provider · model */
|
|
17
25
|
function renderHeader(width, info) {
|
|
18
26
|
const brand = `${theme_1.THEME.bold}${theme_1.THEME.accent}VectorHead${theme_1.THEME.reset}`;
|
|
19
27
|
const dot = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
@@ -22,20 +30,20 @@ function renderHeader(width, info) {
|
|
|
22
30
|
const provider = `${theme_1.THEME.dim}${info.provider}${theme_1.THEME.reset}`;
|
|
23
31
|
const model = `${theme_1.THEME.accentDim}${info.model}${theme_1.THEME.reset}`;
|
|
24
32
|
const right = `${provider} · ${model}`;
|
|
25
|
-
const
|
|
26
|
-
const mid = Math.max(1,
|
|
27
|
-
return
|
|
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;
|
|
28
36
|
}
|
|
29
|
-
/** Render the
|
|
37
|
+
/** Render the project line: Project: <path> ... Mode: ask */
|
|
30
38
|
function renderProjectBar(width, info) {
|
|
31
39
|
const label = `${theme_1.THEME.dim}Project:${theme_1.THEME.reset} ${theme_1.THEME.text}${info.project}${theme_1.THEME.reset}`;
|
|
32
40
|
const modeColor = info.mode === 'yolo' ? theme_1.THEME.warn : theme_1.THEME.muted;
|
|
33
41
|
const mode = `${theme_1.THEME.dim}Mode:${theme_1.THEME.reset} ${modeColor}${info.mode}${theme_1.THEME.reset}`;
|
|
34
|
-
const
|
|
35
|
-
const mid = Math.max(1,
|
|
36
|
-
return
|
|
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;
|
|
37
45
|
}
|
|
38
|
-
/** Render the
|
|
46
|
+
/** Render the bottom hint/status bar with live spinner. */
|
|
39
47
|
function renderStatusBar(width, info) {
|
|
40
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}`;
|
|
41
49
|
let right = '';
|
|
@@ -45,7 +53,7 @@ function renderStatusBar(width, info) {
|
|
|
45
53
|
const conn = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
46
54
|
const mode = info.mode === 'yolo' ? `${theme_1.THEME.warn}YOLO${theme_1.THEME.reset}` : `${theme_1.THEME.muted}ask${theme_1.THEME.reset}`;
|
|
47
55
|
right += `${conn} ${mode}`;
|
|
48
|
-
const
|
|
49
|
-
const mid = Math.max(1,
|
|
50
|
-
return
|
|
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;
|
|
51
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",
|