agen-vektor 0.3.4 → 0.3.6
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 -28
- package/dist/tui/chat.js +128 -47
- package/dist/tui/components.js +17 -15
- package/dist/tui/input.js +5 -2
- package/dist/tui/statusbar.js +31 -41
- package/dist/tui/theme.js +20 -16
- package/dist/utils/terminal.js +61 -12
- 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
|
@@ -78,6 +78,7 @@ class App {
|
|
|
78
78
|
planToExecute = null;
|
|
79
79
|
abortController = null;
|
|
80
80
|
spinner = new theme_1.Spinner();
|
|
81
|
+
runStart = null;
|
|
81
82
|
constructor(agent, opts) {
|
|
82
83
|
this.agent = agent;
|
|
83
84
|
this.opts = opts;
|
|
@@ -90,7 +91,6 @@ class App {
|
|
|
90
91
|
start() {
|
|
91
92
|
this.connected = true;
|
|
92
93
|
this.status = 'Ready';
|
|
93
|
-
this.addSystem('VectorHead ready. Type a request or press ? for help.');
|
|
94
94
|
if (this.opts.continueSession && this.sessionName) {
|
|
95
95
|
const sess = (0, session_1.loadSession)(this.sessionName);
|
|
96
96
|
if (sess) {
|
|
@@ -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);
|
|
@@ -240,6 +241,7 @@ class App {
|
|
|
240
241
|
this.statusColor = (0, theme_1.statusColor)('Thinking');
|
|
241
242
|
this.spinner.reset();
|
|
242
243
|
this.abortController = new AbortController();
|
|
244
|
+
this.runStart = Date.now();
|
|
243
245
|
// Show plan first if requested
|
|
244
246
|
if (this.opts.showPlan) {
|
|
245
247
|
this.status = 'Planning…';
|
|
@@ -355,6 +357,7 @@ class App {
|
|
|
355
357
|
m.content = '(no response)';
|
|
356
358
|
}
|
|
357
359
|
this.running = false;
|
|
360
|
+
this.runStart = null;
|
|
358
361
|
if (this.statusColor !== terminal_1.ANSI.red) {
|
|
359
362
|
this.status = 'Ready';
|
|
360
363
|
this.statusColor = terminal_1.ANSI.green;
|
|
@@ -710,7 +713,7 @@ class App {
|
|
|
710
713
|
return (0, terminal_1.getTerminalSize)().cols;
|
|
711
714
|
}
|
|
712
715
|
chatHeight() {
|
|
713
|
-
return Math.max(1, (0, terminal_1.getTerminalSize)().rows -
|
|
716
|
+
return Math.max(1, (0, terminal_1.getTerminalSize)().rows - 6);
|
|
714
717
|
}
|
|
715
718
|
render() {
|
|
716
719
|
const { rows, cols } = (0, terminal_1.getTerminalSize)();
|
|
@@ -724,50 +727,36 @@ class App {
|
|
|
724
727
|
mode: this.agent.config.permissionMode,
|
|
725
728
|
spinner: this.spinner.frame(),
|
|
726
729
|
running: this.running,
|
|
730
|
+
elapsed: this.running && this.runStart !== null ? Math.floor((Date.now() - this.runStart) / 1000) : undefined,
|
|
727
731
|
};
|
|
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
732
|
const parts = [];
|
|
739
|
-
// Row 1-2:
|
|
733
|
+
// Row 1-2: header + project line
|
|
740
734
|
parts.push((0, terminal_1.cursorTo)(1, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderHeader)(cols, info));
|
|
741
735
|
parts.push((0, terminal_1.cursorTo)(2, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderProjectBar)(cols, info));
|
|
742
736
|
// Row 3: separator
|
|
743
|
-
parts.push((0, terminal_1.cursorTo)(3, 1) + terminal_1.ANSI.clearLineEnd +
|
|
744
|
-
// Chat area (rows 4..) —
|
|
737
|
+
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);
|
|
738
|
+
// Chat area (rows 4..) — Freebuff-style message blocks (welcome screen when empty)
|
|
745
739
|
const chatH = this.chatHeight();
|
|
746
|
-
const
|
|
747
|
-
const all = (0, chat_1.renderMessages)(this.messages, chatW);
|
|
740
|
+
const all = this.messages.length === 0 ? (0, chat_1.renderWelcome)(cols, chatH, this.agent.cwd) : (0, chat_1.renderMessages)(this.messages, cols);
|
|
748
741
|
const maxScroll = Math.max(0, all.length - chatH);
|
|
749
742
|
this.scroll = Math.min(this.scroll, maxScroll);
|
|
750
743
|
const visible = all.slice(this.scroll, this.scroll + chatH);
|
|
751
744
|
const chatTop = 4;
|
|
752
|
-
const empty = ' '.repeat(innerW);
|
|
753
745
|
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 +
|
|
746
|
+
const content = visible[i] !== undefined ? visible[i] : '';
|
|
747
|
+
parts.push((0, terminal_1.cursorTo)(chatTop + i, 1) + terminal_1.ANSI.clearLineEnd + content);
|
|
756
748
|
}
|
|
757
749
|
// Separator between chat and input
|
|
758
750
|
const sepRow = chatTop + chatH;
|
|
759
|
-
parts.push((0, terminal_1.cursorTo)(sepRow, 1) + terminal_1.ANSI.clearLineEnd +
|
|
751
|
+
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
752
|
// Input row
|
|
761
753
|
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 +
|
|
754
|
+
const rendered = this.input.render(cols);
|
|
755
|
+
parts.push((0, terminal_1.cursorTo)(inputRow, 1) + terminal_1.ANSI.clearLineEnd + rendered.line);
|
|
756
|
+
const cursorCol = rendered.cursorCol + 1;
|
|
765
757
|
// Status bar row
|
|
766
758
|
const statusRow = inputRow + 1;
|
|
767
759
|
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
760
|
// Modal overlay
|
|
772
761
|
if (this.modal.type !== 'none') {
|
|
773
762
|
const overlay = this.renderModal(rows, cols);
|
package/dist/tui/chat.js
CHANGED
|
@@ -3,64 +3,104 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.renderMessages = renderMessages;
|
|
4
4
|
exports.defaultScroll = defaultScroll;
|
|
5
5
|
exports.clampScroll = clampScroll;
|
|
6
|
+
exports.renderWelcome = renderWelcome;
|
|
6
7
|
/**
|
|
7
|
-
* Chat view — renders conversation messages
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* Chat view — renders conversation messages the Freebuff way (adopted from
|
|
9
|
+
* CodebuffAI/freebuff TUI):
|
|
10
|
+
* - user messages: green left bar + italic text (no label)
|
|
11
|
+
* - assistant messages: plain foreground text
|
|
12
|
+
* - tool calls: compact dim blocks (⚙ tool · preview)
|
|
13
|
+
* No boxes or frames — stays aligned on narrow/mobile terminals.
|
|
14
|
+
*
|
|
15
|
+
* ██╗...VECTORHEAD logo (welcome only)
|
|
16
|
+
* VectorHead will run commands on your behalf to help you build.
|
|
17
|
+
* Directory /home/x
|
|
18
|
+
*
|
|
19
|
+
* │ Inspecting project... (user, italic, green bar)
|
|
20
|
+
*
|
|
21
|
+
* Saya cek dulu file auth.ts. (assistant)
|
|
22
|
+
*
|
|
23
|
+
* ⚙ shell · $ npm test (tool, dim)
|
|
14
24
|
*/
|
|
15
25
|
const theme_1 = require("./theme");
|
|
16
26
|
const terminal_1 = require("../utils/terminal");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
warning: { prefix: '⚠', color: theme_1.THEME.warn },
|
|
25
|
-
};
|
|
26
|
-
/** Render one message as a boxed block. Box width = `width` (whole chat area). */
|
|
27
|
-
function boxMessage(m, width) {
|
|
28
|
-
const style = KIND_STYLE[m.kind];
|
|
29
|
-
const prefix = m.prefix || style.prefix;
|
|
30
|
-
const dot = m.streaming ? `${theme_1.THEME.warn}◐${theme_1.THEME.reset} ` : `${style.color}●${theme_1.THEME.reset} `;
|
|
31
|
-
const title = `${dot}${theme_1.THEME.bold}${style.color}${prefix}${theme_1.THEME.reset}`;
|
|
32
|
-
const meta = m.meta ? ` ${theme_1.THEME.dim}${m.meta}${theme_1.THEME.reset}` : '';
|
|
33
|
-
const toolName = m.tool ? ` ${theme_1.THEME.warn}${m.tool}${theme_1.THEME.reset}` : '';
|
|
34
|
-
const inner = Math.max(8, width - 4);
|
|
35
|
-
const border = style.color;
|
|
36
|
-
// Narrow terminals (mobile) fall back to ASCII so borders stay aligned.
|
|
37
|
-
const ascii = width < 58;
|
|
38
|
-
const tl = ascii ? '+' : terminal_1.BOX.tl;
|
|
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
|
|
27
|
+
/** Center a string (with ANSI) horizontally within `width`. */
|
|
28
|
+
function center(str, width) {
|
|
29
|
+
const pad = Math.max(0, Math.floor((width - (0, terminal_1.visibleWidth)(str)) / 2));
|
|
30
|
+
return ' '.repeat(pad) + str;
|
|
31
|
+
}
|
|
32
|
+
/** Render one message as Freebuff-style lines. */
|
|
33
|
+
function renderMessage(m, width) {
|
|
50
34
|
const body = m.content || '(no content)';
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
const out = [];
|
|
36
|
+
switch (m.kind) {
|
|
37
|
+
case 'user': {
|
|
38
|
+
// Green vertical bar + italic text — Freebuff's signature user style.
|
|
39
|
+
const bar = `${theme_1.THEME.userLine}│${theme_1.THEME.reset}`;
|
|
40
|
+
const inner = Math.max(4, width - 2);
|
|
41
|
+
for (const line of (0, terminal_1.wrapText)(body, inner)) {
|
|
42
|
+
out.push(`${bar} ${terminal_1.ANSI.italic}${line}${theme_1.THEME.reset}`);
|
|
43
|
+
}
|
|
44
|
+
out.push('');
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
case 'tool': {
|
|
48
|
+
// Compact dim block: ⚙ tool · preview, then dim result lines.
|
|
49
|
+
const label = m.tool ? `⚙ ${m.tool}` : '⚙';
|
|
50
|
+
const parts = body.split('\n');
|
|
51
|
+
const first = `${theme_1.THEME.warn}${label}${theme_1.THEME.reset} ${theme_1.THEME.faint}${(parts[0] || '').trim()}${theme_1.THEME.reset}`;
|
|
52
|
+
if (first.trim()) {
|
|
53
|
+
for (const line of (0, terminal_1.wrapText)(first, width))
|
|
54
|
+
out.push(line);
|
|
55
|
+
}
|
|
56
|
+
const rest = parts.slice(1).filter((l) => l.trim());
|
|
57
|
+
if (rest.length > 0) {
|
|
58
|
+
for (const line of (0, terminal_1.wrapText)(rest.join('\n'), Math.max(4, width - 2))) {
|
|
59
|
+
out.push(`${theme_1.THEME.faint}${line}${theme_1.THEME.reset}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
out.push('');
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
case 'system':
|
|
66
|
+
for (const line of (0, terminal_1.wrapText)(body, width))
|
|
67
|
+
out.push(`${theme_1.THEME.faint}${line}${theme_1.THEME.reset}`);
|
|
68
|
+
out.push('');
|
|
69
|
+
return out;
|
|
70
|
+
case 'error':
|
|
71
|
+
for (const line of (0, terminal_1.wrapText)(body, width))
|
|
72
|
+
out.push(`${theme_1.THEME.error}${line}${theme_1.THEME.reset}`);
|
|
73
|
+
out.push('');
|
|
74
|
+
return out;
|
|
75
|
+
case 'success':
|
|
76
|
+
for (const line of (0, terminal_1.wrapText)(body, width))
|
|
77
|
+
out.push(`${theme_1.THEME.success}${line}${theme_1.THEME.reset}`);
|
|
78
|
+
out.push('');
|
|
79
|
+
return out;
|
|
80
|
+
case 'warning':
|
|
81
|
+
for (const line of (0, terminal_1.wrapText)(body, width))
|
|
82
|
+
out.push(`${theme_1.THEME.warn}${line}${theme_1.THEME.reset}`);
|
|
83
|
+
out.push('');
|
|
84
|
+
return out;
|
|
85
|
+
default: {
|
|
86
|
+
// Assistant: plain foreground text (Freebuff style — no label).
|
|
87
|
+
const marker = m.streaming ? `${theme_1.THEME.info}◐ ${theme_1.THEME.reset}` : '';
|
|
88
|
+
const lines = (0, terminal_1.wrapText)(body, width);
|
|
89
|
+
if (lines.length === 0)
|
|
90
|
+
lines.push('(no content)');
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
out.push(i === 0 ? `${marker}${lines[i]}` : lines[i]);
|
|
93
|
+
}
|
|
94
|
+
out.push('');
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
53
97
|
}
|
|
54
|
-
// Bottom border + blank separator
|
|
55
|
-
out.push(border + bl + hz.repeat(inner + 2) + br + theme_1.THEME.reset);
|
|
56
|
-
out.push('');
|
|
57
|
-
return out;
|
|
58
98
|
}
|
|
59
99
|
/** Precompute the full list of rendered lines for all messages. */
|
|
60
100
|
function renderMessages(messages, width) {
|
|
61
101
|
const out = [];
|
|
62
102
|
for (const m of messages) {
|
|
63
|
-
out.push(...
|
|
103
|
+
out.push(...renderMessage(m, width));
|
|
64
104
|
}
|
|
65
105
|
return out;
|
|
66
106
|
}
|
|
@@ -73,3 +113,44 @@ function clampScroll(offset, messages, width, height) {
|
|
|
73
113
|
const max = Math.max(0, all.length - height);
|
|
74
114
|
return Math.min(Math.max(0, offset), max);
|
|
75
115
|
}
|
|
116
|
+
/** Full-width VECTORHEAD ASCII logo (white, Freebuff block style). */
|
|
117
|
+
const VECTORHEAD_LOGO = [
|
|
118
|
+
'██╗ ██╗███████╗ ██████╗████████╗ ██████╗ ██████╗ ██╗ ██╗███████╗ █████╗ ██████╗',
|
|
119
|
+
'██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗██╔══██╗',
|
|
120
|
+
'██║ ██║█████╗ ██║ ██║ ██║ ██║██████╔╝███████║█████╗ ███████║██║ ██║',
|
|
121
|
+
'╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║ ██║██╔══██╗██╔══██║██╔══╝ ██╔══██║██║ ██║',
|
|
122
|
+
' ╚████╔╝ ███████╗╚██████╗ ██║ ╚██████╔╝██║ ██║██║ ██║███████╗██║ ██║██████╔╝',
|
|
123
|
+
' ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ ',
|
|
124
|
+
];
|
|
125
|
+
/**
|
|
126
|
+
* Freebuff-style welcome screen (shown while the chat is empty): logo,
|
|
127
|
+
* tagline and Directory line, centered in the chat area. On narrow
|
|
128
|
+
* terminals the full logo is replaced by a compact wordmark.
|
|
129
|
+
*/
|
|
130
|
+
function renderWelcome(width, height, cwd) {
|
|
131
|
+
const lines = [];
|
|
132
|
+
if (width >= 85) { // full logo is ~82 cols; narrower terminals get the wordmark
|
|
133
|
+
for (const l of VECTORHEAD_LOGO)
|
|
134
|
+
lines.push(`${theme_1.THEME.textBright}${l}${theme_1.THEME.reset}`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
lines.push(`${theme_1.THEME.accent}▮${theme_1.THEME.reset} ${theme_1.THEME.bold}${theme_1.THEME.textBright}VectorHead${theme_1.THEME.reset}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push('');
|
|
140
|
+
for (const t of ['VectorHead will run commands on your behalf', 'to help you build.']) {
|
|
141
|
+
for (const line of (0, terminal_1.wrapText)(t, Math.max(10, width - 4))) {
|
|
142
|
+
lines.push(`${theme_1.THEME.muted}${line}${theme_1.THEME.reset}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(`${theme_1.THEME.muted}Directory ${theme_1.THEME.directory}${(0, terminal_1.truncate)(cwd, Math.max(10, width - 12))}${theme_1.THEME.reset}`);
|
|
147
|
+
const centered = lines.map((l) => center(l, width));
|
|
148
|
+
const padTop = Math.max(0, Math.floor((height - centered.length) / 2));
|
|
149
|
+
const out = [];
|
|
150
|
+
for (let i = 0; i < padTop; i++)
|
|
151
|
+
out.push('');
|
|
152
|
+
out.push(...centered);
|
|
153
|
+
while (out.length < height)
|
|
154
|
+
out.push('');
|
|
155
|
+
return out.slice(0, Math.max(0, height));
|
|
156
|
+
}
|
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);
|
|
@@ -51,11 +51,13 @@ function renderSelector(opts) {
|
|
|
51
51
|
const top = Math.max(1, Math.floor((opts.rows - height) / 2));
|
|
52
52
|
const left = Math.max(1, Math.floor((opts.cols - innerWidth - 2) / 2));
|
|
53
53
|
const lines = [];
|
|
54
|
-
const borderColor =
|
|
55
|
-
const
|
|
54
|
+
const borderColor = theme_1.THEME.accent;
|
|
55
|
+
const contentW = innerWidth - 2;
|
|
56
|
+
// Title row: ╭── Title ────────────╮ (exactly innerWidth+2 wide, green accent)
|
|
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
|
-
terminal_1.ANSI.bold +
|
|
60
|
+
terminal_1.ANSI.bold + theme_1.THEME.textBright + t + terminal_1.ANSI.reset + borderColor +
|
|
59
61
|
terminal_1.BOX.h.repeat(Math.max(0, innerWidth - 2 - (0, terminal_1.visibleWidth)(t))) + terminal_1.BOX.tr);
|
|
60
62
|
for (let i = 0; i < visible; i++) {
|
|
61
63
|
const idx = start + i;
|
|
@@ -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 +
|
|
72
|
+
content = terminal_1.ANSI.bold + theme_1.THEME.accent + (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
|
@@ -5,6 +5,7 @@ exports.InputBox = void 0;
|
|
|
5
5
|
* Input box — single-line text input with cursor, history, and hints.
|
|
6
6
|
*/
|
|
7
7
|
const terminal_1 = require("../utils/terminal");
|
|
8
|
+
const theme_1 = require("./theme");
|
|
8
9
|
class InputBox {
|
|
9
10
|
text = '';
|
|
10
11
|
cursor = 0;
|
|
@@ -97,7 +98,7 @@ class InputBox {
|
|
|
97
98
|
}
|
|
98
99
|
/** Render the input line, returns the cursor column (0-based within line). */
|
|
99
100
|
render(width) {
|
|
100
|
-
const prefix = `${
|
|
101
|
+
const prefix = `${theme_1.THEME.accent}❯${terminal_1.ANSI.reset} `;
|
|
101
102
|
const prefixWidth = 2; // ❯ + space (ANSI codes are invisible)
|
|
102
103
|
const available = Math.max(1, width - prefixWidth - 1);
|
|
103
104
|
let display = this.text;
|
|
@@ -107,7 +108,9 @@ class InputBox {
|
|
|
107
108
|
let start = this.cursor - Math.floor(available / 2);
|
|
108
109
|
start = Math.max(0, Math.min(start, this.text.length - available));
|
|
109
110
|
display = this.text.slice(start, start + available);
|
|
110
|
-
|
|
111
|
+
// Visible column: prefix width (2) + offset inside the window.
|
|
112
|
+
// (prefix.length is the raw ANSI byte length — must not be used here.)
|
|
113
|
+
cursorCol = prefixWidth + (this.cursor - start);
|
|
111
114
|
}
|
|
112
115
|
let suffix = '';
|
|
113
116
|
if (!this.text && this.hint) {
|
package/dist/tui/statusbar.js
CHANGED
|
@@ -4,27 +4,16 @@ exports.renderHeader = renderHeader;
|
|
|
4
4
|
exports.renderProjectBar = renderProjectBar;
|
|
5
5
|
exports.renderStatusBar = renderStatusBar;
|
|
6
6
|
/**
|
|
7
|
-
* Status bar — top header + project
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* │ ENTER Send · TAB Commands · ? Help ● ask │
|
|
7
|
+
* Status bar — top header + project line + bottom status bar.
|
|
8
|
+
* Freebuff-style clean lines (no shortcut hints), truncated to fit
|
|
9
|
+
* narrow/mobile terminals. Bottom bar shows live status, spinner and
|
|
10
|
+
* an elapsed timer while the agent is working.
|
|
12
11
|
*/
|
|
13
12
|
const terminal_1 = require("../utils/terminal");
|
|
14
13
|
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
14
|
/** Split two ANSI strings across `inner` columns, truncating with … if needed. */
|
|
26
15
|
function fitTwo(left, right, inner) {
|
|
27
|
-
const avail = inner
|
|
16
|
+
const avail = Math.max(4, inner);
|
|
28
17
|
const l = (0, terminal_1.stripAnsi)(left).length;
|
|
29
18
|
const r = (0, terminal_1.stripAnsi)(right).length;
|
|
30
19
|
if (l + r <= avail)
|
|
@@ -33,45 +22,46 @@ function fitTwo(left, right, inner) {
|
|
|
33
22
|
const maxR = Math.max(0, avail - maxL);
|
|
34
23
|
return { left: (0, terminal_1.truncate)(left, maxL), right: (0, terminal_1.truncate)(right, maxR) };
|
|
35
24
|
}
|
|
36
|
-
/** Render the top
|
|
25
|
+
/** Render the top header line: VectorHead ● status ... provider · model */
|
|
37
26
|
function renderHeader(width, info) {
|
|
38
27
|
const brand = `${theme_1.THEME.bold}${theme_1.THEME.accent}VectorHead${theme_1.THEME.reset}`;
|
|
39
28
|
const dot = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
40
29
|
const status = `${dot} ${(0, theme_1.statusColor)(info.status)}${info.status}${theme_1.THEME.reset}`;
|
|
41
30
|
const left = `${brand} ${status}`;
|
|
42
31
|
const provider = `${theme_1.THEME.dim}${info.provider}${theme_1.THEME.reset}`;
|
|
43
|
-
const model = `${theme_1.THEME.
|
|
32
|
+
const model = `${theme_1.THEME.faint}${info.model}${theme_1.THEME.reset}`;
|
|
44
33
|
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}`;
|
|
34
|
+
const fit = fitTwo(left, right, width);
|
|
35
|
+
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
36
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
50
37
|
}
|
|
51
|
-
/** Render the
|
|
38
|
+
/** Render the project line: Project: <path> ... Mode: ask */
|
|
52
39
|
function renderProjectBar(width, info) {
|
|
53
|
-
const label = `${theme_1.THEME.dim}Project:${theme_1.THEME.reset} ${theme_1.THEME.
|
|
54
|
-
const modeColor = info.mode === 'yolo' ? theme_1.THEME.warn : theme_1.THEME.
|
|
40
|
+
const label = `${theme_1.THEME.dim}Project:${theme_1.THEME.reset} ${theme_1.THEME.muted}${info.project}${theme_1.THEME.reset}`;
|
|
41
|
+
const modeColor = info.mode === 'yolo' ? theme_1.THEME.warn : theme_1.THEME.faint;
|
|
55
42
|
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}`;
|
|
43
|
+
const fit = fitTwo(label, mode, width);
|
|
44
|
+
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
45
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
61
46
|
}
|
|
62
|
-
/** Render the
|
|
47
|
+
/** Render the bottom status bar: live status + spinner + elapsed timer. */
|
|
63
48
|
function renderStatusBar(width, info) {
|
|
64
|
-
const
|
|
65
|
-
let
|
|
49
|
+
const statusColorCode = (0, theme_1.statusColor)(info.status);
|
|
50
|
+
let left;
|
|
66
51
|
if (info.running && info.spinner) {
|
|
67
|
-
|
|
52
|
+
left = `${statusColorCode}${info.spinner}${theme_1.THEME.reset} ${statusColorCode}${info.status}${theme_1.THEME.reset}`;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
left = `${statusColorCode}${info.status}${theme_1.THEME.reset}`;
|
|
56
|
+
}
|
|
57
|
+
let right = '';
|
|
58
|
+
if (info.running && info.elapsed !== undefined) {
|
|
59
|
+
right += `${theme_1.THEME.dim}${info.elapsed}s${theme_1.THEME.reset} `;
|
|
68
60
|
}
|
|
69
61
|
const conn = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
70
|
-
const mode = info.mode === 'yolo' ? `${theme_1.THEME.warn}YOLO${theme_1.THEME.reset}` : `${theme_1.THEME.
|
|
62
|
+
const mode = info.mode === 'yolo' ? `${theme_1.THEME.warn}YOLO${theme_1.THEME.reset}` : `${theme_1.THEME.faint}ask${theme_1.THEME.reset}`;
|
|
71
63
|
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}`;
|
|
64
|
+
const fit = fitTwo(left, right, width);
|
|
65
|
+
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
66
|
+
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
77
67
|
}
|
package/dist/tui/theme.js
CHANGED
|
@@ -3,25 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Spinner = exports.SPINNERS = exports.THEME = void 0;
|
|
4
4
|
exports.statusColor = statusColor;
|
|
5
5
|
/**
|
|
6
|
-
* VectorHead theme —
|
|
7
|
-
*
|
|
6
|
+
* VectorHead theme — Freebuff-style palette (adopted from CodebuffAI/freebuff dark theme).
|
|
7
|
+
* Primary brand green #9EFC62, slate neutrals, gray AI line, green user line.
|
|
8
8
|
*/
|
|
9
9
|
exports.THEME = {
|
|
10
|
-
accent: '\x1b[38;5;
|
|
11
|
-
accentDim: '\x1b[38;5;
|
|
12
|
-
gold: '\x1b[38;5;220m',
|
|
13
|
-
text: '\x1b[38;5;
|
|
10
|
+
accent: '\x1b[38;5;156m', // Freebuff primary green (#9EFC62 ≈ 156)
|
|
11
|
+
accentDim: '\x1b[38;5;120m',
|
|
12
|
+
gold: '\x1b[38;5;220m', // markdown heading yellow (#facc15 ≈ 220)
|
|
13
|
+
text: '\x1b[38;5;255m', // foreground #f1f5f9 ≈ 255
|
|
14
14
|
textBright: '\x1b[38;5;255m',
|
|
15
|
-
muted: '\x1b[38;5;
|
|
16
|
-
faint: '\x1b[38;5;
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
muted: '\x1b[38;5;250m', // #acb3bf ≈ 250
|
|
16
|
+
faint: '\x1b[38;5;244m',
|
|
17
|
+
directory: '\x1b[38;5;249m', // #9CA3AF ≈ 249
|
|
18
|
+
border: '\x1b[38;5;60m', // slate #536175 ≈ 60
|
|
19
|
+
borderBright: '\x1b[38;5;156m',
|
|
19
20
|
success: '\x1b[38;5;114m',
|
|
20
|
-
warn: '\x1b[38;5;215m',
|
|
21
|
+
warn: '\x1b[38;5;215m', // #FFA500 ≈ 215
|
|
21
22
|
error: '\x1b[38;5;203m',
|
|
22
|
-
info: '\x1b[38;5;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
info: '\x1b[38;5;156m',
|
|
24
|
+
// Message indicator lines (Freebuff): gray for AI, green for user
|
|
25
|
+
aiLine: '\x1b[38;5;244m',
|
|
26
|
+
userLine: '\x1b[38;5;156m',
|
|
27
|
+
bgBar: '\x1b[48;5;236m',
|
|
28
|
+
bgPanel: '\x1b[48;5;237m',
|
|
25
29
|
bold: '\x1b[1m',
|
|
26
30
|
dim: '\x1b[2m',
|
|
27
31
|
reset: '\x1b[0m',
|
|
@@ -39,8 +43,8 @@ function statusColor(status) {
|
|
|
39
43
|
return exports.THEME.success;
|
|
40
44
|
if (s.includes('stopped') || s.includes('cancel'))
|
|
41
45
|
return exports.THEME.muted;
|
|
42
|
-
if (s.includes('thinking') || s.includes('plan'))
|
|
43
|
-
return exports.THEME.
|
|
46
|
+
if (s.includes('thinking') || s.includes('plan') || s.includes('work'))
|
|
47
|
+
return exports.THEME.accent;
|
|
44
48
|
if (s.includes('run') || s.includes('exec'))
|
|
45
49
|
return exports.THEME.accent;
|
|
46
50
|
return exports.THEME.info;
|
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,34 @@ 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
|
+
// Word-aware wrap (Freebuff uses wrapMode: 'word'): break at the last
|
|
139
|
+
// space so words are not split mid-word. ANSI codes never contain a
|
|
140
|
+
// space, so lastIndexOf(' ') on the raw buffer is safe.
|
|
141
|
+
const lastSpace = current.lastIndexOf(' ');
|
|
142
|
+
if (lastSpace > 0) {
|
|
143
|
+
const carried = current.slice(lastSpace + 1);
|
|
144
|
+
const carriedW = visibleWidth(carried);
|
|
145
|
+
if (carriedW > 0 && carriedW + w <= width) {
|
|
146
|
+
lines.push(current.slice(0, lastSpace));
|
|
147
|
+
current = carried;
|
|
148
|
+
visibleLen = carriedW;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
flushLine();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
flushLine();
|
|
156
|
+
}
|
|
136
157
|
}
|
|
137
|
-
current +=
|
|
158
|
+
current += unit;
|
|
138
159
|
visibleLen += w;
|
|
139
|
-
i
|
|
160
|
+
i += unit.length;
|
|
140
161
|
}
|
|
141
162
|
flushLine();
|
|
142
163
|
return lines;
|
|
@@ -145,11 +166,11 @@ function wrapText(text, width) {
|
|
|
145
166
|
function stripAnsi(text) {
|
|
146
167
|
return text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
147
168
|
}
|
|
148
|
-
/** Visible width of a string without ANSI codes. */
|
|
169
|
+
/** Visible width of a string without ANSI codes (emoji counted as 2). */
|
|
149
170
|
function visibleWidth(text) {
|
|
150
171
|
let w = 0;
|
|
151
172
|
for (const ch of stripAnsi(text)) {
|
|
152
|
-
w += ch.
|
|
173
|
+
w += ch.codePointAt(0) > 0x2fff ? 2 : 1;
|
|
153
174
|
}
|
|
154
175
|
return w;
|
|
155
176
|
}
|
|
@@ -160,12 +181,40 @@ function pad(str, width) {
|
|
|
160
181
|
return str;
|
|
161
182
|
return str + ' '.repeat(width - len);
|
|
162
183
|
}
|
|
163
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Truncate a string (with ANSI) to a max visible width. ANSI sequences are
|
|
186
|
+
* copied verbatim so colors survive; when truncated, one column is reserved
|
|
187
|
+
* for '…' and a closing reset is appended so styling never bleeds out.
|
|
188
|
+
*/
|
|
164
189
|
function truncate(str, width) {
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
if (width <= 0)
|
|
191
|
+
return '';
|
|
192
|
+
if (visibleWidth(str) <= width)
|
|
167
193
|
return str;
|
|
168
|
-
|
|
194
|
+
const ESC_RE = /^\x1b\[[0-9;]*[A-Za-z]/;
|
|
195
|
+
let out = '';
|
|
196
|
+
let vis = 0;
|
|
197
|
+
let i = 0;
|
|
198
|
+
while (i < str.length && vis < width - 1) {
|
|
199
|
+
const ch = str[i];
|
|
200
|
+
if (ch === '\x1b') {
|
|
201
|
+
const m = str.slice(i).match(ESC_RE);
|
|
202
|
+
if (m) {
|
|
203
|
+
out += m[0];
|
|
204
|
+
i += m[0].length;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const cp = str.codePointAt(i);
|
|
209
|
+
const unit = String.fromCodePoint(cp);
|
|
210
|
+
const w = cp > 0x2fff ? 2 : 1;
|
|
211
|
+
if (vis + w > width - 1)
|
|
212
|
+
break; // wide char leaves no room for '…'
|
|
213
|
+
out += unit;
|
|
214
|
+
vis += w;
|
|
215
|
+
i += unit.length;
|
|
216
|
+
}
|
|
217
|
+
return out + '…' + exports.ANSI.reset;
|
|
169
218
|
}
|
|
170
219
|
/** Render a line with a trailing newline reset applied. */
|
|
171
220
|
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.6",
|
|
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",
|