codeep 2.14.0 → 2.15.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 (61) hide show
  1. package/README.md +35 -24
  2. package/dist/acp/commands.js +22 -1
  3. package/dist/acp/server.js +13 -2
  4. package/dist/config/index.d.ts +10 -0
  5. package/dist/config/index.js +2 -2
  6. package/dist/config/providers.js +15 -10
  7. package/dist/renderer/App.d.ts +0 -30
  8. package/dist/renderer/App.js +149 -659
  9. package/dist/renderer/agentExecution.d.ts +1 -0
  10. package/dist/renderer/agentExecution.js +3 -2
  11. package/dist/renderer/commands/helpers.d.ts +63 -0
  12. package/dist/renderer/commands/helpers.js +108 -0
  13. package/dist/renderer/commands/registry.js +5 -0
  14. package/dist/renderer/commands.d.ts +4 -0
  15. package/dist/renderer/commands.js +179 -63
  16. package/dist/renderer/components/ActionFormatting.d.ts +17 -0
  17. package/dist/renderer/components/ActionFormatting.js +67 -0
  18. package/dist/renderer/components/Autocomplete.d.ts +33 -0
  19. package/dist/renderer/components/Autocomplete.js +40 -0
  20. package/dist/renderer/components/Intro.d.ts +9 -0
  21. package/dist/renderer/components/Intro.js +5 -15
  22. package/dist/renderer/components/MessageFormatter.d.ts +96 -0
  23. package/dist/renderer/components/MessageFormatter.js +375 -0
  24. package/dist/renderer/components/Permission.d.ts +4 -0
  25. package/dist/renderer/components/Permission.js +1 -1
  26. package/dist/renderer/components/Status.d.ts +4 -0
  27. package/dist/renderer/components/Status.js +2 -3
  28. package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
  29. package/dist/renderer/components/WelcomeFormatter.js +79 -0
  30. package/dist/renderer/components/uiConstants.d.ts +8 -0
  31. package/dist/renderer/components/uiConstants.js +24 -0
  32. package/dist/renderer/inputParsing.d.ts +22 -0
  33. package/dist/renderer/inputParsing.js +28 -0
  34. package/dist/renderer/layout.d.ts +215 -0
  35. package/dist/renderer/layout.js +326 -0
  36. package/dist/renderer/main.d.ts +2 -1
  37. package/dist/renderer/main.js +45 -10
  38. package/dist/renderer/ollamaHint.d.ts +12 -0
  39. package/dist/renderer/ollamaHint.js +29 -0
  40. package/dist/utils/agentChat.js +23 -1
  41. package/dist/utils/codeepCloud.d.ts +54 -0
  42. package/dist/utils/codeepCloud.js +95 -0
  43. package/dist/utils/export.d.ts +12 -0
  44. package/dist/utils/export.js +3 -3
  45. package/dist/utils/hooks.d.ts +26 -0
  46. package/dist/utils/hooks.js +69 -1
  47. package/dist/utils/keychain.js +45 -29
  48. package/dist/utils/logger.d.ts +12 -0
  49. package/dist/utils/logger.js +1 -1
  50. package/dist/utils/mcpConfig.d.ts +26 -0
  51. package/dist/utils/mcpConfig.js +109 -4
  52. package/dist/utils/skillBundles.d.ts +14 -0
  53. package/dist/utils/skillBundles.js +3 -3
  54. package/dist/utils/skillBundlesCloud.d.ts +7 -0
  55. package/dist/utils/skillBundlesCloud.js +1 -1
  56. package/dist/utils/tokenTracker.js +12 -2
  57. package/dist/utils/toolParsing.d.ts +11 -0
  58. package/dist/utils/toolParsing.js +6 -0
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/package.json +2 -2
@@ -4,25 +4,17 @@
4
4
  */
5
5
  import { Screen } from './Screen.js';
6
6
  import { Input, LineEditor } from './Input.js';
7
- import { fg, style, stringWidth } from './ansi.js';
8
- import { SYNTAX, highlightCode } from './highlight.js';
7
+ import { fg, style } from './ansi.js';
8
+ import { PRIMARY_COLOR, SPINNER_FRAMES, LOGO_LINES } from './components/uiConstants.js';
9
+ import { bottomPanelHeight, chatLayout, messageOffsets, scrollOffsetForTarget, scrollWindow, formatTokenCount, statusBarRightHint, activePanel, computeInputDisplay, agentProgressBar, truncateNotification, shouldShowPasteDialog, buildPasteInfo } from './layout.js';
10
+ import { parseCommandInput } from './inputParsing.js';
11
+ import { formatWelcomeMessage } from './components/WelcomeFormatter.js';
12
+ import { filterCommands } from './components/Autocomplete.js';
9
13
  import { handleInlineStatusKey, handleInlineHelpKey, handleMenuKey, handleInlinePermissionKey, handleInlineSessionPickerKey, handleInlineConfirmKey, handleLoginKey, } from './handlers.js';
10
14
  import clipboardy from 'clipboardy';
11
15
  import { readImageFromClipboard } from '../utils/clipboard.js';
12
- // Primary color: #f02a30 (Codeep red)
13
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
14
- // 8-bit block spinner frames
15
- const SPINNER_FRAMES = ['▖', '▘', '▝', '▗', '▌', '▀', '▐', '▄'];
16
- // ASCII Logo
17
- const LOGO_LINES = [
18
- ' ██████╗ ██████╗ ██████╗ ███████╗███████╗██████╗ ',
19
- '██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔════╝██╔══██╗',
20
- '██║ ██║ ██║██║ ██║█████╗ █████╗ ██████╔╝',
21
- '██║ ██║ ██║██║ ██║██╔══╝ ██╔══╝ ██╔═══╝ ',
22
- '╚██████╗╚██████╔╝██████╔╝███████╗███████╗██║ ',
23
- ' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ',
24
- ];
25
- const LOGO_HEIGHT = LOGO_LINES.length;
16
+ // (PRIMARY_COLOR, SPINNER_FRAMES, LOGO_LINES, LOGO_HEIGHT moved to
17
+ // ./components/uiConstants — imported above.)
26
18
  // ─── Command metadata ────────────────────────────────────────────────────────
27
19
  //
28
20
  // `COMMAND_DESCRIPTIONS` used to be hand-maintained here in App.ts and kept in
@@ -34,6 +26,7 @@ import { handleSettingsKey, SETTINGS } from './components/Settings.js';
34
26
  import { renderExportPanel, handleExportKey as handleExportKeyComponent } from './components/Export.js';
35
27
  import { renderLogoutPanel, handleLogoutKey as handleLogoutKeyComponent } from './components/Logout.js';
36
28
  import { renderSearchPanel, handleSearchKey as handleSearchKeyComponent } from './components/Search.js';
29
+ import { formatMessage as formatMessageFn, } from './components/MessageFormatter.js';
37
30
  export class App {
38
31
  screen;
39
32
  input;
@@ -66,7 +59,7 @@ export class App {
66
59
  // Paste detection state
67
60
  pasteInfo = null;
68
61
  pasteInfoOpen = false;
69
- codeBlockCounter = 0; // Global code block counter for /copy numbering
62
+ codeBlockCounter = { current: 0 }; // Global code block counter for /copy numbering
70
63
  // Message render cache: index → { lines, width, startBlock, blockCount }
71
64
  messageCache = [];
72
65
  // Inline help state
@@ -228,26 +221,10 @@ export class App {
228
221
  scrollToMessage(messageIndex) {
229
222
  const { width, height } = this.screen.getSize();
230
223
  const maxWidth = width - 4; // Account for margins
231
- // Calculate actual line count for messages up to target
232
- let totalLines = 0;
233
- let targetStartLine = 0;
234
- for (let i = 0; i < this.messages.length; i++) {
235
- const msg = this.messages[i];
236
- if (i === messageIndex) {
237
- targetStartLine = totalLines;
238
- }
239
- // Count lines for this message (header + content)
240
- const contentLines = msg.content.split('\n');
241
- let msgLines = 2; // Header + empty line after
242
- for (const line of contentLines) {
243
- // Account for word wrapping
244
- msgLines += Math.ceil(Math.max(1, line.length) / maxWidth);
245
- }
246
- totalLines += msgLines + 1; // +1 for spacing between messages
247
- }
224
+ const { totalLines, targetStartLine } = messageOffsets(this.messages.map((m) => m.content), maxWidth, messageIndex);
248
225
  const visibleLines = height - 12; // Approximate visible area
249
226
  // Set scroll offset to show the target message near the top
250
- this.scrollOffset = Math.max(0, totalLines - targetStartLine - Math.floor(visibleLines / 2));
227
+ this.scrollOffset = scrollOffsetForTarget(totalLines, targetStartLine, visibleLines);
251
228
  this.scheduleRender();
252
229
  this.notify(`Jumped to message #${messageIndex + 1}`);
253
230
  }
@@ -409,10 +386,8 @@ export class App {
409
386
  * Handle paste detection - call this when large text is pasted
410
387
  */
411
388
  handlePaste(text) {
412
- const lines = text.split('\n');
413
- const chars = text.length;
414
389
  // Only show paste info for significant pastes (>100 chars or >3 lines)
415
- if (chars < 100 && lines.length <= 3) {
390
+ if (!shouldShowPasteDialog(text)) {
416
391
  // Small paste - just add to input directly
417
392
  this.editor.insert(text);
418
393
  this.updateAutocomplete();
@@ -420,13 +395,7 @@ export class App {
420
395
  return;
421
396
  }
422
397
  // Large paste - show info box
423
- const preview = text.length > 200 ? text.slice(0, 197) + '...' : text;
424
- this.pasteInfo = {
425
- chars,
426
- lines: lines.length,
427
- preview,
428
- fullText: text,
429
- };
398
+ this.pasteInfo = buildPasteInfo(text);
430
399
  this.pasteInfoOpen = true;
431
400
  this.scheduleRender();
432
401
  }
@@ -720,71 +689,64 @@ export class App {
720
689
  * Handle chat screen keys
721
690
  */
722
691
  handleChatKey(event) {
723
- // If paste info is open, handle paste keys first
724
- if (this.pasteInfoOpen) {
725
- this.handlePasteInfoKey(event);
726
- return;
727
- }
728
- // If permission is open, handle permission keys first
729
- if (this.permissionOpen) {
730
- this.handleInlinePermissionKey(event);
731
- return;
732
- }
733
- // If session picker is open, handle session picker keys first
734
- if (this.sessionPickerOpen) {
735
- this.handleInlineSessionPickerKey(event);
736
- return;
737
- }
738
- // If confirm is open, handle confirm keys first
739
- if (this.confirmOpen) {
740
- this.handleInlineConfirmKey(event);
741
- return;
742
- }
743
- // If status is open, handle status keys first
744
- if (this.statusOpen) {
745
- this.handleInlineStatusKey(event);
746
- return;
747
- }
748
- // If help is open, handle help keys first
749
- if (this.helpOpen) {
750
- this.handleInlineHelpKey(event);
751
- return;
752
- }
753
- // If settings is open, handle settings keys first
754
- if (this.settingsOpen) {
755
- this.handleInlineSettingsKey(event);
756
- return;
757
- }
758
- // If search is open, handle search keys first
759
- if (this.searchOpen) {
760
- this.handleSearchKey(event);
761
- return;
762
- }
763
- // If export is open, handle export keys first
764
- if (this.exportOpen) {
765
- this.handleExportKey(event);
766
- return;
767
- }
768
- // If logout is open, handle logout keys first
769
- if (this.logoutOpen) {
770
- this.handleLogoutKey(event);
771
- return;
772
- }
773
- // If login is open, handle login keys first
774
- if (this.loginOpen) {
775
- this.handleLoginKey(event);
776
- return;
692
+ // Dispatch to whichever inline panel currently owns focus.
693
+ switch (activePanel({
694
+ pasteInfoOpen: this.pasteInfoOpen,
695
+ permissionOpen: this.permissionOpen,
696
+ sessionPickerOpen: this.sessionPickerOpen,
697
+ confirmOpen: this.confirmOpen,
698
+ statusOpen: this.statusOpen,
699
+ helpOpen: this.helpOpen,
700
+ settingsOpen: this.settingsOpen,
701
+ searchOpen: this.searchOpen,
702
+ exportOpen: this.exportOpen,
703
+ logoutOpen: this.logoutOpen,
704
+ loginOpen: this.loginOpen,
705
+ menuOpen: this.menuOpen,
706
+ showAutocomplete: this.showAutocomplete,
707
+ })) {
708
+ case 'pasteInfo':
709
+ this.handlePasteInfoKey(event);
710
+ return;
711
+ case 'permission':
712
+ this.handleInlinePermissionKey(event);
713
+ return;
714
+ case 'sessionPicker':
715
+ this.handleInlineSessionPickerKey(event);
716
+ return;
717
+ case 'confirm':
718
+ this.handleInlineConfirmKey(event);
719
+ return;
720
+ case 'status':
721
+ this.handleInlineStatusKey(event);
722
+ return;
723
+ case 'help':
724
+ this.handleInlineHelpKey(event);
725
+ return;
726
+ case 'settings':
727
+ this.handleInlineSettingsKey(event);
728
+ return;
729
+ case 'search':
730
+ this.handleSearchKey(event);
731
+ return;
732
+ case 'export':
733
+ this.handleExportKey(event);
734
+ return;
735
+ case 'logout':
736
+ this.handleLogoutKey(event);
737
+ return;
738
+ case 'login':
739
+ this.handleLoginKey(event);
740
+ return;
741
+ case 'menu':
742
+ this.handleMenuKey(event);
743
+ return;
777
744
  }
778
- // If intro is playing, skip on any key
745
+ // If intro is playing, skip on any key.
779
746
  if (this.showIntro) {
780
747
  this.skipIntro();
781
748
  return;
782
749
  }
783
- // If menu is open, handle menu keys first
784
- if (this.menuOpen) {
785
- this.handleMenuKey(event);
786
- return;
787
- }
788
750
  // Escape to cancel streaming/loading/agent or close autocomplete
789
751
  if (event.key === 'escape') {
790
752
  if (this.showAutocomplete) {
@@ -967,18 +929,16 @@ export class App {
967
929
  * Update autocomplete suggestions
968
930
  */
969
931
  updateAutocomplete() {
970
- const value = this.editor.getValue();
971
- // Show autocomplete only when typing a command
972
- if (value.startsWith('/') && !value.includes(' ')) {
973
- const query = value.slice(1).toLowerCase();
974
- this.autocompleteItems = App.COMMANDS.filter(cmd => cmd.startsWith(query)).slice(0, 8); // Max 8 items
975
- this.showAutocomplete = this.autocompleteItems.length > 0 && query.length > 0;
976
- this.autocompleteIndex = 0;
977
- }
978
- else {
932
+ const result = filterCommands(this.editor.getValue(), App.COMMANDS);
933
+ if (result === null) {
979
934
  this.showAutocomplete = false;
980
935
  this.autocompleteItems = [];
981
936
  }
937
+ else {
938
+ this.autocompleteItems = result.items;
939
+ this.showAutocomplete = result.items.length > 0;
940
+ this.autocompleteIndex = result.index;
941
+ }
982
942
  }
983
943
  /**
984
944
  * Handle inline status keys
@@ -1228,9 +1188,10 @@ export class App {
1228
1188
  * Handle command
1229
1189
  */
1230
1190
  handleCommand(input) {
1231
- const parts = input.slice(1).split(' ');
1232
- const command = parts[0].toLowerCase();
1233
- const args = parts.slice(1);
1191
+ const parsed = parseCommandInput(input);
1192
+ if (!parsed)
1193
+ return;
1194
+ const { command, args } = parsed;
1234
1195
  switch (command) {
1235
1196
  case 'help':
1236
1197
  this.helpOpen = true;
@@ -1297,59 +1258,40 @@ export class App {
1297
1258
  }
1298
1259
  this.screen.clear();
1299
1260
  // If menu or settings is open, reserve space for it at bottom
1300
- let bottomPanelHeight = 0;
1301
- if (this.pasteInfoOpen && this.pasteInfo) {
1302
- const previewLines = Math.min(this.pasteInfo.preview.split('\n').length, 5);
1303
- bottomPanelHeight = previewLines + 6; // title + preview + extra line indicator + options
1304
- }
1305
- else if (this.isAgentRunning && !(this.confirmOpen && this.confirmOptions)) {
1306
- bottomPanelHeight = 9; // Agent progress box: top + 5 log lines + stats + bottom + 1 margin
1307
- }
1308
- else if (this.permissionOpen) {
1309
- bottomPanelHeight = 10; // Permission dialog
1310
- }
1311
- else if (this.sessionPickerOpen) {
1312
- bottomPanelHeight = Math.min(this.sessionPickerItems.length + 6, 14); // Session picker
1313
- }
1314
- else if (this.confirmOpen && this.confirmOptions) {
1315
- bottomPanelHeight = this.confirmOptions.message.length + 5; // title + messages + buttons + padding
1316
- }
1317
- else if (this.statusOpen) {
1318
- bottomPanelHeight = 16; // Status info panel
1319
- }
1320
- else if (this.helpOpen) {
1321
- bottomPanelHeight = Math.min(height - 6, 20); // Help takes more space
1322
- }
1323
- else if (this.searchOpen) {
1324
- bottomPanelHeight = Math.min(this.searchResults.length * 3 + 6, 18); // Search results
1325
- }
1326
- else if (this.exportOpen) {
1327
- bottomPanelHeight = 10; // Export dialog
1328
- }
1329
- else if (this.logoutOpen) {
1330
- bottomPanelHeight = Math.min(this.logoutProviders.length + 6, 12); // Logout picker
1331
- }
1332
- else if (this.loginOpen) {
1333
- bottomPanelHeight = this.loginStep === 'provider'
1334
- ? Math.min(this.loginProviders.length + 5, 14)
1335
- : 8; // Login dialog
1336
- }
1337
- else if (this.menuOpen) {
1338
- bottomPanelHeight = Math.min(this.menuItems.length + 4, 14);
1339
- }
1340
- else if (this.settingsOpen) {
1341
- bottomPanelHeight = Math.min(SETTINGS.length + 4, 16);
1342
- }
1343
- else if (this.showAutocomplete && this.autocompleteItems.length > 0) {
1344
- bottomPanelHeight = Math.min(this.autocompleteItems.length + 3, 12);
1345
- }
1346
- const mainHeight = height - bottomPanelHeight;
1347
- // Layout - main UI takes top portion
1348
- const messagesStart = 0;
1349
- const messagesEnd = Math.max(0, mainHeight - 4);
1350
- const separatorLine = Math.max(0, mainHeight - 3);
1351
- const inputLine = Math.max(0, mainHeight - 2);
1352
- const statusLine = Math.max(0, mainHeight - 1);
1261
+ const panelHeight = bottomPanelHeight({
1262
+ height,
1263
+ pasteInfoOpen: this.pasteInfoOpen,
1264
+ pasteInfoPreviewLines: this.pasteInfo ? this.pasteInfo.preview.split('\n').length : 0,
1265
+ isAgentRunning: this.isAgentRunning,
1266
+ confirmOpen: this.confirmOpen && !!this.confirmOptions,
1267
+ permissionOpen: this.permissionOpen,
1268
+ sessionPickerOpen: this.sessionPickerOpen,
1269
+ sessionPickerItemCount: this.sessionPickerItems.length,
1270
+ confirmMessageCount: this.confirmOptions?.message.length ?? 0,
1271
+ statusOpen: this.statusOpen,
1272
+ helpOpen: this.helpOpen,
1273
+ searchOpen: this.searchOpen,
1274
+ searchResultCount: this.searchResults.length,
1275
+ exportOpen: this.exportOpen,
1276
+ logoutOpen: this.logoutOpen,
1277
+ logoutProviderCount: this.logoutProviders.length,
1278
+ loginOpen: this.loginOpen,
1279
+ loginStep: this.loginStep,
1280
+ loginProviderCount: this.loginProviders.length,
1281
+ menuOpen: this.menuOpen,
1282
+ menuItemCount: this.menuItems.length,
1283
+ settingsOpen: this.settingsOpen,
1284
+ settingsCount: SETTINGS.length,
1285
+ showAutocomplete: this.showAutocomplete,
1286
+ autocompleteItemCount: this.autocompleteItems.length,
1287
+ });
1288
+ const layout = chatLayout(height, panelHeight);
1289
+ const mainHeight = layout.mainHeight;
1290
+ const messagesStart = layout.messagesStart;
1291
+ const messagesEnd = layout.messagesEnd;
1292
+ const separatorLine = layout.separatorLine;
1293
+ const inputLine = layout.inputLine;
1294
+ const statusLine = layout.statusLine;
1353
1295
  // Messages
1354
1296
  const messagesHeight = Math.max(1, messagesEnd - messagesStart + 1);
1355
1297
  const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
@@ -1584,20 +1526,18 @@ export class App {
1584
1526
  return;
1585
1527
  }
1586
1528
  // Build prompt prefix
1587
- const lines = inputValue.split('\n');
1588
- const lineCount = lines.length;
1589
- // ❯ for normal, ❯❯ for multiline, [n] for multi-line with count
1590
- const promptSymbol = lineCount > 1 ? `[${lineCount}] ❯ ` : this.isMultilineMode ? '❯❯ ' : '❯ ';
1591
- const maxInputWidth = width - promptSymbol.length - 1;
1529
+ const display = computeInputDisplay({
1530
+ value: inputValue,
1531
+ cursorPos,
1532
+ width,
1533
+ isMultilineMode: this.isMultilineMode,
1534
+ });
1592
1535
  // Show placeholder when input is empty
1593
- if (!inputValue) {
1594
- this.screen.write(0, y, promptSymbol, PRIMARY_COLOR);
1595
- const placeholder = this.isMultilineMode
1596
- ? 'Multi-line mode Enter=newline · Esc=send'
1597
- : 'Message or /command';
1598
- this.screen.write(promptSymbol.length, y, placeholder, fg.gray);
1536
+ if (display.isEmpty) {
1537
+ this.screen.write(0, y, display.promptSymbol, PRIMARY_COLOR);
1538
+ this.screen.write(display.promptSymbol.length, y, display.placeholder, fg.gray);
1599
1539
  if (!hideCursor) {
1600
- this.screen.setCursor(promptSymbol.length, y);
1540
+ this.screen.setCursor(display.promptSymbol.length, y);
1601
1541
  this.screen.showCursor(true);
1602
1542
  }
1603
1543
  else {
@@ -1605,38 +1545,15 @@ export class App {
1605
1545
  }
1606
1546
  return;
1607
1547
  }
1608
- // For multi-line content, show the last line being edited
1609
- const lastLine = lines[lines.length - 1];
1610
- const displayInput = lineCount > 1 ? lastLine : inputValue;
1611
- const charsBeforeLastLine = lineCount > 1 ? inputValue.lastIndexOf('\n') + 1 : 0;
1612
- const cursorInLine = cursorPos - charsBeforeLastLine;
1613
- let displayValue;
1614
- let cursorX;
1615
- if (displayInput.length <= maxInputWidth) {
1616
- displayValue = displayInput;
1617
- cursorX = promptSymbol.length + Math.max(0, cursorInLine);
1618
- }
1619
- else {
1620
- const effectiveCursor = Math.max(0, cursorInLine);
1621
- const visibleStart = Math.max(0, effectiveCursor - Math.floor(maxInputWidth * 0.7));
1622
- const visibleEnd = visibleStart + maxInputWidth;
1623
- if (visibleStart > 0) {
1624
- displayValue = '…' + displayInput.slice(visibleStart + 1, visibleEnd);
1625
- }
1626
- else {
1627
- displayValue = displayInput.slice(0, maxInputWidth);
1628
- }
1629
- cursorX = promptSymbol.length + (effectiveCursor - visibleStart);
1630
- }
1631
1548
  // Prompt symbol in primary color, input text in white
1632
- this.screen.write(0, y, promptSymbol, PRIMARY_COLOR);
1633
- this.screen.write(promptSymbol.length, y, displayValue, fg.white);
1549
+ this.screen.write(0, y, display.promptSymbol, PRIMARY_COLOR);
1550
+ this.screen.write(display.promptSymbol.length, y, display.displayValue, fg.white);
1634
1551
  // Hide cursor when menu/settings is open
1635
1552
  if (hideCursor) {
1636
1553
  this.screen.showCursor(false);
1637
1554
  }
1638
1555
  else {
1639
- this.screen.setCursor(Math.min(cursorX, width - 1), y);
1556
+ this.screen.setCursor(Math.min(display.cursorX, width - 1), y);
1640
1557
  this.screen.showCursor(true);
1641
1558
  }
1642
1559
  }
@@ -2091,21 +2008,7 @@ export class App {
2091
2008
  // 8-bit gradient progress bar (right side, if max iterations known)
2092
2009
  if (this.agentMaxIterations > 0) {
2093
2010
  const barWidth = 14;
2094
- const progress = Math.min(this.agentIteration / this.agentMaxIterations, 1);
2095
- const filled = Math.round(progress * barWidth);
2096
- // Use block chars: █▓▒░ for gradient fill effect
2097
- const BLOCKS = ['░', '▒', '▓', '█'];
2098
- let bar = '';
2099
- for (let i = 0; i < barWidth; i++) {
2100
- if (i < filled - 1)
2101
- bar += '█';
2102
- else if (i === filled - 1)
2103
- bar += '▓';
2104
- else if (i === filled)
2105
- bar += '▒';
2106
- else
2107
- bar += '░';
2108
- }
2011
+ const bar = agentProgressBar(this.agentIteration, this.agentMaxIterations, barWidth);
2109
2012
  const barColored = PRIMARY_COLOR + bar + style.reset;
2110
2013
  const stepText = `${this.agentIteration}/${this.agentMaxIterations}`;
2111
2014
  const barX = width - barWidth - stepText.length - 3;
@@ -2128,51 +2031,6 @@ export class App {
2128
2031
  /**
2129
2032
  * Get color for action type
2130
2033
  */
2131
- getActionColor(type) {
2132
- const colors = {
2133
- 'read': fg.blue,
2134
- 'write': fg.green,
2135
- 'edit': fg.yellow,
2136
- 'delete': fg.red,
2137
- 'command': fg.magenta,
2138
- 'search': fg.cyan,
2139
- 'list': fg.white,
2140
- 'mkdir': fg.blue,
2141
- 'fetch': fg.cyan,
2142
- };
2143
- return colors[type] || fg.white;
2144
- }
2145
- /**
2146
- * Format action target for display
2147
- */
2148
- formatActionTarget(target, maxLen) {
2149
- if (target.includes('/')) {
2150
- const parts = target.split('/');
2151
- const filename = parts[parts.length - 1];
2152
- if (parts.length > 2) {
2153
- const short = `.../${parts[parts.length - 2]}/${filename}`;
2154
- return short.length > maxLen ? '...' + short.slice(-(maxLen - 3)) : short;
2155
- }
2156
- }
2157
- return target.length > maxLen ? '...' + target.slice(-(maxLen - 3)) : target;
2158
- }
2159
- /**
2160
- * Get action label for display
2161
- */
2162
- getActionLabel(type) {
2163
- const labels = {
2164
- 'read': 'Reading',
2165
- 'write': 'Creating',
2166
- 'edit': 'Editing',
2167
- 'delete': 'Deleting',
2168
- 'command': 'Running',
2169
- 'search': 'Searching',
2170
- 'list': 'Listing',
2171
- 'mkdir': 'Creating dir',
2172
- 'fetch': 'Fetching',
2173
- };
2174
- return labels[type] || type;
2175
- }
2176
2034
  /**
2177
2035
  * Render status bar
2178
2036
  */
@@ -2182,7 +2040,7 @@ export class App {
2182
2040
  if (this.notification) {
2183
2041
  const notifColor = this.notificationIsWarn ? '\x1b[38;5;208m' : PRIMARY_COLOR; // orange for warn
2184
2042
  const maxLen = width - 2;
2185
- const msg = this.notification.length > maxLen ? this.notification.slice(0, maxLen - 1) + '…' : this.notification;
2043
+ const msg = truncateNotification(this.notification, maxLen);
2186
2044
  this.screen.write(0, y, notifColor + ' ' + msg + style.reset);
2187
2045
  return;
2188
2046
  }
@@ -2192,7 +2050,7 @@ export class App {
2192
2050
  const modelName = status.model || '';
2193
2051
  const msgCount = `${this.messages.length} msg`;
2194
2052
  const tokenStr = stats && stats.totalTokens > 0
2195
- ? `${stats.totalTokens < 1000 ? stats.totalTokens : (stats.totalTokens / 1000).toFixed(1) + 'K'} tok`
2053
+ ? `${formatTokenCount(stats.totalTokens)} tok`
2196
2054
  : '';
2197
2055
  let leftX = 1;
2198
2056
  if (modelName) {
@@ -2218,18 +2076,16 @@ export class App {
2218
2076
  // Right: context-sensitive hints. While scrolled up, the "new
2219
2077
  // messages below" badge takes priority — it's the only signal that
2220
2078
  // the conversation moved on (addMessage no longer yanks the view).
2079
+ const rightText = statusBarRightHint({
2080
+ scrollOffset: this.scrollOffset,
2081
+ unseenWhileScrolled: this.unseenWhileScrolled,
2082
+ isStreaming: this.isStreaming,
2083
+ isLoading: this.isLoading,
2084
+ });
2221
2085
  if (this.scrollOffset > 0 && this.unseenWhileScrolled > 0) {
2222
- const badge = `↓ ${this.unseenWhileScrolled} new · PgDn `;
2223
- this.screen.write(width - badge.length, y, badge, PRIMARY_COLOR);
2086
+ this.screen.write(width - rightText.length, y, rightText, PRIMARY_COLOR);
2224
2087
  return;
2225
2088
  }
2226
- let rightText;
2227
- if (this.isStreaming || this.isLoading) {
2228
- rightText = 'Esc to stop ';
2229
- }
2230
- else {
2231
- rightText = '/help · ↑↓ history ';
2232
- }
2233
2089
  this.screen.write(width - rightText.length, y, rightText, fg.gray);
2234
2090
  }
2235
2091
  /**
@@ -2237,7 +2093,7 @@ export class App {
2237
2093
  */
2238
2094
  getVisibleMessages(height, width) {
2239
2095
  const allLines = [];
2240
- this.codeBlockCounter = 0; // Reset block counter for each render pass
2096
+ this.codeBlockCounter.current = 0; // Reset block counter for each render pass
2241
2097
  // Logo at the top, scrolls with content
2242
2098
  if (height >= 20) {
2243
2099
  const logoWidth = LOGO_LINES[0].length;
@@ -2255,365 +2111,36 @@ export class App {
2255
2111
  for (let i = 0; i < this.messages.length; i++) {
2256
2112
  const msg = this.messages[i];
2257
2113
  const cached = this.messageCache[i];
2258
- if (cached && cached.width === width && cached.startBlock === this.codeBlockCounter) {
2114
+ if (cached && cached.width === width && cached.startBlock === this.codeBlockCounter.current) {
2259
2115
  // Cache hit — preskoči formatiranje
2260
- this.codeBlockCounter += cached.blockCount;
2116
+ this.codeBlockCounter.current += cached.blockCount;
2261
2117
  allLines.push(...cached.lines);
2262
2118
  }
2263
2119
  else {
2264
2120
  // Cache miss — formatiraj i spremi
2265
- const startBlock = this.codeBlockCounter;
2121
+ const startBlock = this.codeBlockCounter.current;
2266
2122
  const msgLines = msg.role === 'welcome'
2267
- ? this.formatWelcomeMessage(msg.content)
2268
- : this.formatMessage(msg.role, msg.content, width);
2269
- const blockCount = this.codeBlockCounter - startBlock;
2123
+ ? formatWelcomeMessage(msg.content)
2124
+ : formatMessageFn(msg.role, msg.content, width, this.codeBlockCounter);
2125
+ const blockCount = this.codeBlockCounter.current - startBlock;
2270
2126
  this.messageCache[i] = { lines: msgLines, width, startBlock, blockCount };
2271
2127
  allLines.push(...msgLines);
2272
2128
  }
2273
2129
  }
2274
2130
  if (this.isStreaming && this.streamingContent) {
2275
- const streamLines = this.formatMessage('assistant', this.streamingContent + '▊', width);
2131
+ const streamLines = formatMessageFn('assistant', this.streamingContent + '▊', width, this.codeBlockCounter);
2276
2132
  allLines.push(...streamLines);
2277
2133
  }
2278
2134
  // Calculate visible window based on scroll offset
2279
2135
  const totalLines = allLines.length;
2280
- const maxScroll = Math.max(0, totalLines - height);
2281
- if (this.scrollOffset > maxScroll) {
2282
- this.scrollOffset = maxScroll;
2283
- }
2284
- const endIndex = totalLines - this.scrollOffset;
2285
- const startIndex = Math.max(0, endIndex - height);
2136
+ const { startIndex, endIndex, clampedScrollOffset } = scrollWindow({
2137
+ totalLines,
2138
+ height,
2139
+ scrollOffset: this.scrollOffset,
2140
+ });
2141
+ this.scrollOffset = clampedScrollOffset;
2286
2142
  return allLines.slice(startIndex, endIndex);
2287
2143
  }
2288
- /**
2289
- * Format message into lines with syntax highlighting for code blocks
2290
- */
2291
- formatWelcomeMessage(content) {
2292
- const lines = [];
2293
- const DIM = fg.rgb(80, 80, 80);
2294
- const LABEL = fg.rgb(100, 100, 100);
2295
- const SEP = DIM + ' · ' + style.reset;
2296
- for (const line of content.split('\n')) {
2297
- if (line.trim() === '') {
2298
- lines.push({ text: '', style: '' });
2299
- continue;
2300
- }
2301
- // Version line: "Codeep vX.X.X · Provider · Model"
2302
- if (line.startsWith('Codeep ')) {
2303
- const parts = line.split(' · ');
2304
- const colored = PRIMARY_COLOR + style.bold + (parts[0] || '') + style.reset
2305
- + SEP + fg.rgb(180, 180, 180) + (parts[1] || '') + style.reset
2306
- + SEP + fg.rgb(130, 130, 130) + (parts[2] || '') + style.reset;
2307
- lines.push({ text: colored, style: '', raw: true });
2308
- continue;
2309
- }
2310
- // Project line
2311
- if (/^\s+Project\s/.test(line)) {
2312
- const value = line.replace(/^\s+Project\s+/, '');
2313
- lines.push({ text: LABEL + ' Project ' + style.reset + fg.rgb(100, 180, 220) + value + style.reset, style: '', raw: true });
2314
- continue;
2315
- }
2316
- // Access line
2317
- if (/^\s+Access\s/.test(line)) {
2318
- const value = line.replace(/^\s+Access\s+/, '');
2319
- const parts = value.split(' · ');
2320
- const accessColored = fg.rgb(100, 200, 120) + style.bold + (parts[0] || '') + style.reset;
2321
- const rest = parts.slice(1).map(p => fg.rgb(80, 160, 100) + p + style.reset).join(SEP);
2322
- lines.push({ text: LABEL + ' Access ' + style.reset + accessColored + (rest ? SEP + rest : ''), style: '', raw: true });
2323
- continue;
2324
- }
2325
- // Mode line
2326
- if (/^\s+Mode\s/.test(line)) {
2327
- const value = line.replace(/^\s+Mode\s+/, '');
2328
- lines.push({ text: LABEL + ' Mode ' + style.reset + fg.rgb(160, 160, 160) + value + style.reset, style: '', raw: true });
2329
- continue;
2330
- }
2331
- // Agent Mode warning
2332
- if (line.includes('⚠')) {
2333
- lines.push({ text: ' ' + fg.rgb(220, 160, 40) + line.trim() + style.reset, style: '', raw: true });
2334
- continue;
2335
- }
2336
- // Shortcuts line
2337
- if (line.includes('/help')) {
2338
- const parts = line.trim().split(' · ');
2339
- const colored = parts.map(p => fg.rgb(150, 150, 150) + p.trim() + style.reset).join(DIM + ' · ' + style.reset);
2340
- lines.push({ text: ' ' + colored, style: '', raw: true });
2341
- continue;
2342
- }
2343
- lines.push({ text: line, style: '' });
2344
- }
2345
- lines.push({ text: '', style: '' });
2346
- return lines;
2347
- }
2348
- formatMessage(role, content, maxWidth) {
2349
- const lines = [];
2350
- // Role-specific prefix — user gets primary color bar, assistant gets dim header, system gets diamond
2351
- const contIndent = ' ';
2352
- let firstPrefix;
2353
- const firstStyle = '';
2354
- if (role === 'user') {
2355
- firstPrefix = PRIMARY_COLOR + '\u258c ' + style.reset;
2356
- }
2357
- else if (role === 'assistant') {
2358
- lines.push({ text: PRIMARY_COLOR + '\u254c\u254c' + style.reset + fg.rgb(120, 120, 120) + ' codeep' + style.reset, style: '', raw: true });
2359
- firstPrefix = ' ';
2360
- }
2361
- else {
2362
- firstPrefix = PRIMARY_COLOR + '\u25b8 ' + style.reset;
2363
- }
2364
- const codeBlockRegex = /```([^\n]*)\n([\s\S]*?)```/g;
2365
- let lastIndex = 0;
2366
- let match;
2367
- let isFirstLine = true;
2368
- while ((match = codeBlockRegex.exec(content)) !== null) {
2369
- const textBefore = content.slice(lastIndex, match.index);
2370
- if (textBefore) {
2371
- const prefix = isFirstLine ? firstPrefix : (role === 'user' ? contIndent : ' ');
2372
- const textLines = this.formatTextLines(textBefore, maxWidth, prefix, firstStyle, role === 'user' && isFirstLine);
2373
- lines.push(...textLines);
2374
- isFirstLine = false;
2375
- }
2376
- this.codeBlockCounter++;
2377
- const rawLang = (match[1] || 'text').trim();
2378
- let lang = rawLang;
2379
- if (rawLang.includes(':') || rawLang.includes('.')) {
2380
- lang = rawLang.split('.').pop() || rawLang;
2381
- }
2382
- lines.push(...this.formatCodeBlock(match[2], lang, maxWidth, this.codeBlockCounter));
2383
- lastIndex = match.index + match[0].length;
2384
- isFirstLine = false;
2385
- }
2386
- const textAfter = content.slice(lastIndex);
2387
- if (textAfter) {
2388
- const prefix = isFirstLine ? firstPrefix : (role === 'user' ? contIndent : ' ');
2389
- const textLines = this.formatTextLines(textAfter, maxWidth, prefix, firstStyle, role === 'user' && isFirstLine);
2390
- lines.push(...textLines);
2391
- }
2392
- lines.push({ text: '', style: '' });
2393
- return lines;
2394
- }
2395
- /**
2396
- * Apply inline markdown formatting (bold, italic, inline code) to a line
2397
- */
2398
- applyInlineMarkdown(text) {
2399
- let result = '';
2400
- let hasFormatting = false;
2401
- let i = 0;
2402
- while (i < text.length) {
2403
- // Inline code: `code`
2404
- if (text[i] === '`' && text[i + 1] !== '`') {
2405
- const end = text.indexOf('`', i + 1);
2406
- if (end !== -1) {
2407
- const code = text.slice(i + 1, end);
2408
- result += fg.rgb(209, 154, 102) + code + '\x1b[0m';
2409
- hasFormatting = true;
2410
- i = end + 1;
2411
- continue;
2412
- }
2413
- }
2414
- // Bold + italic: ***text***
2415
- if (text.slice(i, i + 3) === '***') {
2416
- const end = text.indexOf('***', i + 3);
2417
- if (end !== -1) {
2418
- const inner = text.slice(i + 3, end);
2419
- result += style.bold + style.italic + PRIMARY_COLOR + inner + '\x1b[0m';
2420
- hasFormatting = true;
2421
- i = end + 3;
2422
- continue;
2423
- }
2424
- }
2425
- // Bold: **text**
2426
- if (text.slice(i, i + 2) === '**') {
2427
- const end = text.indexOf('**', i + 2);
2428
- if (end !== -1) {
2429
- const inner = text.slice(i + 2, end);
2430
- result += style.bold + PRIMARY_COLOR + inner + '\x1b[0m';
2431
- hasFormatting = true;
2432
- i = end + 2;
2433
- continue;
2434
- }
2435
- }
2436
- // Italic: *text*
2437
- if (text[i] === '*' && text[i + 1] !== '*') {
2438
- const end = text.indexOf('*', i + 1);
2439
- if (end !== -1 && end > i + 1) {
2440
- const inner = text.slice(i + 1, end);
2441
- result += style.italic + inner + '\x1b[0m';
2442
- hasFormatting = true;
2443
- i = end + 1;
2444
- continue;
2445
- }
2446
- }
2447
- // Strikethrough: ~~text~~ — using the SGR strikethrough escape (\x1b[9m).
2448
- // Widely supported in modern terminals (iTerm2, Kitty, WezTerm, Alacritty,
2449
- // gnome-terminal, Windows Terminal). Falls back gracefully to the dim
2450
- // text colour on terminals that don't render the SGR.
2451
- if (text.slice(i, i + 2) === '~~') {
2452
- const end = text.indexOf('~~', i + 2);
2453
- if (end !== -1) {
2454
- const inner = text.slice(i + 2, end);
2455
- result += '\x1b[9m' + fg.rgb(140, 140, 140) + inner + '\x1b[0m';
2456
- hasFormatting = true;
2457
- i = end + 2;
2458
- continue;
2459
- }
2460
- }
2461
- result += text[i];
2462
- i++;
2463
- }
2464
- return { formatted: result, hasFormatting };
2465
- }
2466
- /**
2467
- * Format plain text lines with markdown support
2468
- */
2469
- formatTextLines(text, maxWidth, firstPrefix, firstStyle, rawPrefix = false) {
2470
- const lines = [];
2471
- const contentLines = text.split('\n');
2472
- for (let i = 0; i < contentLines.length; i++) {
2473
- const line = contentLines[i];
2474
- const prefix = i === 0 ? firstPrefix : ' ';
2475
- const prefixStyle = i === 0 ? firstStyle : '';
2476
- const isRaw = i === 0 ? rawPrefix : false;
2477
- // Heading: ## or ### etc.
2478
- const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
2479
- if (headingMatch) {
2480
- const level = headingMatch[1].length;
2481
- const headingText = headingMatch[2];
2482
- const headingColor = level <= 2 ? fg.rgb(97, 175, 239) : fg.rgb(198, 120, 221);
2483
- lines.push({
2484
- text: prefix + headingColor + style.bold + headingText + '\x1b[0m',
2485
- style: prefixStyle,
2486
- raw: true,
2487
- });
2488
- continue;
2489
- }
2490
- // Horizontal rule: --- or *** or ___
2491
- if (/^[-*_]{3,}\s*$/.test(line)) {
2492
- const ruleWidth = Math.min(maxWidth - 4, 40);
2493
- lines.push({
2494
- text: prefix + fg.gray + '─'.repeat(ruleWidth) + '\x1b[0m',
2495
- style: prefixStyle,
2496
- raw: true,
2497
- });
2498
- continue;
2499
- }
2500
- // Blockquote: `> text` — render with a left accent bar (PRIMARY_COLOR)
2501
- // and the body in a dimmer grey so it visually sits behind regular text.
2502
- // Strips one level of `> ` so nested quotes render with their own bar.
2503
- const quoteMatch = line.match(/^(\s*)>\s?(.*)$/);
2504
- if (quoteMatch) {
2505
- const indent = quoteMatch[1];
2506
- const quoteText = quoteMatch[2];
2507
- const { formatted, hasFormatting } = this.applyInlineMarkdown(quoteText);
2508
- const body = hasFormatting ? formatted : quoteText;
2509
- // Vertical bar + space, then dim grey body. Skip if the body is
2510
- // empty so an isolated `>` renders cleanly.
2511
- const barred = PRIMARY_COLOR + '│' + '\x1b[0m' + (body ? ' ' + fg.rgb(160, 160, 160) + body + '\x1b[0m' : '');
2512
- lines.push({
2513
- text: prefix + indent + barred,
2514
- style: prefixStyle,
2515
- raw: true,
2516
- });
2517
- continue;
2518
- }
2519
- // List items: - item or * item or numbered 1. item
2520
- const listMatch = line.match(/^(\s*)([-*]|\d+\.)\s+(.+)$/);
2521
- if (listMatch) {
2522
- const indent = listMatch[1];
2523
- const bullet = listMatch[2];
2524
- const content = listMatch[3];
2525
- const { formatted, hasFormatting } = this.applyInlineMarkdown(content);
2526
- const bulletChar = bullet === '-' || bullet === '*' ? '\u25b8' : bullet;
2527
- if (hasFormatting) {
2528
- lines.push({
2529
- text: prefix + indent + fg.gray + bulletChar + '\x1b[0m' + ' ' + formatted,
2530
- style: prefixStyle,
2531
- raw: true,
2532
- });
2533
- }
2534
- else {
2535
- lines.push({
2536
- text: prefix + indent + bulletChar + ' ' + content,
2537
- style: prefixStyle,
2538
- });
2539
- }
2540
- continue;
2541
- }
2542
- // Regular text with possible inline markdown
2543
- const { formatted, hasFormatting } = this.applyInlineMarkdown(line);
2544
- if (hasFormatting) {
2545
- // Use original (no-ANSI) line to measure and wrap, then apply markdown per segment
2546
- if (stringWidth(line) > maxWidth - prefix.length) {
2547
- const wrapped = this.wordWrap(line, maxWidth - prefix.length);
2548
- for (let j = 0; j < wrapped.length; j++) {
2549
- const { formatted: segFormatted } = this.applyInlineMarkdown(wrapped[j]);
2550
- lines.push({
2551
- text: (j === 0 ? prefix : ' ') + segFormatted,
2552
- style: j === 0 ? prefixStyle : '',
2553
- raw: true,
2554
- });
2555
- }
2556
- }
2557
- else {
2558
- lines.push({
2559
- text: prefix + formatted,
2560
- style: prefixStyle,
2561
- raw: true,
2562
- });
2563
- }
2564
- }
2565
- else {
2566
- // Plain text - word wrap as before
2567
- if (stringWidth(line) > maxWidth - prefix.length) {
2568
- const wrapped = this.wordWrap(line, maxWidth - prefix.length);
2569
- for (let j = 0; j < wrapped.length; j++) {
2570
- const lineIsRaw = j === 0 ? isRaw : false;
2571
- lines.push({
2572
- text: (j === 0 ? prefix : ' ') + wrapped[j],
2573
- style: j === 0 ? prefixStyle : '',
2574
- ...(lineIsRaw ? { raw: true } : {}),
2575
- });
2576
- }
2577
- }
2578
- else {
2579
- lines.push({
2580
- text: prefix + line,
2581
- style: prefixStyle,
2582
- ...(isRaw ? { raw: true } : {}),
2583
- });
2584
- }
2585
- }
2586
- }
2587
- return lines;
2588
- }
2589
- /**
2590
- * Format code block with syntax highlighting (no border)
2591
- */
2592
- formatCodeBlock(code, lang, maxWidth, blockNum) {
2593
- const lines = [];
2594
- const codeLines = code.split('\n');
2595
- // Remove trailing empty line if exists
2596
- if (codeLines.length > 0 && codeLines[codeLines.length - 1] === '') {
2597
- codeLines.pop();
2598
- }
2599
- // Language label with block number for /copy
2600
- const label = blockNum ? (lang ? ` ${lang} [${blockNum}]` : ` [${blockNum}]`) : (lang ? ' ' + lang : '');
2601
- if (label) {
2602
- lines.push({ text: label, style: SYNTAX.codeLang, raw: false });
2603
- }
2604
- // Code lines with highlighting and indent
2605
- for (const codeLine of codeLines) {
2606
- const highlighted = highlightCode(codeLine, lang);
2607
- lines.push({
2608
- text: ' ' + highlighted,
2609
- style: '',
2610
- raw: true // Don't apply additional styling, code is pre-highlighted
2611
- });
2612
- }
2613
- // Empty line after code block
2614
- lines.push({ text: '', style: '', raw: false });
2615
- return lines;
2616
- }
2617
2144
  /**
2618
2145
  * Render inline search screen
2619
2146
  */
@@ -2772,41 +2299,4 @@ export class App {
2772
2299
  return resultLine;
2773
2300
  }).join('\n');
2774
2301
  }
2775
- /**
2776
- * Word wrap
2777
- */
2778
- wordWrap(text, maxWidth) {
2779
- const words = text.split(' ');
2780
- const lines = [];
2781
- let currentLine = '';
2782
- for (const word of words) {
2783
- const wordW = stringWidth(word);
2784
- // Hard-break words wider than maxWidth (e.g. long file paths with no spaces)
2785
- if (wordW > maxWidth) {
2786
- if (currentLine) {
2787
- lines.push(currentLine);
2788
- currentLine = '';
2789
- }
2790
- // Slice the word into maxWidth chunks
2791
- let remaining = word;
2792
- while (stringWidth(remaining) > maxWidth) {
2793
- lines.push(remaining.slice(0, maxWidth));
2794
- remaining = remaining.slice(maxWidth);
2795
- }
2796
- currentLine = remaining;
2797
- continue;
2798
- }
2799
- if (stringWidth(currentLine) + wordW + 1 > maxWidth && currentLine) {
2800
- lines.push(currentLine);
2801
- currentLine = word;
2802
- }
2803
- else {
2804
- currentLine += (currentLine ? ' ' : '') + word;
2805
- }
2806
- }
2807
- if (currentLine) {
2808
- lines.push(currentLine);
2809
- }
2810
- return lines.length > 0 ? lines : [''];
2811
- }
2812
2302
  }