agen-vektor 0.3.5 → 0.3.7
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/index.js +19 -4
- package/dist/tui/app.js +62 -8
- package/dist/tui/chat.js +121 -36
- package/dist/tui/components.js +4 -4
- package/dist/tui/input.js +2 -1
- package/dist/tui/statusbar.js +19 -11
- package/dist/tui/theme.js +20 -16
- package/dist/utils/terminal.js +19 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -247,7 +247,10 @@ async function runTui(opts) {
|
|
|
247
247
|
const out = app.render();
|
|
248
248
|
process.stdout.write(terminal_1.ANSI.hideCursor + out);
|
|
249
249
|
};
|
|
250
|
-
const onResize = () =>
|
|
250
|
+
const onResize = () => {
|
|
251
|
+
app.markDirty();
|
|
252
|
+
doRender();
|
|
253
|
+
};
|
|
251
254
|
process.stdout.on('resize', onResize);
|
|
252
255
|
process.on('SIGINT', () => {
|
|
253
256
|
// In raw mode Ctrl+C arrives as a key event (0x03), but some terminals
|
|
@@ -258,6 +261,12 @@ async function runTui(opts) {
|
|
|
258
261
|
else
|
|
259
262
|
process.exit(0);
|
|
260
263
|
});
|
|
264
|
+
process.on('SIGTERM', () => {
|
|
265
|
+
if (app)
|
|
266
|
+
app.requestExit();
|
|
267
|
+
else
|
|
268
|
+
process.exit(0);
|
|
269
|
+
});
|
|
261
270
|
process.on('SIGWINCH', onResize);
|
|
262
271
|
const keyHandler = (ev) => {
|
|
263
272
|
void app.handleKey(ev);
|
|
@@ -269,11 +278,17 @@ async function runTui(opts) {
|
|
|
269
278
|
(0, keyboard_1.keyStream)(keyHandler);
|
|
270
279
|
app.start();
|
|
271
280
|
doRender();
|
|
272
|
-
|
|
281
|
+
app.consumeRender();
|
|
282
|
+
// Main tick loop — repaint only when something changed. Repainting the
|
|
283
|
+
// whole frame every tick caused constant flicker and made keystrokes
|
|
284
|
+
// (Enter, Ctrl+C) feel laggy on slow/mobile connections.
|
|
273
285
|
while (!exit) {
|
|
274
286
|
await app.tick();
|
|
275
|
-
|
|
276
|
-
|
|
287
|
+
if (app.needsRender()) {
|
|
288
|
+
doRender();
|
|
289
|
+
app.consumeRender();
|
|
290
|
+
}
|
|
291
|
+
await sleep(80);
|
|
277
292
|
}
|
|
278
293
|
}
|
|
279
294
|
finally {
|
package/dist/tui/app.js
CHANGED
|
@@ -78,6 +78,9 @@ class App {
|
|
|
78
78
|
planToExecute = null;
|
|
79
79
|
abortController = null;
|
|
80
80
|
spinner = new theme_1.Spinner();
|
|
81
|
+
runStart = null;
|
|
82
|
+
/** set on every mutation so the render loop only repaints when needed */
|
|
83
|
+
dirty = true;
|
|
81
84
|
constructor(agent, opts) {
|
|
82
85
|
this.agent = agent;
|
|
83
86
|
this.opts = opts;
|
|
@@ -87,10 +90,22 @@ class App {
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
// ─── Public API ───────────────────────────────────────────────
|
|
93
|
+
/** Mark the screen as needing a repaint. */
|
|
94
|
+
markDirty() {
|
|
95
|
+
this.dirty = true;
|
|
96
|
+
}
|
|
97
|
+
/** True when the screen must be repainted (dirty, or streaming). */
|
|
98
|
+
needsRender() {
|
|
99
|
+
return this.dirty || this.running;
|
|
100
|
+
}
|
|
101
|
+
/** Clear the dirty flag after a repaint. */
|
|
102
|
+
consumeRender() {
|
|
103
|
+
this.dirty = false;
|
|
104
|
+
}
|
|
90
105
|
start() {
|
|
106
|
+
this.markDirty();
|
|
91
107
|
this.connected = true;
|
|
92
108
|
this.status = 'Ready';
|
|
93
|
-
this.addSystem('VectorHead ready. Type a request or press ? for help.');
|
|
94
109
|
if (this.opts.continueSession && this.sessionName) {
|
|
95
110
|
const sess = (0, session_1.loadSession)(this.sessionName);
|
|
96
111
|
if (sess) {
|
|
@@ -106,6 +121,7 @@ class App {
|
|
|
106
121
|
}
|
|
107
122
|
}
|
|
108
123
|
async handleKey(ev) {
|
|
124
|
+
this.markDirty();
|
|
109
125
|
if (this.modal.type !== 'none') {
|
|
110
126
|
await this.handleModalKey(ev);
|
|
111
127
|
return;
|
|
@@ -119,6 +135,10 @@ class App {
|
|
|
119
135
|
this.running = false;
|
|
120
136
|
this.addSystem('⏹ Stopped. Press Ctrl+C again to quit.');
|
|
121
137
|
}
|
|
138
|
+
else if (ev.name === 'ctrl_d') {
|
|
139
|
+
// Ctrl+D stops and quits in one go (handy on mobile)
|
|
140
|
+
this.requestExit();
|
|
141
|
+
}
|
|
122
142
|
return;
|
|
123
143
|
}
|
|
124
144
|
switch (ev.name) {
|
|
@@ -204,11 +224,13 @@ class App {
|
|
|
204
224
|
}
|
|
205
225
|
async tick() {
|
|
206
226
|
if (this.pendingPrompt) {
|
|
227
|
+
this.markDirty();
|
|
207
228
|
const p = this.pendingPrompt;
|
|
208
229
|
this.pendingPrompt = '';
|
|
209
230
|
await this.submit(p);
|
|
210
231
|
}
|
|
211
232
|
if (this.planToExecute) {
|
|
233
|
+
this.markDirty();
|
|
212
234
|
const plan = this.planToExecute;
|
|
213
235
|
this.planToExecute = null;
|
|
214
236
|
await this.executeWithPlan(plan);
|
|
@@ -222,6 +244,7 @@ class App {
|
|
|
222
244
|
* Used by Ctrl+C and SIGINT so the TUI never needs to be killed.
|
|
223
245
|
*/
|
|
224
246
|
requestExit() {
|
|
247
|
+
this.markDirty();
|
|
225
248
|
if (this.running) {
|
|
226
249
|
this.abortController?.abort();
|
|
227
250
|
this.running = false;
|
|
@@ -230,6 +253,7 @@ class App {
|
|
|
230
253
|
}
|
|
231
254
|
// ─── Submission ───────────────────────────────────────────────
|
|
232
255
|
async submit(text) {
|
|
256
|
+
this.markDirty();
|
|
233
257
|
if (text.startsWith('/')) {
|
|
234
258
|
await this.handleCommand(text);
|
|
235
259
|
return;
|
|
@@ -241,6 +265,7 @@ class App {
|
|
|
241
265
|
this.statusColor = (0, theme_1.statusColor)('Thinking');
|
|
242
266
|
this.spinner.reset();
|
|
243
267
|
this.abortController = new AbortController();
|
|
268
|
+
this.runStart = Date.now();
|
|
244
269
|
// Show plan first if requested
|
|
245
270
|
if (this.opts.showPlan) {
|
|
246
271
|
this.status = 'Planning…';
|
|
@@ -283,15 +308,18 @@ class App {
|
|
|
283
308
|
}) - 1;
|
|
284
309
|
this.agent.setCallbacks({
|
|
285
310
|
onDelta: (delta) => {
|
|
311
|
+
this.markDirty();
|
|
286
312
|
const m = this.messages[msgIndex];
|
|
287
313
|
if (m)
|
|
288
314
|
m.content += delta;
|
|
289
315
|
},
|
|
290
316
|
onStatus: (status) => {
|
|
317
|
+
this.markDirty();
|
|
291
318
|
this.status = status;
|
|
292
319
|
this.statusColor = (0, theme_1.statusColor)(status);
|
|
293
320
|
},
|
|
294
321
|
onToolCall: (tool, args) => {
|
|
322
|
+
this.markDirty();
|
|
295
323
|
let preview = '';
|
|
296
324
|
try {
|
|
297
325
|
const parsed = JSON.parse(args);
|
|
@@ -305,6 +333,7 @@ class App {
|
|
|
305
333
|
this.messages.push({ kind: 'tool', tool, content: `→ ${preview}` });
|
|
306
334
|
},
|
|
307
335
|
onToolResult: (tool, result) => {
|
|
336
|
+
this.markDirty();
|
|
308
337
|
const last = this.messages[this.messages.length - 1];
|
|
309
338
|
if (last && last.kind === 'tool' && last.tool === tool) {
|
|
310
339
|
last.content = result.split('\n').slice(0, 12).join('\n');
|
|
@@ -313,6 +342,7 @@ class App {
|
|
|
313
342
|
}
|
|
314
343
|
},
|
|
315
344
|
onError: (err) => {
|
|
345
|
+
this.markDirty();
|
|
316
346
|
const m = this.messages[msgIndex];
|
|
317
347
|
if (m) {
|
|
318
348
|
m.kind = 'error';
|
|
@@ -321,6 +351,7 @@ class App {
|
|
|
321
351
|
}
|
|
322
352
|
},
|
|
323
353
|
onFinal: (result) => {
|
|
354
|
+
this.markDirty();
|
|
324
355
|
const m = this.messages[msgIndex];
|
|
325
356
|
if (m) {
|
|
326
357
|
m.content = result.content || m.content;
|
|
@@ -356,6 +387,7 @@ class App {
|
|
|
356
387
|
m.content = '(no response)';
|
|
357
388
|
}
|
|
358
389
|
this.running = false;
|
|
390
|
+
this.runStart = null;
|
|
359
391
|
if (this.statusColor !== terminal_1.ANSI.red) {
|
|
360
392
|
this.status = 'Ready';
|
|
361
393
|
this.statusColor = terminal_1.ANSI.green;
|
|
@@ -384,6 +416,7 @@ class App {
|
|
|
384
416
|
}));
|
|
385
417
|
}
|
|
386
418
|
async executeWithPlan(plan) {
|
|
419
|
+
this.markDirty();
|
|
387
420
|
this.running = true;
|
|
388
421
|
this.status = 'Executing plan';
|
|
389
422
|
this.statusColor = terminal_1.ANSI.yellow;
|
|
@@ -476,6 +509,21 @@ class App {
|
|
|
476
509
|
this.addSystem(`Current session: ${this.sessionName || '(none)'}`);
|
|
477
510
|
}
|
|
478
511
|
break;
|
|
512
|
+
case '/connect':
|
|
513
|
+
// Wizard for a custom OpenAI-compatible API: base URL + token
|
|
514
|
+
this.agent.config.provider = 'custom';
|
|
515
|
+
this.agent.rebuildProvider();
|
|
516
|
+
(0, config_1.saveConfig)(this.agent.config);
|
|
517
|
+
this.addSystem('Custom provider — masukkan base URL API (mis. https://…/v1), lalu token.');
|
|
518
|
+
this.modal = { type: 'custom-url', value: this.agent.config.apiUrl || '' };
|
|
519
|
+
break;
|
|
520
|
+
case '/agent':
|
|
521
|
+
this.agent.setMode(this.agent.config.permissionMode === 'yolo' ? 'ask' : 'yolo');
|
|
522
|
+
(0, config_1.saveConfig)(this.agent.config);
|
|
523
|
+
this.addSystem(this.agent.config.permissionMode === 'yolo'
|
|
524
|
+
? 'Agent mode: YOLO — prompt permission untuk aksi ask-level dilewati.'
|
|
525
|
+
: 'Agent mode: ask — prompt permission aktif.');
|
|
526
|
+
break;
|
|
479
527
|
default:
|
|
480
528
|
this.addSystem(`Unknown command: ${name} (type /help for commands)`);
|
|
481
529
|
break;
|
|
@@ -687,10 +735,12 @@ class App {
|
|
|
687
735
|
}
|
|
688
736
|
// ─── Message helpers ──────────────────────────────────────────
|
|
689
737
|
addSystem(text) {
|
|
738
|
+
this.markDirty();
|
|
690
739
|
this.messages.push({ kind: 'system', content: text });
|
|
691
740
|
}
|
|
692
741
|
/** Show a permission modal (called by the permission callback). */
|
|
693
742
|
setPermissionModal(req) {
|
|
743
|
+
this.markDirty();
|
|
694
744
|
this.modal = {
|
|
695
745
|
type: 'permission',
|
|
696
746
|
req,
|
|
@@ -704,6 +754,7 @@ class App {
|
|
|
704
754
|
this.permissionResolver = fn;
|
|
705
755
|
}
|
|
706
756
|
addTool(tool, content) {
|
|
757
|
+
this.markDirty();
|
|
707
758
|
this.messages.push({ kind: 'tool', tool, content });
|
|
708
759
|
}
|
|
709
760
|
// ─── Rendering ────────────────────────────────────────────────
|
|
@@ -725,6 +776,7 @@ class App {
|
|
|
725
776
|
mode: this.agent.config.permissionMode,
|
|
726
777
|
spinner: this.spinner.frame(),
|
|
727
778
|
running: this.running,
|
|
779
|
+
elapsed: this.running && this.runStart !== null ? Math.floor((Date.now() - this.runStart) / 1000) : undefined,
|
|
728
780
|
};
|
|
729
781
|
const parts = [];
|
|
730
782
|
// Row 1-2: header + project line
|
|
@@ -732,9 +784,9 @@ class App {
|
|
|
732
784
|
parts.push((0, terminal_1.cursorTo)(2, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderProjectBar)(cols, info));
|
|
733
785
|
// Row 3: separator
|
|
734
786
|
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..) —
|
|
787
|
+
// Chat area (rows 4..) — Freebuff-style message blocks (welcome screen when empty)
|
|
736
788
|
const chatH = this.chatHeight();
|
|
737
|
-
const all = (0, chat_1.renderMessages)(this.messages, cols);
|
|
789
|
+
const all = this.messages.length === 0 ? (0, chat_1.renderWelcome)(cols, chatH, this.agent.cwd) : (0, chat_1.renderMessages)(this.messages, cols);
|
|
738
790
|
const maxScroll = Math.max(0, all.length - chatH);
|
|
739
791
|
this.scroll = Math.min(this.scroll, maxScroll);
|
|
740
792
|
const visible = all.slice(this.scroll, this.scroll + chatH);
|
|
@@ -796,11 +848,12 @@ class App {
|
|
|
796
848
|
' PgUp/Dn scroll chat',
|
|
797
849
|
' ? this help',
|
|
798
850
|
' Ctrl+C stop agent / quit',
|
|
851
|
+
' Ctrl+D stop + quit (mobile)',
|
|
799
852
|
' Ctrl+L clear conversation',
|
|
800
853
|
'',
|
|
801
|
-
'Commands: /setup /model /provider
|
|
802
|
-
'/settings /status /clear /continue
|
|
803
|
-
'/git /compact /
|
|
854
|
+
'Commands: /connect /setup /model /provider',
|
|
855
|
+
'/session /settings /status /clear /continue',
|
|
856
|
+
'/diff /git /compact /agent /exit',
|
|
804
857
|
],
|
|
805
858
|
}, rows, cols);
|
|
806
859
|
case 'commands':
|
|
@@ -808,7 +861,8 @@ class App {
|
|
|
808
861
|
title: 'Commands',
|
|
809
862
|
body: [
|
|
810
863
|
'/help show help',
|
|
811
|
-
'/
|
|
864
|
+
'/connect set custom API base URL + token',
|
|
865
|
+
'/setup setup provider',
|
|
812
866
|
'/model select model',
|
|
813
867
|
'/provider select provider',
|
|
814
868
|
'/session manage sessions',
|
|
@@ -819,7 +873,7 @@ class App {
|
|
|
819
873
|
'/diff show diff',
|
|
820
874
|
'/git git status',
|
|
821
875
|
'/compact compact context',
|
|
822
|
-
'/
|
|
876
|
+
'/agent toggle permission mode (ask/yolo)',
|
|
823
877
|
'/exit quit',
|
|
824
878
|
],
|
|
825
879
|
}, rows, cols);
|
package/dist/tui/chat.js
CHANGED
|
@@ -3,54 +3,98 @@ 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 the Freebuff way
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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)
|
|
10
13
|
* No boxes or frames — stays aligned on narrow/mobile terminals.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* ██╗...VECTORHEAD logo (welcome only)
|
|
16
|
+
* VectorHead will run commands on your behalf to help you build.
|
|
17
|
+
* Directory /home/x
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
* perbaiki error auth
|
|
19
|
+
* │ Inspecting project... (user, italic, green bar)
|
|
17
20
|
*
|
|
18
|
-
*
|
|
21
|
+
* Saya cek dulu file auth.ts. (assistant)
|
|
22
|
+
*
|
|
23
|
+
* ⚙ shell · $ npm test (tool, dim)
|
|
19
24
|
*/
|
|
20
25
|
const theme_1 = require("./theme");
|
|
21
26
|
const terminal_1 = require("../utils/terminal");
|
|
22
|
-
/**
|
|
23
|
-
function
|
|
24
|
-
|
|
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;
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
user: { prefix: 'YOU', color: theme_1.THEME.gold },
|
|
28
|
-
assistant: { prefix: 'VectorHead', color: theme_1.THEME.accent },
|
|
29
|
-
tool: { prefix: 'TOOL', color: theme_1.THEME.warn },
|
|
30
|
-
system: { prefix: '···', color: theme_1.THEME.muted },
|
|
31
|
-
error: { prefix: '✗', color: theme_1.THEME.error },
|
|
32
|
-
success: { prefix: '✓', color: theme_1.THEME.success },
|
|
33
|
-
warning: { prefix: '⚠', color: theme_1.THEME.warn },
|
|
34
|
-
};
|
|
35
|
-
/** Render one message as a label row + wrapped content (user right-aligned). */
|
|
32
|
+
/** Render one message as Freebuff-style lines. */
|
|
36
33
|
function renderMessage(m, width) {
|
|
37
|
-
const style = KIND_STYLE[m.kind];
|
|
38
|
-
const prefix = m.prefix || style.prefix;
|
|
39
|
-
const dot = m.streaming ? `${theme_1.THEME.warn}◐${theme_1.THEME.reset}` : `${style.color}●${theme_1.THEME.reset}`;
|
|
40
|
-
const meta = m.meta ? ` ${theme_1.THEME.dim}${m.meta}${theme_1.THEME.reset}` : '';
|
|
41
|
-
const toolName = m.tool ? ` ${theme_1.THEME.warn}${m.tool}${theme_1.THEME.reset}` : '';
|
|
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
|
|
47
34
|
const body = m.content || '(no content)';
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
97
|
}
|
|
52
|
-
out.push(''); // blank separator between messages
|
|
53
|
-
return out;
|
|
54
98
|
}
|
|
55
99
|
/** Precompute the full list of rendered lines for all messages. */
|
|
56
100
|
function renderMessages(messages, width) {
|
|
@@ -69,3 +113,44 @@ function clampScroll(offset, messages, width, height) {
|
|
|
69
113
|
const max = Math.max(0, all.length - height);
|
|
70
114
|
return Math.min(Math.max(0, offset), max);
|
|
71
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
|
@@ -51,13 +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 =
|
|
54
|
+
const borderColor = theme_1.THEME.accent;
|
|
55
55
|
const contentW = innerWidth - 2;
|
|
56
|
-
// Title row: ╭── Title ────────────╮ (exactly innerWidth+2 wide)
|
|
56
|
+
// Title row: ╭── Title ────────────╮ (exactly innerWidth+2 wide, green accent)
|
|
57
57
|
const t = (0, terminal_1.truncate)(opts.title, contentW);
|
|
58
58
|
lines.push((0, terminal_1.cursorTo)(top, left) +
|
|
59
59
|
borderColor + terminal_1.BOX.tl + terminal_1.BOX.h.repeat(2) +
|
|
60
|
-
terminal_1.ANSI.bold +
|
|
60
|
+
terminal_1.ANSI.bold + theme_1.THEME.textBright + t + terminal_1.ANSI.reset + borderColor +
|
|
61
61
|
terminal_1.BOX.h.repeat(Math.max(0, innerWidth - 2 - (0, terminal_1.visibleWidth)(t))) + terminal_1.BOX.tr);
|
|
62
62
|
for (let i = 0; i < visible; i++) {
|
|
63
63
|
const idx = start + i;
|
|
@@ -69,7 +69,7 @@ function renderSelector(opts) {
|
|
|
69
69
|
const label = marker + option.label;
|
|
70
70
|
const hint = option.hint ? terminal_1.ANSI.dim + ' — ' + option.hint + terminal_1.ANSI.reset : '';
|
|
71
71
|
if (idx === opts.selected) {
|
|
72
|
-
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;
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
75
|
content = (0, terminal_1.truncate)(label, contentW - (0, terminal_1.visibleWidth)(hint)) + hint;
|
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;
|
package/dist/tui/statusbar.js
CHANGED
|
@@ -4,9 +4,10 @@ exports.renderHeader = renderHeader;
|
|
|
4
4
|
exports.renderProjectBar = renderProjectBar;
|
|
5
5
|
exports.renderStatusBar = renderStatusBar;
|
|
6
6
|
/**
|
|
7
|
-
* Status bar — top header + project line + bottom
|
|
8
|
-
*
|
|
9
|
-
* terminals.
|
|
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.
|
|
10
11
|
*/
|
|
11
12
|
const terminal_1 = require("../utils/terminal");
|
|
12
13
|
const theme_1 = require("./theme");
|
|
@@ -28,7 +29,7 @@ function renderHeader(width, info) {
|
|
|
28
29
|
const status = `${dot} ${(0, theme_1.statusColor)(info.status)}${info.status}${theme_1.THEME.reset}`;
|
|
29
30
|
const left = `${brand} ${status}`;
|
|
30
31
|
const provider = `${theme_1.THEME.dim}${info.provider}${theme_1.THEME.reset}`;
|
|
31
|
-
const model = `${theme_1.THEME.
|
|
32
|
+
const model = `${theme_1.THEME.faint}${info.model}${theme_1.THEME.reset}`;
|
|
32
33
|
const right = `${provider} · ${model}`;
|
|
33
34
|
const fit = fitTwo(left, right, width);
|
|
34
35
|
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
@@ -36,22 +37,29 @@ function renderHeader(width, info) {
|
|
|
36
37
|
}
|
|
37
38
|
/** Render the project line: Project: <path> ... Mode: ask */
|
|
38
39
|
function renderProjectBar(width, info) {
|
|
39
|
-
const label = `${theme_1.THEME.dim}Project:${theme_1.THEME.reset} ${theme_1.THEME.
|
|
40
|
-
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;
|
|
41
42
|
const mode = `${theme_1.THEME.dim}Mode:${theme_1.THEME.reset} ${modeColor}${info.mode}${theme_1.THEME.reset}`;
|
|
42
43
|
const fit = fitTwo(label, mode, width);
|
|
43
44
|
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
44
45
|
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
45
46
|
}
|
|
46
|
-
/** Render the bottom
|
|
47
|
+
/** Render the bottom status bar: live status + spinner + elapsed timer. */
|
|
47
48
|
function renderStatusBar(width, info) {
|
|
48
|
-
const
|
|
49
|
-
let
|
|
49
|
+
const statusColorCode = (0, theme_1.statusColor)(info.status);
|
|
50
|
+
let left;
|
|
50
51
|
if (info.running && info.spinner) {
|
|
51
|
-
|
|
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} `;
|
|
52
60
|
}
|
|
53
61
|
const conn = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
|
|
54
|
-
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}`;
|
|
55
63
|
right += `${conn} ${mode}`;
|
|
56
64
|
const fit = fitTwo(left, right, width);
|
|
57
65
|
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
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
|
@@ -135,7 +135,25 @@ function wrapText(text, width) {
|
|
|
135
135
|
const unit = String.fromCodePoint(cp);
|
|
136
136
|
const w = cp > 0x2fff ? 2 : 1;
|
|
137
137
|
if (visibleLen + w > width && visibleLen > 0) {
|
|
138
|
-
|
|
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
|
+
}
|
|
139
157
|
}
|
|
140
158
|
current += unit;
|
|
141
159
|
visibleLen += w;
|
package/package.json
CHANGED