agen-vektor 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/agent/agent.js +90 -0
  4. package/dist/agent/context.js +106 -0
  5. package/dist/agent/loop.js +152 -0
  6. package/dist/agent/memory.js +87 -0
  7. package/dist/agent/planner.js +38 -0
  8. package/dist/agent/prompts.js +41 -0
  9. package/dist/agent/session.js +125 -0
  10. package/dist/cli/index.js +304 -0
  11. package/dist/cli/keyboard.js +283 -0
  12. package/dist/config/config.js +92 -0
  13. package/dist/config/credentials.js +127 -0
  14. package/dist/config/models.js +47 -0
  15. package/dist/providers/anthropic.js +276 -0
  16. package/dist/providers/custom.js +24 -0
  17. package/dist/providers/factory.js +58 -0
  18. package/dist/providers/gemini.js +223 -0
  19. package/dist/providers/ollama.js +195 -0
  20. package/dist/providers/openai-compat.js +296 -0
  21. package/dist/providers/openai.js +17 -0
  22. package/dist/providers/openrouter.js +23 -0
  23. package/dist/providers/provider.js +39 -0
  24. package/dist/security/command-policy.js +99 -0
  25. package/dist/security/permissions.js +66 -0
  26. package/dist/tools/filesystem.js +297 -0
  27. package/dist/tools/git.js +66 -0
  28. package/dist/tools/registry.js +39 -0
  29. package/dist/tools/search.js +184 -0
  30. package/dist/tools/shell.js +108 -0
  31. package/dist/tools/web.js +76 -0
  32. package/dist/tui/app.js +913 -0
  33. package/dist/tui/chat.js +76 -0
  34. package/dist/tui/components.js +85 -0
  35. package/dist/tui/diff.js +105 -0
  36. package/dist/tui/input.js +121 -0
  37. package/dist/tui/statusbar.js +32 -0
  38. package/dist/utils/logger.js +96 -0
  39. package/dist/utils/paths.js +90 -0
  40. package/dist/utils/terminal.js +191 -0
  41. package/package.json +56 -0
@@ -0,0 +1,913 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.App = void 0;
37
+ /**
38
+ * TUI App — the main interactive loop.
39
+ *
40
+ * Layout (top to bottom):
41
+ * header VECTOR ● status provider · model
42
+ * project bar Project: ~/x Mode: ask
43
+ * ───────────────────────────────────────────────
44
+ * chat scrollable message area
45
+ * ───────────────────────────────────────────────
46
+ * input ❯ _
47
+ * statusbar ENTER Send · TAB Commands · CTRL+C Stop · ? Help
48
+ *
49
+ * Modals (diff, selector, help, permission) overlay the chat area.
50
+ */
51
+ const terminal_1 = require("../utils/terminal");
52
+ const chat_1 = require("./chat");
53
+ const input_1 = require("./input");
54
+ const statusbar_1 = require("./statusbar");
55
+ const components_1 = require("./components");
56
+ const diff_1 = require("./diff");
57
+ const planner_1 = require("../agent/planner");
58
+ const session_1 = require("../agent/session");
59
+ const factory_1 = require("../providers/factory");
60
+ const models_1 = require("../config/models");
61
+ const config_1 = require("../config/config");
62
+ const credentials_1 = require("../config/credentials");
63
+ class App {
64
+ agent;
65
+ opts;
66
+ messages = [];
67
+ input = new input_1.InputBox();
68
+ scroll = 0;
69
+ status = 'Ready';
70
+ statusColor = terminal_1.ANSI.green;
71
+ connected = false;
72
+ running = false;
73
+ exitRequested = false;
74
+ modal = { type: 'none' };
75
+ sessionName;
76
+ pendingPrompt = '';
77
+ planToExecute = null;
78
+ abortController = null;
79
+ constructor(agent, opts) {
80
+ this.agent = agent;
81
+ this.opts = opts;
82
+ this.sessionName = opts.sessionName || null;
83
+ if (opts.askPermission) {
84
+ agent.setPermissionAsk(opts.askPermission);
85
+ }
86
+ }
87
+ // ─── Public API ───────────────────────────────────────────────
88
+ start() {
89
+ this.connected = true;
90
+ this.status = 'Ready';
91
+ this.addSystem('VECTOR ready. Type a request or press ? for help.');
92
+ if (this.opts.continueSession && this.sessionName) {
93
+ const sess = (0, session_1.loadSession)(this.sessionName);
94
+ if (sess) {
95
+ this.addSystem(`Continuing session "${this.sessionName}" (${sess.messages.length} messages)`);
96
+ }
97
+ else {
98
+ this.addSystem(`No saved session "${this.sessionName}" — starting fresh.`);
99
+ }
100
+ }
101
+ if (this.opts.initialPrompt) {
102
+ // Defer to after first render
103
+ this.pendingPrompt = this.opts.initialPrompt;
104
+ }
105
+ }
106
+ async handleKey(ev) {
107
+ if (this.modal.type !== 'none') {
108
+ await this.handleModalKey(ev);
109
+ return;
110
+ }
111
+ if (this.running) {
112
+ // Only allow stop while running
113
+ if (ev.name === 'ctrl_c' || ev.name === 'escape') {
114
+ this.abortController?.abort();
115
+ this.status = 'Stopping…';
116
+ this.statusColor = terminal_1.ANSI.yellow;
117
+ this.running = false;
118
+ }
119
+ return;
120
+ }
121
+ switch (ev.name) {
122
+ case 'char':
123
+ if (ev.char === '?')
124
+ this.modal = { type: 'help' };
125
+ else if (ev.char)
126
+ this.input.insert(ev.char);
127
+ break;
128
+ case 'enter': {
129
+ const text = this.input.value.trim();
130
+ if (text) {
131
+ this.input.pushHistory(text);
132
+ this.input.clear();
133
+ await this.submit(text);
134
+ }
135
+ break;
136
+ }
137
+ case 'backspace':
138
+ this.input.backspace();
139
+ break;
140
+ case 'delete':
141
+ this.input.deleteForward();
142
+ break;
143
+ case 'left':
144
+ this.input.moveLeft();
145
+ break;
146
+ case 'right':
147
+ this.input.moveRight();
148
+ break;
149
+ case 'home':
150
+ this.input.moveHome();
151
+ break;
152
+ case 'end':
153
+ this.input.moveEnd();
154
+ break;
155
+ case 'up':
156
+ this.input.historyPrev();
157
+ break;
158
+ case 'down':
159
+ this.input.historyNext();
160
+ break;
161
+ case 'pageup':
162
+ this.scroll = (0, chat_1.clampScroll)(this.scroll - 10, this.messages, this.cols(), this.chatHeight());
163
+ break;
164
+ case 'pagedown':
165
+ this.scroll = (0, chat_1.clampScroll)(this.scroll + 10, this.messages, this.cols(), this.chatHeight());
166
+ break;
167
+ case 'ctrl_u':
168
+ this.input.deleteToStart();
169
+ break;
170
+ case 'ctrl_w':
171
+ this.input.deleteWordBack();
172
+ break;
173
+ case 'ctrl_a':
174
+ this.input.moveHome();
175
+ break;
176
+ case 'ctrl_e':
177
+ this.input.moveEnd();
178
+ break;
179
+ case 'ctrl_l':
180
+ this.messages = [];
181
+ this.scroll = 0;
182
+ this.addSystem('Conversation cleared.');
183
+ break;
184
+ case 'tab':
185
+ this.modal = { type: 'commands' };
186
+ break;
187
+ case 'ctrl_c':
188
+ this.exitRequested = true;
189
+ break;
190
+ case 'escape':
191
+ this.input.clear();
192
+ break;
193
+ default:
194
+ break;
195
+ }
196
+ }
197
+ async tick() {
198
+ if (this.pendingPrompt) {
199
+ const p = this.pendingPrompt;
200
+ this.pendingPrompt = '';
201
+ await this.submit(p);
202
+ }
203
+ if (this.planToExecute) {
204
+ const plan = this.planToExecute;
205
+ this.planToExecute = null;
206
+ await this.executeWithPlan(plan);
207
+ }
208
+ if (this.exitRequested) {
209
+ await this.shutdown();
210
+ }
211
+ }
212
+ // ─── Submission ───────────────────────────────────────────────
213
+ async submit(text) {
214
+ if (text.startsWith('/')) {
215
+ await this.handleCommand(text);
216
+ return;
217
+ }
218
+ this.messages.push({ kind: 'user', content: text });
219
+ this.scroll = 0;
220
+ this.running = true;
221
+ this.status = 'Thinking';
222
+ this.statusColor = terminal_1.ANSI.yellow;
223
+ this.abortController = new AbortController();
224
+ // Show plan first if requested
225
+ if (this.opts.showPlan) {
226
+ this.status = 'Planning…';
227
+ try {
228
+ const plan = await (0, planner_1.generatePlan)(this.agent.provider, text, this.agent.cwd, this.agent.config.model, this.abortController.signal);
229
+ if (plan.steps.length > 0) {
230
+ this.modal = {
231
+ type: 'plan',
232
+ plan,
233
+ resolve: async (ok) => {
234
+ if (ok) {
235
+ this.planToExecute = plan;
236
+ }
237
+ else {
238
+ this.running = false;
239
+ this.status = 'Ready';
240
+ this.statusColor = terminal_1.ANSI.green;
241
+ this.addSystem('Plan cancelled.');
242
+ }
243
+ },
244
+ };
245
+ return;
246
+ }
247
+ }
248
+ catch {
249
+ /* fall through to direct execution */
250
+ }
251
+ }
252
+ await this.runTask(text);
253
+ }
254
+ async runTask(text, planSteps) {
255
+ const task = planSteps
256
+ ? `TASK: ${text}\n\nFollow this plan:\n${planSteps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`
257
+ : text;
258
+ const msgIndex = this.messages.push({
259
+ kind: 'assistant',
260
+ content: '',
261
+ streaming: true,
262
+ }) - 1;
263
+ this.agent.setCallbacks({
264
+ onDelta: (delta) => {
265
+ const m = this.messages[msgIndex];
266
+ if (m)
267
+ m.content += delta;
268
+ },
269
+ onStatus: (status) => {
270
+ this.status = status;
271
+ this.statusColor = status.startsWith('✗') || status.includes('error') || status.includes('Error') ? terminal_1.ANSI.red : terminal_1.ANSI.yellow;
272
+ },
273
+ onToolCall: (tool, args) => {
274
+ let preview = '';
275
+ try {
276
+ const parsed = JSON.parse(args);
277
+ preview = Object.entries(parsed)
278
+ .map(([k, v]) => `${k}=${String(v).slice(0, 60)}`)
279
+ .join(' ');
280
+ }
281
+ catch {
282
+ preview = args.slice(0, 80);
283
+ }
284
+ this.messages.push({ kind: 'tool', tool, content: `→ ${preview}` });
285
+ },
286
+ onToolResult: (tool, result) => {
287
+ const last = this.messages[this.messages.length - 1];
288
+ if (last && last.kind === 'tool' && last.tool === tool) {
289
+ last.content = result.split('\n').slice(0, 12).join('\n');
290
+ if (result.split('\n').length > 12)
291
+ last.content += '\n…';
292
+ }
293
+ },
294
+ onError: (err) => {
295
+ const m = this.messages[msgIndex];
296
+ if (m) {
297
+ m.kind = 'error';
298
+ m.content = `⚠️ ${err.message}`;
299
+ m.streaming = false;
300
+ }
301
+ },
302
+ onFinal: (result) => {
303
+ const m = this.messages[msgIndex];
304
+ if (m) {
305
+ m.content = result.content || m.content;
306
+ m.streaming = false;
307
+ }
308
+ },
309
+ });
310
+ try {
311
+ await this.agent.run(task, this.abortController?.signal);
312
+ }
313
+ catch (err) {
314
+ if (err.message === 'aborted') {
315
+ const m = this.messages[msgIndex];
316
+ if (m) {
317
+ m.content += '\n\n⏹ (stopped by user)';
318
+ m.streaming = false;
319
+ }
320
+ }
321
+ else {
322
+ const m = this.messages[msgIndex];
323
+ if (m) {
324
+ m.kind = 'error';
325
+ m.content = `⚠️ ${err.message}`;
326
+ m.streaming = false;
327
+ }
328
+ }
329
+ }
330
+ finally {
331
+ const m = this.messages[msgIndex];
332
+ if (m) {
333
+ m.streaming = false;
334
+ if (!m.content.trim())
335
+ m.content = '(no response)';
336
+ }
337
+ this.running = false;
338
+ if (this.statusColor !== terminal_1.ANSI.red) {
339
+ this.status = 'Ready';
340
+ this.statusColor = terminal_1.ANSI.green;
341
+ }
342
+ if (this.sessionName) {
343
+ try {
344
+ (0, session_1.saveSession)(this.sessionName, this.messagesToSession(), {
345
+ cwd: this.agent.cwd,
346
+ model: this.agent.config.model,
347
+ provider: this.agent.config.provider,
348
+ });
349
+ }
350
+ catch {
351
+ /* ignore */
352
+ }
353
+ }
354
+ }
355
+ }
356
+ messagesToSession() {
357
+ // Convert chat messages back to provider messages (approximation)
358
+ return this.messages
359
+ .filter((m) => m.kind === 'user' || m.kind === 'assistant')
360
+ .map((m) => ({
361
+ role: (m.kind === 'user' ? 'user' : 'assistant'),
362
+ content: m.content,
363
+ }));
364
+ }
365
+ async executeWithPlan(plan) {
366
+ this.running = true;
367
+ this.status = 'Executing plan';
368
+ this.statusColor = terminal_1.ANSI.yellow;
369
+ this.abortController = new AbortController();
370
+ await this.runTask(this.lastUserText() || 'Follow the plan.', plan.steps);
371
+ }
372
+ lastUserText() {
373
+ for (let i = this.messages.length - 1; i >= 0; i--) {
374
+ if (this.messages[i].kind === 'user')
375
+ return this.messages[i].content;
376
+ }
377
+ return '';
378
+ }
379
+ // ─── Commands ─────────────────────────────────────────────────
380
+ async handleCommand(cmd) {
381
+ const [name, ...rest] = cmd.split(/\s+/);
382
+ const arg = rest.join(' ').trim();
383
+ switch (name) {
384
+ case '/help':
385
+ this.modal = { type: 'help' };
386
+ break;
387
+ case '/commands':
388
+ this.modal = { type: 'commands' };
389
+ break;
390
+ case '/model':
391
+ this.modal = { type: 'model', selected: 0 };
392
+ break;
393
+ case '/provider':
394
+ this.modal = { type: 'provider', selected: 0 };
395
+ break;
396
+ case '/session':
397
+ this.modal = { type: 'session', selected: 0 };
398
+ break;
399
+ case '/settings':
400
+ this.modal = { type: 'settings' };
401
+ break;
402
+ case '/status':
403
+ this.modal = { type: 'status' };
404
+ break;
405
+ case '/clear':
406
+ this.messages = [];
407
+ this.scroll = 0;
408
+ this.addSystem('Conversation cleared.');
409
+ break;
410
+ case '/continue':
411
+ if (this.sessionName) {
412
+ const sess = (0, session_1.loadSession)(this.sessionName);
413
+ if (sess && sess.messages.length > 0) {
414
+ this.addSystem(`Loaded ${sess.messages.length} messages from session "${this.sessionName}".`);
415
+ this.opts.continueSession = true;
416
+ }
417
+ }
418
+ else {
419
+ const sessions = (0, session_1.listSessions)();
420
+ if (sessions.length === 0) {
421
+ this.addSystem('No saved sessions.');
422
+ }
423
+ else {
424
+ this.modal = { type: 'session', selected: 0 };
425
+ }
426
+ }
427
+ break;
428
+ case '/diff':
429
+ this.showLastDiff();
430
+ break;
431
+ case '/git':
432
+ await this.runGitStatus();
433
+ break;
434
+ case '/compact':
435
+ this.addSystem('Context compaction: truncating older messages.');
436
+ this.messages = this.messages.slice(-20);
437
+ break;
438
+ case '/yolo':
439
+ this.agent.setMode(this.agent.config.permissionMode === 'yolo' ? 'ask' : 'yolo');
440
+ this.addSystem(this.agent.config.permissionMode === 'yolo'
441
+ ? 'YOLO mode enabled — permission prompts skipped for ask-level actions.'
442
+ : 'Ask mode enabled.');
443
+ break;
444
+ case '/exit':
445
+ case '/quit':
446
+ this.exitRequested = true;
447
+ break;
448
+ case '/session-name':
449
+ if (arg) {
450
+ this.sessionName = arg;
451
+ this.addSystem(`Session set to "${arg}".`);
452
+ }
453
+ else {
454
+ this.addSystem(`Current session: ${this.sessionName || '(none)'}`);
455
+ }
456
+ break;
457
+ default:
458
+ this.addSystem(`Unknown command: ${name} (type /help for commands)`);
459
+ break;
460
+ }
461
+ }
462
+ async runGitStatus() {
463
+ this.addTool('git', '$ git status --short');
464
+ const res = await Promise.resolve().then(() => __importStar(require('../tools/shell'))).then((m) => m.runShell('git status --short', this.agent.cwd, { timeoutMs: 30_000 }));
465
+ this.addTool('git', res.stdout || '(clean)');
466
+ }
467
+ showLastDiff() {
468
+ this.addSystem('Diff viewer: select a changed file (coming via git diff).');
469
+ this.addTool('git', '$ git diff --stat');
470
+ }
471
+ // ─── Modal key handling ───────────────────────────────────────
472
+ async handleModalKey(ev) {
473
+ const modal = this.modal;
474
+ switch (modal.type) {
475
+ case 'none':
476
+ return;
477
+ case 'help':
478
+ case 'commands':
479
+ case 'status':
480
+ case 'settings':
481
+ if (ev.name === 'escape' || ev.name === 'enter' || ev.name === 'ctrl_c' || (ev.name === 'char' && ev.char === '?')) {
482
+ this.modal = { type: 'none' };
483
+ }
484
+ else if (ev.name === 'pageup') {
485
+ this.scroll = (0, chat_1.clampScroll)(this.scroll - 10, this.messages, this.cols(), this.chatHeight());
486
+ }
487
+ else if (ev.name === 'pagedown') {
488
+ this.scroll = (0, chat_1.clampScroll)(this.scroll + 10, this.messages, this.cols(), this.chatHeight());
489
+ }
490
+ break;
491
+ case 'provider':
492
+ await this.handleSelector(modal, ev, (0, factory_1.providerDisplayNames)().map((p) => ({ label: p.name, value: p.id })), async (opt) => {
493
+ const pid = opt.value;
494
+ this.agent.config.provider = pid;
495
+ this.agent.rebuildProvider();
496
+ (0, config_1.saveConfig)(this.agent.config);
497
+ this.addSystem(`Provider set to ${opt.label}.`);
498
+ if (!(0, credentials_1.hasApiKey)(pid) && pid !== 'ollama') {
499
+ this.modal = { type: 'apikey', provider: pid, value: '' };
500
+ return;
501
+ }
502
+ this.modal = { type: 'none' };
503
+ });
504
+ break;
505
+ case 'model': {
506
+ const models = (0, models_1.modelsForProvider)(this.agent.config.provider);
507
+ await this.handleSelector(modal, ev, models.map((m) => ({ label: m.id, value: m.id })), async (m) => {
508
+ this.agent.config.model = m.value;
509
+ (0, config_1.saveConfig)(this.agent.config);
510
+ this.addSystem(`Model set to ${m.value}.`);
511
+ this.modal = { type: 'none' };
512
+ });
513
+ break;
514
+ }
515
+ case 'session': {
516
+ const sessions = (0, session_1.listSessions)();
517
+ await this.handleSelector(modal, ev, sessions.map((s) => ({ label: s.name, value: s.name, hint: `${s.messageCount} msgs` })), async (s) => {
518
+ this.sessionName = s.value;
519
+ const sess = (0, session_1.loadSession)(s.value);
520
+ if (sess) {
521
+ this.opts.continueSession = true;
522
+ this.addSystem(`Loaded session "${s.value}".`);
523
+ }
524
+ this.modal = { type: 'none' };
525
+ });
526
+ break;
527
+ }
528
+ case 'diff':
529
+ if (ev.name === 'escape' || ev.name === 'ctrl_c') {
530
+ this.modal = { type: 'none' };
531
+ }
532
+ else if (ev.name === 'pageup') {
533
+ this.modal = { ...modal, scroll: modal.scroll - 10 };
534
+ }
535
+ else if (ev.name === 'pagedown') {
536
+ this.modal = { ...modal, scroll: modal.scroll + 10 };
537
+ }
538
+ else if (ev.name === 'left' || ev.name === 'right') {
539
+ this.modal = { ...modal, accept: modal.accept === 'accept' ? 'reject' : 'accept' };
540
+ }
541
+ else if (ev.name === 'enter') {
542
+ const m = modal;
543
+ this.modal = { type: 'none' };
544
+ if (m.accept === 'accept')
545
+ m.action();
546
+ else
547
+ m.rejectAction();
548
+ }
549
+ break;
550
+ case 'permission':
551
+ if (ev.name === 'enter' || (ev.name === 'char' && (ev.char === 'y' || ev.char === 'Y'))) {
552
+ const m = modal;
553
+ this.modal = { type: 'none' };
554
+ m.resolve({ allow: true, remember: false });
555
+ }
556
+ else if (ev.name === 'escape' || (ev.name === 'char' && (ev.char === 'n' || ev.char === 'N'))) {
557
+ const m = modal;
558
+ this.modal = { type: 'none' };
559
+ m.resolve({ allow: false, remember: false });
560
+ }
561
+ else if (ev.name === 'char' && (ev.char === 'a' || ev.char === 'A')) {
562
+ const m = modal;
563
+ this.modal = { type: 'none' };
564
+ m.resolve({ allow: true, remember: true });
565
+ }
566
+ break;
567
+ case 'plan':
568
+ if (ev.name === 'enter' || (ev.name === 'char' && (ev.char === 'y' || ev.char === 'Y'))) {
569
+ const m = modal;
570
+ this.modal = { type: 'none' };
571
+ m.resolve(true);
572
+ }
573
+ else if (ev.name === 'escape' || (ev.name === 'char' && (ev.char === 'n' || ev.char === 'N'))) {
574
+ const m = modal;
575
+ this.modal = { type: 'none' };
576
+ m.resolve(false);
577
+ }
578
+ break;
579
+ case 'confirm':
580
+ if (ev.name === 'enter' || (ev.name === 'char' && (ev.char === 'y' || ev.char === 'Y'))) {
581
+ const m = modal;
582
+ this.modal = { type: 'none' };
583
+ m.resolve(true);
584
+ }
585
+ else if (ev.name === 'escape' || (ev.name === 'char' && (ev.char === 'n' || ev.char === 'N'))) {
586
+ const m = modal;
587
+ this.modal = { type: 'none' };
588
+ m.resolve(false);
589
+ }
590
+ break;
591
+ case 'apikey':
592
+ this.handleTextInput(ev, (value) => {
593
+ (0, credentials_1.setApiKey)(modal.provider, value);
594
+ this.addSystem(`API key stored for ${modal.provider} (not printed).`);
595
+ this.modal = { type: 'none' };
596
+ });
597
+ break;
598
+ case 'custom-url':
599
+ this.handleTextInput(ev, (value) => {
600
+ this.agent.config.apiUrl = value;
601
+ (0, config_1.saveConfig)(this.agent.config);
602
+ this.addSystem('Custom API URL set.');
603
+ this.modal = { type: 'none' };
604
+ });
605
+ break;
606
+ case 'custom-key':
607
+ this.handleTextInput(ev, (value) => {
608
+ (0, credentials_1.setApiKey)('custom', value);
609
+ this.addSystem('Custom API key stored (not printed).');
610
+ this.modal = { type: 'none' };
611
+ });
612
+ break;
613
+ default:
614
+ break;
615
+ }
616
+ }
617
+ async handleSelector(modal, ev, options, onSelect) {
618
+ if (options.length === 0) {
619
+ this.modal = { type: 'none' };
620
+ this.addSystem('No options available.');
621
+ return;
622
+ }
623
+ if (ev.name === 'escape' || ev.name === 'ctrl_c') {
624
+ this.modal = { type: 'none' };
625
+ }
626
+ else if (ev.name === 'up') {
627
+ this.modal = { ...modal, selected: Math.max(0, modal.selected - 1) };
628
+ }
629
+ else if (ev.name === 'down') {
630
+ this.modal = { ...modal, selected: Math.min(options.length - 1, modal.selected + 1) };
631
+ }
632
+ else if (ev.name === 'enter') {
633
+ await onSelect(options[modal.selected]);
634
+ }
635
+ }
636
+ handleTextInput(ev, onEnter) {
637
+ const m = this.modal;
638
+ if (ev.name === 'char' && ev.char) {
639
+ m.value += ev.char;
640
+ }
641
+ else if (ev.name === 'backspace') {
642
+ m.value = m.value.slice(0, -1);
643
+ }
644
+ else if (ev.name === 'enter') {
645
+ onEnter(m.value);
646
+ }
647
+ else if (ev.name === 'escape') {
648
+ this.modal = { type: 'none' };
649
+ }
650
+ }
651
+ // ─── Message helpers ──────────────────────────────────────────
652
+ addSystem(text) {
653
+ this.messages.push({ kind: 'system', content: text });
654
+ }
655
+ /** Show a permission modal (called by the permission callback). */
656
+ setPermissionModal(req) {
657
+ this.modal = {
658
+ type: 'permission',
659
+ req,
660
+ resolve: (d) => {
661
+ this.permissionResolver?.(d);
662
+ },
663
+ };
664
+ }
665
+ permissionResolver = null;
666
+ setPermissionResolver(fn) {
667
+ this.permissionResolver = fn;
668
+ }
669
+ addTool(tool, content) {
670
+ this.messages.push({ kind: 'tool', tool, content });
671
+ }
672
+ // ─── Rendering ────────────────────────────────────────────────
673
+ cols() {
674
+ return (0, terminal_1.getTerminalSize)().cols;
675
+ }
676
+ chatHeight() {
677
+ return Math.max(1, (0, terminal_1.getTerminalSize)().rows - 6);
678
+ }
679
+ render() {
680
+ const { rows, cols } = (0, terminal_1.getTerminalSize)();
681
+ const info = {
682
+ status: this.status,
683
+ statusColor: this.statusColor,
684
+ model: this.agent.config.model,
685
+ provider: this.agent.config.provider,
686
+ project: this.agent.cwd,
687
+ connected: this.connected,
688
+ mode: this.agent.config.permissionMode,
689
+ };
690
+ const parts = [];
691
+ parts.push((0, statusbar_1.renderHeader)(cols, info));
692
+ parts.push((0, statusbar_1.renderProjectBar)(cols, info));
693
+ // Chat area
694
+ const chatH = this.chatHeight();
695
+ const chatW = cols;
696
+ const all = (0, chat_1.renderMessages)(this.messages, chatW);
697
+ const maxScroll = Math.max(0, all.length - chatH);
698
+ this.scroll = Math.min(this.scroll, maxScroll);
699
+ const visible = all.slice(this.scroll, this.scroll + chatH);
700
+ const chatTop = 3;
701
+ for (let i = 0; i < chatH; i++) {
702
+ const content = visible[i] !== undefined ? visible[i] : '';
703
+ parts.push((0, terminal_1.cursorTo)(chatTop + i, 1) + terminal_1.ANSI.clearLineEnd + content);
704
+ }
705
+ // Separator
706
+ 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);
708
+ // Input
709
+ const inputRow = sepRow + 1;
710
+ const rendered = this.input.render(cols);
711
+ parts.push((0, terminal_1.cursorTo)(inputRow, 1) + terminal_1.ANSI.clearLineEnd + rendered.line);
712
+ const cursorCol = rendered.cursorCol + 1;
713
+ // Status bar
714
+ const statusRow = inputRow + 1;
715
+ parts.push((0, terminal_1.cursorTo)(statusRow, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderStatusBar)(info, cols));
716
+ // Modal overlay
717
+ if (this.modal.type !== 'none') {
718
+ const overlay = this.renderModal(rows, cols);
719
+ for (const l of overlay.lines)
720
+ parts.push(l);
721
+ }
722
+ // Cursor placement
723
+ if (this.modal.type === 'none') {
724
+ parts.push((0, terminal_1.cursorTo)(inputRow, cursorCol));
725
+ parts.push(terminal_1.ANSI.showCursor);
726
+ }
727
+ else {
728
+ parts.push(terminal_1.ANSI.hideCursor);
729
+ }
730
+ return parts.join('');
731
+ }
732
+ renderModal(rows, cols) {
733
+ const m = this.modal;
734
+ switch (m.type) {
735
+ case 'help':
736
+ return (0, components_1.renderModal)({
737
+ title: 'Help',
738
+ body: [
739
+ 'VECTOR — AI Coding Agent',
740
+ '',
741
+ 'Type a task and press Enter. The agent inspects,',
742
+ 'plans, edits, runs tests, and iterates until done.',
743
+ '',
744
+ 'Keys:',
745
+ ' Enter send message',
746
+ ' Tab commands menu',
747
+ ' ↑/↓ input history',
748
+ ' PgUp/Dn scroll chat',
749
+ ' ? this help',
750
+ ' Ctrl+C stop agent / quit',
751
+ ' Ctrl+L clear conversation',
752
+ '',
753
+ 'Commands: /model /provider /session /settings',
754
+ '/status /clear /continue /diff /git /compact',
755
+ '/yolo /exit',
756
+ ],
757
+ }, rows, cols);
758
+ case 'commands':
759
+ return (0, components_1.renderModal)({
760
+ title: 'Commands',
761
+ body: [
762
+ '/help show help',
763
+ '/model select model',
764
+ '/provider select provider',
765
+ '/session manage sessions',
766
+ '/settings view settings',
767
+ '/status agent status',
768
+ '/clear clear chat',
769
+ '/continue continue session',
770
+ '/diff show diff',
771
+ '/git git status',
772
+ '/compact compact context',
773
+ '/yolo toggle permission mode',
774
+ '/exit quit',
775
+ ],
776
+ }, rows, cols);
777
+ case 'settings':
778
+ return (0, components_1.renderModal)({
779
+ title: 'Settings',
780
+ body: [
781
+ `Provider: ${this.agent.config.provider}`,
782
+ `Model: ${this.agent.config.model}`,
783
+ `Mode: ${this.agent.config.permissionMode}`,
784
+ `Max iterations: ${this.agent.config.maxIterations}`,
785
+ `Max retries: ${this.agent.config.maxRetries}`,
786
+ `API URL: ${this.agent.config.apiUrl || '(default)'}`,
787
+ `Key configured: ${(0, credentials_1.hasApiKey)(this.agent.config.provider) ? 'yes' : 'no'}`,
788
+ '',
789
+ 'Env vars: VECTOR_API_KEY, VECTOR_API_URL,',
790
+ 'VECTOR_MODEL, VECTOR_PROVIDER',
791
+ ],
792
+ }, rows, cols);
793
+ case 'status':
794
+ return (0, components_1.renderModal)({
795
+ title: 'Status',
796
+ body: [
797
+ `Status: ${this.status}`,
798
+ `Running: ${this.running ? 'yes' : 'no'}`,
799
+ `Provider: ${this.agent.config.provider}`,
800
+ `Model: ${this.agent.config.model}`,
801
+ `CWD: ${this.agent.cwd}`,
802
+ `Session: ${this.sessionName || '(none)'}`,
803
+ `Messages: ${this.messages.length}`,
804
+ ],
805
+ }, rows, cols);
806
+ case 'provider':
807
+ return (0, components_1.renderSelector)({
808
+ title: 'Select Provider',
809
+ options: (0, factory_1.providerDisplayNames)().map((p) => ({
810
+ label: p.name,
811
+ value: p.id,
812
+ hint: (0, credentials_1.hasApiKey)(p.id) || p.id === 'ollama' ? 'configured' : 'no key',
813
+ })),
814
+ selected: m.selected,
815
+ rows,
816
+ cols,
817
+ });
818
+ case 'model': {
819
+ const models = (0, models_1.modelsForProvider)(this.agent.config.provider);
820
+ return (0, components_1.renderSelector)({
821
+ title: `Model (${this.agent.config.provider})`,
822
+ options: models.map((mo) => ({ label: mo.id, value: mo.id })),
823
+ selected: m.selected,
824
+ rows,
825
+ cols,
826
+ });
827
+ }
828
+ case 'session': {
829
+ const sessions = (0, session_1.listSessions)();
830
+ return (0, components_1.renderSelector)({
831
+ title: 'Sessions',
832
+ options: sessions.length > 0
833
+ ? sessions.map((s) => ({ label: s.name, value: s.name, hint: `${s.messageCount} msgs` }))
834
+ : [{ label: '(no sessions)', value: '' }],
835
+ selected: m.selected,
836
+ rows,
837
+ cols,
838
+ });
839
+ }
840
+ case 'diff':
841
+ return (0, diff_1.renderDiffModal)(m.entries, rows, cols, m.scroll, m.accept);
842
+ case 'permission': {
843
+ const danger = m.req.level === 'dangerous' || m.req.level === 'blocked';
844
+ return (0, components_1.renderModal)({
845
+ title: `Permission (${m.req.level})`,
846
+ body: [
847
+ `${danger ? terminal_1.ANSI.brightRed + '⚠' : terminal_1.ANSI.yellow + '⚠'}${terminal_1.ANSI.reset} VECTOR wants to:`,
848
+ '',
849
+ `${terminal_1.ANSI.bold}${m.req.summary}${terminal_1.ANSI.reset}`,
850
+ '',
851
+ m.req.level === 'blocked'
852
+ ? 'This action is BLOCKED by policy and cannot be approved.'
853
+ : 'Allow? [y/N] (a = always for this session)',
854
+ ],
855
+ danger,
856
+ }, rows, cols);
857
+ }
858
+ case 'plan':
859
+ return (0, components_1.renderModal)({
860
+ title: 'Plan',
861
+ body: [
862
+ 'Proposed plan:',
863
+ '',
864
+ ...m.plan.steps.map((s, i) => `${i + 1}. ${s}`),
865
+ '',
866
+ 'Execute? [y/N]',
867
+ ],
868
+ }, rows, cols);
869
+ case 'confirm':
870
+ return (0, components_1.renderModal)({ title: m.title, body: m.body }, rows, cols);
871
+ case 'apikey':
872
+ return (0, components_1.renderModal)({
873
+ title: `API key for ${m.provider}`,
874
+ body: [
875
+ 'Paste your API key (masked as you type):',
876
+ '',
877
+ terminal_1.ANSI.bold + (m.value ? '•'.repeat(m.value.length) : '') + terminal_1.ANSI.reset,
878
+ '',
879
+ 'Enter to save · Esc to cancel',
880
+ ],
881
+ }, rows, cols);
882
+ case 'custom-url':
883
+ return (0, components_1.renderModal)({
884
+ title: 'Custom API URL',
885
+ body: ['Endpoint, e.g. https://example.com/v1', '', m.value || '_', '', 'Enter to save · Esc to cancel'],
886
+ }, rows, cols);
887
+ case 'custom-key':
888
+ return (0, components_1.renderModal)({
889
+ title: 'Custom API key',
890
+ body: ['API key (masked)', '', m.value ? '•'.repeat(m.value.length) : '', '', 'Enter to save · Esc to cancel'],
891
+ }, rows, cols);
892
+ default:
893
+ return { lines: [] };
894
+ }
895
+ }
896
+ async shutdown() {
897
+ this.exitRequested = false;
898
+ if (this.sessionName && this.messages.length > 0) {
899
+ try {
900
+ (0, session_1.saveSession)(this.sessionName, this.messagesToSession(), {
901
+ cwd: this.agent.cwd,
902
+ model: this.agent.config.model,
903
+ provider: this.agent.config.provider,
904
+ });
905
+ }
906
+ catch {
907
+ /* ignore */
908
+ }
909
+ }
910
+ this.opts.onExit?.();
911
+ }
912
+ }
913
+ exports.App = App;