codeep 1.1.11 → 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.
- package/bin/codeep.js +1 -1
- package/dist/components/AgentActions.d.ts +4 -5
- package/dist/components/AgentActions.js +10 -9
- package/dist/config/index.js +10 -10
- package/dist/renderer/App.d.ts +430 -0
- package/dist/renderer/App.js +2712 -0
- package/dist/renderer/ChatUI.d.ts +71 -0
- package/dist/renderer/ChatUI.js +286 -0
- package/dist/renderer/Input.d.ts +72 -0
- package/dist/renderer/Input.js +371 -0
- package/dist/renderer/Screen.d.ts +79 -0
- package/dist/renderer/Screen.js +278 -0
- package/dist/renderer/ansi.d.ts +99 -0
- package/dist/renderer/ansi.js +176 -0
- package/dist/renderer/components/Box.d.ts +64 -0
- package/dist/renderer/components/Box.js +90 -0
- package/dist/renderer/components/Help.d.ts +30 -0
- package/dist/renderer/components/Help.js +195 -0
- package/dist/renderer/components/Intro.d.ts +12 -0
- package/dist/renderer/components/Intro.js +128 -0
- package/dist/renderer/components/Login.d.ts +42 -0
- package/dist/renderer/components/Login.js +178 -0
- package/dist/renderer/components/Modal.d.ts +43 -0
- package/dist/renderer/components/Modal.js +207 -0
- package/dist/renderer/components/Permission.d.ts +20 -0
- package/dist/renderer/components/Permission.js +113 -0
- package/dist/renderer/components/SelectScreen.d.ts +26 -0
- package/dist/renderer/components/SelectScreen.js +101 -0
- package/dist/renderer/components/Settings.d.ts +37 -0
- package/dist/renderer/components/Settings.js +333 -0
- package/dist/renderer/components/Status.d.ts +18 -0
- package/dist/renderer/components/Status.js +78 -0
- package/dist/renderer/demo-app.d.ts +6 -0
- package/dist/renderer/demo-app.js +85 -0
- package/dist/renderer/demo.d.ts +6 -0
- package/dist/renderer/demo.js +52 -0
- package/dist/renderer/index.d.ts +16 -0
- package/dist/renderer/index.js +17 -0
- package/dist/renderer/main.d.ts +6 -0
- package/dist/renderer/main.js +1634 -0
- package/dist/utils/agent.d.ts +21 -0
- package/dist/utils/agent.js +29 -0
- package/dist/utils/clipboard.d.ts +15 -0
- package/dist/utils/clipboard.js +95 -0
- package/package.json +7 -11
- package/dist/utils/console.d.ts +0 -55
- 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
|
+
}
|