codeep 1.1.12 → 1.1.13

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 (45) hide show
  1. package/bin/codeep.js +1 -1
  2. package/dist/config/index.js +10 -10
  3. package/dist/renderer/App.d.ts +430 -0
  4. package/dist/renderer/App.js +2712 -0
  5. package/dist/renderer/ChatUI.d.ts +71 -0
  6. package/dist/renderer/ChatUI.js +286 -0
  7. package/dist/renderer/Input.d.ts +72 -0
  8. package/dist/renderer/Input.js +371 -0
  9. package/dist/renderer/Screen.d.ts +79 -0
  10. package/dist/renderer/Screen.js +278 -0
  11. package/dist/renderer/ansi.d.ts +99 -0
  12. package/dist/renderer/ansi.js +176 -0
  13. package/dist/renderer/components/Box.d.ts +64 -0
  14. package/dist/renderer/components/Box.js +90 -0
  15. package/dist/renderer/components/Help.d.ts +30 -0
  16. package/dist/renderer/components/Help.js +195 -0
  17. package/dist/renderer/components/Intro.d.ts +12 -0
  18. package/dist/renderer/components/Intro.js +128 -0
  19. package/dist/renderer/components/Login.d.ts +42 -0
  20. package/dist/renderer/components/Login.js +178 -0
  21. package/dist/renderer/components/Modal.d.ts +43 -0
  22. package/dist/renderer/components/Modal.js +207 -0
  23. package/dist/renderer/components/Permission.d.ts +20 -0
  24. package/dist/renderer/components/Permission.js +113 -0
  25. package/dist/renderer/components/SelectScreen.d.ts +26 -0
  26. package/dist/renderer/components/SelectScreen.js +101 -0
  27. package/dist/renderer/components/Settings.d.ts +37 -0
  28. package/dist/renderer/components/Settings.js +333 -0
  29. package/dist/renderer/components/Status.d.ts +18 -0
  30. package/dist/renderer/components/Status.js +78 -0
  31. package/dist/renderer/demo-app.d.ts +6 -0
  32. package/dist/renderer/demo-app.js +85 -0
  33. package/dist/renderer/demo.d.ts +6 -0
  34. package/dist/renderer/demo.js +52 -0
  35. package/dist/renderer/index.d.ts +16 -0
  36. package/dist/renderer/index.js +17 -0
  37. package/dist/renderer/main.d.ts +6 -0
  38. package/dist/renderer/main.js +1634 -0
  39. package/dist/utils/agent.d.ts +21 -0
  40. package/dist/utils/agent.js +29 -0
  41. package/dist/utils/clipboard.d.ts +15 -0
  42. package/dist/utils/clipboard.js +95 -0
  43. package/package.json +7 -11
  44. package/dist/utils/console.d.ts +0 -55
  45. package/dist/utils/console.js +0 -188
@@ -0,0 +1,2712 @@
1
+ /**
2
+ * Main Application using custom renderer
3
+ * Replaces Ink-based App
4
+ */
5
+ import { Screen } from './Screen.js';
6
+ import { Input, LineEditor } from './Input.js';
7
+ import { fg, style } from './ansi.js';
8
+ import clipboardy from 'clipboardy';
9
+ // Primary color: #f02a30 (Codeep red)
10
+ const PRIMARY_COLOR = fg.rgb(240, 42, 48);
11
+ // Syntax highlighting colors (One Dark theme inspired)
12
+ const SYNTAX = {
13
+ keyword: fg.rgb(198, 120, 221), // Purple - keywords
14
+ string: fg.rgb(152, 195, 121), // Green - strings
15
+ number: fg.rgb(209, 154, 102), // Orange - numbers
16
+ comment: fg.rgb(92, 99, 112), // Gray - comments
17
+ function: fg.rgb(97, 175, 239), // Blue - functions
18
+ type: fg.rgb(229, 192, 123), // Yellow - types
19
+ operator: fg.rgb(86, 182, 194), // Cyan - operators
20
+ variable: fg.white, // White - variables
21
+ punctuation: fg.gray, // Gray - punctuation
22
+ codeFrame: fg.rgb(100, 105, 115), // Frame color
23
+ codeLang: fg.rgb(150, 155, 165), // Language label
24
+ };
25
+ // Keywords for different languages
26
+ const KEYWORDS = {
27
+ js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'try', 'catch', 'throw', 'finally', 'new', 'class', 'extends', 'import', 'export', 'from', 'default', 'async', 'await', 'yield', 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'this', 'super', 'null', 'undefined', 'true', 'false', 'NaN', 'Infinity'],
28
+ ts: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'try', 'catch', 'throw', 'finally', 'new', 'class', 'extends', 'import', 'export', 'from', 'default', 'async', 'await', 'yield', 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'this', 'super', 'null', 'undefined', 'true', 'false', 'type', 'interface', 'enum', 'namespace', 'module', 'declare', 'abstract', 'implements', 'private', 'public', 'protected', 'readonly', 'static', 'as', 'is', 'keyof', 'infer', 'never', 'unknown', 'any'],
29
+ py: ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'try', 'except', 'finally', 'raise', 'import', 'from', 'as', 'with', 'yield', 'lambda', 'pass', 'break', 'continue', 'and', 'or', 'not', 'in', 'is', 'None', 'True', 'False', 'global', 'nonlocal', 'assert', 'del', 'async', 'await'],
30
+ go: ['func', 'return', 'if', 'else', 'for', 'range', 'switch', 'case', 'break', 'continue', 'fallthrough', 'default', 'go', 'select', 'chan', 'defer', 'panic', 'recover', 'type', 'struct', 'interface', 'map', 'package', 'import', 'const', 'var', 'nil', 'true', 'false', 'iota', 'make', 'new', 'append', 'len', 'cap', 'copy', 'delete'],
31
+ rust: ['fn', 'let', 'mut', 'const', 'static', 'return', 'if', 'else', 'match', 'for', 'while', 'loop', 'break', 'continue', 'struct', 'enum', 'trait', 'impl', 'type', 'where', 'use', 'mod', 'pub', 'crate', 'self', 'super', 'async', 'await', 'move', 'ref', 'true', 'false', 'Some', 'None', 'Ok', 'Err', 'Self', 'dyn', 'unsafe', 'extern'],
32
+ sh: ['if', 'then', 'else', 'elif', 'fi', 'case', 'esac', 'for', 'while', 'until', 'do', 'done', 'in', 'function', 'return', 'local', 'export', 'readonly', 'declare', 'typeset', 'unset', 'shift', 'exit', 'break', 'continue', 'source', 'alias', 'echo', 'printf', 'read', 'test', 'true', 'false'],
33
+ };
34
+ // Map language aliases
35
+ const LANG_ALIASES = {
36
+ javascript: 'js', typescript: 'ts', python: 'py', golang: 'go',
37
+ bash: 'sh', shell: 'sh', zsh: 'sh', tsx: 'ts', jsx: 'js',
38
+ };
39
+ /**
40
+ * Syntax highlighter for code with better token handling
41
+ */
42
+ function highlightCode(code, lang) {
43
+ const normalizedLang = LANG_ALIASES[lang.toLowerCase()] || lang.toLowerCase();
44
+ const keywords = KEYWORDS[normalizedLang] || KEYWORDS['js'] || [];
45
+ // Tokenize and highlight
46
+ let result = '';
47
+ let i = 0;
48
+ while (i < code.length) {
49
+ // Check for comments first
50
+ if (code.slice(i, i + 2) === '//' || (normalizedLang === 'py' && code[i] === '#') ||
51
+ (normalizedLang === 'sh' && code[i] === '#')) {
52
+ // Line comment - highlight rest of line
53
+ let end = code.indexOf('\n', i);
54
+ if (end === -1)
55
+ end = code.length;
56
+ result += SYNTAX.comment + code.slice(i, end) + '\x1b[0m';
57
+ i = end;
58
+ continue;
59
+ }
60
+ // Multi-line comment /*
61
+ if (code.slice(i, i + 2) === '/*') {
62
+ let end = code.indexOf('*/', i + 2);
63
+ if (end === -1)
64
+ end = code.length;
65
+ else
66
+ end += 2;
67
+ result += SYNTAX.comment + code.slice(i, end) + '\x1b[0m';
68
+ i = end;
69
+ continue;
70
+ }
71
+ // Strings
72
+ if (code[i] === '"' || code[i] === "'" || code[i] === '`') {
73
+ const quote = code[i];
74
+ let end = i + 1;
75
+ while (end < code.length) {
76
+ if (code[end] === '\\') {
77
+ end += 2; // Skip escaped char
78
+ }
79
+ else if (code[end] === quote) {
80
+ end++;
81
+ break;
82
+ }
83
+ else {
84
+ end++;
85
+ }
86
+ }
87
+ result += SYNTAX.string + code.slice(i, end) + '\x1b[0m';
88
+ i = end;
89
+ continue;
90
+ }
91
+ // Numbers (including hex, binary, floats)
92
+ const numMatch = code.slice(i).match(/^(0x[0-9a-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*(?:e[+-]?\d+)?)/);
93
+ if (numMatch && (i === 0 || !/[a-zA-Z_]/.test(code[i - 1]))) {
94
+ result += SYNTAX.number + numMatch[1] + '\x1b[0m';
95
+ i += numMatch[1].length;
96
+ continue;
97
+ }
98
+ // Identifiers (keywords, functions, variables)
99
+ const identMatch = code.slice(i).match(/^[a-zA-Z_][a-zA-Z0-9_]*/);
100
+ if (identMatch) {
101
+ const ident = identMatch[0];
102
+ const nextChar = code[i + ident.length];
103
+ if (keywords.includes(ident)) {
104
+ // Keyword
105
+ result += SYNTAX.keyword + ident + '\x1b[0m';
106
+ }
107
+ else if (nextChar === '(') {
108
+ // Function call
109
+ result += SYNTAX.function + ident + '\x1b[0m';
110
+ }
111
+ else if (ident[0] === ident[0].toUpperCase() && /^[A-Z]/.test(ident)) {
112
+ // Type/Class (PascalCase)
113
+ result += SYNTAX.type + ident + '\x1b[0m';
114
+ }
115
+ else {
116
+ // Regular identifier
117
+ result += ident;
118
+ }
119
+ i += ident.length;
120
+ continue;
121
+ }
122
+ // Operators
123
+ const opMatch = code.slice(i).match(/^(===|!==|==|!=|<=|>=|=>|->|\+\+|--|&&|\|\||<<|>>|\+=|-=|\*=|\/=|[+\-*/%=<>!&|^~?:])/);
124
+ if (opMatch) {
125
+ result += SYNTAX.operator + opMatch[1] + '\x1b[0m';
126
+ i += opMatch[1].length;
127
+ continue;
128
+ }
129
+ // Punctuation
130
+ if ('{}[]();,.'.includes(code[i])) {
131
+ result += SYNTAX.punctuation + code[i] + '\x1b[0m';
132
+ i++;
133
+ continue;
134
+ }
135
+ // Default - just add the character
136
+ result += code[i];
137
+ i++;
138
+ }
139
+ return result;
140
+ }
141
+ // Spinner frames for animation
142
+ const SPINNER_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
143
+ // ASCII Logo
144
+ const LOGO_LINES = [
145
+ ' ██████╗ ██████╗ ██████╗ ███████╗███████╗██████╗ ',
146
+ '██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔════╝██╔══██╗',
147
+ '██║ ██║ ██║██║ ██║█████╗ █████╗ ██████╔╝',
148
+ '██║ ██║ ██║██║ ██║██╔══╝ ██╔══╝ ██╔═══╝ ',
149
+ '╚██████╗╚██████╔╝██████╔╝███████╗███████╗██║ ',
150
+ ' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ',
151
+ ];
152
+ const LOGO_HEIGHT = LOGO_LINES.length;
153
+ // Command descriptions for autocomplete
154
+ const COMMAND_DESCRIPTIONS = {
155
+ 'help': 'Show help',
156
+ 'status': 'Show status',
157
+ 'settings': 'Adjust settings',
158
+ 'version': 'Show version',
159
+ 'update': 'Check updates',
160
+ 'clear': 'Clear chat',
161
+ 'exit': 'Quit',
162
+ 'sessions': 'Manage sessions',
163
+ 'new': 'New session',
164
+ 'rename': 'Rename session',
165
+ 'search': 'Search history',
166
+ 'export': 'Export chat',
167
+ 'agent': 'Run agent for a task',
168
+ 'agent-dry': 'Preview agent actions',
169
+ 'stop': 'Stop running agent',
170
+ 'undo': 'Undo last action',
171
+ 'undo-all': 'Undo all actions',
172
+ 'history': 'Show agent history',
173
+ 'changes': 'Show session changes',
174
+ 'diff': 'Review git changes',
175
+ 'commit': 'Generate commit message',
176
+ 'git-commit': 'Commit with message',
177
+ 'push': 'Git push',
178
+ 'pull': 'Git pull',
179
+ 'scan': 'Scan project',
180
+ 'review': 'Code review',
181
+ 'copy': 'Copy code block',
182
+ 'paste': 'Paste from clipboard',
183
+ 'apply': 'Apply file changes',
184
+ 'test': 'Generate/run tests',
185
+ 'docs': 'Add documentation',
186
+ 'refactor': 'Improve code quality',
187
+ 'fix': 'Debug and fix issues',
188
+ 'explain': 'Explain code',
189
+ 'optimize': 'Optimize performance',
190
+ 'debug': 'Debug problems',
191
+ 'skills': 'List all skills',
192
+ 'provider': 'Switch provider',
193
+ 'model': 'Switch model',
194
+ 'protocol': 'Switch protocol',
195
+ 'lang': 'Set language',
196
+ 'grant': 'Grant write permission',
197
+ 'login': 'Change API key',
198
+ 'logout': 'Logout',
199
+ 'context-save': 'Save conversation',
200
+ 'context-load': 'Load conversation',
201
+ 'context-clear': 'Clear saved context',
202
+ 'learn': 'Learn code preferences',
203
+ };
204
+ import { helpCategories, keyboardShortcuts } from './components/Help.js';
205
+ import { renderStatusScreen } from './components/Status.js';
206
+ import { handleSettingsKey, SETTINGS } from './components/Settings.js';
207
+ export class App {
208
+ screen;
209
+ input;
210
+ editor;
211
+ messages = [];
212
+ streamingContent = '';
213
+ isStreaming = false;
214
+ isLoading = false;
215
+ currentScreen = 'chat';
216
+ options;
217
+ scrollOffset = 0;
218
+ notification = '';
219
+ notificationTimeout = null;
220
+ // Spinner animation state
221
+ spinnerFrame = 0;
222
+ spinnerInterval = null;
223
+ // Agent progress state
224
+ isAgentRunning = false;
225
+ agentIteration = 0;
226
+ agentActions = [];
227
+ agentThinking = '';
228
+ // Paste detection state
229
+ pasteInfo = null;
230
+ pasteInfoOpen = false;
231
+ // Inline help state
232
+ helpOpen = false;
233
+ helpScrollIndex = 0;
234
+ // Settings screen state
235
+ settingsState = {
236
+ selectedIndex: 0,
237
+ editing: false,
238
+ editValue: '',
239
+ };
240
+ // Autocomplete state
241
+ showAutocomplete = false;
242
+ autocompleteIndex = 0;
243
+ autocompleteItems = [];
244
+ // Inline confirmation dialog state
245
+ confirmOpen = false;
246
+ confirmOptions = null;
247
+ confirmSelection = 'no';
248
+ // Inline menu state (renders below input/status)
249
+ menuOpen = false;
250
+ menuTitle = '';
251
+ menuItems = [];
252
+ menuIndex = 0;
253
+ menuCurrentValue = '';
254
+ menuCallback = null;
255
+ // Inline settings state
256
+ settingsOpen = false;
257
+ // Inline permission state
258
+ permissionOpen = false;
259
+ permissionIndex = 0;
260
+ permissionPath = '';
261
+ permissionIsProject = false;
262
+ permissionCallback = null;
263
+ // Inline session picker state
264
+ sessionPickerOpen = false;
265
+ sessionPickerIndex = 0;
266
+ sessionPickerItems = [];
267
+ sessionPickerCallback = null;
268
+ sessionPickerDeleteMode = false;
269
+ sessionPickerDeleteCallback = null;
270
+ // Search screen state
271
+ searchOpen = false;
272
+ searchQuery = '';
273
+ searchResults = [];
274
+ searchIndex = 0;
275
+ searchCallback = null;
276
+ // Export screen state
277
+ exportOpen = false;
278
+ exportIndex = 0;
279
+ exportCallback = null;
280
+ // Logout picker state
281
+ logoutOpen = false;
282
+ logoutIndex = 0;
283
+ logoutProviders = [];
284
+ logoutCallback = null;
285
+ // Intro animation state
286
+ showIntro = false;
287
+ introPhase = 'init';
288
+ introProgress = 0;
289
+ introInterval = null;
290
+ introCallback = null;
291
+ // Inline login state
292
+ loginOpen = false;
293
+ loginStep = 'provider';
294
+ loginProviders = [];
295
+ loginProviderIndex = 0;
296
+ loginApiKey = '';
297
+ loginError = '';
298
+ loginCallback = null;
299
+ // Glitch characters for intro animation
300
+ static GLITCH_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ@#$%&*<>?/;:[]=';
301
+ // All available commands
302
+ static COMMANDS = [
303
+ 'help', 'status', 'settings', 'version', 'update', 'clear', 'exit',
304
+ 'sessions', 'new', 'rename', 'search', 'export',
305
+ 'agent', 'agent-dry', 'stop', 'undo', 'undo-all', 'history', 'changes',
306
+ 'diff', 'commit', 'git-commit', 'push', 'pull', 'scan', 'review',
307
+ 'copy', 'paste', 'apply',
308
+ 'test', 'docs', 'refactor', 'fix', 'explain', 'optimize', 'debug', 'skills',
309
+ 'provider', 'model', 'protocol', 'lang', 'grant', 'login', 'logout',
310
+ 'context-save', 'context-load', 'context-clear', 'learn',
311
+ 'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
312
+ ];
313
+ constructor(options) {
314
+ this.screen = new Screen();
315
+ this.input = new Input();
316
+ this.editor = new LineEditor();
317
+ this.options = options;
318
+ }
319
+ /**
320
+ * Start the application
321
+ */
322
+ start() {
323
+ this.screen.init();
324
+ this.input.start();
325
+ this.input.onKey((event) => this.handleKey(event));
326
+ this.render();
327
+ }
328
+ /**
329
+ * Stop the application
330
+ */
331
+ stop() {
332
+ this.input.stop();
333
+ this.screen.cleanup();
334
+ }
335
+ /**
336
+ * Add a message
337
+ */
338
+ addMessage(message) {
339
+ this.messages.push(message);
340
+ this.scrollOffset = 0;
341
+ this.render();
342
+ }
343
+ /**
344
+ * Set messages (for loading session)
345
+ */
346
+ setMessages(messages) {
347
+ this.messages = messages;
348
+ this.scrollOffset = 0;
349
+ this.render();
350
+ }
351
+ /**
352
+ * Clear messages
353
+ */
354
+ clearMessages() {
355
+ this.messages = [];
356
+ this.scrollOffset = 0;
357
+ this.render();
358
+ }
359
+ /**
360
+ * Get all messages (for API history)
361
+ */
362
+ getMessages() {
363
+ return this.messages;
364
+ }
365
+ /**
366
+ * Scroll to a specific message by index
367
+ */
368
+ scrollToMessage(messageIndex) {
369
+ const { width, height } = this.screen.getSize();
370
+ const maxWidth = width - 4; // Account for margins
371
+ // Calculate actual line count for messages up to target
372
+ let totalLines = 0;
373
+ let targetStartLine = 0;
374
+ for (let i = 0; i < this.messages.length; i++) {
375
+ const msg = this.messages[i];
376
+ if (i === messageIndex) {
377
+ targetStartLine = totalLines;
378
+ }
379
+ // Count lines for this message (header + content)
380
+ const contentLines = msg.content.split('\n');
381
+ let msgLines = 2; // Header + empty line after
382
+ for (const line of contentLines) {
383
+ // Account for word wrapping
384
+ msgLines += Math.ceil(Math.max(1, line.length) / maxWidth);
385
+ }
386
+ totalLines += msgLines + 1; // +1 for spacing between messages
387
+ }
388
+ const visibleLines = height - 12; // Approximate visible area
389
+ // Set scroll offset to show the target message near the top
390
+ this.scrollOffset = Math.max(0, totalLines - targetStartLine - Math.floor(visibleLines / 2));
391
+ this.render();
392
+ this.notify(`Jumped to message #${messageIndex + 1}`);
393
+ }
394
+ /**
395
+ * Get messages without system messages (for API)
396
+ */
397
+ getChatHistory() {
398
+ return this.messages
399
+ .filter(m => m.role !== 'system')
400
+ .map(m => ({ role: m.role, content: m.content }));
401
+ }
402
+ /**
403
+ * Start streaming
404
+ */
405
+ startStreaming() {
406
+ this.isStreaming = true;
407
+ this.isLoading = false;
408
+ this.streamingContent = '';
409
+ this.startSpinner();
410
+ this.render();
411
+ }
412
+ /**
413
+ * Add streaming chunk
414
+ */
415
+ addStreamChunk(chunk) {
416
+ this.streamingContent += chunk;
417
+ this.render();
418
+ }
419
+ /**
420
+ * End streaming
421
+ */
422
+ endStreaming() {
423
+ if (this.streamingContent) {
424
+ this.messages.push({
425
+ role: 'assistant',
426
+ content: this.streamingContent,
427
+ });
428
+ }
429
+ this.streamingContent = '';
430
+ this.isStreaming = false;
431
+ this.stopSpinner();
432
+ this.render();
433
+ }
434
+ /**
435
+ * Set loading state
436
+ */
437
+ setLoading(loading) {
438
+ this.isLoading = loading;
439
+ if (loading) {
440
+ this.startSpinner();
441
+ }
442
+ else {
443
+ this.stopSpinner();
444
+ }
445
+ this.render();
446
+ }
447
+ /**
448
+ * Start spinner animation
449
+ */
450
+ startSpinner() {
451
+ if (this.spinnerInterval)
452
+ return;
453
+ this.spinnerInterval = setInterval(() => {
454
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
455
+ this.render();
456
+ }, 100);
457
+ }
458
+ /**
459
+ * Stop spinner animation
460
+ */
461
+ stopSpinner() {
462
+ if (this.spinnerInterval) {
463
+ clearInterval(this.spinnerInterval);
464
+ this.spinnerInterval = null;
465
+ }
466
+ }
467
+ /**
468
+ * Set agent running state
469
+ */
470
+ setAgentRunning(running) {
471
+ this.isAgentRunning = running;
472
+ if (running) {
473
+ this.agentIteration = 0;
474
+ this.agentActions = [];
475
+ this.agentThinking = '';
476
+ this.startSpinner();
477
+ }
478
+ else {
479
+ this.stopSpinner();
480
+ }
481
+ this.render();
482
+ }
483
+ /**
484
+ * Update agent progress
485
+ */
486
+ updateAgentProgress(iteration, action) {
487
+ this.agentIteration = iteration;
488
+ if (action) {
489
+ this.agentActions.push(action);
490
+ }
491
+ this.render();
492
+ }
493
+ /**
494
+ * Set agent thinking text
495
+ */
496
+ setAgentThinking(text) {
497
+ this.agentThinking = text;
498
+ this.render();
499
+ }
500
+ /**
501
+ * Paste from system clipboard (Ctrl+V)
502
+ */
503
+ pasteFromClipboard() {
504
+ try {
505
+ const clipboardContent = clipboardy.readSync();
506
+ if (clipboardContent && clipboardContent.trim()) {
507
+ this.handlePaste(clipboardContent.trim());
508
+ }
509
+ else {
510
+ this.notify('Clipboard is empty');
511
+ }
512
+ }
513
+ catch (err) {
514
+ const error = err;
515
+ this.notify(`Clipboard error: ${error.message || 'unknown'}`);
516
+ }
517
+ }
518
+ /**
519
+ * Handle paste detection - call this when large text is pasted
520
+ */
521
+ handlePaste(text) {
522
+ const lines = text.split('\n');
523
+ const chars = text.length;
524
+ // Only show paste info for significant pastes (>100 chars or >3 lines)
525
+ if (chars < 100 && lines.length <= 3) {
526
+ // Small paste - just add to input directly
527
+ this.editor.insert(text);
528
+ this.updateAutocomplete();
529
+ this.render();
530
+ return;
531
+ }
532
+ // Large paste - show info box
533
+ const preview = text.length > 200 ? text.slice(0, 197) + '...' : text;
534
+ this.pasteInfo = {
535
+ chars,
536
+ lines: lines.length,
537
+ preview,
538
+ fullText: text,
539
+ };
540
+ this.pasteInfoOpen = true;
541
+ this.render();
542
+ }
543
+ /**
544
+ * Handle paste info key events
545
+ */
546
+ handlePasteInfoKey(event) {
547
+ if (event.key === 'escape' || event.key === 'n') {
548
+ // Cancel paste
549
+ this.pasteInfo = null;
550
+ this.pasteInfoOpen = false;
551
+ this.notify('Paste cancelled');
552
+ this.render();
553
+ return;
554
+ }
555
+ if (event.key === 'enter' || event.key === 'y') {
556
+ // Accept paste - add to input
557
+ if (this.pasteInfo) {
558
+ this.editor.insert(this.pasteInfo.fullText);
559
+ this.updateAutocomplete();
560
+ }
561
+ this.pasteInfo = null;
562
+ this.pasteInfoOpen = false;
563
+ this.render();
564
+ return;
565
+ }
566
+ if (event.key === 's') {
567
+ // Submit paste directly as message
568
+ if (this.pasteInfo) {
569
+ const text = this.pasteInfo.fullText;
570
+ this.pasteInfo = null;
571
+ this.pasteInfoOpen = false;
572
+ this.render();
573
+ // Submit directly
574
+ this.addMessage({ role: 'user', content: text });
575
+ this.setLoading(true);
576
+ this.options.onSubmit(text).catch(err => {
577
+ this.notify(`Error: ${err.message}`);
578
+ this.setLoading(false);
579
+ });
580
+ }
581
+ return;
582
+ }
583
+ }
584
+ /**
585
+ * Show notification
586
+ */
587
+ notify(message, duration = 3000) {
588
+ this.notification = message;
589
+ this.render();
590
+ if (this.notificationTimeout) {
591
+ clearTimeout(this.notificationTimeout);
592
+ }
593
+ this.notificationTimeout = setTimeout(() => {
594
+ this.notification = '';
595
+ this.render();
596
+ }, duration);
597
+ }
598
+ /**
599
+ * Show list selection (inline menu below status bar)
600
+ */
601
+ showList(title, items, callback) {
602
+ // Convert string items to SelectItem format and use inline menu
603
+ const selectItems = items.map((label, index) => ({
604
+ key: String(index),
605
+ label,
606
+ }));
607
+ this.menuTitle = title;
608
+ this.menuItems = selectItems;
609
+ this.menuCurrentValue = '';
610
+ this.menuIndex = 0;
611
+ this.menuCallback = (item) => callback(parseInt(item.key, 10));
612
+ this.menuOpen = true;
613
+ this.render();
614
+ }
615
+ /**
616
+ * Show settings (inline, below status bar)
617
+ */
618
+ showSettings() {
619
+ this.settingsState = { selectedIndex: 0, editing: false, editValue: '' };
620
+ this.settingsOpen = true;
621
+ this.render();
622
+ }
623
+ /**
624
+ * Show confirmation dialog
625
+ */
626
+ showConfirm(options) {
627
+ this.confirmOptions = options;
628
+ this.confirmSelection = 'no'; // Default to No for safety
629
+ this.confirmOpen = true;
630
+ this.render();
631
+ }
632
+ /**
633
+ * Show permission dialog (inline, below status bar)
634
+ */
635
+ showPermission(projectPath, isProject, callback) {
636
+ this.permissionPath = projectPath;
637
+ this.permissionIsProject = isProject;
638
+ this.permissionIndex = 0;
639
+ this.permissionCallback = callback;
640
+ this.permissionOpen = true;
641
+ this.render();
642
+ }
643
+ /**
644
+ * Show session picker (inline, below status bar)
645
+ */
646
+ showSessionPicker(sessions, callback, deleteCallback) {
647
+ this.sessionPickerItems = sessions;
648
+ this.sessionPickerIndex = 0;
649
+ this.sessionPickerCallback = callback;
650
+ this.sessionPickerDeleteCallback = deleteCallback || null;
651
+ this.sessionPickerDeleteMode = false;
652
+ this.sessionPickerOpen = true;
653
+ this.render();
654
+ }
655
+ /**
656
+ * Show search screen
657
+ */
658
+ showSearch(query, results, callback) {
659
+ this.searchQuery = query;
660
+ this.searchResults = results;
661
+ this.searchIndex = 0;
662
+ this.searchCallback = callback;
663
+ this.searchOpen = true;
664
+ this.render();
665
+ }
666
+ /**
667
+ * Show export screen
668
+ */
669
+ showExport(callback) {
670
+ this.exportIndex = 0;
671
+ this.exportCallback = callback;
672
+ this.exportOpen = true;
673
+ this.render();
674
+ }
675
+ /**
676
+ * Show logout picker
677
+ */
678
+ showLogoutPicker(providers, callback) {
679
+ this.logoutProviders = providers;
680
+ this.logoutIndex = 0;
681
+ this.logoutCallback = callback;
682
+ this.logoutOpen = true;
683
+ this.render();
684
+ }
685
+ /**
686
+ * Start intro animation
687
+ */
688
+ startIntro(callback) {
689
+ this.showIntro = true;
690
+ this.introPhase = 'init';
691
+ this.introProgress = 0;
692
+ this.introCallback = callback;
693
+ // Phase 1: Initial noise (500ms)
694
+ let noiseCount = 0;
695
+ const noiseInterval = setInterval(() => {
696
+ noiseCount++;
697
+ this.introProgress = Math.random();
698
+ this.render();
699
+ if (noiseCount >= 10) {
700
+ clearInterval(noiseInterval);
701
+ // Phase 2: Decryption animation (1500ms)
702
+ this.introPhase = 'decrypt';
703
+ const startTime = Date.now();
704
+ const duration = 1500;
705
+ this.introInterval = setInterval(() => {
706
+ const elapsed = Date.now() - startTime;
707
+ this.introProgress = Math.min(elapsed / duration, 1);
708
+ this.render();
709
+ if (this.introProgress >= 1) {
710
+ this.finishIntro();
711
+ }
712
+ }, 16); // ~60 FPS
713
+ }
714
+ }, 50);
715
+ this.render();
716
+ }
717
+ /**
718
+ * Skip intro animation
719
+ */
720
+ skipIntro() {
721
+ this.finishIntro();
722
+ }
723
+ /**
724
+ * Finish intro animation
725
+ */
726
+ finishIntro() {
727
+ if (this.introInterval) {
728
+ clearInterval(this.introInterval);
729
+ this.introInterval = null;
730
+ }
731
+ this.introPhase = 'done';
732
+ this.showIntro = false;
733
+ if (this.introCallback) {
734
+ this.introCallback();
735
+ this.introCallback = null;
736
+ }
737
+ this.render();
738
+ }
739
+ /**
740
+ * Show inline login dialog
741
+ */
742
+ showLogin(providers, callback) {
743
+ this.loginProviders = providers;
744
+ this.loginProviderIndex = 0;
745
+ this.loginStep = 'provider';
746
+ this.loginApiKey = '';
747
+ this.loginError = '';
748
+ this.loginCallback = callback;
749
+ this.loginOpen = true;
750
+ this.render();
751
+ }
752
+ /**
753
+ * Reinitialize screen (after external screen takeover)
754
+ */
755
+ reinitScreen() {
756
+ this.screen.init();
757
+ this.input.start();
758
+ this.input.onKey((event) => this.handleKey(event));
759
+ this.render();
760
+ }
761
+ /**
762
+ * Show inline menu (renders below status bar)
763
+ */
764
+ showSelect(title, items, currentValue, callback) {
765
+ this.menuTitle = title;
766
+ this.menuItems = items;
767
+ this.menuCurrentValue = currentValue;
768
+ this.menuCallback = callback;
769
+ this.menuOpen = true;
770
+ // Find current value index
771
+ const currentIndex = items.findIndex(item => item.key === currentValue);
772
+ this.menuIndex = currentIndex >= 0 ? currentIndex : 0;
773
+ this.render();
774
+ }
775
+ /**
776
+ * Handle keyboard input
777
+ */
778
+ handleKey(event) {
779
+ // Global shortcuts
780
+ if (event.ctrl && (event.key === 'c' || event.key === 'd')) {
781
+ this.stop();
782
+ this.options.onExit();
783
+ return;
784
+ }
785
+ // Screen-specific handling
786
+ switch (this.currentScreen) {
787
+ case 'status':
788
+ if (event.key === 'escape' || event.key === 'q') {
789
+ this.currentScreen = 'chat';
790
+ this.render();
791
+ }
792
+ break;
793
+ case 'chat':
794
+ default:
795
+ this.handleChatKey(event);
796
+ break;
797
+ }
798
+ }
799
+ /**
800
+ * Handle chat screen keys
801
+ */
802
+ handleChatKey(event) {
803
+ // If paste info is open, handle paste keys first
804
+ if (this.pasteInfoOpen) {
805
+ this.handlePasteInfoKey(event);
806
+ return;
807
+ }
808
+ // If permission is open, handle permission keys first
809
+ if (this.permissionOpen) {
810
+ this.handleInlinePermissionKey(event);
811
+ return;
812
+ }
813
+ // If session picker is open, handle session picker keys first
814
+ if (this.sessionPickerOpen) {
815
+ this.handleInlineSessionPickerKey(event);
816
+ return;
817
+ }
818
+ // If confirm is open, handle confirm keys first
819
+ if (this.confirmOpen) {
820
+ this.handleInlineConfirmKey(event);
821
+ return;
822
+ }
823
+ // If help is open, handle help keys first
824
+ if (this.helpOpen) {
825
+ this.handleInlineHelpKey(event);
826
+ return;
827
+ }
828
+ // If settings is open, handle settings keys first
829
+ if (this.settingsOpen) {
830
+ this.handleInlineSettingsKey(event);
831
+ return;
832
+ }
833
+ // If search is open, handle search keys first
834
+ if (this.searchOpen) {
835
+ this.handleSearchKey(event);
836
+ return;
837
+ }
838
+ // If export is open, handle export keys first
839
+ if (this.exportOpen) {
840
+ this.handleExportKey(event);
841
+ return;
842
+ }
843
+ // If logout is open, handle logout keys first
844
+ if (this.logoutOpen) {
845
+ this.handleLogoutKey(event);
846
+ return;
847
+ }
848
+ // If login is open, handle login keys first
849
+ if (this.loginOpen) {
850
+ this.handleLoginKey(event);
851
+ return;
852
+ }
853
+ // If intro is playing, skip on any key
854
+ if (this.showIntro) {
855
+ this.skipIntro();
856
+ return;
857
+ }
858
+ // If menu is open, handle menu keys first
859
+ if (this.menuOpen) {
860
+ this.handleMenuKey(event);
861
+ return;
862
+ }
863
+ // Escape to cancel streaming/loading or close autocomplete
864
+ if (event.key === 'escape') {
865
+ if (this.showAutocomplete) {
866
+ this.showAutocomplete = false;
867
+ this.render();
868
+ return;
869
+ }
870
+ if (this.isStreaming) {
871
+ this.endStreaming();
872
+ }
873
+ return;
874
+ }
875
+ // Handle autocomplete navigation
876
+ if (this.showAutocomplete) {
877
+ if (event.key === 'up') {
878
+ this.autocompleteIndex = Math.max(0, this.autocompleteIndex - 1);
879
+ this.render();
880
+ return;
881
+ }
882
+ if (event.key === 'down') {
883
+ this.autocompleteIndex = Math.min(this.autocompleteItems.length - 1, this.autocompleteIndex + 1);
884
+ this.render();
885
+ return;
886
+ }
887
+ if (event.key === 'tab' || event.key === 'enter') {
888
+ // Select autocomplete item
889
+ if (this.autocompleteItems.length > 0) {
890
+ const selected = this.autocompleteItems[this.autocompleteIndex];
891
+ this.editor.setValue('/' + selected + ' ');
892
+ this.showAutocomplete = false;
893
+ this.render();
894
+ return;
895
+ }
896
+ }
897
+ }
898
+ // Ctrl+L to clear
899
+ if (event.ctrl && event.key === 'l') {
900
+ this.clearMessages();
901
+ this.notify('Chat cleared');
902
+ return;
903
+ }
904
+ // Ctrl+V to paste from clipboard
905
+ if (event.ctrl && event.key === 'v') {
906
+ this.pasteFromClipboard();
907
+ return;
908
+ }
909
+ // Ctrl+A - go to beginning of line
910
+ if (event.ctrl && event.key === 'a') {
911
+ this.editor.setCursorPos(0);
912
+ this.render();
913
+ return;
914
+ }
915
+ // Ctrl+E - go to end of line
916
+ if (event.ctrl && event.key === 'e') {
917
+ this.editor.setCursorPos(this.editor.getValue().length);
918
+ this.render();
919
+ return;
920
+ }
921
+ // Ctrl+U - clear line
922
+ if (event.ctrl && event.key === 'u') {
923
+ this.editor.clear();
924
+ this.showAutocomplete = false;
925
+ this.render();
926
+ return;
927
+ }
928
+ // Ctrl+W - delete word backward
929
+ if (event.ctrl && event.key === 'w') {
930
+ this.editor.deleteWordBackward();
931
+ this.updateAutocomplete();
932
+ this.render();
933
+ return;
934
+ }
935
+ // Ctrl+K - delete to end of line
936
+ if (event.ctrl && event.key === 'k') {
937
+ this.editor.deleteToEnd();
938
+ this.updateAutocomplete();
939
+ this.render();
940
+ return;
941
+ }
942
+ // Page up/down for scrolling chat history
943
+ if (event.key === 'pageup') {
944
+ // Scroll up (show older messages)
945
+ this.scrollOffset += 10;
946
+ this.render();
947
+ return;
948
+ }
949
+ if (event.key === 'pagedown') {
950
+ // Scroll down (show newer messages)
951
+ this.scrollOffset = Math.max(0, this.scrollOffset - 10);
952
+ this.render();
953
+ return;
954
+ }
955
+ // Arrow up/down can also scroll when input is empty
956
+ if (event.key === 'up' && !this.editor.getValue() && !this.showAutocomplete) {
957
+ this.scrollOffset += 3;
958
+ this.render();
959
+ return;
960
+ }
961
+ if (event.key === 'down' && !this.editor.getValue() && !this.showAutocomplete && this.scrollOffset > 0) {
962
+ this.scrollOffset = Math.max(0, this.scrollOffset - 3);
963
+ this.render();
964
+ return;
965
+ }
966
+ // Mouse scroll
967
+ if (event.key === 'scrollup') {
968
+ this.scrollOffset += 3;
969
+ this.render();
970
+ return;
971
+ }
972
+ if (event.key === 'scrolldown') {
973
+ this.scrollOffset = Math.max(0, this.scrollOffset - 3);
974
+ this.render();
975
+ return;
976
+ }
977
+ // Ignore other mouse events
978
+ if (event.key === 'mouse') {
979
+ return;
980
+ }
981
+ // Enter to submit (only if not in autocomplete)
982
+ if (event.key === 'enter' && !this.isLoading && !this.isStreaming && !this.showAutocomplete) {
983
+ const value = this.editor.getValue().trim();
984
+ if (value) {
985
+ this.editor.addToHistory(value);
986
+ this.editor.clear();
987
+ this.showAutocomplete = false;
988
+ // Check for commands
989
+ if (value.startsWith('/')) {
990
+ this.handleCommand(value);
991
+ }
992
+ else {
993
+ // Regular message
994
+ this.addMessage({ role: 'user', content: value });
995
+ this.setLoading(true);
996
+ this.options.onSubmit(value).catch(err => {
997
+ this.notify(`Error: ${err.message}`);
998
+ this.setLoading(false);
999
+ });
1000
+ }
1001
+ }
1002
+ return;
1003
+ }
1004
+ // Handle paste detection
1005
+ if (event.isPaste && event.key.length > 1) {
1006
+ this.handlePaste(event.key);
1007
+ return;
1008
+ }
1009
+ // Handle editor keys
1010
+ if (this.editor.handleKey(event)) {
1011
+ // Update autocomplete based on input
1012
+ this.updateAutocomplete();
1013
+ this.render();
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Update autocomplete suggestions
1018
+ */
1019
+ updateAutocomplete() {
1020
+ const value = this.editor.getValue();
1021
+ // Show autocomplete only when typing a command
1022
+ if (value.startsWith('/') && !value.includes(' ')) {
1023
+ const query = value.slice(1).toLowerCase();
1024
+ this.autocompleteItems = App.COMMANDS.filter(cmd => cmd.startsWith(query)).slice(0, 8); // Max 8 items
1025
+ this.showAutocomplete = this.autocompleteItems.length > 0 && query.length > 0;
1026
+ this.autocompleteIndex = 0;
1027
+ }
1028
+ else {
1029
+ this.showAutocomplete = false;
1030
+ this.autocompleteItems = [];
1031
+ }
1032
+ }
1033
+ /**
1034
+ * Handle help screen keys
1035
+ */
1036
+ handleInlineHelpKey(event) {
1037
+ if (event.key === 'escape' || event.key === 'q') {
1038
+ this.helpOpen = false;
1039
+ this.render();
1040
+ return;
1041
+ }
1042
+ // Calculate total help items
1043
+ let totalItems = 0;
1044
+ for (const cat of helpCategories) {
1045
+ totalItems += 1 + cat.items.length; // category header + items
1046
+ }
1047
+ totalItems += 1 + keyboardShortcuts.length; // shortcuts header + items
1048
+ if (event.key === 'down') {
1049
+ this.helpScrollIndex = Math.min(this.helpScrollIndex + 1, Math.max(0, totalItems - 5));
1050
+ this.render();
1051
+ return;
1052
+ }
1053
+ if (event.key === 'up') {
1054
+ this.helpScrollIndex = Math.max(0, this.helpScrollIndex - 1);
1055
+ this.render();
1056
+ return;
1057
+ }
1058
+ if (event.key === 'pagedown') {
1059
+ this.helpScrollIndex = Math.min(this.helpScrollIndex + 5, Math.max(0, totalItems - 5));
1060
+ this.render();
1061
+ return;
1062
+ }
1063
+ if (event.key === 'pageup') {
1064
+ this.helpScrollIndex = Math.max(0, this.helpScrollIndex - 5);
1065
+ this.render();
1066
+ return;
1067
+ }
1068
+ }
1069
+ /**
1070
+ * Handle inline settings keys
1071
+ */
1072
+ handleInlineSettingsKey(event) {
1073
+ const result = handleSettingsKey(event.key, event.ctrl, this.settingsState);
1074
+ this.settingsState = result.newState;
1075
+ if (result.close) {
1076
+ this.settingsOpen = false;
1077
+ }
1078
+ if (result.notify) {
1079
+ this.notify(result.notify);
1080
+ }
1081
+ this.render();
1082
+ }
1083
+ /**
1084
+ * Handle search screen keys
1085
+ */
1086
+ handleSearchKey(event) {
1087
+ if (event.key === 'escape') {
1088
+ this.searchOpen = false;
1089
+ this.searchCallback = null;
1090
+ this.render();
1091
+ return;
1092
+ }
1093
+ if (event.key === 'up') {
1094
+ this.searchIndex = Math.max(0, this.searchIndex - 1);
1095
+ this.render();
1096
+ return;
1097
+ }
1098
+ if (event.key === 'down') {
1099
+ this.searchIndex = Math.min(this.searchResults.length - 1, this.searchIndex + 1);
1100
+ this.render();
1101
+ return;
1102
+ }
1103
+ if (event.key === 'enter' && this.searchResults.length > 0) {
1104
+ const selectedResult = this.searchResults[this.searchIndex];
1105
+ const callback = this.searchCallback;
1106
+ this.searchOpen = false;
1107
+ this.searchCallback = null;
1108
+ this.render();
1109
+ if (callback) {
1110
+ callback(selectedResult.messageIndex);
1111
+ }
1112
+ return;
1113
+ }
1114
+ }
1115
+ /**
1116
+ * Handle export screen keys
1117
+ */
1118
+ handleExportKey(event) {
1119
+ const formats = ['md', 'json', 'txt'];
1120
+ if (event.key === 'escape') {
1121
+ this.exportOpen = false;
1122
+ this.exportCallback = null;
1123
+ this.render();
1124
+ return;
1125
+ }
1126
+ if (event.key === 'up') {
1127
+ this.exportIndex = this.exportIndex > 0 ? this.exportIndex - 1 : formats.length - 1;
1128
+ this.render();
1129
+ return;
1130
+ }
1131
+ if (event.key === 'down') {
1132
+ this.exportIndex = this.exportIndex < formats.length - 1 ? this.exportIndex + 1 : 0;
1133
+ this.render();
1134
+ return;
1135
+ }
1136
+ if (event.key === 'enter') {
1137
+ const selectedFormat = formats[this.exportIndex];
1138
+ const callback = this.exportCallback;
1139
+ this.exportOpen = false;
1140
+ this.exportCallback = null;
1141
+ this.render();
1142
+ if (callback) {
1143
+ callback(selectedFormat);
1144
+ }
1145
+ return;
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Handle logout picker keys
1150
+ */
1151
+ handleLogoutKey(event) {
1152
+ // Options: providers + "all" + "cancel"
1153
+ const totalOptions = this.logoutProviders.length + 2;
1154
+ if (event.key === 'escape') {
1155
+ this.logoutOpen = false;
1156
+ this.logoutCallback = null;
1157
+ this.render();
1158
+ return;
1159
+ }
1160
+ if (event.key === 'up') {
1161
+ this.logoutIndex = Math.max(0, this.logoutIndex - 1);
1162
+ this.render();
1163
+ return;
1164
+ }
1165
+ if (event.key === 'down') {
1166
+ this.logoutIndex = Math.min(totalOptions - 1, this.logoutIndex + 1);
1167
+ this.render();
1168
+ return;
1169
+ }
1170
+ if (event.key === 'enter') {
1171
+ const callback = this.logoutCallback;
1172
+ this.logoutOpen = false;
1173
+ this.logoutCallback = null;
1174
+ let result = null;
1175
+ if (this.logoutIndex < this.logoutProviders.length) {
1176
+ result = this.logoutProviders[this.logoutIndex].id;
1177
+ }
1178
+ else if (this.logoutIndex === this.logoutProviders.length) {
1179
+ result = 'all';
1180
+ }
1181
+ else {
1182
+ result = null; // Cancel
1183
+ }
1184
+ this.render();
1185
+ if (callback) {
1186
+ callback(result);
1187
+ }
1188
+ return;
1189
+ }
1190
+ }
1191
+ /**
1192
+ * Handle login keys
1193
+ */
1194
+ handleLoginKey(event) {
1195
+ if (this.loginStep === 'provider') {
1196
+ // Provider selection step
1197
+ if (event.key === 'escape') {
1198
+ this.loginOpen = false;
1199
+ const callback = this.loginCallback;
1200
+ this.loginCallback = null;
1201
+ this.render();
1202
+ if (callback)
1203
+ callback(null);
1204
+ return;
1205
+ }
1206
+ if (event.key === 'up') {
1207
+ this.loginProviderIndex = Math.max(0, this.loginProviderIndex - 1);
1208
+ this.render();
1209
+ return;
1210
+ }
1211
+ if (event.key === 'down') {
1212
+ this.loginProviderIndex = Math.min(this.loginProviders.length - 1, this.loginProviderIndex + 1);
1213
+ this.render();
1214
+ return;
1215
+ }
1216
+ if (event.key === 'enter') {
1217
+ // Move to API key entry
1218
+ this.loginStep = 'apikey';
1219
+ this.loginApiKey = '';
1220
+ this.loginError = '';
1221
+ this.render();
1222
+ return;
1223
+ }
1224
+ }
1225
+ else {
1226
+ // API key entry step
1227
+ if (event.key === 'escape') {
1228
+ // Go back to provider selection
1229
+ this.loginStep = 'provider';
1230
+ this.loginApiKey = '';
1231
+ this.loginError = '';
1232
+ this.render();
1233
+ return;
1234
+ }
1235
+ if (event.key === 'enter') {
1236
+ // Validate and submit
1237
+ if (this.loginApiKey.length < 10) {
1238
+ this.loginError = 'API key too short (min 10 characters)';
1239
+ this.render();
1240
+ return;
1241
+ }
1242
+ const callback = this.loginCallback;
1243
+ const result = {
1244
+ providerId: this.loginProviders[this.loginProviderIndex].id,
1245
+ apiKey: this.loginApiKey,
1246
+ };
1247
+ this.loginOpen = false;
1248
+ this.loginCallback = null;
1249
+ this.render();
1250
+ if (callback)
1251
+ callback(result);
1252
+ return;
1253
+ }
1254
+ if (event.key === 'backspace') {
1255
+ this.loginApiKey = this.loginApiKey.slice(0, -1);
1256
+ this.loginError = '';
1257
+ this.render();
1258
+ return;
1259
+ }
1260
+ // Ctrl+V to paste
1261
+ if (event.ctrl && event.key === 'v') {
1262
+ this.pasteApiKey();
1263
+ return;
1264
+ }
1265
+ // Handle paste detection (fast input)
1266
+ if (event.isPaste && event.key.length > 1) {
1267
+ this.loginApiKey += event.key.trim();
1268
+ this.loginError = '';
1269
+ this.render();
1270
+ return;
1271
+ }
1272
+ // Regular character input
1273
+ if (event.key.length === 1 && !event.ctrl) {
1274
+ this.loginApiKey += event.key;
1275
+ this.loginError = '';
1276
+ this.render();
1277
+ return;
1278
+ }
1279
+ }
1280
+ }
1281
+ /**
1282
+ * Paste API key from clipboard
1283
+ */
1284
+ async pasteApiKey() {
1285
+ try {
1286
+ const text = await clipboardy.read();
1287
+ if (text) {
1288
+ this.loginApiKey = text.trim();
1289
+ this.loginError = '';
1290
+ this.render();
1291
+ }
1292
+ }
1293
+ catch {
1294
+ this.loginError = 'Could not read clipboard';
1295
+ this.render();
1296
+ }
1297
+ }
1298
+ /**
1299
+ * Handle inline menu keys
1300
+ */
1301
+ handleMenuKey(event) {
1302
+ if (event.key === 'escape') {
1303
+ this.menuOpen = false;
1304
+ this.menuCallback = null;
1305
+ this.render();
1306
+ return;
1307
+ }
1308
+ if (event.key === 'up') {
1309
+ this.menuIndex = Math.max(0, this.menuIndex - 1);
1310
+ this.render();
1311
+ return;
1312
+ }
1313
+ if (event.key === 'down') {
1314
+ this.menuIndex = Math.min(this.menuItems.length - 1, this.menuIndex + 1);
1315
+ this.render();
1316
+ return;
1317
+ }
1318
+ if (event.key === 'enter') {
1319
+ const selectedItem = this.menuItems[this.menuIndex];
1320
+ const callback = this.menuCallback;
1321
+ this.menuOpen = false;
1322
+ this.menuCallback = null;
1323
+ this.render();
1324
+ if (callback) {
1325
+ callback(selectedItem);
1326
+ }
1327
+ return;
1328
+ }
1329
+ if (event.key === 'pageup') {
1330
+ this.menuIndex = Math.max(0, this.menuIndex - 5);
1331
+ this.render();
1332
+ return;
1333
+ }
1334
+ if (event.key === 'pagedown') {
1335
+ this.menuIndex = Math.min(this.menuItems.length - 1, this.menuIndex + 5);
1336
+ this.render();
1337
+ return;
1338
+ }
1339
+ // Ignore other keys when menu is open
1340
+ }
1341
+ /**
1342
+ * Handle confirmation dialog keys
1343
+ */
1344
+ handleInlinePermissionKey(event) {
1345
+ const options = ['read', 'write', 'none'];
1346
+ if (event.key === 'escape') {
1347
+ const callback = this.permissionCallback;
1348
+ this.permissionOpen = false;
1349
+ this.permissionCallback = null;
1350
+ this.render();
1351
+ if (callback)
1352
+ callback('none');
1353
+ return;
1354
+ }
1355
+ if (event.key === 'up') {
1356
+ this.permissionIndex = Math.max(0, this.permissionIndex - 1);
1357
+ this.render();
1358
+ return;
1359
+ }
1360
+ if (event.key === 'down') {
1361
+ this.permissionIndex = Math.min(options.length - 1, this.permissionIndex + 1);
1362
+ this.render();
1363
+ return;
1364
+ }
1365
+ if (event.key === 'enter') {
1366
+ const selected = options[this.permissionIndex];
1367
+ const callback = this.permissionCallback;
1368
+ this.permissionOpen = false;
1369
+ this.permissionCallback = null;
1370
+ this.render();
1371
+ if (callback)
1372
+ callback(selected);
1373
+ return;
1374
+ }
1375
+ }
1376
+ handleInlineSessionPickerKey(event) {
1377
+ // N = new session
1378
+ if (event.key === 'n' && !this.sessionPickerDeleteMode) {
1379
+ const callback = this.sessionPickerCallback;
1380
+ this.sessionPickerOpen = false;
1381
+ this.sessionPickerCallback = null;
1382
+ this.sessionPickerDeleteMode = false;
1383
+ this.render();
1384
+ if (callback)
1385
+ callback(null); // null means new session
1386
+ return;
1387
+ }
1388
+ // D = toggle delete mode
1389
+ if (event.key === 'd' && this.sessionPickerDeleteCallback && this.sessionPickerItems.length > 0) {
1390
+ this.sessionPickerDeleteMode = !this.sessionPickerDeleteMode;
1391
+ this.render();
1392
+ return;
1393
+ }
1394
+ if (event.key === 'escape') {
1395
+ if (this.sessionPickerDeleteMode) {
1396
+ // Exit delete mode
1397
+ this.sessionPickerDeleteMode = false;
1398
+ this.render();
1399
+ return;
1400
+ }
1401
+ // Escape = new session
1402
+ const callback = this.sessionPickerCallback;
1403
+ this.sessionPickerOpen = false;
1404
+ this.sessionPickerCallback = null;
1405
+ this.sessionPickerDeleteMode = false;
1406
+ this.render();
1407
+ if (callback)
1408
+ callback(null);
1409
+ return;
1410
+ }
1411
+ if (event.key === 'up') {
1412
+ this.sessionPickerIndex = Math.max(0, this.sessionPickerIndex - 1);
1413
+ this.render();
1414
+ return;
1415
+ }
1416
+ if (event.key === 'down') {
1417
+ this.sessionPickerIndex = Math.min(this.sessionPickerItems.length - 1, this.sessionPickerIndex + 1);
1418
+ this.render();
1419
+ return;
1420
+ }
1421
+ if (event.key === 'enter' && this.sessionPickerItems.length > 0) {
1422
+ const selected = this.sessionPickerItems[this.sessionPickerIndex];
1423
+ if (this.sessionPickerDeleteMode) {
1424
+ // Delete the selected session
1425
+ const deleteCallback = this.sessionPickerDeleteCallback;
1426
+ if (deleteCallback) {
1427
+ deleteCallback(selected.name);
1428
+ // Remove from list
1429
+ this.sessionPickerItems = this.sessionPickerItems.filter(s => s.name !== selected.name);
1430
+ // Adjust index if needed
1431
+ if (this.sessionPickerIndex >= this.sessionPickerItems.length) {
1432
+ this.sessionPickerIndex = Math.max(0, this.sessionPickerItems.length - 1);
1433
+ }
1434
+ // Exit delete mode if no more items
1435
+ if (this.sessionPickerItems.length === 0) {
1436
+ this.sessionPickerDeleteMode = false;
1437
+ }
1438
+ this.notify(`Deleted: ${selected.name}`);
1439
+ this.render();
1440
+ }
1441
+ return;
1442
+ }
1443
+ // Load selected session
1444
+ const callback = this.sessionPickerCallback;
1445
+ this.sessionPickerOpen = false;
1446
+ this.sessionPickerCallback = null;
1447
+ this.sessionPickerDeleteMode = false;
1448
+ this.render();
1449
+ if (callback)
1450
+ callback(selected.name);
1451
+ return;
1452
+ }
1453
+ }
1454
+ handleInlineConfirmKey(event) {
1455
+ if (!this.confirmOptions) {
1456
+ this.confirmOpen = false;
1457
+ this.render();
1458
+ return;
1459
+ }
1460
+ if (event.key === 'escape') {
1461
+ const onCancel = this.confirmOptions.onCancel;
1462
+ this.confirmOptions = null;
1463
+ this.confirmOpen = false;
1464
+ this.render();
1465
+ if (onCancel)
1466
+ onCancel();
1467
+ return;
1468
+ }
1469
+ if (event.key === 'left' || event.key === 'right' || event.key === 'tab') {
1470
+ this.confirmSelection = this.confirmSelection === 'yes' ? 'no' : 'yes';
1471
+ this.render();
1472
+ return;
1473
+ }
1474
+ if (event.key === 'y') {
1475
+ this.confirmSelection = 'yes';
1476
+ this.render();
1477
+ return;
1478
+ }
1479
+ if (event.key === 'n') {
1480
+ this.confirmSelection = 'no';
1481
+ this.render();
1482
+ return;
1483
+ }
1484
+ if (event.key === 'enter') {
1485
+ const options = this.confirmOptions;
1486
+ this.confirmOptions = null;
1487
+ this.confirmOpen = false;
1488
+ this.render();
1489
+ if (this.confirmSelection === 'yes') {
1490
+ options.onConfirm();
1491
+ }
1492
+ else if (options.onCancel) {
1493
+ options.onCancel();
1494
+ }
1495
+ return;
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Handle command
1500
+ */
1501
+ handleCommand(input) {
1502
+ const parts = input.slice(1).split(' ');
1503
+ const command = parts[0].toLowerCase();
1504
+ const args = parts.slice(1);
1505
+ switch (command) {
1506
+ case 'help':
1507
+ this.helpOpen = true;
1508
+ this.helpScrollIndex = 0;
1509
+ this.render();
1510
+ break;
1511
+ case 'status':
1512
+ this.currentScreen = 'status';
1513
+ this.render();
1514
+ break;
1515
+ case 'clear':
1516
+ this.clearMessages();
1517
+ this.notify('Chat cleared');
1518
+ break;
1519
+ case 'exit':
1520
+ case 'quit':
1521
+ this.stop();
1522
+ this.options.onExit();
1523
+ break;
1524
+ default:
1525
+ // Pass to external handler
1526
+ this.options.onCommand(command, args);
1527
+ break;
1528
+ }
1529
+ }
1530
+ /**
1531
+ * Render current screen
1532
+ */
1533
+ render() {
1534
+ // Intro animation takes over the whole screen
1535
+ if (this.showIntro) {
1536
+ this.renderIntro();
1537
+ return;
1538
+ }
1539
+ switch (this.currentScreen) {
1540
+ case 'status':
1541
+ renderStatusScreen(this.screen, this.options.getStatus());
1542
+ break;
1543
+ case 'chat':
1544
+ default:
1545
+ this.renderChat();
1546
+ break;
1547
+ }
1548
+ }
1549
+ /**
1550
+ * Render chat screen
1551
+ */
1552
+ renderChat() {
1553
+ const { width, height } = this.screen.getSize();
1554
+ this.screen.clear();
1555
+ // If menu or settings is open, reserve space for it at bottom
1556
+ let bottomPanelHeight = 0;
1557
+ if (this.pasteInfoOpen && this.pasteInfo) {
1558
+ const previewLines = Math.min(this.pasteInfo.preview.split('\n').length, 5);
1559
+ bottomPanelHeight = previewLines + 6; // title + preview + extra line indicator + options
1560
+ }
1561
+ else if (this.isAgentRunning) {
1562
+ bottomPanelHeight = 8; // Agent progress box
1563
+ }
1564
+ else if (this.permissionOpen) {
1565
+ bottomPanelHeight = 10; // Permission dialog
1566
+ }
1567
+ else if (this.sessionPickerOpen) {
1568
+ bottomPanelHeight = Math.min(this.sessionPickerItems.length + 6, 14); // Session picker
1569
+ }
1570
+ else if (this.confirmOpen && this.confirmOptions) {
1571
+ bottomPanelHeight = this.confirmOptions.message.length + 5; // title + messages + buttons + padding
1572
+ }
1573
+ else if (this.helpOpen) {
1574
+ bottomPanelHeight = Math.min(height - 6, 20); // Help takes more space
1575
+ }
1576
+ else if (this.searchOpen) {
1577
+ bottomPanelHeight = Math.min(this.searchResults.length * 3 + 6, 18); // Search results
1578
+ }
1579
+ else if (this.exportOpen) {
1580
+ bottomPanelHeight = 10; // Export dialog
1581
+ }
1582
+ else if (this.logoutOpen) {
1583
+ bottomPanelHeight = Math.min(this.logoutProviders.length + 6, 12); // Logout picker
1584
+ }
1585
+ else if (this.loginOpen) {
1586
+ bottomPanelHeight = this.loginStep === 'provider'
1587
+ ? Math.min(this.loginProviders.length + 5, 14)
1588
+ : 8; // Login dialog
1589
+ }
1590
+ else if (this.menuOpen) {
1591
+ bottomPanelHeight = Math.min(this.menuItems.length + 4, 14);
1592
+ }
1593
+ else if (this.settingsOpen) {
1594
+ bottomPanelHeight = Math.min(SETTINGS.length + 4, 16);
1595
+ }
1596
+ else if (this.showAutocomplete && this.autocompleteItems.length > 0) {
1597
+ bottomPanelHeight = Math.min(this.autocompleteItems.length + 3, 12);
1598
+ }
1599
+ const mainHeight = height - bottomPanelHeight;
1600
+ // Determine if we have enough space for logo (need at least 20 lines)
1601
+ const showLogo = height >= 24;
1602
+ const logoSpace = showLogo ? LOGO_HEIGHT + 1 : 1; // +1 for tagline or simple header
1603
+ // Layout - main UI takes top portion
1604
+ const headerLine = 0;
1605
+ const messagesStart = logoSpace;
1606
+ const messagesEnd = mainHeight - 4;
1607
+ const separatorLine = mainHeight - 3;
1608
+ const inputLine = mainHeight - 2;
1609
+ const statusLine = mainHeight - 1;
1610
+ // Header - show logo if space permits, otherwise simple text
1611
+ if (showLogo) {
1612
+ // Center the logo
1613
+ const logoWidth = LOGO_LINES[0].length;
1614
+ const logoX = Math.max(0, Math.floor((width - logoWidth) / 2));
1615
+ for (let i = 0; i < LOGO_LINES.length; i++) {
1616
+ this.screen.write(logoX, headerLine + i, LOGO_LINES[i], PRIMARY_COLOR);
1617
+ }
1618
+ }
1619
+ else {
1620
+ // Simple header for small terminals
1621
+ const header = ' Codeep ';
1622
+ const headerPadding = '─'.repeat(Math.max(0, (width - header.length) / 2));
1623
+ this.screen.writeLine(headerLine, headerPadding + header + headerPadding, PRIMARY_COLOR);
1624
+ }
1625
+ // Messages
1626
+ const messagesHeight = messagesEnd - messagesStart + 1;
1627
+ const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
1628
+ let y = messagesStart;
1629
+ for (const line of messagesToRender) {
1630
+ if (y > messagesEnd)
1631
+ break;
1632
+ if (line.raw) {
1633
+ // Line contains pre-formatted ANSI codes (e.g., syntax highlighted code)
1634
+ this.screen.writeRaw(y, line.text, line.style);
1635
+ }
1636
+ else {
1637
+ this.screen.writeLine(y, line.text, line.style);
1638
+ }
1639
+ y++;
1640
+ }
1641
+ // Separator
1642
+ this.screen.horizontalLine(separatorLine, '─', fg.gray);
1643
+ // Input (don't render cursor when menu/settings is open)
1644
+ this.renderInput(inputLine, width, this.menuOpen || this.settingsOpen);
1645
+ // Status bar
1646
+ this.renderStatusBar(statusLine, width);
1647
+ // Inline menu renders BELOW status bar
1648
+ if (this.menuOpen && this.menuItems.length > 0) {
1649
+ this.renderInlineMenu(statusLine + 1, width);
1650
+ }
1651
+ // Inline settings renders BELOW status bar
1652
+ if (this.settingsOpen) {
1653
+ this.renderInlineSettings(statusLine + 1, width, height - statusLine - 1);
1654
+ }
1655
+ // Inline help renders BELOW status bar
1656
+ if (this.helpOpen) {
1657
+ this.renderInlineHelp(statusLine + 1, width, height - statusLine - 1);
1658
+ }
1659
+ // Inline search renders BELOW status bar
1660
+ if (this.searchOpen) {
1661
+ this.renderInlineSearch(statusLine + 1, width, height - statusLine - 1);
1662
+ }
1663
+ // Inline export renders BELOW status bar
1664
+ if (this.exportOpen) {
1665
+ this.renderInlineExport(statusLine + 1, width);
1666
+ }
1667
+ // Inline logout renders BELOW status bar
1668
+ if (this.logoutOpen) {
1669
+ this.renderInlineLogout(statusLine + 1, width);
1670
+ }
1671
+ // Inline login renders BELOW status bar
1672
+ if (this.loginOpen) {
1673
+ this.renderInlineLogin(statusLine + 1, width);
1674
+ }
1675
+ // Inline confirm renders BELOW status bar
1676
+ if (this.confirmOpen && this.confirmOptions) {
1677
+ this.renderInlineConfirm(statusLine + 1, width);
1678
+ }
1679
+ // Inline autocomplete renders BELOW status bar
1680
+ if (this.showAutocomplete && this.autocompleteItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
1681
+ this.renderInlineAutocomplete(statusLine + 1, width);
1682
+ }
1683
+ // Inline permission renders BELOW status bar
1684
+ if (this.permissionOpen) {
1685
+ this.renderInlinePermission(statusLine + 1, width);
1686
+ }
1687
+ // Inline session picker renders BELOW status bar
1688
+ if (this.sessionPickerOpen) {
1689
+ this.renderInlineSessionPicker(statusLine + 1, width);
1690
+ }
1691
+ // Inline agent progress renders BELOW status bar
1692
+ if (this.isAgentRunning) {
1693
+ this.renderInlineAgentProgress(statusLine + 1, width);
1694
+ }
1695
+ // Inline paste info renders BELOW status bar
1696
+ if (this.pasteInfoOpen && this.pasteInfo) {
1697
+ this.renderInlinePasteInfo(statusLine + 1, width);
1698
+ }
1699
+ this.screen.fullRender();
1700
+ }
1701
+ /**
1702
+ * Render inline confirmation dialog below status bar
1703
+ */
1704
+ renderInlineConfirm(startY, width) {
1705
+ if (!this.confirmOptions)
1706
+ return;
1707
+ let y = startY;
1708
+ // Separator line
1709
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
1710
+ // Title
1711
+ this.screen.writeLine(y++, this.confirmOptions.title, PRIMARY_COLOR + style.bold);
1712
+ // Message lines
1713
+ for (const line of this.confirmOptions.message) {
1714
+ this.screen.writeLine(y++, line, fg.white);
1715
+ }
1716
+ // Buttons
1717
+ y++;
1718
+ const yesLabel = this.confirmOptions.confirmLabel || 'Yes';
1719
+ const noLabel = this.confirmOptions.cancelLabel || 'No';
1720
+ const yesStyle = this.confirmSelection === 'yes' ? PRIMARY_COLOR + style.bold : fg.gray;
1721
+ const noStyle = this.confirmSelection === 'no' ? PRIMARY_COLOR + style.bold : fg.gray;
1722
+ const yesButton = this.confirmSelection === 'yes' ? `► ${yesLabel}` : ` ${yesLabel}`;
1723
+ const noButton = this.confirmSelection === 'no' ? `► ${noLabel}` : ` ${noLabel}`;
1724
+ this.screen.write(2, y, yesButton, yesStyle);
1725
+ this.screen.write(2 + yesButton.length + 4, y, noButton, noStyle);
1726
+ y++;
1727
+ // Footer
1728
+ this.screen.writeLine(y, '←/→ select • y/n quick • Enter confirm • Esc cancel', fg.gray);
1729
+ }
1730
+ /**
1731
+ * Render input line
1732
+ */
1733
+ renderInput(y, width, hideCursor = false) {
1734
+ const inputValue = this.editor.getValue();
1735
+ const cursorPos = this.editor.getCursorPos();
1736
+ // Session picker open - show different prompt
1737
+ if (this.sessionPickerOpen) {
1738
+ this.screen.write(0, y, '> ', fg.gray);
1739
+ this.screen.write(2, y, 'Select a session below or press N for new...', fg.yellow);
1740
+ this.screen.showCursor(false);
1741
+ return;
1742
+ }
1743
+ // Permission dialog open
1744
+ if (this.permissionOpen) {
1745
+ this.screen.write(0, y, '> ', fg.gray);
1746
+ this.screen.write(2, y, 'Select access level below...', fg.yellow);
1747
+ this.screen.showCursor(false);
1748
+ return;
1749
+ }
1750
+ // Paste info open
1751
+ if (this.pasteInfoOpen) {
1752
+ this.screen.write(0, y, '> ', fg.gray);
1753
+ this.screen.write(2, y, 'Confirm paste action below...', fg.yellow);
1754
+ this.screen.showCursor(false);
1755
+ return;
1756
+ }
1757
+ // Menu open (provider, model, lang, etc.)
1758
+ if (this.menuOpen) {
1759
+ this.screen.write(0, y, '> ', fg.gray);
1760
+ this.screen.write(2, y, 'Select an option below...', fg.yellow);
1761
+ this.screen.showCursor(false);
1762
+ return;
1763
+ }
1764
+ // Search open
1765
+ if (this.searchOpen) {
1766
+ this.screen.write(0, y, '> ', fg.gray);
1767
+ this.screen.write(2, y, 'Navigate search results below...', fg.yellow);
1768
+ this.screen.showCursor(false);
1769
+ return;
1770
+ }
1771
+ // Export open
1772
+ if (this.exportOpen) {
1773
+ this.screen.write(0, y, '> ', fg.gray);
1774
+ this.screen.write(2, y, 'Select export format below...', fg.yellow);
1775
+ this.screen.showCursor(false);
1776
+ return;
1777
+ }
1778
+ // Logout open
1779
+ if (this.logoutOpen) {
1780
+ this.screen.write(0, y, '> ', fg.gray);
1781
+ this.screen.write(2, y, 'Select provider to logout...', fg.yellow);
1782
+ this.screen.showCursor(false);
1783
+ return;
1784
+ }
1785
+ // Login open
1786
+ if (this.loginOpen) {
1787
+ this.screen.write(0, y, '> ', fg.gray);
1788
+ const msg = this.loginStep === 'provider'
1789
+ ? 'Select a provider below...'
1790
+ : 'Enter your API key below...';
1791
+ this.screen.write(2, y, msg, fg.yellow);
1792
+ this.screen.showCursor(false);
1793
+ return;
1794
+ }
1795
+ // Agent running state - show special prompt
1796
+ if (this.isAgentRunning) {
1797
+ const spinner = SPINNER_FRAMES[this.spinnerFrame];
1798
+ const agentText = `${spinner} Agent working... step ${this.agentIteration} | ${this.agentActions.length} actions (Esc to stop)`;
1799
+ this.screen.writeLine(y, agentText, PRIMARY_COLOR);
1800
+ this.screen.showCursor(false);
1801
+ return;
1802
+ }
1803
+ // Loading/streaming state with animated spinner
1804
+ if (this.isLoading || this.isStreaming) {
1805
+ const spinner = SPINNER_FRAMES[this.spinnerFrame];
1806
+ const message = this.isStreaming ? 'Writing' : 'Thinking';
1807
+ this.screen.writeLine(y, `${spinner} ${message}...`, PRIMARY_COLOR);
1808
+ this.screen.showCursor(false);
1809
+ return;
1810
+ }
1811
+ const prompt = '> ';
1812
+ const maxInputWidth = width - prompt.length - 1;
1813
+ // Show placeholder when input is empty
1814
+ if (!inputValue) {
1815
+ this.screen.write(0, y, prompt, fg.green);
1816
+ this.screen.write(prompt.length, y, 'Type a message or /command...', fg.gray);
1817
+ if (!hideCursor) {
1818
+ this.screen.setCursor(prompt.length, y);
1819
+ this.screen.showCursor(true);
1820
+ }
1821
+ else {
1822
+ this.screen.showCursor(false);
1823
+ }
1824
+ return;
1825
+ }
1826
+ let displayValue;
1827
+ let cursorX;
1828
+ if (inputValue.length <= maxInputWidth) {
1829
+ displayValue = inputValue;
1830
+ cursorX = prompt.length + cursorPos;
1831
+ }
1832
+ else {
1833
+ const visibleStart = Math.max(0, cursorPos - Math.floor(maxInputWidth * 0.7));
1834
+ const visibleEnd = visibleStart + maxInputWidth;
1835
+ if (visibleStart > 0) {
1836
+ displayValue = '…' + inputValue.slice(visibleStart + 1, visibleEnd);
1837
+ }
1838
+ else {
1839
+ displayValue = inputValue.slice(0, maxInputWidth);
1840
+ }
1841
+ cursorX = prompt.length + (cursorPos - visibleStart);
1842
+ }
1843
+ this.screen.writeLine(y, prompt + displayValue, fg.green);
1844
+ // Hide cursor when menu/settings is open
1845
+ if (hideCursor) {
1846
+ this.screen.showCursor(false);
1847
+ }
1848
+ else {
1849
+ this.screen.setCursor(cursorX, y);
1850
+ this.screen.showCursor(true);
1851
+ }
1852
+ }
1853
+ /**
1854
+ * Render inline menu below status bar
1855
+ */
1856
+ renderInlineMenu(startY, width) {
1857
+ const items = this.menuItems;
1858
+ const maxVisible = Math.min(items.length, 10);
1859
+ // Calculate visible range with scroll
1860
+ let visibleStart = 0;
1861
+ if (items.length > maxVisible) {
1862
+ visibleStart = Math.max(0, Math.min(this.menuIndex - Math.floor(maxVisible / 2), items.length - maxVisible));
1863
+ }
1864
+ const visibleItems = items.slice(visibleStart, visibleStart + maxVisible);
1865
+ let y = startY;
1866
+ // Separator line
1867
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
1868
+ // Title
1869
+ this.screen.writeLine(y++, this.menuTitle, PRIMARY_COLOR + style.bold);
1870
+ // Items
1871
+ for (let i = 0; i < visibleItems.length; i++) {
1872
+ const item = visibleItems[i];
1873
+ const actualIndex = visibleStart + i;
1874
+ const isSelected = actualIndex === this.menuIndex;
1875
+ const isCurrent = item.key === this.menuCurrentValue;
1876
+ const prefix = isSelected ? '► ' : ' ';
1877
+ const suffix = isCurrent ? ' ✓' : '';
1878
+ let itemStyle = fg.white;
1879
+ if (isSelected) {
1880
+ itemStyle = PRIMARY_COLOR + style.bold;
1881
+ }
1882
+ else if (isCurrent) {
1883
+ itemStyle = fg.green;
1884
+ }
1885
+ this.screen.writeLine(y++, prefix + item.label + suffix, itemStyle);
1886
+ }
1887
+ // Footer with navigation hints
1888
+ const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
1889
+ this.screen.writeLine(y, `↑↓ navigate • Enter select • Esc cancel${scrollInfo}`, fg.gray);
1890
+ }
1891
+ /**
1892
+ * Render inline settings below status bar
1893
+ */
1894
+ renderInlineSettings(startY, width, availableHeight) {
1895
+ const maxVisible = Math.min(SETTINGS.length, availableHeight - 3);
1896
+ const scrollOffset = Math.max(0, this.settingsState.selectedIndex - maxVisible + 3);
1897
+ let y = startY;
1898
+ // Separator line
1899
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
1900
+ // Title
1901
+ this.screen.writeLine(y++, 'Settings', PRIMARY_COLOR + style.bold);
1902
+ // Settings items
1903
+ for (let i = 0; i < maxVisible && (i + scrollOffset) < SETTINGS.length; i++) {
1904
+ const settingIdx = i + scrollOffset;
1905
+ const setting = SETTINGS[settingIdx];
1906
+ const isSelected = settingIdx === this.settingsState.selectedIndex;
1907
+ const prefix = isSelected ? '► ' : ' ';
1908
+ // Format value
1909
+ let valueStr;
1910
+ if (this.settingsState.editing && isSelected) {
1911
+ valueStr = this.settingsState.editValue + '█';
1912
+ }
1913
+ else {
1914
+ const value = setting.getValue();
1915
+ if (setting.type === 'select' && setting.options) {
1916
+ const option = setting.options.find(o => o.value === value);
1917
+ valueStr = option ? option.label : String(value);
1918
+ }
1919
+ else {
1920
+ valueStr = String(value);
1921
+ }
1922
+ }
1923
+ const labelStyle = isSelected ? PRIMARY_COLOR + style.bold : fg.white;
1924
+ const valueStyle = this.settingsState.editing && isSelected ? fg.cyan : fg.green;
1925
+ this.screen.write(2, y, prefix, isSelected ? PRIMARY_COLOR : '');
1926
+ this.screen.write(4, y, setting.label + ': ', labelStyle);
1927
+ this.screen.write(4 + setting.label.length + 2, y, valueStr, valueStyle);
1928
+ // Hint for selected item
1929
+ if (isSelected && !this.settingsState.editing) {
1930
+ const hintX = 4 + setting.label.length + 2 + valueStr.length + 2;
1931
+ const hint = setting.type === 'number' ? '(←/→ adjust)' : '(←/→ toggle)';
1932
+ this.screen.write(hintX, y, hint, fg.gray);
1933
+ }
1934
+ y++;
1935
+ }
1936
+ // Footer
1937
+ const scrollInfo = SETTINGS.length > maxVisible ? ` (${scrollOffset + 1}-${scrollOffset + maxVisible}/${SETTINGS.length})` : '';
1938
+ this.screen.writeLine(y, `↑↓ navigate • ←/→ adjust • Esc close${scrollInfo}`, fg.gray);
1939
+ }
1940
+ /**
1941
+ * Render inline help below status bar
1942
+ */
1943
+ renderInlineHelp(startY, width, availableHeight) {
1944
+ // Build all help items
1945
+ const allItems = [];
1946
+ for (const category of helpCategories) {
1947
+ allItems.push({ text: category.title, isHeader: true });
1948
+ for (const item of category.items) {
1949
+ allItems.push({ text: ` ${item.key.padEnd(22)} ${item.description}`, isHeader: false });
1950
+ }
1951
+ }
1952
+ // Add keyboard shortcuts
1953
+ allItems.push({ text: 'Keyboard Shortcuts', isHeader: true });
1954
+ for (const shortcut of keyboardShortcuts) {
1955
+ allItems.push({ text: ` ${shortcut.key.padEnd(22)} ${shortcut.description}`, isHeader: false });
1956
+ }
1957
+ const maxVisible = availableHeight - 3;
1958
+ const visibleItems = allItems.slice(this.helpScrollIndex, this.helpScrollIndex + maxVisible);
1959
+ let y = startY;
1960
+ // Separator line
1961
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
1962
+ // Title
1963
+ this.screen.writeLine(y++, 'Help - Commands & Shortcuts', PRIMARY_COLOR + style.bold);
1964
+ // Help items
1965
+ for (const item of visibleItems) {
1966
+ if (item.isHeader) {
1967
+ this.screen.writeLine(y, item.text, fg.yellow + style.bold);
1968
+ }
1969
+ else {
1970
+ // Highlight command part
1971
+ const match = item.text.match(/^(\s*)(\S+)(\s+)(.*)$/);
1972
+ if (match) {
1973
+ const [, indent, cmd, space, desc] = match;
1974
+ this.screen.write(0, y, indent, '');
1975
+ this.screen.write(indent.length, y, cmd, fg.green);
1976
+ this.screen.write(indent.length + cmd.length, y, space + desc, fg.white);
1977
+ }
1978
+ else {
1979
+ this.screen.writeLine(y, item.text, fg.white);
1980
+ }
1981
+ }
1982
+ y++;
1983
+ }
1984
+ // Footer
1985
+ const scrollInfo = allItems.length > maxVisible ? ` (${this.helpScrollIndex + 1}-${Math.min(this.helpScrollIndex + maxVisible, allItems.length)}/${allItems.length})` : '';
1986
+ this.screen.writeLine(y, `↑↓ scroll • PgUp/PgDn fast scroll • Esc close${scrollInfo}`, fg.gray);
1987
+ }
1988
+ /**
1989
+ * Render inline autocomplete below status bar
1990
+ */
1991
+ renderInlineAutocomplete(startY, width) {
1992
+ const items = this.autocompleteItems;
1993
+ const maxVisible = Math.min(items.length, 8);
1994
+ let y = startY;
1995
+ // Separator line
1996
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
1997
+ // Title
1998
+ this.screen.writeLine(y++, 'Commands', PRIMARY_COLOR + style.bold);
1999
+ // Items with descriptions
2000
+ const visibleStart = Math.max(0, this.autocompleteIndex - maxVisible + 1);
2001
+ const visibleItems = items.slice(visibleStart, visibleStart + maxVisible);
2002
+ for (let i = 0; i < visibleItems.length; i++) {
2003
+ const item = visibleItems[i];
2004
+ const actualIndex = visibleStart + i;
2005
+ const isSelected = actualIndex === this.autocompleteIndex;
2006
+ const desc = COMMAND_DESCRIPTIONS[item] || '';
2007
+ const prefix = isSelected ? '► ' : ' ';
2008
+ const cmdText = ('/' + item).padEnd(18);
2009
+ if (isSelected) {
2010
+ this.screen.write(0, y, prefix, PRIMARY_COLOR);
2011
+ this.screen.write(prefix.length, y, cmdText, PRIMARY_COLOR + style.bold);
2012
+ this.screen.write(prefix.length + cmdText.length, y, desc, fg.white);
2013
+ }
2014
+ else {
2015
+ this.screen.write(0, y, prefix, '');
2016
+ this.screen.write(prefix.length, y, cmdText, fg.green);
2017
+ this.screen.write(prefix.length + cmdText.length, y, desc, fg.gray);
2018
+ }
2019
+ y++;
2020
+ }
2021
+ // Footer
2022
+ const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
2023
+ this.screen.writeLine(y, `↑↓ navigate • Tab/Enter select • Esc cancel${scrollInfo}`, fg.gray);
2024
+ }
2025
+ /**
2026
+ * Render inline permission dialog
2027
+ */
2028
+ renderInlinePermission(startY, width) {
2029
+ const options = [
2030
+ { level: 'read', label: 'Read Only', desc: 'AI can read files, no modifications' },
2031
+ { level: 'write', label: 'Read & Write', desc: 'AI can read and modify files (Agent mode)' },
2032
+ { level: 'none', label: 'No Access', desc: 'Chat without project context' },
2033
+ ];
2034
+ let y = startY;
2035
+ // Separator line
2036
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
2037
+ // Title
2038
+ this.screen.writeLine(y++, 'Folder Access', PRIMARY_COLOR + style.bold);
2039
+ // Project path
2040
+ const displayPath = this.permissionPath.length > width - 12
2041
+ ? '...' + this.permissionPath.slice(-(width - 15))
2042
+ : this.permissionPath;
2043
+ this.screen.writeLine(y++, `Project: ${displayPath}`, fg.cyan);
2044
+ // Description
2045
+ const desc = this.permissionIsProject ? 'This looks like a project folder.' : 'Grant access to enable AI assistance.';
2046
+ this.screen.writeLine(y++, desc, fg.white);
2047
+ y++;
2048
+ // Options
2049
+ for (let i = 0; i < options.length; i++) {
2050
+ const opt = options[i];
2051
+ const isSelected = i === this.permissionIndex;
2052
+ const prefix = isSelected ? '► ' : ' ';
2053
+ const labelStyle = isSelected ? PRIMARY_COLOR + style.bold : fg.white;
2054
+ this.screen.write(2, y, prefix + opt.label.padEnd(16), labelStyle);
2055
+ this.screen.write(22, y, opt.desc, fg.gray);
2056
+ y++;
2057
+ }
2058
+ // Footer
2059
+ this.screen.writeLine(y, '↑↓ navigate • Enter select • Esc skip', fg.gray);
2060
+ }
2061
+ /**
2062
+ * Render inline session picker
2063
+ */
2064
+ renderInlineSessionPicker(startY, width) {
2065
+ const sessions = this.sessionPickerItems;
2066
+ const maxVisible = Math.min(sessions.length, 8);
2067
+ const deleteMode = this.sessionPickerDeleteMode;
2068
+ let y = startY;
2069
+ // Separator line
2070
+ this.screen.horizontalLine(y++, '─', deleteMode ? fg.red : PRIMARY_COLOR);
2071
+ // Title
2072
+ if (deleteMode) {
2073
+ this.screen.writeLine(y++, 'Delete Session (Enter to confirm, Esc to cancel)', fg.red + style.bold);
2074
+ }
2075
+ else {
2076
+ this.screen.writeLine(y++, 'Select Session', PRIMARY_COLOR + style.bold);
2077
+ }
2078
+ if (sessions.length === 0) {
2079
+ this.screen.writeLine(y++, 'No previous sessions found.', fg.gray);
2080
+ this.screen.writeLine(y, 'Press N or Enter to start a new session.', fg.white);
2081
+ }
2082
+ else {
2083
+ // Sessions list
2084
+ const visibleStart = Math.max(0, this.sessionPickerIndex - maxVisible + 1);
2085
+ const visibleSessions = sessions.slice(visibleStart, visibleStart + maxVisible);
2086
+ for (let i = 0; i < visibleSessions.length; i++) {
2087
+ const session = visibleSessions[i];
2088
+ const actualIndex = visibleStart + i;
2089
+ const isSelected = actualIndex === this.sessionPickerIndex;
2090
+ const prefix = isSelected ? (deleteMode ? '✗ ' : '► ') : ' ';
2091
+ // Format relative time
2092
+ const date = new Date(session.createdAt);
2093
+ const now = new Date();
2094
+ const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
2095
+ const timeStr = diffDays === 0 ? 'today' : diffDays === 1 ? 'yesterday' : `${diffDays}d ago`;
2096
+ const name = session.name.length > 25 ? session.name.slice(0, 22) + '...' : session.name;
2097
+ const meta = `${session.messageCount} msg, ${timeStr}`;
2098
+ let nameStyle = fg.white;
2099
+ if (isSelected && deleteMode) {
2100
+ nameStyle = fg.red + style.bold;
2101
+ }
2102
+ else if (isSelected) {
2103
+ nameStyle = PRIMARY_COLOR + style.bold;
2104
+ }
2105
+ this.screen.write(2, y, prefix + name, nameStyle);
2106
+ this.screen.write(32, y, meta, fg.cyan);
2107
+ y++;
2108
+ }
2109
+ // Scroll info
2110
+ if (sessions.length > maxVisible) {
2111
+ this.screen.write(2, y++, `(${visibleStart + 1}-${visibleStart + visibleSessions.length}/${sessions.length})`, fg.gray);
2112
+ }
2113
+ }
2114
+ y++;
2115
+ // Options
2116
+ if (deleteMode) {
2117
+ this.screen.writeLine(y++, '[Enter] Delete selected • [Esc] Cancel', fg.red);
2118
+ }
2119
+ else {
2120
+ this.screen.write(0, y, '[N] ', fg.yellow);
2121
+ this.screen.write(4, y, 'New session', fg.white);
2122
+ if (this.sessionPickerDeleteCallback && sessions.length > 0) {
2123
+ this.screen.write(18, y, ' [D] ', fg.red);
2124
+ this.screen.write(23, y, 'Delete mode', fg.white);
2125
+ }
2126
+ y++;
2127
+ // Footer
2128
+ this.screen.writeLine(y, '↑↓ navigate • Enter select', fg.gray);
2129
+ }
2130
+ }
2131
+ /**
2132
+ * Render inline paste info below status bar
2133
+ */
2134
+ renderInlinePasteInfo(startY, width) {
2135
+ if (!this.pasteInfo)
2136
+ return;
2137
+ let y = startY;
2138
+ // Separator line
2139
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
2140
+ // Title with stats
2141
+ this.screen.write(0, y, 'Paste Detected ', PRIMARY_COLOR + style.bold);
2142
+ this.screen.write(15, y, `(${this.pasteInfo.chars} chars, ${this.pasteInfo.lines} lines)`, fg.cyan);
2143
+ y++;
2144
+ // Preview box
2145
+ y++;
2146
+ const previewLines = this.pasteInfo.preview.split('\n').slice(0, 5);
2147
+ for (const line of previewLines) {
2148
+ const displayLine = line.length > width - 4 ? line.slice(0, width - 7) + '...' : line;
2149
+ this.screen.writeLine(y++, ' ' + displayLine, fg.gray);
2150
+ }
2151
+ if (this.pasteInfo.lines > 5) {
2152
+ this.screen.writeLine(y++, ` ... (${this.pasteInfo.lines - 5} more lines)`, fg.gray);
2153
+ }
2154
+ y++;
2155
+ // Options
2156
+ this.screen.write(0, y, '[Y/Enter] ', fg.green);
2157
+ this.screen.write(10, y, 'Add to input', fg.white);
2158
+ this.screen.write(25, y, '[S] ', fg.yellow);
2159
+ this.screen.write(29, y, 'Send directly', fg.white);
2160
+ this.screen.write(45, y, '[N/Esc] ', fg.red);
2161
+ this.screen.write(53, y, 'Cancel', fg.white);
2162
+ }
2163
+ /**
2164
+ * Render inline agent progress below status bar (LiveCodeStream style)
2165
+ */
2166
+ renderInlineAgentProgress(startY, width) {
2167
+ let y = startY;
2168
+ const spinner = SPINNER_FRAMES[this.spinnerFrame];
2169
+ const boxWidth = Math.min(width - 4, 60);
2170
+ // Calculate stats
2171
+ const stats = {
2172
+ reads: this.agentActions.filter(a => a.type === 'read').length,
2173
+ writes: this.agentActions.filter(a => a.type === 'write').length,
2174
+ edits: this.agentActions.filter(a => a.type === 'edit').length,
2175
+ deletes: this.agentActions.filter(a => a.type === 'delete').length,
2176
+ commands: this.agentActions.filter(a => a.type === 'command').length,
2177
+ searches: this.agentActions.filter(a => a.type === 'search').length,
2178
+ errors: this.agentActions.filter(a => a.result === 'error').length,
2179
+ };
2180
+ // Top border with title
2181
+ const title = ` ${spinner} AGENT `;
2182
+ const borderLeft = '╭' + '─'.repeat(2);
2183
+ const borderRight = '─'.repeat(Math.max(0, boxWidth - title.length - 4)) + '╮';
2184
+ this.screen.write(0, y, borderLeft, PRIMARY_COLOR);
2185
+ this.screen.write(borderLeft.length, y, title, PRIMARY_COLOR + style.bold);
2186
+ this.screen.write(borderLeft.length + title.length, y, borderRight, PRIMARY_COLOR);
2187
+ y++;
2188
+ // Current action line
2189
+ this.screen.write(0, y, '│ ', PRIMARY_COLOR);
2190
+ if (this.agentActions.length > 0) {
2191
+ const lastAction = this.agentActions[this.agentActions.length - 1];
2192
+ const actionLabel = this.getActionLabel(lastAction.type);
2193
+ const actionColor = this.getActionColor(lastAction.type);
2194
+ const target = this.formatActionTarget(lastAction.target, boxWidth - actionLabel.length - 6);
2195
+ this.screen.write(2, y, actionLabel + ' ', actionColor + style.bold);
2196
+ this.screen.write(2 + actionLabel.length + 1, y, target, fg.white);
2197
+ }
2198
+ else {
2199
+ this.screen.write(2, y, 'Starting...', fg.gray);
2200
+ }
2201
+ this.screen.write(boxWidth - 1, y, ' │', PRIMARY_COLOR);
2202
+ y++;
2203
+ // Separator
2204
+ this.screen.write(0, y, '├' + '─'.repeat(boxWidth - 2) + '┤', fg.gray);
2205
+ y++;
2206
+ // File changes line
2207
+ this.screen.write(0, y, '│ ', PRIMARY_COLOR);
2208
+ this.screen.write(2, y, 'Files: ', fg.cyan);
2209
+ let fileX = 9;
2210
+ if (stats.writes > 0) {
2211
+ const txt = `+${stats.writes} `;
2212
+ this.screen.write(fileX, y, txt, fg.green);
2213
+ fileX += txt.length;
2214
+ }
2215
+ if (stats.edits > 0) {
2216
+ const txt = `~${stats.edits} `;
2217
+ this.screen.write(fileX, y, txt, fg.yellow);
2218
+ fileX += txt.length;
2219
+ }
2220
+ if (stats.deletes > 0) {
2221
+ const txt = `-${stats.deletes} `;
2222
+ this.screen.write(fileX, y, txt, fg.red);
2223
+ fileX += txt.length;
2224
+ }
2225
+ if (stats.writes === 0 && stats.edits === 0 && stats.deletes === 0) {
2226
+ this.screen.write(fileX, y, 'no changes yet', fg.gray);
2227
+ }
2228
+ this.screen.write(boxWidth - 1, y, ' │', PRIMARY_COLOR);
2229
+ y++;
2230
+ // Stats line
2231
+ this.screen.write(0, y, '│ ', PRIMARY_COLOR);
2232
+ this.screen.write(2, y, 'Stats: ', fg.cyan);
2233
+ let statX = 9;
2234
+ const statParts = [];
2235
+ if (stats.reads > 0)
2236
+ statParts.push({ text: `${stats.reads}R`, color: fg.blue });
2237
+ if (stats.commands > 0)
2238
+ statParts.push({ text: `${stats.commands}C`, color: fg.magenta });
2239
+ if (stats.searches > 0)
2240
+ statParts.push({ text: `${stats.searches}S`, color: fg.cyan });
2241
+ statParts.push({ text: `step ${this.agentIteration}`, color: fg.white });
2242
+ for (let i = 0; i < statParts.length; i++) {
2243
+ if (i > 0) {
2244
+ this.screen.write(statX, y, ' | ', fg.gray);
2245
+ statX += 3;
2246
+ }
2247
+ this.screen.write(statX, y, statParts[i].text, statParts[i].color);
2248
+ statX += statParts[i].text.length;
2249
+ }
2250
+ this.screen.write(boxWidth - 1, y, ' │', PRIMARY_COLOR);
2251
+ y++;
2252
+ // Errors line (if any)
2253
+ this.screen.write(0, y, '│ ', PRIMARY_COLOR);
2254
+ if (stats.errors > 0) {
2255
+ this.screen.write(2, y, `${stats.errors} error(s)`, fg.red);
2256
+ }
2257
+ else if (this.agentThinking) {
2258
+ const thinking = this.agentThinking.length > boxWidth - 6
2259
+ ? this.agentThinking.slice(0, boxWidth - 9) + '...'
2260
+ : this.agentThinking;
2261
+ this.screen.write(2, y, '> ' + thinking, fg.gray);
2262
+ }
2263
+ this.screen.write(boxWidth - 1, y, ' │', PRIMARY_COLOR);
2264
+ y++;
2265
+ // Bottom border with help
2266
+ const helpText = ' Esc to stop ';
2267
+ const bottomLeft = '╰' + '─'.repeat(Math.floor((boxWidth - helpText.length - 2) / 2));
2268
+ const bottomRight = '─'.repeat(Math.ceil((boxWidth - helpText.length - 2) / 2)) + '╯';
2269
+ this.screen.write(0, y, bottomLeft, PRIMARY_COLOR);
2270
+ this.screen.write(bottomLeft.length, y, helpText, fg.gray);
2271
+ this.screen.write(bottomLeft.length + helpText.length, y, bottomRight, PRIMARY_COLOR);
2272
+ }
2273
+ /**
2274
+ * Get color for action type
2275
+ */
2276
+ getActionColor(type) {
2277
+ const colors = {
2278
+ 'read': fg.blue,
2279
+ 'write': fg.green,
2280
+ 'edit': fg.yellow,
2281
+ 'delete': fg.red,
2282
+ 'command': fg.magenta,
2283
+ 'search': fg.cyan,
2284
+ 'list': fg.white,
2285
+ 'mkdir': fg.blue,
2286
+ 'fetch': fg.cyan,
2287
+ };
2288
+ return colors[type] || fg.white;
2289
+ }
2290
+ /**
2291
+ * Format action target for display
2292
+ */
2293
+ formatActionTarget(target, maxLen) {
2294
+ if (target.includes('/')) {
2295
+ const parts = target.split('/');
2296
+ const filename = parts[parts.length - 1];
2297
+ if (parts.length > 2) {
2298
+ const short = `.../${parts[parts.length - 2]}/${filename}`;
2299
+ return short.length > maxLen ? '...' + short.slice(-(maxLen - 3)) : short;
2300
+ }
2301
+ }
2302
+ return target.length > maxLen ? '...' + target.slice(-(maxLen - 3)) : target;
2303
+ }
2304
+ /**
2305
+ * Get action label for display
2306
+ */
2307
+ getActionLabel(type) {
2308
+ const labels = {
2309
+ 'read': 'Reading',
2310
+ 'write': 'Creating',
2311
+ 'edit': 'Editing',
2312
+ 'delete': 'Deleting',
2313
+ 'command': 'Running',
2314
+ 'search': 'Searching',
2315
+ 'list': 'Listing',
2316
+ 'mkdir': 'Creating dir',
2317
+ 'fetch': 'Fetching',
2318
+ };
2319
+ return labels[type] || type;
2320
+ }
2321
+ /**
2322
+ * Render status bar
2323
+ */
2324
+ renderStatusBar(y, width) {
2325
+ let leftText = '';
2326
+ let rightText = '';
2327
+ if (this.notification) {
2328
+ leftText = ` ${this.notification}`;
2329
+ }
2330
+ else {
2331
+ leftText = ` ${this.messages.length} messages`;
2332
+ }
2333
+ if (this.isStreaming) {
2334
+ rightText = 'Streaming... (Esc to cancel)';
2335
+ }
2336
+ else if (this.isLoading) {
2337
+ rightText = 'Thinking...';
2338
+ }
2339
+ else {
2340
+ rightText = 'Enter send | /help commands';
2341
+ }
2342
+ const padding = ' '.repeat(Math.max(0, width - leftText.length - rightText.length));
2343
+ this.screen.writeLine(y, leftText + padding + rightText, fg.gray);
2344
+ }
2345
+ /**
2346
+ * Get visible messages (including streaming)
2347
+ */
2348
+ getVisibleMessages(height, width) {
2349
+ const allLines = [];
2350
+ for (const msg of this.messages) {
2351
+ const msgLines = this.formatMessage(msg.role, msg.content, width);
2352
+ allLines.push(...msgLines);
2353
+ }
2354
+ if (this.isStreaming && this.streamingContent) {
2355
+ const streamLines = this.formatMessage('assistant', this.streamingContent + '▊', width);
2356
+ allLines.push(...streamLines);
2357
+ }
2358
+ // Calculate visible window based on scroll offset
2359
+ // scrollOffset=0 means show the most recent (bottom) lines
2360
+ // scrollOffset>0 means scroll up to see older messages
2361
+ const totalLines = allLines.length;
2362
+ // Clamp scrollOffset to valid range
2363
+ const maxScroll = Math.max(0, totalLines - height);
2364
+ if (this.scrollOffset > maxScroll) {
2365
+ this.scrollOffset = maxScroll;
2366
+ }
2367
+ const endIndex = totalLines - this.scrollOffset;
2368
+ const startIndex = Math.max(0, endIndex - height);
2369
+ return allLines.slice(startIndex, endIndex);
2370
+ }
2371
+ /**
2372
+ * Format message into lines with syntax highlighting for code blocks
2373
+ */
2374
+ formatMessage(role, content, maxWidth) {
2375
+ const lines = [];
2376
+ const roleStyle = role === 'user' ? fg.green : role === 'assistant' ? PRIMARY_COLOR : fg.yellow;
2377
+ const roleLabel = role === 'user' ? '> ' : role === 'assistant' ? ' ' : '# ';
2378
+ // Parse content for code blocks
2379
+ const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
2380
+ let lastIndex = 0;
2381
+ let match;
2382
+ let isFirstLine = true;
2383
+ while ((match = codeBlockRegex.exec(content)) !== null) {
2384
+ // Add text before code block
2385
+ const textBefore = content.slice(lastIndex, match.index);
2386
+ if (textBefore) {
2387
+ const textLines = this.formatTextLines(textBefore, maxWidth, isFirstLine ? roleLabel : ' ', isFirstLine ? roleStyle : '');
2388
+ lines.push(...textLines);
2389
+ isFirstLine = false;
2390
+ }
2391
+ // Add code block with syntax highlighting
2392
+ const lang = match[1] || 'text';
2393
+ const code = match[2];
2394
+ const codeLines = this.formatCodeBlock(code, lang, maxWidth);
2395
+ lines.push(...codeLines);
2396
+ lastIndex = match.index + match[0].length;
2397
+ isFirstLine = false;
2398
+ }
2399
+ // Add remaining text after last code block
2400
+ const textAfter = content.slice(lastIndex);
2401
+ if (textAfter) {
2402
+ const textLines = this.formatTextLines(textAfter, maxWidth, isFirstLine ? roleLabel : ' ', isFirstLine ? roleStyle : '');
2403
+ lines.push(...textLines);
2404
+ }
2405
+ lines.push({ text: '', style: '' });
2406
+ return lines;
2407
+ }
2408
+ /**
2409
+ * Format plain text lines
2410
+ */
2411
+ formatTextLines(text, maxWidth, firstPrefix, firstStyle) {
2412
+ const lines = [];
2413
+ const contentLines = text.split('\n');
2414
+ for (let i = 0; i < contentLines.length; i++) {
2415
+ const line = contentLines[i];
2416
+ const prefix = i === 0 ? firstPrefix : ' ';
2417
+ const prefixStyle = i === 0 ? firstStyle : '';
2418
+ if (line.length > maxWidth - prefix.length) {
2419
+ const wrapped = this.wordWrap(line, maxWidth - prefix.length);
2420
+ for (let j = 0; j < wrapped.length; j++) {
2421
+ lines.push({
2422
+ text: (j === 0 ? prefix : ' ') + wrapped[j],
2423
+ style: j === 0 ? prefixStyle : '',
2424
+ });
2425
+ }
2426
+ }
2427
+ else {
2428
+ lines.push({
2429
+ text: prefix + line,
2430
+ style: prefixStyle,
2431
+ });
2432
+ }
2433
+ }
2434
+ return lines;
2435
+ }
2436
+ /**
2437
+ * Format code block with syntax highlighting (no border)
2438
+ */
2439
+ formatCodeBlock(code, lang, maxWidth) {
2440
+ const lines = [];
2441
+ const codeLines = code.split('\n');
2442
+ // Remove trailing empty line if exists
2443
+ if (codeLines.length > 0 && codeLines[codeLines.length - 1] === '') {
2444
+ codeLines.pop();
2445
+ }
2446
+ // Language label (if present)
2447
+ if (lang) {
2448
+ lines.push({ text: ' ' + lang, style: SYNTAX.codeLang, raw: false });
2449
+ }
2450
+ // Code lines with highlighting and indent
2451
+ for (const codeLine of codeLines) {
2452
+ const highlighted = highlightCode(codeLine, lang);
2453
+ lines.push({
2454
+ text: ' ' + highlighted,
2455
+ style: '',
2456
+ raw: true // Don't apply additional styling, code is pre-highlighted
2457
+ });
2458
+ }
2459
+ // Empty line after code block
2460
+ lines.push({ text: '', style: '', raw: false });
2461
+ return lines;
2462
+ }
2463
+ /**
2464
+ * Render inline search screen
2465
+ */
2466
+ renderInlineSearch(startY, width, availableHeight) {
2467
+ let y = startY;
2468
+ // Separator line
2469
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
2470
+ // Title
2471
+ this.screen.writeLine(y++, 'Search Results', PRIMARY_COLOR + style.bold);
2472
+ // Query
2473
+ this.screen.write(0, y, 'Query: ', fg.white);
2474
+ this.screen.write(7, y, `"${this.searchQuery}"`, fg.cyan);
2475
+ if (this.searchResults.length > 0) {
2476
+ this.screen.write(9 + this.searchQuery.length, y, ` (${this.searchResults.length} ${this.searchResults.length === 1 ? 'result' : 'results'})`, fg.gray);
2477
+ }
2478
+ y++;
2479
+ y++;
2480
+ if (this.searchResults.length === 0) {
2481
+ this.screen.writeLine(y++, 'No results found.', fg.yellow);
2482
+ }
2483
+ else {
2484
+ const maxVisible = availableHeight - 6;
2485
+ const visibleStart = Math.max(0, this.searchIndex - Math.floor(maxVisible / 2));
2486
+ const visibleResults = this.searchResults.slice(visibleStart, visibleStart + maxVisible);
2487
+ for (let i = 0; i < visibleResults.length; i++) {
2488
+ const result = visibleResults[i];
2489
+ const actualIndex = visibleStart + i;
2490
+ const isSelected = actualIndex === this.searchIndex;
2491
+ const prefix = isSelected ? '▸ ' : ' ';
2492
+ const roleColor = result.role === 'user' ? fg.green : fg.blue;
2493
+ // First line: role and message number
2494
+ this.screen.write(0, y, prefix, isSelected ? PRIMARY_COLOR : '');
2495
+ this.screen.write(2, y, `[${result.role.toUpperCase()}]`, roleColor + style.bold);
2496
+ this.screen.write(2 + result.role.length + 2, y, ` Message #${result.messageIndex + 1}`, fg.gray);
2497
+ y++;
2498
+ // Second line: matched text (truncated)
2499
+ const maxTextWidth = width - 4;
2500
+ const matchedText = result.matchedText.length > maxTextWidth
2501
+ ? result.matchedText.slice(0, maxTextWidth - 3) + '...'
2502
+ : result.matchedText;
2503
+ this.screen.writeLine(y, ' ' + matchedText, fg.white);
2504
+ y++;
2505
+ if (i < visibleResults.length - 1)
2506
+ y++; // spacing between results
2507
+ }
2508
+ }
2509
+ // Footer
2510
+ y = startY + availableHeight - 1;
2511
+ this.screen.writeLine(y, '↑↓ Navigate • Enter Jump to message • Esc Close', fg.gray);
2512
+ }
2513
+ /**
2514
+ * Render inline export screen
2515
+ */
2516
+ renderInlineExport(startY, width) {
2517
+ const formats = [
2518
+ { id: 'md', name: 'Markdown', desc: 'Formatted with headers and separators' },
2519
+ { id: 'json', name: 'JSON', desc: 'Structured data format' },
2520
+ { id: 'txt', name: 'Plain Text', desc: 'Simple text format' },
2521
+ ];
2522
+ let y = startY;
2523
+ // Separator line
2524
+ this.screen.horizontalLine(y++, '─', fg.green);
2525
+ // Title
2526
+ this.screen.writeLine(y++, 'Export Chat', fg.green + style.bold);
2527
+ y++;
2528
+ this.screen.writeLine(y++, 'Select export format:', fg.white);
2529
+ y++;
2530
+ for (let i = 0; i < formats.length; i++) {
2531
+ const format = formats[i];
2532
+ const isSelected = i === this.exportIndex;
2533
+ const prefix = isSelected ? '› ' : ' ';
2534
+ this.screen.write(0, y, prefix, isSelected ? fg.green : '');
2535
+ this.screen.write(2, y, format.name.padEnd(12), isSelected ? fg.green + style.bold : fg.white);
2536
+ this.screen.write(14, y, ' - ' + format.desc, fg.gray);
2537
+ y++;
2538
+ }
2539
+ y++;
2540
+ this.screen.writeLine(y, '↑↓ Navigate • Enter Export • Esc Cancel', fg.gray);
2541
+ }
2542
+ /**
2543
+ * Render inline logout picker
2544
+ */
2545
+ renderInlineLogout(startY, width) {
2546
+ let y = startY;
2547
+ // Separator line
2548
+ this.screen.horizontalLine(y++, '─', fg.cyan);
2549
+ // Title
2550
+ this.screen.writeLine(y++, 'Select provider to logout:', fg.cyan + style.bold);
2551
+ y++;
2552
+ if (this.logoutProviders.length === 0) {
2553
+ this.screen.writeLine(y++, 'No providers configured.', fg.yellow);
2554
+ this.screen.writeLine(y++, 'Press Escape to go back.', fg.gray);
2555
+ return;
2556
+ }
2557
+ // Provider options
2558
+ for (let i = 0; i < this.logoutProviders.length; i++) {
2559
+ const provider = this.logoutProviders[i];
2560
+ const isSelected = i === this.logoutIndex;
2561
+ const prefix = isSelected ? '→ ' : ' ';
2562
+ this.screen.write(0, y, prefix, isSelected ? fg.green : '');
2563
+ this.screen.write(2, y, provider.name, isSelected ? fg.green + style.bold : fg.white);
2564
+ if (provider.isCurrent) {
2565
+ this.screen.write(2 + provider.name.length + 1, y, '(current)', fg.cyan);
2566
+ }
2567
+ y++;
2568
+ }
2569
+ // "All" option
2570
+ const allIndex = this.logoutProviders.length;
2571
+ const isAllSelected = this.logoutIndex === allIndex;
2572
+ this.screen.write(0, y, isAllSelected ? '→ ' : ' ', isAllSelected ? fg.red : '');
2573
+ this.screen.write(2, y, 'Logout from all providers', isAllSelected ? fg.red + style.bold : fg.yellow);
2574
+ y++;
2575
+ // "Cancel" option
2576
+ const cancelIndex = this.logoutProviders.length + 1;
2577
+ const isCancelSelected = this.logoutIndex === cancelIndex;
2578
+ this.screen.write(0, y, isCancelSelected ? '→ ' : ' ', isCancelSelected ? fg.blue : '');
2579
+ this.screen.write(2, y, 'Cancel', isCancelSelected ? fg.blue + style.bold : fg.gray);
2580
+ y++;
2581
+ y++;
2582
+ this.screen.writeLine(y, '↑↓ Navigate • Enter Select • Esc Cancel', fg.gray);
2583
+ }
2584
+ /**
2585
+ * Render inline login dialog
2586
+ */
2587
+ renderInlineLogin(startY, width) {
2588
+ let y = startY;
2589
+ // Separator line
2590
+ this.screen.horizontalLine(y++, '─', fg.cyan);
2591
+ if (this.loginStep === 'provider') {
2592
+ // Provider selection
2593
+ this.screen.writeLine(y++, 'Select Provider', fg.cyan + style.bold);
2594
+ y++;
2595
+ for (let i = 0; i < this.loginProviders.length; i++) {
2596
+ const provider = this.loginProviders[i];
2597
+ const isSelected = i === this.loginProviderIndex;
2598
+ const prefix = isSelected ? '→ ' : ' ';
2599
+ this.screen.write(0, y, prefix, isSelected ? fg.green : '');
2600
+ this.screen.write(2, y, provider.name, isSelected ? fg.green + style.bold : fg.white);
2601
+ y++;
2602
+ }
2603
+ y++;
2604
+ this.screen.writeLine(y, '↑↓ Navigate • Enter Select • Esc Cancel', fg.gray);
2605
+ }
2606
+ else {
2607
+ // API key entry
2608
+ const selectedProvider = this.loginProviders[this.loginProviderIndex];
2609
+ this.screen.writeLine(y++, `Enter API Key for ${selectedProvider.name}`, fg.cyan + style.bold);
2610
+ y++;
2611
+ // API key input (masked)
2612
+ const maskedKey = this.loginApiKey.length > 0
2613
+ ? '*'.repeat(Math.min(this.loginApiKey.length, 40)) + (this.loginApiKey.length > 40 ? '...' : '')
2614
+ : '(type your API key)';
2615
+ this.screen.write(0, y, 'Key: ', fg.white);
2616
+ this.screen.write(5, y, maskedKey, this.loginApiKey.length > 0 ? fg.green : fg.gray);
2617
+ y++;
2618
+ // Error message
2619
+ if (this.loginError) {
2620
+ y++;
2621
+ this.screen.writeLine(y, this.loginError, fg.red);
2622
+ }
2623
+ y++;
2624
+ this.screen.writeLine(y, 'Ctrl+V Paste • Enter Submit • Esc Back', fg.gray);
2625
+ }
2626
+ }
2627
+ /**
2628
+ * Render intro animation
2629
+ */
2630
+ renderIntro() {
2631
+ const { width, height } = this.screen.getSize();
2632
+ this.screen.clear();
2633
+ // Get decrypted logo text
2634
+ const logoText = this.getDecryptedLogo();
2635
+ const logoLines = logoText.split('\n');
2636
+ // Center logo vertically
2637
+ const startY = Math.max(0, Math.floor((height - logoLines.length - 2) / 2));
2638
+ // Center logo horizontally
2639
+ const logoWidth = LOGO_LINES[0].length;
2640
+ const startX = Math.max(0, Math.floor((width - logoWidth) / 2));
2641
+ for (let i = 0; i < logoLines.length; i++) {
2642
+ this.screen.write(startX, startY + i, logoLines[i], PRIMARY_COLOR + style.bold);
2643
+ }
2644
+ // Tagline (only show when done)
2645
+ if (this.introPhase === 'done') {
2646
+ const tagline = 'Deep into Code.';
2647
+ const taglineX = Math.floor((width - tagline.length) / 2);
2648
+ this.screen.write(taglineX, startY + logoLines.length + 1, tagline, PRIMARY_COLOR);
2649
+ }
2650
+ this.screen.fullRender();
2651
+ }
2652
+ /**
2653
+ * Get decrypted logo for intro animation
2654
+ */
2655
+ getDecryptedLogo() {
2656
+ const lines = LOGO_LINES;
2657
+ return lines.map((line) => {
2658
+ let resultLine = '';
2659
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
2660
+ const char = line[charIndex];
2661
+ let isDecrypted = false;
2662
+ if (this.introPhase === 'init') {
2663
+ isDecrypted = false;
2664
+ }
2665
+ else if (this.introPhase === 'decrypt' || this.introPhase === 'done') {
2666
+ const threshold = line.length > 0 ? charIndex / line.length : 0;
2667
+ if (this.introProgress >= threshold - 0.1) {
2668
+ isDecrypted = Math.random() > 0.2;
2669
+ }
2670
+ }
2671
+ if (this.introPhase === 'done')
2672
+ isDecrypted = true;
2673
+ if (char === ' ' && this.introPhase !== 'init') {
2674
+ resultLine += ' ';
2675
+ }
2676
+ else if (isDecrypted) {
2677
+ resultLine += char;
2678
+ }
2679
+ else {
2680
+ if (char === ' ' && Math.random() > 0.1) {
2681
+ resultLine += ' ';
2682
+ }
2683
+ else {
2684
+ resultLine += App.GLITCH_CHARS.charAt(Math.floor(Math.random() * App.GLITCH_CHARS.length));
2685
+ }
2686
+ }
2687
+ }
2688
+ return resultLine;
2689
+ }).join('\n');
2690
+ }
2691
+ /**
2692
+ * Word wrap
2693
+ */
2694
+ wordWrap(text, maxWidth) {
2695
+ const words = text.split(' ');
2696
+ const lines = [];
2697
+ let currentLine = '';
2698
+ for (const word of words) {
2699
+ if (currentLine.length + word.length + 1 > maxWidth && currentLine) {
2700
+ lines.push(currentLine);
2701
+ currentLine = word;
2702
+ }
2703
+ else {
2704
+ currentLine += (currentLine ? ' ' : '') + word;
2705
+ }
2706
+ }
2707
+ if (currentLine) {
2708
+ lines.push(currentLine);
2709
+ }
2710
+ return lines.length > 0 ? lines : [''];
2711
+ }
2712
+ }