codeep 1.1.35 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +90 -4
  2. package/dist/api/index.js +64 -2
  3. package/dist/renderer/App.d.ts +5 -10
  4. package/dist/renderer/App.js +165 -314
  5. package/dist/renderer/components/Export.d.ts +22 -0
  6. package/dist/renderer/components/Export.js +64 -0
  7. package/dist/renderer/components/Help.js +5 -1
  8. package/dist/renderer/components/Logout.d.ts +29 -0
  9. package/dist/renderer/components/Logout.js +91 -0
  10. package/dist/renderer/components/Search.d.ts +30 -0
  11. package/dist/renderer/components/Search.js +83 -0
  12. package/dist/renderer/components/Settings.js +20 -0
  13. package/dist/renderer/components/Status.d.ts +6 -0
  14. package/dist/renderer/components/Status.js +20 -1
  15. package/dist/renderer/main.js +296 -156
  16. package/dist/utils/agent.d.ts +5 -0
  17. package/dist/utils/agent.js +238 -3
  18. package/dist/utils/agent.test.d.ts +1 -0
  19. package/dist/utils/agent.test.js +250 -0
  20. package/dist/utils/diffPreview.js +104 -35
  21. package/dist/utils/gitignore.d.ts +24 -0
  22. package/dist/utils/gitignore.js +161 -0
  23. package/dist/utils/gitignore.test.d.ts +1 -0
  24. package/dist/utils/gitignore.test.js +167 -0
  25. package/dist/utils/skills.d.ts +21 -0
  26. package/dist/utils/skills.js +51 -0
  27. package/dist/utils/smartContext.js +8 -0
  28. package/dist/utils/smartContext.test.d.ts +1 -0
  29. package/dist/utils/smartContext.test.js +382 -0
  30. package/dist/utils/tokenTracker.d.ts +52 -0
  31. package/dist/utils/tokenTracker.js +86 -0
  32. package/dist/utils/tools.d.ts +16 -0
  33. package/dist/utils/tools.js +146 -19
  34. package/dist/utils/tools.test.d.ts +1 -0
  35. package/dist/utils/tools.test.js +664 -0
  36. package/package.json +1 -1
@@ -199,6 +199,9 @@ const COMMAND_DESCRIPTIONS = {
199
199
  'copy': 'Copy code block',
200
200
  'paste': 'Paste from clipboard',
201
201
  'apply': 'Apply file changes',
202
+ 'add': 'Add file to context',
203
+ 'drop': 'Remove file from context',
204
+ 'multiline': 'Toggle multi-line input',
202
205
  'test': 'Generate/run tests',
203
206
  'docs': 'Add documentation',
204
207
  'refactor': 'Improve code quality',
@@ -221,6 +224,9 @@ const COMMAND_DESCRIPTIONS = {
221
224
  };
222
225
  import { helpCategories, keyboardShortcuts } from './components/Help.js';
223
226
  import { handleSettingsKey, SETTINGS } from './components/Settings.js';
227
+ import { renderExportPanel, handleExportKey as handleExportKeyComponent } from './components/Export.js';
228
+ import { renderLogoutPanel, handleLogoutKey as handleLogoutKeyComponent } from './components/Logout.js';
229
+ import { renderSearchPanel, handleSearchKey as handleSearchKeyComponent } from './components/Search.js';
224
230
  export class App {
225
231
  screen;
226
232
  input;
@@ -241,7 +247,6 @@ export class App {
241
247
  agentIteration = 0;
242
248
  agentActions = [];
243
249
  agentThinking = '';
244
- agentCodePreview = null;
245
250
  // Paste detection state
246
251
  pasteInfo = null;
247
252
  pasteInfoOpen = false;
@@ -307,6 +312,8 @@ export class App {
307
312
  introProgress = 0;
308
313
  introInterval = null;
309
314
  introCallback = null;
315
+ // Multi-line input state
316
+ isMultilineMode = false;
310
317
  // Inline login state
311
318
  loginOpen = false;
312
319
  loginStep = 'provider';
@@ -323,8 +330,16 @@ export class App {
323
330
  'sessions', 'new', 'rename', 'search', 'export',
324
331
  'agent', 'agent-dry', 'stop', 'undo', 'undo-all', 'history', 'changes',
325
332
  'diff', 'commit', 'git-commit', 'push', 'pull', 'scan', 'review',
326
- 'copy', 'paste', 'apply',
333
+ 'copy', 'paste', 'apply', 'add', 'drop',
327
334
  'test', 'docs', 'refactor', 'fix', 'explain', 'optimize', 'debug', 'skills',
335
+ 'amend', 'pr', 'changelog', 'branch', 'stash', 'unstash',
336
+ 'build', 'deploy', 'release', 'publish',
337
+ 'component', 'api', 'model', 'hook', 'service', 'page', 'form', 'crud',
338
+ 'security', 'profile', 'log', 'types', 'cleanup', 'modernize', 'migrate',
339
+ 'split', 'rename', 'coverage', 'e2e', 'mock', 'readme', 'translate',
340
+ 'docker', 'ci', 'env', 'k8s', 'terraform', 'nginx', 'monitor',
341
+ 'test-fix', 'api-docs',
342
+ 'multiline',
328
343
  'provider', 'model', 'protocol', 'lang', 'grant', 'login', 'logout',
329
344
  'context-save', 'context-load', 'context-clear', 'learn',
330
345
  'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
@@ -492,12 +507,10 @@ export class App {
492
507
  this.agentIteration = 0;
493
508
  this.agentActions = [];
494
509
  this.agentThinking = '';
495
- this.agentCodePreview = null;
496
510
  this.isLoading = false; // Clear loading state when agent takes over
497
511
  this.startSpinner();
498
512
  }
499
513
  else {
500
- this.agentCodePreview = null;
501
514
  this.isLoading = false; // Ensure loading is cleared when agent finishes
502
515
  this.stopSpinner();
503
516
  }
@@ -518,36 +531,6 @@ export class App {
518
531
  */
519
532
  setAgentThinking(text) {
520
533
  this.agentThinking = text;
521
- // Clear code preview when agent moves to a non-write/edit action
522
- if (text && !text.startsWith('write:') && !text.startsWith('edit:')) {
523
- this.agentCodePreview = null;
524
- }
525
- this.render();
526
- }
527
- /**
528
- * Set code preview for agent progress panel
529
- */
530
- setAgentCodePreview(preview) {
531
- if (preview && preview.content) {
532
- const ext = preview.path.split('.').pop()?.toLowerCase() || '';
533
- const lang = LANG_ALIASES[ext] || ext;
534
- const lines = preview.content.split('\n');
535
- const MAX_STORED_LINES = 50;
536
- const trimmedContent = lines.length > MAX_STORED_LINES
537
- ? lines.slice(-MAX_STORED_LINES).join('\n')
538
- : preview.content;
539
- this.agentCodePreview = {
540
- path: preview.path,
541
- actionType: preview.actionType,
542
- content: trimmedContent,
543
- oldContent: preview.oldContent,
544
- lang,
545
- totalLines: lines.length,
546
- };
547
- }
548
- else {
549
- this.agentCodePreview = null;
550
- }
551
534
  this.render();
552
535
  }
553
536
  /**
@@ -913,6 +896,13 @@ export class App {
913
896
  this.render();
914
897
  return;
915
898
  }
899
+ // In multiline mode, Escape submits the buffered input
900
+ if (this.isMultilineMode && !this.isLoading && !this.isStreaming) {
901
+ if (this.editor.getValue().trim()) {
902
+ this.submitInput();
903
+ return;
904
+ }
905
+ }
916
906
  if (this.isAgentRunning && this.options.onStopAgent) {
917
907
  this.options.onStopAgent();
918
908
  return;
@@ -1030,25 +1020,20 @@ export class App {
1030
1020
  }
1031
1021
  // Enter to submit (only if not in autocomplete)
1032
1022
  if (event.key === 'enter' && !this.isLoading && !this.isStreaming && !this.showAutocomplete) {
1033
- const value = this.editor.getValue().trim();
1034
- if (value) {
1035
- this.editor.addToHistory(value);
1036
- this.editor.clear();
1037
- this.showAutocomplete = false;
1038
- // Check for commands
1039
- if (value.startsWith('/')) {
1040
- this.handleCommand(value);
1041
- }
1042
- else {
1043
- // Regular message
1044
- this.addMessage({ role: 'user', content: value });
1045
- this.setLoading(true);
1046
- this.options.onSubmit(value).catch(err => {
1047
- this.notify(`Error: ${err.message}`);
1048
- this.setLoading(false);
1049
- });
1050
- }
1023
+ const rawValue = this.editor.getValue();
1024
+ // Backslash continuation: if line ends with \, add newline instead of submitting
1025
+ if (rawValue.endsWith('\\')) {
1026
+ this.editor.setValue(rawValue.slice(0, -1) + '\n');
1027
+ this.render();
1028
+ return;
1051
1029
  }
1030
+ // Multiline mode: Enter adds newline, Ctrl+Enter submits
1031
+ if (this.isMultilineMode && !event.ctrl) {
1032
+ this.editor.insert('\n');
1033
+ this.render();
1034
+ return;
1035
+ }
1036
+ this.submitInput();
1052
1037
  return;
1053
1038
  }
1054
1039
  // Handle paste detection
@@ -1143,109 +1128,75 @@ export class App {
1143
1128
  * Handle search screen keys
1144
1129
  */
1145
1130
  handleSearchKey(event) {
1146
- if (event.key === 'escape') {
1147
- this.searchOpen = false;
1148
- this.searchCallback = null;
1149
- this.render();
1150
- return;
1151
- }
1152
- if (event.key === 'up') {
1153
- this.searchIndex = Math.max(0, this.searchIndex - 1);
1154
- this.render();
1155
- return;
1156
- }
1157
- if (event.key === 'down') {
1158
- this.searchIndex = Math.min(this.searchResults.length - 1, this.searchIndex + 1);
1159
- this.render();
1160
- return;
1161
- }
1162
- if (event.key === 'enter' && this.searchResults.length > 0) {
1163
- const selectedResult = this.searchResults[this.searchIndex];
1164
- const callback = this.searchCallback;
1165
- this.searchOpen = false;
1166
- this.searchCallback = null;
1167
- this.render();
1168
- if (callback) {
1169
- callback(selectedResult.messageIndex);
1170
- }
1171
- return;
1172
- }
1131
+ const state = {
1132
+ searchOpen: this.searchOpen,
1133
+ searchQuery: this.searchQuery,
1134
+ searchResults: this.searchResults,
1135
+ searchIndex: this.searchIndex,
1136
+ searchCallback: this.searchCallback,
1137
+ };
1138
+ const callback = this.searchCallback;
1139
+ handleSearchKeyComponent(event, state, {
1140
+ onClose: () => {
1141
+ this.searchOpen = false;
1142
+ this.searchCallback = null;
1143
+ },
1144
+ onRender: () => {
1145
+ this.searchIndex = state.searchIndex;
1146
+ this.render();
1147
+ },
1148
+ onResult: (messageIndex) => {
1149
+ if (callback) {
1150
+ callback(messageIndex);
1151
+ }
1152
+ },
1153
+ });
1173
1154
  }
1174
1155
  /**
1175
1156
  * Handle export screen keys
1176
1157
  */
1177
1158
  handleExportKey(event) {
1178
- const formats = ['md', 'json', 'txt'];
1179
- if (event.key === 'escape') {
1180
- this.exportOpen = false;
1181
- this.exportCallback = null;
1182
- this.render();
1183
- return;
1184
- }
1185
- if (event.key === 'up') {
1186
- this.exportIndex = this.exportIndex > 0 ? this.exportIndex - 1 : formats.length - 1;
1187
- this.render();
1188
- return;
1189
- }
1190
- if (event.key === 'down') {
1191
- this.exportIndex = this.exportIndex < formats.length - 1 ? this.exportIndex + 1 : 0;
1192
- this.render();
1193
- return;
1194
- }
1195
- if (event.key === 'enter') {
1196
- const selectedFormat = formats[this.exportIndex];
1197
- const callback = this.exportCallback;
1198
- this.exportOpen = false;
1199
- this.exportCallback = null;
1159
+ const state = {
1160
+ exportOpen: this.exportOpen,
1161
+ exportIndex: this.exportIndex,
1162
+ exportCallback: this.exportCallback,
1163
+ };
1164
+ const syncState = () => {
1165
+ this.exportOpen = state.exportOpen;
1166
+ this.exportIndex = state.exportIndex;
1167
+ this.exportCallback = state.exportCallback;
1200
1168
  this.render();
1201
- if (callback) {
1202
- callback(selectedFormat);
1203
- }
1204
- return;
1205
- }
1169
+ };
1170
+ handleExportKeyComponent(event, state, {
1171
+ onClose: syncState,
1172
+ onRender: syncState,
1173
+ onExport: (format) => {
1174
+ const callback = this.exportCallback;
1175
+ syncState();
1176
+ if (callback) {
1177
+ callback(format);
1178
+ }
1179
+ },
1180
+ });
1206
1181
  }
1207
1182
  /**
1208
1183
  * Handle logout picker keys
1209
1184
  */
1210
1185
  handleLogoutKey(event) {
1211
- // Options: providers + "all" + "cancel"
1212
- const totalOptions = this.logoutProviders.length + 2;
1213
- if (event.key === 'escape') {
1214
- this.logoutOpen = false;
1215
- this.logoutCallback = null;
1216
- this.render();
1217
- return;
1218
- }
1219
- if (event.key === 'up') {
1220
- this.logoutIndex = Math.max(0, this.logoutIndex - 1);
1221
- this.render();
1222
- return;
1223
- }
1224
- if (event.key === 'down') {
1225
- this.logoutIndex = Math.min(totalOptions - 1, this.logoutIndex + 1);
1226
- this.render();
1227
- return;
1228
- }
1229
- if (event.key === 'enter') {
1230
- const callback = this.logoutCallback;
1231
- this.logoutOpen = false;
1232
- this.logoutCallback = null;
1233
- let result = null;
1234
- if (this.logoutIndex < this.logoutProviders.length) {
1235
- result = this.logoutProviders[this.logoutIndex].id;
1236
- }
1237
- else if (this.logoutIndex === this.logoutProviders.length) {
1238
- result = 'all';
1239
- }
1240
- else {
1241
- result = null; // Cancel
1242
- }
1243
- this.render();
1244
- if (callback) {
1245
- callback(result);
1246
- }
1247
- return;
1248
- }
1186
+ const state = {
1187
+ logoutOpen: this.logoutOpen,
1188
+ logoutIndex: this.logoutIndex,
1189
+ logoutProviders: this.logoutProviders,
1190
+ logoutCallback: this.logoutCallback,
1191
+ };
1192
+ handleLogoutKeyComponent(event, state, {
1193
+ onClose: () => { },
1194
+ onRender: () => this.render(),
1195
+ onSelect: () => { },
1196
+ });
1197
+ this.logoutOpen = state.logoutOpen;
1198
+ this.logoutIndex = state.logoutIndex;
1199
+ this.logoutCallback = state.logoutCallback;
1249
1200
  }
1250
1201
  /**
1251
1202
  * Handle login keys
@@ -1569,6 +1520,28 @@ export class App {
1569
1520
  return;
1570
1521
  }
1571
1522
  }
1523
+ /**
1524
+ * Submit the current input buffer (used by Enter and Escape-in-multiline)
1525
+ */
1526
+ submitInput() {
1527
+ const value = this.editor.getValue().trim();
1528
+ if (!value)
1529
+ return;
1530
+ this.editor.addToHistory(value);
1531
+ this.editor.clear();
1532
+ this.showAutocomplete = false;
1533
+ if (value.startsWith('/')) {
1534
+ this.handleCommand(value);
1535
+ }
1536
+ else {
1537
+ this.addMessage({ role: 'user', content: value });
1538
+ this.setLoading(true);
1539
+ this.options.onSubmit(value).catch(err => {
1540
+ this.notify(`Error: ${err.message}`);
1541
+ this.setLoading(false);
1542
+ });
1543
+ }
1544
+ }
1572
1545
  /**
1573
1546
  * Handle command
1574
1547
  */
@@ -1590,6 +1563,12 @@ export class App {
1590
1563
  this.clearMessages();
1591
1564
  this.notify('Chat cleared');
1592
1565
  break;
1566
+ case 'multiline':
1567
+ this.isMultilineMode = !this.isMultilineMode;
1568
+ this.notify(this.isMultilineMode
1569
+ ? 'Multi-line mode ON — Enter adds line, Esc sends'
1570
+ : 'Multi-line mode OFF — Enter sends');
1571
+ break;
1593
1572
  case 'exit':
1594
1573
  case 'quit':
1595
1574
  this.stop();
@@ -1625,20 +1604,7 @@ export class App {
1625
1604
  bottomPanelHeight = previewLines + 6; // title + preview + extra line indicator + options
1626
1605
  }
1627
1606
  else if (this.isAgentRunning) {
1628
- if (this.agentCodePreview) {
1629
- const MAX_PREVIEW_LINES = 12;
1630
- const storedLines = this.agentCodePreview.content.split('\n').length;
1631
- const visibleCodeLines = Math.min(storedLines, MAX_PREVIEW_LINES);
1632
- const startIdx = Math.max(0, storedLines - MAX_PREVIEW_LINES);
1633
- const startLineNum = this.agentCodePreview.totalLines - storedLines + startIdx;
1634
- const hasOverflow = startLineNum > 0 ? 1 : 0;
1635
- // top border + action + code separator + code lines + overflow + stats + bottom border + margin
1636
- bottomPanelHeight = 1 + 1 + 1 + visibleCodeLines + hasOverflow + 1 + 1 + 1;
1637
- bottomPanelHeight = Math.min(bottomPanelHeight, Math.floor(height * 0.6));
1638
- }
1639
- else {
1640
- bottomPanelHeight = 5; // Agent progress box (4 lines + 1 margin)
1641
- }
1607
+ bottomPanelHeight = 5; // Agent progress box (4 lines + 1 margin)
1642
1608
  }
1643
1609
  else if (this.permissionOpen) {
1644
1610
  bottomPanelHeight = 10; // Permission dialog
@@ -1875,12 +1841,18 @@ export class App {
1875
1841
  this.screen.showCursor(false);
1876
1842
  return;
1877
1843
  }
1878
- const prompt = '> ';
1844
+ // Build prompt prefix show line count for multi-line buffers
1845
+ const lines = inputValue.split('\n');
1846
+ const lineCount = lines.length;
1847
+ const prompt = lineCount > 1 ? `[${lineCount}] > ` : this.isMultilineMode ? 'M> ' : '> ';
1879
1848
  const maxInputWidth = width - prompt.length - 1;
1880
1849
  // Show placeholder when input is empty
1881
1850
  if (!inputValue) {
1882
1851
  this.screen.write(0, y, prompt, fg.green);
1883
- this.screen.write(prompt.length, y, 'Type a message or /command...', fg.gray);
1852
+ const placeholder = this.isMultilineMode
1853
+ ? 'Multi-line mode (Enter=newline, Esc=send)...'
1854
+ : 'Type a message or /command...';
1855
+ this.screen.write(prompt.length, y, placeholder, fg.gray);
1884
1856
  if (!hideCursor) {
1885
1857
  this.screen.setCursor(prompt.length, y);
1886
1858
  this.screen.showCursor(true);
@@ -1890,22 +1862,29 @@ export class App {
1890
1862
  }
1891
1863
  return;
1892
1864
  }
1865
+ // For multi-line content, show the last line being edited
1866
+ const lastLine = lines[lines.length - 1];
1867
+ const displayInput = lineCount > 1 ? lastLine : inputValue;
1868
+ // Cursor position within the last line
1869
+ const charsBeforeLastLine = lineCount > 1 ? inputValue.lastIndexOf('\n') + 1 : 0;
1870
+ const cursorInLine = cursorPos - charsBeforeLastLine;
1893
1871
  let displayValue;
1894
1872
  let cursorX;
1895
- if (inputValue.length <= maxInputWidth) {
1896
- displayValue = inputValue;
1897
- cursorX = prompt.length + cursorPos;
1873
+ if (displayInput.length <= maxInputWidth) {
1874
+ displayValue = displayInput;
1875
+ cursorX = prompt.length + Math.max(0, cursorInLine);
1898
1876
  }
1899
1877
  else {
1900
- const visibleStart = Math.max(0, cursorPos - Math.floor(maxInputWidth * 0.7));
1878
+ const effectiveCursor = Math.max(0, cursorInLine);
1879
+ const visibleStart = Math.max(0, effectiveCursor - Math.floor(maxInputWidth * 0.7));
1901
1880
  const visibleEnd = visibleStart + maxInputWidth;
1902
1881
  if (visibleStart > 0) {
1903
- displayValue = '…' + inputValue.slice(visibleStart + 1, visibleEnd);
1882
+ displayValue = '…' + displayInput.slice(visibleStart + 1, visibleEnd);
1904
1883
  }
1905
1884
  else {
1906
- displayValue = inputValue.slice(0, maxInputWidth);
1885
+ displayValue = displayInput.slice(0, maxInputWidth);
1907
1886
  }
1908
- cursorX = prompt.length + (cursorPos - visibleStart);
1887
+ cursorX = prompt.length + (effectiveCursor - visibleStart);
1909
1888
  }
1910
1889
  this.screen.writeLine(y, prompt + displayValue, fg.green);
1911
1890
  // Hide cursor when menu/settings is open
@@ -1913,7 +1892,7 @@ export class App {
1913
1892
  this.screen.showCursor(false);
1914
1893
  }
1915
1894
  else {
1916
- this.screen.setCursor(cursorX, y);
1895
+ this.screen.setCursor(Math.min(cursorX, width - 1), y);
1917
1896
  this.screen.showCursor(true);
1918
1897
  }
1919
1898
  }
@@ -2294,46 +2273,6 @@ export class App {
2294
2273
  this.screen.write(1, y, 'Starting...', fg.gray);
2295
2274
  }
2296
2275
  y++;
2297
- // Code preview section (for write/edit operations)
2298
- if (this.agentCodePreview) {
2299
- const MAX_PREVIEW_LINES = 12;
2300
- // Code separator with language label
2301
- const langLabel = this.agentCodePreview.lang ? ` ${this.agentCodePreview.lang} ` : '';
2302
- const sepPadLeft = 2;
2303
- const sepPadRight = width - sepPadLeft - langLabel.length - 1;
2304
- this.screen.write(0, y, '─'.repeat(sepPadLeft), SYNTAX.codeFrame);
2305
- this.screen.write(sepPadLeft, y, langLabel, SYNTAX.codeLang);
2306
- this.screen.write(sepPadLeft + langLabel.length, y, '─'.repeat(Math.max(0, sepPadRight)), SYNTAX.codeFrame);
2307
- y++;
2308
- // Determine visible code lines (show tail for long content)
2309
- const codeLines = this.agentCodePreview.content.split('\n');
2310
- const totalLines = this.agentCodePreview.totalLines;
2311
- const displayCount = Math.min(codeLines.length, MAX_PREVIEW_LINES);
2312
- const startIdx = Math.max(0, codeLines.length - MAX_PREVIEW_LINES);
2313
- const startLineNum = totalLines - codeLines.length + startIdx;
2314
- // Overflow indicator if truncated from top
2315
- if (startLineNum > 0) {
2316
- this.screen.write(1, y, `... (${startLineNum} lines above)`, fg.gray);
2317
- y++;
2318
- }
2319
- // Render highlighted code lines with line numbers
2320
- const lineNumWidth = String(totalLines).length;
2321
- const codeAreaWidth = width - lineNumWidth - 4;
2322
- for (let i = 0; i < displayCount; i++) {
2323
- const lineNum = startLineNum + i + 1;
2324
- const lineNumStr = String(lineNum).padStart(lineNumWidth, ' ');
2325
- // Truncate wide lines
2326
- let codeLine = codeLines[startIdx + i];
2327
- if (codeLine.length > codeAreaWidth) {
2328
- codeLine = codeLine.slice(0, codeAreaWidth - 1) + '\u2026';
2329
- }
2330
- const highlighted = highlightCode(codeLine, this.agentCodePreview.lang);
2331
- // Use writeRaw with line number embedded as ANSI since writeRaw clears the full line
2332
- const lineNumAnsi = SYNTAX.comment + lineNumStr + '\x1b[0m' + ' ';
2333
- this.screen.writeRaw(y, ' ' + lineNumAnsi + highlighted);
2334
- y++;
2335
- }
2336
- }
2337
2276
  // Stats line: Files and step info
2338
2277
  let x = 1;
2339
2278
  // File changes
@@ -2705,122 +2644,34 @@ export class App {
2705
2644
  * Render inline search screen
2706
2645
  */
2707
2646
  renderInlineSearch(startY, width, availableHeight) {
2708
- let y = startY;
2709
- // Separator line
2710
- this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
2711
- // Title
2712
- this.screen.writeLine(y++, 'Search Results', PRIMARY_COLOR + style.bold);
2713
- // Query
2714
- this.screen.write(0, y, 'Query: ', fg.white);
2715
- this.screen.write(7, y, `"${this.searchQuery}"`, fg.cyan);
2716
- if (this.searchResults.length > 0) {
2717
- this.screen.write(9 + this.searchQuery.length, y, ` (${this.searchResults.length} ${this.searchResults.length === 1 ? 'result' : 'results'})`, fg.gray);
2718
- }
2719
- y++;
2720
- y++;
2721
- if (this.searchResults.length === 0) {
2722
- this.screen.writeLine(y++, 'No results found.', fg.yellow);
2723
- }
2724
- else {
2725
- const maxVisible = availableHeight - 6;
2726
- const visibleStart = Math.max(0, this.searchIndex - Math.floor(maxVisible / 2));
2727
- const visibleResults = this.searchResults.slice(visibleStart, visibleStart + maxVisible);
2728
- for (let i = 0; i < visibleResults.length; i++) {
2729
- const result = visibleResults[i];
2730
- const actualIndex = visibleStart + i;
2731
- const isSelected = actualIndex === this.searchIndex;
2732
- const prefix = isSelected ? '▸ ' : ' ';
2733
- const roleColor = result.role === 'user' ? fg.green : fg.blue;
2734
- // First line: role and message number
2735
- this.screen.write(0, y, prefix, isSelected ? PRIMARY_COLOR : '');
2736
- this.screen.write(2, y, `[${result.role.toUpperCase()}]`, roleColor + style.bold);
2737
- this.screen.write(2 + result.role.length + 2, y, ` Message #${result.messageIndex + 1}`, fg.gray);
2738
- y++;
2739
- // Second line: matched text (truncated)
2740
- const maxTextWidth = width - 4;
2741
- const matchedText = result.matchedText.length > maxTextWidth
2742
- ? result.matchedText.slice(0, maxTextWidth - 3) + '...'
2743
- : result.matchedText;
2744
- this.screen.writeLine(y, ' ' + matchedText, fg.white);
2745
- y++;
2746
- if (i < visibleResults.length - 1)
2747
- y++; // spacing between results
2748
- }
2749
- }
2750
- // Footer
2751
- y = startY + availableHeight - 1;
2752
- this.screen.writeLine(y, '↑↓ Navigate • Enter Jump to message • Esc Close', fg.gray);
2647
+ renderSearchPanel(this.screen, startY, width, availableHeight, {
2648
+ searchOpen: this.searchOpen,
2649
+ searchQuery: this.searchQuery,
2650
+ searchResults: this.searchResults,
2651
+ searchIndex: this.searchIndex,
2652
+ searchCallback: this.searchCallback,
2653
+ });
2753
2654
  }
2754
2655
  /**
2755
2656
  * Render inline export screen
2756
2657
  */
2757
2658
  renderInlineExport(startY, width) {
2758
- const formats = [
2759
- { id: 'md', name: 'Markdown', desc: 'Formatted with headers and separators' },
2760
- { id: 'json', name: 'JSON', desc: 'Structured data format' },
2761
- { id: 'txt', name: 'Plain Text', desc: 'Simple text format' },
2762
- ];
2763
- let y = startY;
2764
- // Separator line
2765
- this.screen.horizontalLine(y++, '─', fg.green);
2766
- // Title
2767
- this.screen.writeLine(y++, 'Export Chat', fg.green + style.bold);
2768
- y++;
2769
- this.screen.writeLine(y++, 'Select export format:', fg.white);
2770
- y++;
2771
- for (let i = 0; i < formats.length; i++) {
2772
- const format = formats[i];
2773
- const isSelected = i === this.exportIndex;
2774
- const prefix = isSelected ? '› ' : ' ';
2775
- this.screen.write(0, y, prefix, isSelected ? fg.green : '');
2776
- this.screen.write(2, y, format.name.padEnd(12), isSelected ? fg.green + style.bold : fg.white);
2777
- this.screen.write(14, y, ' - ' + format.desc, fg.gray);
2778
- y++;
2779
- }
2780
- y++;
2781
- this.screen.writeLine(y, '↑↓ Navigate • Enter Export • Esc Cancel', fg.gray);
2659
+ renderExportPanel(this.screen, startY, width, {
2660
+ exportOpen: this.exportOpen,
2661
+ exportIndex: this.exportIndex,
2662
+ exportCallback: this.exportCallback,
2663
+ });
2782
2664
  }
2783
2665
  /**
2784
2666
  * Render inline logout picker
2785
2667
  */
2786
2668
  renderInlineLogout(startY, width) {
2787
- let y = startY;
2788
- // Separator line
2789
- this.screen.horizontalLine(y++, '─', fg.cyan);
2790
- // Title
2791
- this.screen.writeLine(y++, 'Select provider to logout:', fg.cyan + style.bold);
2792
- y++;
2793
- if (this.logoutProviders.length === 0) {
2794
- this.screen.writeLine(y++, 'No providers configured.', fg.yellow);
2795
- this.screen.writeLine(y++, 'Press Escape to go back.', fg.gray);
2796
- return;
2797
- }
2798
- // Provider options
2799
- for (let i = 0; i < this.logoutProviders.length; i++) {
2800
- const provider = this.logoutProviders[i];
2801
- const isSelected = i === this.logoutIndex;
2802
- const prefix = isSelected ? '→ ' : ' ';
2803
- this.screen.write(0, y, prefix, isSelected ? fg.green : '');
2804
- this.screen.write(2, y, provider.name, isSelected ? fg.green + style.bold : fg.white);
2805
- if (provider.isCurrent) {
2806
- this.screen.write(2 + provider.name.length + 1, y, '(current)', fg.cyan);
2807
- }
2808
- y++;
2809
- }
2810
- // "All" option
2811
- const allIndex = this.logoutProviders.length;
2812
- const isAllSelected = this.logoutIndex === allIndex;
2813
- this.screen.write(0, y, isAllSelected ? '→ ' : ' ', isAllSelected ? fg.red : '');
2814
- this.screen.write(2, y, 'Logout from all providers', isAllSelected ? fg.red + style.bold : fg.yellow);
2815
- y++;
2816
- // "Cancel" option
2817
- const cancelIndex = this.logoutProviders.length + 1;
2818
- const isCancelSelected = this.logoutIndex === cancelIndex;
2819
- this.screen.write(0, y, isCancelSelected ? '→ ' : ' ', isCancelSelected ? fg.blue : '');
2820
- this.screen.write(2, y, 'Cancel', isCancelSelected ? fg.blue + style.bold : fg.gray);
2821
- y++;
2822
- y++;
2823
- this.screen.writeLine(y, '↑↓ Navigate • Enter Select • Esc Cancel', fg.gray);
2669
+ renderLogoutPanel(this.screen, startY, width, {
2670
+ logoutOpen: this.logoutOpen,
2671
+ logoutIndex: this.logoutIndex,
2672
+ logoutProviders: this.logoutProviders,
2673
+ logoutCallback: this.logoutCallback,
2674
+ });
2824
2675
  }
2825
2676
  /**
2826
2677
  * Render inline login dialog
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Export panel component
3
+ */
4
+ import { Screen } from '../Screen';
5
+ import { KeyEvent } from '../Input';
6
+ export interface ExportState {
7
+ exportOpen: boolean;
8
+ exportIndex: number;
9
+ exportCallback: ((format: 'md' | 'json' | 'txt') => void) | null;
10
+ }
11
+ /**
12
+ * Render inline export panel
13
+ */
14
+ export declare function renderExportPanel(screen: Screen, startY: number, width: number, state: ExportState): void;
15
+ /**
16
+ * Handle export panel key events
17
+ */
18
+ export declare function handleExportKey(event: KeyEvent, state: ExportState, callbacks: {
19
+ onClose: () => void;
20
+ onRender: () => void;
21
+ onExport: (format: 'md' | 'json' | 'txt') => void;
22
+ }): void;