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 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 = () => doRender();
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
- // Main tick loop
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
- doRender();
276
- await sleep(50);
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..) — clean Freebuff-style message blocks
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 /session',
802
- '/settings /status /clear /continue /diff',
803
- '/git /compact /yolo /exit',
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
- '/setup setup provider (base URL + token)',
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
- '/yolo toggle permission mode',
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
- * clean plain-text blocks with a small colored label row. User messages
9
- * are right-aligned (chat-style bubble), everything else left-aligned.
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
- * VectorHead · model
13
- * Inspecting project...
15
+ * ██╗...VECTORHEAD logo (welcome only)
16
+ * VectorHead will run commands on your behalf to help you build.
17
+ * Directory /home/x
14
18
  *
15
- * YOU
16
- * perbaiki error auth
19
+ * Inspecting project... (user, italic, green bar)
17
20
  *
18
- * shell · $ npm test
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
- /** 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;
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
- const KIND_STYLE = {
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 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}`);
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
+ }
@@ -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 = terminal_1.ANSI.cyan;
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 + terminal_1.ANSI.white + t + terminal_1.ANSI.reset + borderColor +
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 + terminal_1.ANSI.cyan + (0, terminal_1.truncate)(label, contentW - (0, terminal_1.visibleWidth)(hint)) + terminal_1.ANSI.reset + hint;
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 = `${terminal_1.ANSI.fg(220)}❯${terminal_1.ANSI.reset} `;
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;
@@ -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 hint bar.
8
- * Clean Freebuff-style lines (no frames), truncated to fit narrow/mobile
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.accentDim}${info.model}${theme_1.THEME.reset}`;
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.text}${info.project}${theme_1.THEME.reset}`;
40
- const modeColor = info.mode === 'yolo' ? theme_1.THEME.warn : theme_1.THEME.muted;
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 hint/status bar with live spinner. */
47
+ /** Render the bottom status bar: live status + spinner + elapsed timer. */
47
48
  function renderStatusBar(width, info) {
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}`;
49
- let right = '';
49
+ const statusColorCode = (0, theme_1.statusColor)(info.status);
50
+ let left;
50
51
  if (info.running && info.spinner) {
51
- right = `${(0, theme_1.statusColor)(info.status)}${info.spinner}${theme_1.THEME.reset} `;
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.muted}ask${theme_1.THEME.reset}`;
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 — consistent premium palette for the TUI.
7
- * Warm dark palette with orange accent + gold highlights.
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;208m', // bright orange
11
- accentDim: '\x1b[38;5;172m',
12
- gold: '\x1b[38;5;220m',
13
- text: '\x1b[38;5;252m',
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;244m',
16
- faint: '\x1b[38;5;240m',
17
- border: '\x1b[38;5;94m',
18
- borderBright: '\x1b[38;5;208m',
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;111m',
23
- bgBar: '\x1b[48;5;235m',
24
- bgPanel: '\x1b[48;5;236m',
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.info;
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;
@@ -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
- flushLine();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agen-vektor",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
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": {