agen-vektor 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -57,10 +57,10 @@ Then:
57
57
  vector
58
58
  ```
59
59
 
60
- ### From npm (once published)
60
+ ### From npm
61
61
 
62
62
  ```bash
63
- npm install -g vector-cli
63
+ npm install -g agen-vektor
64
64
  vector
65
65
  ```
66
66
 
@@ -69,7 +69,7 @@ vector
69
69
  ```bash
70
70
  pkg update && pkg upgrade
71
71
  pkg install nodejs git
72
- npm install -g vector-cli
72
+ npm install -g agen-vektor
73
73
  vector
74
74
  ```
75
75
 
package/dist/tui/app.js CHANGED
@@ -53,6 +53,7 @@ const chat_1 = require("./chat");
53
53
  const input_1 = require("./input");
54
54
  const statusbar_1 = require("./statusbar");
55
55
  const components_1 = require("./components");
56
+ const theme_1 = require("./theme");
56
57
  const diff_1 = require("./diff");
57
58
  const planner_1 = require("../agent/planner");
58
59
  const session_1 = require("../agent/session");
@@ -76,6 +77,7 @@ class App {
76
77
  pendingPrompt = '';
77
78
  planToExecute = null;
78
79
  abortController = null;
80
+ spinner = new theme_1.Spinner();
79
81
  constructor(agent, opts) {
80
82
  this.agent = agent;
81
83
  this.opts = opts;
@@ -219,7 +221,8 @@ class App {
219
221
  this.scroll = 0;
220
222
  this.running = true;
221
223
  this.status = 'Thinking';
222
- this.statusColor = terminal_1.ANSI.yellow;
224
+ this.statusColor = (0, theme_1.statusColor)('Thinking');
225
+ this.spinner.reset();
223
226
  this.abortController = new AbortController();
224
227
  // Show plan first if requested
225
228
  if (this.opts.showPlan) {
@@ -259,6 +262,7 @@ class App {
259
262
  kind: 'assistant',
260
263
  content: '',
261
264
  streaming: true,
265
+ meta: this.agent.config.model,
262
266
  }) - 1;
263
267
  this.agent.setCallbacks({
264
268
  onDelta: (delta) => {
@@ -268,7 +272,7 @@ class App {
268
272
  },
269
273
  onStatus: (status) => {
270
274
  this.status = status;
271
- this.statusColor = status.startsWith('✗') || status.includes('error') || status.includes('Error') ? terminal_1.ANSI.red : terminal_1.ANSI.yellow;
275
+ this.statusColor = (0, theme_1.statusColor)(status);
272
276
  },
273
277
  onToolCall: (tool, args) => {
274
278
  let preview = '';
@@ -680,12 +684,14 @@ class App {
680
684
  const { rows, cols } = (0, terminal_1.getTerminalSize)();
681
685
  const info = {
682
686
  status: this.status,
683
- statusColor: this.statusColor,
687
+ statusColor: (0, theme_1.statusColor)(this.status),
684
688
  model: this.agent.config.model,
685
689
  provider: this.agent.config.provider,
686
690
  project: this.agent.cwd,
687
691
  connected: this.connected,
688
692
  mode: this.agent.config.permissionMode,
693
+ spinner: this.spinner.frame(),
694
+ running: this.running,
689
695
  };
690
696
  const parts = [];
691
697
  parts.push((0, statusbar_1.renderHeader)(cols, info));
@@ -704,7 +710,7 @@ class App {
704
710
  }
705
711
  // Separator
706
712
  const sepRow = chatTop + chatH;
707
- parts.push((0, terminal_1.cursorTo)(sepRow, 1) + terminal_1.ANSI.clearLineEnd + terminal_1.ANSI.dim + terminal_1.BOX.h.repeat(Math.min(cols, 120)) + terminal_1.ANSI.reset);
713
+ 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);
708
714
  // Input
709
715
  const inputRow = sepRow + 1;
710
716
  const rendered = this.input.render(cols);
@@ -712,7 +718,7 @@ class App {
712
718
  const cursorCol = rendered.cursorCol + 1;
713
719
  // Status bar
714
720
  const statusRow = inputRow + 1;
715
- parts.push((0, terminal_1.cursorTo)(statusRow, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderStatusBar)(info, cols));
721
+ parts.push((0, terminal_1.cursorTo)(statusRow, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderStatusBar)(cols, info));
716
722
  // Modal overlay
717
723
  if (this.modal.type !== 'none') {
718
724
  const overlay = this.renderModal(rows, cols);
@@ -867,7 +873,11 @@ class App {
867
873
  ],
868
874
  }, rows, cols);
869
875
  case 'confirm':
870
- return (0, components_1.renderModal)({ title: m.title, body: m.body }, rows, cols);
876
+ return (0, components_1.renderConfirm)({
877
+ title: m.title,
878
+ body: m.body,
879
+ focused: 'confirm',
880
+ }, rows, cols);
871
881
  case 'apikey':
872
882
  return (0, components_1.renderModal)({
873
883
  title: `API key for ${m.provider}`,
package/dist/tui/chat.js CHANGED
@@ -7,15 +7,15 @@ exports.clampScroll = clampScroll;
7
7
  * Chat view — renders conversation messages with scrolling.
8
8
  * Message types: user, assistant (streamed), tool, system/status, plan.
9
9
  */
10
- const terminal_1 = require("../utils/terminal");
10
+ const theme_1 = require("./theme");
11
11
  const KIND_STYLE = {
12
- user: { prefix: 'YOU', color: terminal_1.ANSI.brightBlue },
13
- assistant: { prefix: 'VECTOR', color: terminal_1.ANSI.brightGreen },
14
- tool: { prefix: 'TOOL', color: terminal_1.ANSI.brightYellow },
15
- system: { prefix: '···', color: terminal_1.ANSI.gray },
16
- error: { prefix: '✗', color: terminal_1.ANSI.brightRed },
17
- success: { prefix: '✓', color: terminal_1.ANSI.brightGreen },
18
- warning: { prefix: '⚠', color: terminal_1.ANSI.brightYellow },
12
+ user: { prefix: 'YOU', color: theme_1.THEME.info },
13
+ assistant: { prefix: 'VECTOR', color: theme_1.THEME.accent },
14
+ tool: { prefix: 'TOOL', color: theme_1.THEME.warn },
15
+ system: { prefix: '···', color: theme_1.THEME.muted },
16
+ error: { prefix: '✗', color: theme_1.THEME.error },
17
+ success: { prefix: '✓', color: theme_1.THEME.success },
18
+ warning: { prefix: '⚠', color: theme_1.THEME.warn },
19
19
  };
20
20
  function plainLines(text, width) {
21
21
  const lines = text.split('\n');
@@ -37,29 +37,15 @@ function renderMessages(messages, width) {
37
37
  for (const m of messages) {
38
38
  const style = KIND_STYLE[m.kind];
39
39
  const prefix = m.prefix || style.prefix;
40
- const header = `${style.color}${terminal_1.ANSI.bold}${prefix}${terminal_1.ANSI.reset}${m.streaming ? terminal_1.ANSI.blink + ' ▍' + terminal_1.ANSI.reset : ''}`;
41
- const headerLine = m.tool ? `${header} ${terminal_1.ANSI.dim}${m.tool}${terminal_1.ANSI.reset}` : header;
42
- out.push(headerLine);
40
+ const dot = m.streaming ? `${theme_1.THEME.warn}◐${theme_1.THEME.reset} ` : `${style.color}●${theme_1.THEME.reset} `;
41
+ const header = `${dot}${theme_1.THEME.bold}${style.color}${prefix}${theme_1.THEME.reset}`;
42
+ const meta = m.meta ? ` ${theme_1.THEME.dim}${m.meta}${theme_1.THEME.reset}` : '';
43
+ const toolName = m.tool ? ` ${theme_1.THEME.warn}${m.tool}${theme_1.THEME.reset}` : '';
44
+ out.push(header + toolName + meta);
43
45
  const body = m.content || '(no content)';
46
+ const bar = `${style.color}│${theme_1.THEME.reset} `;
44
47
  for (const line of plainLines(body, width)) {
45
- if (m.kind === 'user') {
46
- out.push(`${terminal_1.ANSI.brightBlue}│ ${line}${terminal_1.ANSI.reset}`);
47
- }
48
- else if (m.kind === 'tool') {
49
- out.push(`${terminal_1.ANSI.brightYellow}│ ${line}${terminal_1.ANSI.reset}`);
50
- }
51
- else if (m.kind === 'error') {
52
- out.push(`${terminal_1.ANSI.brightRed}│ ${line}${terminal_1.ANSI.reset}`);
53
- }
54
- else if (m.kind === 'success') {
55
- out.push(`${terminal_1.ANSI.brightGreen}│ ${line}${terminal_1.ANSI.reset}`);
56
- }
57
- else if (m.kind === 'warning') {
58
- out.push(`${terminal_1.ANSI.brightYellow}│ ${line}${terminal_1.ANSI.reset}`);
59
- }
60
- else {
61
- out.push(` ${line}`);
62
- }
48
+ out.push(`${bar}${line}`);
63
49
  }
64
50
  out.push(''); // blank line between messages
65
51
  }
@@ -2,10 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderModal = renderModal;
4
4
  exports.renderSelector = renderSelector;
5
+ exports.renderConfirm = renderConfirm;
5
6
  /**
6
7
  * Reusable TUI components — modal overlay and list selector.
7
8
  */
8
9
  const terminal_1 = require("../utils/terminal");
10
+ const theme_1 = require("./theme");
9
11
  /** Render a centered modal box. Returns the box's top row (1-based). */
10
12
  function renderModal(opts, rows, cols) {
11
13
  const title = opts.title;
@@ -15,7 +17,7 @@ function renderModal(opts, rows, cols) {
15
17
  const height = Math.min(rows - 2, bodyLines.length + footerLines.length + 4);
16
18
  const top = Math.max(1, Math.floor((rows - height) / 2));
17
19
  const left = Math.max(1, Math.floor((cols - innerWidth - 2) / 2));
18
- const borderColor = opts.danger ? terminal_1.ANSI.brightRed : terminal_1.ANSI.cyan;
20
+ const borderColor = opts.danger ? theme_1.THEME.error : theme_1.THEME.borderBright;
19
21
  const lines = [];
20
22
  const topBorder = borderColor + terminal_1.BOX.tl + terminal_1.BOX.h.repeat(innerWidth) + terminal_1.BOX.tr;
21
23
  const bottomBorder = borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(innerWidth) + terminal_1.BOX.br;
@@ -23,19 +25,19 @@ function renderModal(opts, rows, cols) {
23
25
  const t = (0, terminal_1.pad)(title.slice(0, innerWidth - 2), innerWidth);
24
26
  lines.push((0, terminal_1.cursorTo)(top, left) +
25
27
  topBorder.slice(0, 2) +
26
- terminal_1.ANSI.bold + terminal_1.ANSI.white + t + terminal_1.ANSI.reset + borderColor +
28
+ theme_1.THEME.bold + theme_1.THEME.textBright + t + theme_1.THEME.reset + borderColor +
27
29
  topBorder.slice(2));
28
30
  const maxBody = height - footerLines.length - 2;
29
31
  for (let i = 0; i < maxBody; i++) {
30
32
  const content = bodyLines[i] ?? '';
31
33
  const lineStr = (0, terminal_1.pad)(content, innerWidth);
32
- lines.push((0, terminal_1.cursorTo)(top + 1 + i, left) + borderColor + terminal_1.BOX.v + ' ' + lineStr + terminal_1.ANSI.reset + borderColor + ' ' + terminal_1.BOX.v);
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);
33
35
  }
34
36
  for (let i = 0; i < footerLines.length; i++) {
35
37
  const row = top + 1 + maxBody + i;
36
38
  const content = footerLines[i] ?? '';
37
39
  const lineStr = (0, terminal_1.pad)(content, innerWidth);
38
- lines.push((0, terminal_1.cursorTo)(row, left) + borderColor + terminal_1.BOX.v + ' ' + terminal_1.ANSI.dim + lineStr + terminal_1.ANSI.reset + borderColor + ' ' + terminal_1.BOX.v);
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);
39
41
  }
40
42
  lines.push((0, terminal_1.cursorTo)(top + height - 1, left) + bottomBorder);
41
43
  return { top, lines };
@@ -79,7 +81,28 @@ function renderSelector(opts) {
79
81
  const footerText = opts.footer ? opts.footer : '↑↓ navigate · Enter select · Esc cancel';
80
82
  const f = (0, terminal_1.pad)(footerText.slice(0, innerWidth - 2), innerWidth);
81
83
  lines.push((0, terminal_1.cursorTo)(top + visible + 1, left) +
82
- borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(2) + terminal_1.ANSI.dim + f + terminal_1.ANSI.reset + borderColor +
84
+ borderColor + terminal_1.BOX.bl + terminal_1.BOX.h.repeat(2) + theme_1.THEME.dim + f + theme_1.THEME.reset + borderColor +
83
85
  terminal_1.BOX.h.repeat(Math.max(0, innerWidth - 2 - (0, terminal_1.visibleWidth)(f))) + terminal_1.BOX.br);
84
86
  return { top, lines };
85
87
  }
88
+ /**
89
+ * Render a confirm dialog with a focused button ([OK] / [Cancel]).
90
+ * Body lines may contain ANSI codes; width is computed from content.
91
+ */
92
+ function renderConfirm(opts, rows, cols) {
93
+ const confirmLabel = opts.confirmLabel || 'OK';
94
+ const cancelLabel = opts.cancelLabel || 'Cancel';
95
+ const focused = opts.focused || 'confirm';
96
+ const confirmBtn = focused === 'confirm'
97
+ ? `${theme_1.THEME.bgPanel}${theme_1.THEME.bold}${opts.danger ? theme_1.THEME.error : theme_1.THEME.accent} ${confirmLabel} ${theme_1.THEME.reset}`
98
+ : `${theme_1.THEME.faint} ${confirmLabel} ${theme_1.THEME.reset}`;
99
+ const cancelBtn = focused === 'cancel'
100
+ ? `${theme_1.THEME.bgPanel}${theme_1.THEME.bold}${theme_1.THEME.muted} ${cancelLabel} ${theme_1.THEME.reset}`
101
+ : `${theme_1.THEME.faint} ${cancelLabel} ${theme_1.THEME.reset}`;
102
+ const body = [...opts.body, '', `${confirmBtn} ${cancelBtn}`];
103
+ return renderModal({
104
+ title: opts.title,
105
+ body,
106
+ danger: opts.danger,
107
+ }, rows, cols);
108
+ }
package/dist/tui/diff.js CHANGED
@@ -4,44 +4,105 @@ exports.diffLines = diffLines;
4
4
  exports.computeDiff = computeDiff;
5
5
  exports.renderDiffModal = renderDiffModal;
6
6
  /**
7
- * Diff viewer — renders unified diffs with color, plus a
8
- * modal with Accept / Reject actions.
7
+ * Diff viewer — renders unified diffs with hunk headers and color,
8
+ * plus a modal with Accept / Reject actions.
9
9
  */
10
- const terminal_1 = require("../utils/terminal");
10
+ const theme_1 = require("./theme");
11
11
  const components_1 = require("./components");
12
- /** Compute a minimal unified diff between two strings (line-based). */
12
+ /**
13
+ * Compute a line-based unified diff with hunk headers (@@ -x,y +x,y @@).
14
+ * Uses LCS for small inputs; falls back to a full replace view for large ones.
15
+ */
13
16
  function diffLines(oldText, newText) {
14
- return computeDiff(oldText, newText);
17
+ return computeDiff(oldText, newText).map((l) => l.text);
15
18
  }
16
19
  function computeDiff(oldText, newText) {
17
- const oldLines = (oldText || '').split('\n');
18
- const newLines = (newText || '').split('\n');
19
- // Simple LCS-based diff for small files; falls back to full replace view.
20
+ const oldLines = splitLines(oldText);
21
+ const newLines = splitLines(newText);
22
+ // Large inputs: LCS is O(n*m); fall back to a simple full-replace view.
20
23
  if (oldLines.length * newLines.length > 250_000) {
21
- const out = ['- (old) file content:'];
24
+ const out = [];
25
+ out.push({ type: 'file', text: `- old (${oldLines.length} lines)` });
22
26
  for (const l of oldLines)
23
- out.push(`- ${l}`);
24
- out.push('+ (new) file content:');
27
+ out.push({ type: 'del', text: `-${l}` });
28
+ out.push({ type: 'file', text: `+ new (${newLines.length} lines)` });
25
29
  for (const l of newLines)
26
- out.push(`+ ${l}`);
30
+ out.push({ type: 'add', text: `+${l}` });
27
31
  return out;
28
32
  }
29
33
  const ops = lcsDiff(oldLines, newLines);
30
34
  const out = [];
35
+ // Rolling window of up to 3 context lines before a change.
36
+ const pendingContext = [];
37
+ // Current hunk buffer + its start positions.
38
+ let hunk = [];
39
+ let hunkOldStart = 0;
40
+ let hunkNewStart = 0;
41
+ // How many context lines currently trail the changes inside the hunk.
42
+ let trailingContext = 0;
43
+ let oldLine = 1;
44
+ let newLine = 1;
45
+ const flushHunk = () => {
46
+ if (hunk.length === 0)
47
+ return;
48
+ const oldCount = hunk.filter((l) => l.type !== 'add').length;
49
+ const newCount = hunk.filter((l) => l.type !== 'del').length;
50
+ out.push({ type: 'hunk', text: `@@ -${hunkOldStart},${oldCount} +${hunkNewStart},${newCount} @@` });
51
+ for (const l of hunk)
52
+ out.push(l);
53
+ hunk = [];
54
+ trailingContext = 0;
55
+ };
31
56
  for (const op of ops) {
32
- if (op.type === 'same')
33
- out.push(` ${op.line}`);
34
- else if (op.type === 'del')
35
- out.push(`${terminal_1.ANSI.red}- ${op.line}${terminal_1.ANSI.reset}`);
57
+ if (op.type === 'same') {
58
+ if (hunk.length > 0 && trailingContext < 3) {
59
+ hunk.push({ type: 'same', text: ` ${op.line}` });
60
+ trailingContext++;
61
+ oldLine++;
62
+ newLine++;
63
+ continue;
64
+ }
65
+ if (hunk.length > 0)
66
+ flushHunk();
67
+ pendingContext.push({ type: 'same', text: ` ${op.line}` });
68
+ if (pendingContext.length > 3) {
69
+ const dropped = pendingContext.shift();
70
+ if (dropped)
71
+ out.push(dropped);
72
+ }
73
+ oldLine++;
74
+ newLine++;
75
+ continue;
76
+ }
77
+ // A change: start or continue the hunk.
78
+ if (hunk.length === 0) {
79
+ hunkOldStart = oldLine - pendingContext.length;
80
+ hunkNewStart = newLine - pendingContext.length;
81
+ hunk = [...pendingContext];
82
+ pendingContext.length = 0;
83
+ }
84
+ hunk.push(op.type === 'del' ? { type: 'del', text: `-${op.line}` } : { type: 'add', text: `+${op.line}` });
85
+ trailingContext = 0;
86
+ if (op.type === 'del')
87
+ oldLine++;
36
88
  else
37
- out.push(`${terminal_1.ANSI.green}+ ${op.line}${terminal_1.ANSI.reset}`);
89
+ newLine++;
90
+ }
91
+ flushHunk();
92
+ if (out.length === 0) {
93
+ out.push({ type: 'file', text: '(no changes)' });
38
94
  }
39
95
  return out;
40
96
  }
97
+ function splitLines(text) {
98
+ const lines = (text || '').split('\n');
99
+ if (text.endsWith('\n'))
100
+ lines.pop();
101
+ return lines;
102
+ }
41
103
  function lcsDiff(a, b) {
42
104
  const n = a.length;
43
105
  const m = b.length;
44
- // DP table
45
106
  const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
46
107
  for (let i = n - 1; i >= 0; i--) {
47
108
  for (let j = m - 1; j >= 0; j--) {
@@ -79,14 +140,26 @@ function lcsDiff(a, b) {
79
140
  }
80
141
  return out;
81
142
  }
143
+ function renderDiffLine(l) {
144
+ if (l.type === 'hunk')
145
+ return `${theme_1.THEME.accentDim}${theme_1.THEME.bold}${l.text}${theme_1.THEME.reset}`;
146
+ if (l.type === 'file')
147
+ return `${theme_1.THEME.gold}${theme_1.THEME.bold}${l.text}${theme_1.THEME.reset}`;
148
+ if (l.type === 'del')
149
+ return `${theme_1.THEME.error}${l.text}${theme_1.THEME.reset}`;
150
+ if (l.type === 'add')
151
+ return `${theme_1.THEME.success}${l.text}${theme_1.THEME.reset}`;
152
+ return `${theme_1.THEME.faint}${l.text}${theme_1.THEME.reset}`;
153
+ }
82
154
  /** Render the diff modal with a header per file. */
83
155
  function renderDiffModal(entries, rows, cols, scroll, selectedAccept) {
84
156
  const innerWidth = Math.max(20, cols - 4);
85
157
  const body = [];
86
158
  for (const e of entries) {
87
- body.push(`${terminal_1.ANSI.bold}${terminal_1.ANSI.brightCyan}── ${e.path}${terminal_1.ANSI.reset}`);
88
- const diffLines = computeDiff(e.oldContent || '', e.newContent);
89
- body.push(...diffLines);
159
+ body.push(`${theme_1.THEME.bold}${theme_1.THEME.accent}── ${e.path}${theme_1.THEME.reset}`);
160
+ for (const l of computeDiff(e.oldContent || '', e.newContent)) {
161
+ body.push(renderDiffLine(l));
162
+ }
90
163
  body.push('');
91
164
  }
92
165
  // Scroll window
@@ -94,7 +167,9 @@ function renderDiffModal(entries, rows, cols, scroll, selectedAccept) {
94
167
  const maxScroll = Math.max(0, body.length - visibleLines);
95
168
  const s = Math.min(scroll, maxScroll);
96
169
  const windowLines = body.slice(s, s + visibleLines);
97
- const footer = `${selectedAccept === 'accept' ? terminal_1.ANSI.bold + terminal_1.ANSI.green + '[Accept]' + terminal_1.ANSI.reset : '[Accept]'} ${selectedAccept === 'reject' ? terminal_1.ANSI.bold + terminal_1.ANSI.red + '[Reject]' + terminal_1.ANSI.reset : '[Reject]'} ↑↓ scroll · ←→ choose · Enter confirm · Esc cancel`;
170
+ const accept = selectedAccept === 'accept' ? `${theme_1.THEME.bold}${theme_1.THEME.success}[Accept]${theme_1.THEME.reset}` : `[Accept]`;
171
+ const reject = selectedAccept === 'reject' ? `${theme_1.THEME.bold}${theme_1.THEME.error}[Reject]${theme_1.THEME.reset}` : `[Reject]`;
172
+ const footer = `${accept} ${reject} ↑↓ scroll · ←→ choose · Enter confirm · Esc cancel`;
98
173
  const modal = (0, components_1.renderModal)({
99
174
  title: `Diff — ${entries.length} file(s)`,
100
175
  body: windowLines,
@@ -1,32 +1,47 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.renderStatusBar = renderStatusBar;
4
3
  exports.renderHeader = renderHeader;
5
4
  exports.renderProjectBar = renderProjectBar;
5
+ exports.renderStatusBar = renderStatusBar;
6
6
  /**
7
- * Status bar — bottom hint bar + top header rendering.
7
+ * Status bar — top header + project bar + bottom hint bar.
8
+ * Uses the VECTOR theme with a live spinner while busy.
8
9
  */
9
10
  const terminal_1 = require("../utils/terminal");
10
- function renderStatusBar(info, width) {
11
- const left = `ENTER Send · TAB Commands · CTRL+C Stop · ? Help`;
12
- const right = `${info.connected ? terminal_1.ANSI.green + '●' : terminal_1.ANSI.red + '○'}${terminal_1.ANSI.reset} ${info.mode === 'yolo' ? terminal_1.ANSI.yellow + 'YOLO' + terminal_1.ANSI.reset : 'ask'}`;
13
- const leftWidth = width - right.length - 1;
14
- return terminal_1.ANSI.bgBlack + (0, terminal_1.pad)(left.slice(0, Math.max(0, leftWidth)), Math.max(0, leftWidth)) + right + terminal_1.ANSI.reset;
15
- }
11
+ const theme_1 = require("./theme");
12
+ /** Render the top header line: VECTOR status .... provider · model */
16
13
  function renderHeader(width, info) {
17
- const brand = `${terminal_1.ANSI.bold}${terminal_1.ANSI.brightCyan}VECTOR${terminal_1.ANSI.reset}`;
18
- const status = `${info.statusColor}●${terminal_1.ANSI.reset} ${info.status}`;
14
+ const brand = `${theme_1.THEME.bold}${theme_1.THEME.accent}VECTOR${theme_1.THEME.reset}`;
15
+ const dot = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
16
+ const status = `${dot} ${(0, theme_1.statusColor)(info.status)}${info.status}${theme_1.THEME.reset}`;
19
17
  const left = `${brand} ${status}`;
20
- const right = `${terminal_1.ANSI.dim}${info.provider}${terminal_1.ANSI.reset} · ${info.model}`;
21
- const rightWidth = visible(right);
18
+ const provider = `${theme_1.THEME.dim}${info.provider}${theme_1.THEME.reset}`;
19
+ const model = `${theme_1.THEME.accentDim}${info.model}${theme_1.THEME.reset}`;
20
+ const right = `${provider} · ${model}`;
21
+ const rightWidth = (0, terminal_1.stripAnsi)(right).length;
22
22
  const leftWidth = Math.max(0, width - rightWidth - 2);
23
23
  const line = (0, terminal_1.pad)(left, leftWidth) + ' ' + right;
24
- return terminal_1.ANSI.bgBlack + (0, terminal_1.pad)(line, width) + terminal_1.ANSI.reset;
24
+ return theme_1.THEME.bgBar + (0, terminal_1.pad)(line, width) + theme_1.THEME.reset;
25
25
  }
26
+ /** Render the project line: Project: <path> .... Mode: ask|yolo */
26
27
  function renderProjectBar(width, info) {
27
- const line = `${terminal_1.ANSI.dim}Project:${terminal_1.ANSI.reset} ${info.project} ${terminal_1.ANSI.dim}Mode:${terminal_1.ANSI.reset} ${info.mode === 'yolo' ? terminal_1.ANSI.yellow + 'yolo' + terminal_1.ANSI.reset : 'ask'}`;
28
- return terminal_1.ANSI.bgBlack + (0, terminal_1.pad)(line, width) + terminal_1.ANSI.reset;
28
+ const label = `${theme_1.THEME.dim}Project:${theme_1.THEME.reset} ${theme_1.THEME.text}${info.project}${theme_1.THEME.reset}`;
29
+ const modeColor = info.mode === 'yolo' ? theme_1.THEME.warn : theme_1.THEME.muted;
30
+ const mode = `${theme_1.THEME.dim}Mode:${theme_1.THEME.reset} ${modeColor}${info.mode}${theme_1.THEME.reset}`;
31
+ const line = `${label} ${mode}`;
32
+ return theme_1.THEME.bgBar + (0, terminal_1.pad)(line, width) + theme_1.THEME.reset;
29
33
  }
30
- function visible(s) {
31
- return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').length;
34
+ /** Render the bottom hint/status bar with live spinner. */
35
+ function renderStatusBar(width, info) {
36
+ 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}`;
37
+ let right = '';
38
+ if (info.running && info.spinner) {
39
+ right = `${(0, theme_1.statusColor)(info.status)}${info.spinner}${theme_1.THEME.reset} `;
40
+ }
41
+ const conn = info.connected ? `${theme_1.THEME.success}●${theme_1.THEME.reset}` : `${theme_1.THEME.error}○${theme_1.THEME.reset}`;
42
+ const mode = info.mode === 'yolo' ? `${theme_1.THEME.warn}YOLO${theme_1.THEME.reset}` : `${theme_1.THEME.muted}ask${theme_1.THEME.reset}`;
43
+ right += `${conn} ${mode}`;
44
+ const rightWidth = (0, terminal_1.stripAnsi)(right).length;
45
+ const leftWidth = Math.max(0, width - rightWidth - 1);
46
+ return theme_1.THEME.bgBar + (0, terminal_1.pad)(left, leftWidth) + ' ' + right + theme_1.THEME.reset;
32
47
  }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Spinner = exports.SPINNERS = exports.THEME = void 0;
4
+ exports.statusColor = statusColor;
5
+ /**
6
+ * VECTOR theme — consistent premium palette for the TUI.
7
+ * Dark cyber palette with cyan accent + gold highlights.
8
+ */
9
+ exports.THEME = {
10
+ accent: '\x1b[38;5;51m', // bright cyan
11
+ accentDim: '\x1b[38;5;37m',
12
+ gold: '\x1b[38;5;220m',
13
+ text: '\x1b[38;5;252m',
14
+ textBright: '\x1b[38;5;255m',
15
+ muted: '\x1b[38;5;244m',
16
+ faint: '\x1b[38;5;240m',
17
+ border: '\x1b[38;5;66m',
18
+ borderBright: '\x1b[38;5;51m',
19
+ success: '\x1b[38;5;114m',
20
+ warn: '\x1b[38;5;215m',
21
+ error: '\x1b[38;5;203m',
22
+ info: '\x1b[38;5;111m',
23
+ bgBar: '\x1b[48;5;235m',
24
+ bgPanel: '\x1b[48;5;236m',
25
+ bold: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ reset: '\x1b[0m',
28
+ };
29
+ /** Status → color mapping used by the header/statusbar. */
30
+ function statusColor(status) {
31
+ const s = status.toLowerCase();
32
+ if (s.includes('error') || s.includes('fail') || s.includes('✗'))
33
+ return exports.THEME.error;
34
+ if (s.includes('warn') || s.includes('permission') || s.includes('⚠'))
35
+ return exports.THEME.warn;
36
+ if (s.includes('completed') || s.includes('success') || s.includes('✓'))
37
+ return exports.THEME.success;
38
+ if (s.includes('ready'))
39
+ return exports.THEME.success;
40
+ if (s.includes('stopped') || s.includes('cancel'))
41
+ return exports.THEME.muted;
42
+ if (s.includes('thinking') || s.includes('plan'))
43
+ return exports.THEME.info;
44
+ if (s.includes('run') || s.includes('exec'))
45
+ return exports.THEME.accent;
46
+ return exports.THEME.info;
47
+ }
48
+ /** Predefined spinner frame sets. */
49
+ exports.SPINNERS = {
50
+ dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
51
+ line: ['─', '╌', '╍', '═', '╍', '╌'],
52
+ arrows: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
53
+ pulse: ['●', '◐', '◑', '○'],
54
+ };
55
+ /** A frame-advancing spinner (call frame() on each render tick). */
56
+ class Spinner {
57
+ frames;
58
+ index = 0;
59
+ ticks = 0;
60
+ constructor(frames = exports.SPINNERS.dots) {
61
+ this.frames = frames;
62
+ }
63
+ /** Advance and return the current frame. */
64
+ frame() {
65
+ // Advance roughly every 3rd call for a calm ~1.5s cycle at 50ms ticks
66
+ this.ticks++;
67
+ if (this.ticks % 3 === 0)
68
+ this.index = (this.index + 1) % this.frames.length;
69
+ return this.frames[this.index];
70
+ }
71
+ reset() {
72
+ this.index = 0;
73
+ this.ticks = 0;
74
+ }
75
+ }
76
+ exports.Spinner = Spinner;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agen-vektor",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "VECTOR (agen-vektor) — AI Coding Agent CLI/TUI for Linux & Termux. Multi-provider, tool calling, session, permission system.",
5
5
  "type": "commonjs",
6
6
  "bin": {