codeep 1.2.17 → 1.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +20 -7
  2. package/dist/api/index.d.ts +7 -0
  3. package/dist/api/index.js +21 -17
  4. package/dist/renderer/App.d.ts +1 -5
  5. package/dist/renderer/App.js +106 -486
  6. package/dist/renderer/agentExecution.d.ts +36 -0
  7. package/dist/renderer/agentExecution.js +394 -0
  8. package/dist/renderer/commands.d.ts +16 -0
  9. package/dist/renderer/commands.js +838 -0
  10. package/dist/renderer/handlers.d.ts +87 -0
  11. package/dist/renderer/handlers.js +260 -0
  12. package/dist/renderer/highlight.d.ts +18 -0
  13. package/dist/renderer/highlight.js +130 -0
  14. package/dist/renderer/main.d.ts +4 -2
  15. package/dist/renderer/main.js +103 -1550
  16. package/dist/utils/agent.d.ts +5 -15
  17. package/dist/utils/agent.js +9 -693
  18. package/dist/utils/agentChat.d.ts +46 -0
  19. package/dist/utils/agentChat.js +343 -0
  20. package/dist/utils/agentStream.d.ts +23 -0
  21. package/dist/utils/agentStream.js +216 -0
  22. package/dist/utils/keychain.js +3 -2
  23. package/dist/utils/learning.js +9 -3
  24. package/dist/utils/mcpIntegration.d.ts +61 -0
  25. package/dist/utils/mcpIntegration.js +154 -0
  26. package/dist/utils/project.js +8 -3
  27. package/dist/utils/skills.js +21 -11
  28. package/dist/utils/smartContext.d.ts +4 -0
  29. package/dist/utils/smartContext.js +51 -14
  30. package/dist/utils/toolExecution.d.ts +27 -0
  31. package/dist/utils/toolExecution.js +525 -0
  32. package/dist/utils/toolParsing.d.ts +18 -0
  33. package/dist/utils/toolParsing.js +302 -0
  34. package/dist/utils/tools.d.ts +11 -24
  35. package/dist/utils/tools.js +22 -1187
  36. package/package.json +3 -1
  37. package/dist/config/config.test.d.ts +0 -1
  38. package/dist/config/config.test.js +0 -157
  39. package/dist/config/providers.test.d.ts +0 -1
  40. package/dist/config/providers.test.js +0 -187
  41. package/dist/hooks/index.d.ts +0 -4
  42. package/dist/hooks/index.js +0 -4
  43. package/dist/hooks/useAgent.d.ts +0 -29
  44. package/dist/hooks/useAgent.js +0 -148
  45. package/dist/utils/agent.test.d.ts +0 -1
  46. package/dist/utils/agent.test.js +0 -315
  47. package/dist/utils/git.test.d.ts +0 -1
  48. package/dist/utils/git.test.js +0 -193
  49. package/dist/utils/gitignore.test.d.ts +0 -1
  50. package/dist/utils/gitignore.test.js +0 -167
  51. package/dist/utils/project.test.d.ts +0 -1
  52. package/dist/utils/project.test.js +0 -212
  53. package/dist/utils/ratelimit.test.d.ts +0 -1
  54. package/dist/utils/ratelimit.test.js +0 -131
  55. package/dist/utils/retry.test.d.ts +0 -1
  56. package/dist/utils/retry.test.js +0 -163
  57. package/dist/utils/smartContext.test.d.ts +0 -1
  58. package/dist/utils/smartContext.test.js +0 -382
  59. package/dist/utils/tools.test.d.ts +0 -1
  60. package/dist/utils/tools.test.js +0 -681
  61. package/dist/utils/validation.test.d.ts +0 -1
  62. package/dist/utils/validation.test.js +0 -164
@@ -5,157 +5,11 @@
5
5
  import { Screen } from './Screen.js';
6
6
  import { Input, LineEditor } from './Input.js';
7
7
  import { fg, style, stringWidth } from './ansi.js';
8
+ import { SYNTAX, highlightCode } from './highlight.js';
9
+ import { handleInlineStatusKey, handleInlineHelpKey, handleMenuKey, handleInlinePermissionKey, handleInlineSessionPickerKey, handleInlineConfirmKey, handleLoginKey, } from './handlers.js';
8
10
  import clipboardy from 'clipboardy';
9
- import { spawn } from 'child_process';
10
11
  // Primary color: #f02a30 (Codeep red)
11
12
  const PRIMARY_COLOR = fg.rgb(240, 42, 48);
12
- // Syntax highlighting colors (One Dark theme inspired)
13
- const SYNTAX = {
14
- keyword: fg.rgb(198, 120, 221), // Purple - keywords
15
- string: fg.rgb(152, 195, 121), // Green - strings
16
- number: fg.rgb(209, 154, 102), // Orange - numbers
17
- comment: fg.rgb(92, 99, 112), // Gray - comments
18
- function: fg.rgb(97, 175, 239), // Blue - functions
19
- type: fg.rgb(229, 192, 123), // Yellow - types
20
- operator: fg.rgb(86, 182, 194), // Cyan - operators
21
- variable: fg.white, // White - variables
22
- punctuation: fg.gray, // Gray - punctuation
23
- codeFrame: fg.rgb(100, 105, 115), // Frame color
24
- codeLang: fg.rgb(150, 155, 165), // Language label
25
- };
26
- // Keywords for different languages
27
- const KEYWORDS = {
28
- 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'],
29
- 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'],
30
- 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'],
31
- 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'],
32
- 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'],
33
- 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'],
34
- html: ['html', 'head', 'body', 'div', 'span', 'p', 'a', 'img', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'tr', 'td', 'th', 'form', 'input', 'button', 'select', 'option', 'textarea', 'label', 'section', 'article', 'nav', 'header', 'footer', 'main', 'aside', 'meta', 'link', 'script', 'style', 'title', 'DOCTYPE'],
35
- css: ['import', 'media', 'keyframes', 'font-face', 'supports', 'charset', 'namespace', 'page', 'inherit', 'initial', 'unset', 'none', 'auto', 'block', 'inline', 'flex', 'grid', 'absolute', 'relative', 'fixed', 'sticky', 'static', 'hidden', 'visible', 'solid', 'dashed', 'dotted', 'transparent', 'important'],
36
- };
37
- // Map language aliases
38
- const LANG_ALIASES = {
39
- javascript: 'js', typescript: 'ts', python: 'py', golang: 'go',
40
- bash: 'sh', shell: 'sh', zsh: 'sh', tsx: 'ts', jsx: 'js',
41
- htm: 'html', scss: 'css', sass: 'css', less: 'css',
42
- };
43
- /**
44
- * Syntax highlighter for code with better token handling
45
- */
46
- function highlightCode(code, lang) {
47
- const normalizedLang = LANG_ALIASES[lang.toLowerCase()] || lang.toLowerCase();
48
- const keywords = KEYWORDS[normalizedLang] || KEYWORDS['js'] || [];
49
- // HTML: highlight tags, attributes, and values
50
- if (normalizedLang === 'html' || normalizedLang === 'xml' || normalizedLang === 'svg') {
51
- return code.replace(/(<\/?)(\w[\w-]*)((?:\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|\S+))?)*)(\s*\/?>)/g, (_match, open, tag, attrs, close) => {
52
- const highlightedAttrs = attrs.replace(/([\w-]+)(=)("[^"]*"|'[^']*')/g, (_m, attr, eq, val) => SYNTAX.function + attr + '\x1b[0m' + SYNTAX.operator + eq + '\x1b[0m' + SYNTAX.string + val + '\x1b[0m');
53
- return SYNTAX.punctuation + open + '\x1b[0m' + SYNTAX.keyword + tag + '\x1b[0m' + highlightedAttrs + SYNTAX.punctuation + close + '\x1b[0m';
54
- }).replace(/<!--[\s\S]*?-->/g, (comment) => SYNTAX.comment + comment + '\x1b[0m');
55
- }
56
- // CSS: highlight selectors, properties, and values
57
- if (normalizedLang === 'css') {
58
- return code
59
- .replace(/\/\*[\s\S]*?\*\//g, (comment) => SYNTAX.comment + comment + '\x1b[0m')
60
- .replace(/([\w-]+)(\s*:\s*)([^;{}]+)/g, (_m, prop, colon, val) => SYNTAX.function + prop + '\x1b[0m' + colon + SYNTAX.string + val + '\x1b[0m')
61
- .replace(/([.#]?[\w-]+(?:\s*[,>+~]\s*[.#]?[\w-]+)*)\s*\{/g, (match, selector) => SYNTAX.keyword + selector + '\x1b[0m' + ' {');
62
- }
63
- // Tokenize and highlight
64
- let result = '';
65
- let i = 0;
66
- while (i < code.length) {
67
- // Check for comments first
68
- if (code.slice(i, i + 2) === '//' || (normalizedLang === 'py' && code[i] === '#') ||
69
- (normalizedLang === 'sh' && code[i] === '#')) {
70
- // Line comment - highlight rest of line
71
- let end = code.indexOf('\n', i);
72
- if (end === -1)
73
- end = code.length;
74
- result += SYNTAX.comment + code.slice(i, end) + '\x1b[0m';
75
- i = end;
76
- continue;
77
- }
78
- // Multi-line comment /*
79
- if (code.slice(i, i + 2) === '/*') {
80
- let end = code.indexOf('*/', i + 2);
81
- if (end === -1)
82
- end = code.length;
83
- else
84
- end += 2;
85
- result += SYNTAX.comment + code.slice(i, end) + '\x1b[0m';
86
- i = end;
87
- continue;
88
- }
89
- // Strings
90
- if (code[i] === '"' || code[i] === "'" || code[i] === '`') {
91
- const quote = code[i];
92
- let end = i + 1;
93
- while (end < code.length) {
94
- if (code[end] === '\\') {
95
- end += 2; // Skip escaped char
96
- }
97
- else if (code[end] === quote) {
98
- end++;
99
- break;
100
- }
101
- else {
102
- end++;
103
- }
104
- }
105
- result += SYNTAX.string + code.slice(i, end) + '\x1b[0m';
106
- i = end;
107
- continue;
108
- }
109
- // Numbers (including hex, binary, floats)
110
- const numMatch = code.slice(i).match(/^(0x[0-9a-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*(?:e[+-]?\d+)?)/);
111
- if (numMatch && (i === 0 || !/[a-zA-Z_]/.test(code[i - 1]))) {
112
- result += SYNTAX.number + numMatch[1] + '\x1b[0m';
113
- i += numMatch[1].length;
114
- continue;
115
- }
116
- // Identifiers (keywords, functions, variables)
117
- const identMatch = code.slice(i).match(/^[a-zA-Z_][a-zA-Z0-9_]*/);
118
- if (identMatch) {
119
- const ident = identMatch[0];
120
- const nextChar = code[i + ident.length];
121
- if (keywords.includes(ident)) {
122
- // Keyword
123
- result += SYNTAX.keyword + ident + '\x1b[0m';
124
- }
125
- else if (nextChar === '(') {
126
- // Function call
127
- result += SYNTAX.function + ident + '\x1b[0m';
128
- }
129
- else if (ident[0] === ident[0].toUpperCase() && /^[A-Z]/.test(ident)) {
130
- // Type/Class (PascalCase)
131
- result += SYNTAX.type + ident + '\x1b[0m';
132
- }
133
- else {
134
- // Regular identifier
135
- result += ident;
136
- }
137
- i += ident.length;
138
- continue;
139
- }
140
- // Operators
141
- const opMatch = code.slice(i).match(/^(===|!==|==|!=|<=|>=|=>|->|\+\+|--|&&|\|\||<<|>>|\+=|-=|\*=|\/=|[+\-*/%=<>!&|^~?:])/);
142
- if (opMatch) {
143
- result += SYNTAX.operator + opMatch[1] + '\x1b[0m';
144
- i += opMatch[1].length;
145
- continue;
146
- }
147
- // Punctuation
148
- if ('{}[]();,.'.includes(code[i])) {
149
- result += SYNTAX.punctuation + code[i] + '\x1b[0m';
150
- i++;
151
- continue;
152
- }
153
- // Default - just add the character
154
- result += code[i];
155
- i++;
156
- }
157
- return result;
158
- }
159
13
  // Spinner frames for animation
160
14
  const SPINNER_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
161
15
  // ASCII Logo
@@ -1071,46 +925,21 @@ export class App {
1071
925
  * Handle inline status keys
1072
926
  */
1073
927
  handleInlineStatusKey(event) {
1074
- if (event.key === 'escape' || event.key === 'q') {
1075
- this.statusOpen = false;
1076
- this.render();
1077
- }
928
+ handleInlineStatusKey(event, {
929
+ close: () => { this.statusOpen = false; },
930
+ render: () => this.render(),
931
+ });
1078
932
  }
1079
933
  /**
1080
934
  * Handle help screen keys
1081
935
  */
1082
936
  handleInlineHelpKey(event) {
1083
- if (event.key === 'escape' || event.key === 'q') {
1084
- this.helpOpen = false;
1085
- this.render();
1086
- return;
1087
- }
1088
- // Calculate total help items
1089
- let totalItems = 0;
1090
- for (const cat of helpCategories) {
1091
- totalItems += 1 + cat.items.length; // category header + items
1092
- }
1093
- totalItems += 1 + keyboardShortcuts.length; // shortcuts header + items
1094
- if (event.key === 'down') {
1095
- this.helpScrollIndex = Math.min(this.helpScrollIndex + 1, Math.max(0, totalItems - 5));
1096
- this.render();
1097
- return;
1098
- }
1099
- if (event.key === 'up') {
1100
- this.helpScrollIndex = Math.max(0, this.helpScrollIndex - 1);
1101
- this.render();
1102
- return;
1103
- }
1104
- if (event.key === 'pagedown') {
1105
- this.helpScrollIndex = Math.min(this.helpScrollIndex + 5, Math.max(0, totalItems - 5));
1106
- this.render();
1107
- return;
1108
- }
1109
- if (event.key === 'pageup') {
1110
- this.helpScrollIndex = Math.max(0, this.helpScrollIndex - 5);
1111
- this.render();
1112
- return;
1113
- }
937
+ handleInlineHelpKey(event, {
938
+ scrollIndex: this.helpScrollIndex,
939
+ setScrollIndex: (v) => { this.helpScrollIndex = v; },
940
+ close: () => { this.helpOpen = false; },
941
+ render: () => this.render(),
942
+ });
1114
943
  }
1115
944
  /**
1116
945
  * Handle inline settings keys
@@ -1207,279 +1036,84 @@ export class App {
1207
1036
  * Handle login keys
1208
1037
  */
1209
1038
  handleLoginKey(event) {
1210
- if (this.loginStep === 'provider') {
1211
- // Provider selection step
1212
- if (event.key === 'escape') {
1213
- this.loginOpen = false;
1039
+ handleLoginKey(event, {
1040
+ step: this.loginStep,
1041
+ providerIndex: this.loginProviderIndex,
1042
+ providers: this.loginProviders,
1043
+ apiKey: this.loginApiKey,
1044
+ setStep: (v) => { this.loginStep = v; },
1045
+ setProviderIndex: (v) => { this.loginProviderIndex = v; },
1046
+ setApiKey: (v) => { this.loginApiKey = v; },
1047
+ setError: (msg) => { this.loginError = msg; },
1048
+ close: (result) => {
1214
1049
  const callback = this.loginCallback;
1215
- this.loginCallback = null;
1216
- this.render();
1217
- if (callback)
1218
- callback(null);
1219
- return;
1220
- }
1221
- if (event.key === 'up') {
1222
- this.loginProviderIndex = Math.max(0, this.loginProviderIndex - 1);
1223
- this.render();
1224
- return;
1225
- }
1226
- if (event.key === 'down') {
1227
- this.loginProviderIndex = Math.min(this.loginProviders.length - 1, this.loginProviderIndex + 1);
1228
- this.render();
1229
- return;
1230
- }
1231
- if (event.key === 'enter') {
1232
- // Move to API key entry
1233
- this.loginStep = 'apikey';
1234
- this.loginApiKey = '';
1235
- this.loginError = '';
1236
- this.render();
1237
- return;
1238
- }
1239
- }
1240
- else {
1241
- // API key entry step
1242
- if (event.key === 'escape') {
1243
- // Go back to provider selection
1244
- this.loginStep = 'provider';
1245
- this.loginApiKey = '';
1246
- this.loginError = '';
1247
- this.render();
1248
- return;
1249
- }
1250
- if (event.key === 'enter') {
1251
- // Validate and submit
1252
- if (this.loginApiKey.length < 10) {
1253
- this.loginError = 'API key too short (min 10 characters)';
1254
- this.render();
1255
- return;
1256
- }
1257
- const callback = this.loginCallback;
1258
- const result = {
1259
- providerId: this.loginProviders[this.loginProviderIndex].id,
1260
- apiKey: this.loginApiKey,
1261
- };
1262
1050
  this.loginOpen = false;
1263
1051
  this.loginCallback = null;
1264
- this.render();
1265
1052
  if (callback)
1266
1053
  callback(result);
1267
- return;
1268
- }
1269
- if (event.key === 'backspace') {
1270
- this.loginApiKey = this.loginApiKey.slice(0, -1);
1271
- this.loginError = '';
1272
- this.render();
1273
- return;
1274
- }
1275
- // Ctrl+V to paste
1276
- if (event.ctrl && event.key === 'v') {
1277
- this.pasteApiKey();
1278
- return;
1279
- }
1280
- // Ctrl+B to open subscribe URL
1281
- if (event.ctrl && event.key === 'b') {
1282
- const provider = this.loginProviders[this.loginProviderIndex];
1283
- if (provider.subscribeUrl) {
1284
- try {
1285
- const cmd = process.platform === 'darwin' ? 'open'
1286
- : process.platform === 'win32' ? 'start'
1287
- : 'xdg-open';
1288
- const child = spawn(cmd, [provider.subscribeUrl], { detached: true, stdio: 'ignore' });
1289
- child.unref();
1290
- }
1291
- catch { /* ignore */ }
1292
- }
1293
- return;
1294
- }
1295
- // Handle paste detection (fast input)
1296
- if (event.isPaste && event.key.length > 1) {
1297
- this.loginApiKey += event.key.trim();
1298
- this.loginError = '';
1299
- this.render();
1300
- return;
1301
- }
1302
- // Regular character input
1303
- if (event.key.length === 1 && !event.ctrl) {
1304
- this.loginApiKey += event.key;
1305
- this.loginError = '';
1306
- this.render();
1307
- return;
1308
- }
1309
- }
1310
- }
1311
- /**
1312
- * Paste API key from clipboard
1313
- */
1314
- async pasteApiKey() {
1315
- try {
1316
- const text = await clipboardy.read();
1317
- if (text) {
1318
- this.loginApiKey = text.trim();
1319
- this.loginError = '';
1320
- this.render();
1321
- }
1322
- }
1323
- catch {
1324
- this.loginError = 'Could not read clipboard';
1325
- this.render();
1326
- }
1054
+ },
1055
+ render: () => this.render(),
1056
+ });
1327
1057
  }
1328
1058
  /**
1329
1059
  * Handle inline menu keys
1330
1060
  */
1331
1061
  handleMenuKey(event) {
1332
- if (event.key === 'escape') {
1333
- this.menuOpen = false;
1334
- this.menuCallback = null;
1335
- this.render();
1336
- return;
1337
- }
1338
- if (event.key === 'up') {
1339
- this.menuIndex = Math.max(0, this.menuIndex - 1);
1340
- this.render();
1341
- return;
1342
- }
1343
- if (event.key === 'down') {
1344
- this.menuIndex = Math.min(this.menuItems.length - 1, this.menuIndex + 1);
1345
- this.render();
1346
- return;
1347
- }
1348
- if (event.key === 'enter') {
1349
- const selectedItem = this.menuItems[this.menuIndex];
1350
- const callback = this.menuCallback;
1351
- this.menuOpen = false;
1352
- this.menuCallback = null;
1353
- this.render();
1354
- if (callback) {
1355
- callback(selectedItem);
1356
- }
1357
- return;
1358
- }
1359
- if (event.key === 'pageup') {
1360
- this.menuIndex = Math.max(0, this.menuIndex - 5);
1361
- this.render();
1362
- return;
1363
- }
1364
- if (event.key === 'pagedown') {
1365
- this.menuIndex = Math.min(this.menuItems.length - 1, this.menuIndex + 5);
1366
- this.render();
1367
- return;
1368
- }
1369
- // Ignore other keys when menu is open
1062
+ handleMenuKey(event, {
1063
+ index: this.menuIndex,
1064
+ items: this.menuItems,
1065
+ setIndex: (v) => { this.menuIndex = v; },
1066
+ close: (_cb, selected) => {
1067
+ const callback = this.menuCallback;
1068
+ this.menuOpen = false;
1069
+ this.menuCallback = null;
1070
+ if (selected && callback)
1071
+ callback(selected);
1072
+ },
1073
+ render: () => this.render(),
1074
+ });
1370
1075
  }
1371
1076
  /**
1372
- * Handle confirmation dialog keys
1077
+ * Handle permission dialog keys
1373
1078
  */
1374
1079
  handleInlinePermissionKey(event) {
1375
- const options = ['read', 'write', 'none'];
1376
- if (event.key === 'escape') {
1377
- const callback = this.permissionCallback;
1378
- this.permissionOpen = false;
1379
- this.permissionCallback = null;
1380
- this.render();
1381
- if (callback)
1382
- callback('none');
1383
- return;
1384
- }
1385
- if (event.key === 'up') {
1386
- this.permissionIndex = Math.max(0, this.permissionIndex - 1);
1387
- this.render();
1388
- return;
1389
- }
1390
- if (event.key === 'down') {
1391
- this.permissionIndex = Math.min(options.length - 1, this.permissionIndex + 1);
1392
- this.render();
1393
- return;
1394
- }
1395
- if (event.key === 'enter') {
1396
- const selected = options[this.permissionIndex];
1397
- const callback = this.permissionCallback;
1398
- this.permissionOpen = false;
1399
- this.permissionCallback = null;
1400
- this.render();
1401
- if (callback)
1402
- callback(selected);
1403
- return;
1404
- }
1080
+ handleInlinePermissionKey(event, {
1081
+ index: this.permissionIndex,
1082
+ setIndex: (v) => { this.permissionIndex = v; },
1083
+ close: (level) => {
1084
+ const callback = this.permissionCallback;
1085
+ this.permissionOpen = false;
1086
+ this.permissionCallback = null;
1087
+ if (callback)
1088
+ callback(level);
1089
+ },
1090
+ render: () => this.render(),
1091
+ });
1405
1092
  }
1406
1093
  handleInlineSessionPickerKey(event) {
1407
- // N = new session
1408
- if (event.key === 'n' && !this.sessionPickerDeleteMode) {
1409
- const callback = this.sessionPickerCallback;
1410
- this.sessionPickerOpen = false;
1411
- this.sessionPickerCallback = null;
1412
- this.sessionPickerDeleteMode = false;
1413
- this.render();
1414
- if (callback)
1415
- callback(null); // null means new session
1416
- return;
1417
- }
1418
- // D = toggle delete mode
1419
- if (event.key === 'd' && this.sessionPickerDeleteCallback && this.sessionPickerItems.length > 0) {
1420
- this.sessionPickerDeleteMode = !this.sessionPickerDeleteMode;
1421
- this.render();
1422
- return;
1423
- }
1424
- if (event.key === 'escape') {
1425
- if (this.sessionPickerDeleteMode) {
1426
- // Exit delete mode
1094
+ handleInlineSessionPickerKey(event, {
1095
+ index: this.sessionPickerIndex,
1096
+ items: this.sessionPickerItems,
1097
+ deleteMode: this.sessionPickerDeleteMode,
1098
+ hasDeleteCallback: !!this.sessionPickerDeleteCallback,
1099
+ setIndex: (v) => { this.sessionPickerIndex = v; },
1100
+ setItems: (items) => { this.sessionPickerItems = items; },
1101
+ setDeleteMode: (v) => { this.sessionPickerDeleteMode = v; },
1102
+ close: (sessionName) => {
1103
+ const callback = this.sessionPickerCallback;
1104
+ this.sessionPickerOpen = false;
1105
+ this.sessionPickerCallback = null;
1427
1106
  this.sessionPickerDeleteMode = false;
1428
- this.render();
1429
- return;
1430
- }
1431
- // Escape = new session
1432
- const callback = this.sessionPickerCallback;
1433
- this.sessionPickerOpen = false;
1434
- this.sessionPickerCallback = null;
1435
- this.sessionPickerDeleteMode = false;
1436
- this.render();
1437
- if (callback)
1438
- callback(null);
1439
- return;
1440
- }
1441
- if (event.key === 'up') {
1442
- this.sessionPickerIndex = Math.max(0, this.sessionPickerIndex - 1);
1443
- this.render();
1444
- return;
1445
- }
1446
- if (event.key === 'down') {
1447
- this.sessionPickerIndex = Math.min(this.sessionPickerItems.length - 1, this.sessionPickerIndex + 1);
1448
- this.render();
1449
- return;
1450
- }
1451
- if (event.key === 'enter' && this.sessionPickerItems.length > 0) {
1452
- const selected = this.sessionPickerItems[this.sessionPickerIndex];
1453
- if (this.sessionPickerDeleteMode) {
1454
- // Delete the selected session
1455
- const deleteCallback = this.sessionPickerDeleteCallback;
1456
- if (deleteCallback) {
1457
- deleteCallback(selected.name);
1458
- // Remove from list
1459
- this.sessionPickerItems = this.sessionPickerItems.filter(s => s.name !== selected.name);
1460
- // Adjust index if needed
1461
- if (this.sessionPickerIndex >= this.sessionPickerItems.length) {
1462
- this.sessionPickerIndex = Math.max(0, this.sessionPickerItems.length - 1);
1463
- }
1464
- // Exit delete mode if no more items
1465
- if (this.sessionPickerItems.length === 0) {
1466
- this.sessionPickerDeleteMode = false;
1467
- }
1468
- this.notify(`Deleted: ${selected.name}`);
1469
- this.render();
1470
- }
1471
- return;
1472
- }
1473
- // Load selected session
1474
- const callback = this.sessionPickerCallback;
1475
- this.sessionPickerOpen = false;
1476
- this.sessionPickerCallback = null;
1477
- this.sessionPickerDeleteMode = false;
1478
- this.render();
1479
- if (callback)
1480
- callback(selected.name);
1481
- return;
1482
- }
1107
+ if (callback)
1108
+ callback(sessionName);
1109
+ },
1110
+ onDelete: (name) => {
1111
+ if (this.sessionPickerDeleteCallback)
1112
+ this.sessionPickerDeleteCallback(name);
1113
+ },
1114
+ notify: (msg) => this.notify(msg),
1115
+ render: () => this.render(),
1116
+ });
1483
1117
  }
1484
1118
  handleInlineConfirmKey(event) {
1485
1119
  if (!this.confirmOptions) {
@@ -1487,43 +1121,21 @@ export class App {
1487
1121
  this.render();
1488
1122
  return;
1489
1123
  }
1490
- if (event.key === 'escape') {
1491
- const onCancel = this.confirmOptions.onCancel;
1492
- this.confirmOptions = null;
1493
- this.confirmOpen = false;
1494
- this.render();
1495
- if (onCancel)
1496
- onCancel();
1497
- return;
1498
- }
1499
- if (event.key === 'left' || event.key === 'right' || event.key === 'tab') {
1500
- this.confirmSelection = this.confirmSelection === 'yes' ? 'no' : 'yes';
1501
- this.render();
1502
- return;
1503
- }
1504
- if (event.key === 'y') {
1505
- this.confirmSelection = 'yes';
1506
- this.render();
1507
- return;
1508
- }
1509
- if (event.key === 'n') {
1510
- this.confirmSelection = 'no';
1511
- this.render();
1512
- return;
1513
- }
1514
- if (event.key === 'enter') {
1515
- const options = this.confirmOptions;
1516
- this.confirmOptions = null;
1517
- this.confirmOpen = false;
1518
- this.render();
1519
- if (this.confirmSelection === 'yes') {
1520
- options.onConfirm();
1521
- }
1522
- else if (options.onCancel) {
1523
- options.onCancel();
1524
- }
1525
- return;
1526
- }
1124
+ handleInlineConfirmKey(event, {
1125
+ options: this.confirmOptions,
1126
+ selection: this.confirmSelection,
1127
+ setSelection: (v) => { this.confirmSelection = v; },
1128
+ close: (confirmed) => {
1129
+ const options = this.confirmOptions;
1130
+ this.confirmOptions = null;
1131
+ this.confirmOpen = false;
1132
+ if (confirmed)
1133
+ options.onConfirm();
1134
+ else if (options.onCancel)
1135
+ options.onCancel();
1136
+ },
1137
+ render: () => this.render(),
1138
+ });
1527
1139
  }
1528
1140
  /**
1529
1141
  * Submit the current input buffer (used by Enter and Escape-in-multiline)
@@ -2246,16 +1858,24 @@ export class App {
2246
1858
  renderInlineAgentProgress(startY, width) {
2247
1859
  let y = startY;
2248
1860
  const spinner = SPINNER_FRAMES[this.spinnerFrame];
2249
- // Calculate stats
2250
- const stats = {
2251
- reads: this.agentActions.filter(a => a.type === 'read').length,
2252
- writes: this.agentActions.filter(a => a.type === 'write').length,
2253
- edits: this.agentActions.filter(a => a.type === 'edit').length,
2254
- deletes: this.agentActions.filter(a => a.type === 'delete').length,
2255
- commands: this.agentActions.filter(a => a.type === 'command').length,
2256
- searches: this.agentActions.filter(a => a.type === 'search').length,
2257
- errors: this.agentActions.filter(a => a.result === 'error').length,
2258
- };
1861
+ // Calculate stats in a single pass
1862
+ const stats = this.agentActions.reduce((acc, a) => {
1863
+ if (a.type === 'read')
1864
+ acc.reads++;
1865
+ else if (a.type === 'write')
1866
+ acc.writes++;
1867
+ else if (a.type === 'edit')
1868
+ acc.edits++;
1869
+ else if (a.type === 'delete')
1870
+ acc.deletes++;
1871
+ else if (a.type === 'command')
1872
+ acc.commands++;
1873
+ else if (a.type === 'search')
1874
+ acc.searches++;
1875
+ if (a.result === 'error')
1876
+ acc.errors++;
1877
+ return acc;
1878
+ }, { reads: 0, writes: 0, edits: 0, deletes: 0, commands: 0, searches: 0, errors: 0 });
2259
1879
  // Top border with title
2260
1880
  const title = ` ${spinner} AGENT `;
2261
1881
  const titlePadLeft = 2;