codeep 2.13.2 → 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.
- package/README.md +35 -24
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +278 -251
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +35 -22
- package/dist/renderer/App.d.ts +0 -30
- package/dist/renderer/App.js +149 -659
- package/dist/renderer/agentExecution.d.ts +2 -1
- package/dist/renderer/agentExecution.js +10 -6
- package/dist/renderer/commands/helpers.d.ts +63 -0
- package/dist/renderer/commands/helpers.js +108 -0
- package/dist/renderer/commands/registry.js +5 -0
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +183 -64
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +33 -0
- package/dist/renderer/components/Autocomplete.js +40 -0
- package/dist/renderer/components/Intro.d.ts +9 -0
- package/dist/renderer/components/Intro.js +5 -15
- package/dist/renderer/components/MessageFormatter.d.ts +96 -0
- package/dist/renderer/components/MessageFormatter.js +375 -0
- package/dist/renderer/components/Permission.d.ts +4 -0
- package/dist/renderer/components/Permission.js +1 -1
- package/dist/renderer/components/Status.d.ts +4 -0
- package/dist/renderer/components/Status.js +2 -3
- package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
- package/dist/renderer/components/WelcomeFormatter.js +79 -0
- package/dist/renderer/components/uiConstants.d.ts +8 -0
- package/dist/renderer/components/uiConstants.js +24 -0
- package/dist/renderer/inputParsing.d.ts +22 -0
- package/dist/renderer/inputParsing.js +28 -0
- package/dist/renderer/layout.d.ts +215 -0
- package/dist/renderer/layout.js +326 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +45 -10
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +28 -2
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +69 -1
- package/dist/utils/keychain.js +45 -29
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/mcpConfig.d.ts +26 -0
- package/dist/utils/mcpConfig.js +109 -4
- package/dist/utils/skillBundles.d.ts +14 -0
- package/dist/utils/skillBundles.js +3 -3
- package/dist/utils/skillBundlesCloud.d.ts +7 -0
- package/dist/utils/skillBundlesCloud.js +1 -1
- package/dist/utils/tokenTracker.d.ts +30 -3
- package/dist/utils/tokenTracker.js +71 -13
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/renderer/App.js
CHANGED
|
@@ -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
|
|
8
|
-
import {
|
|
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
|
-
//
|
|
13
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
724
|
-
|
|
725
|
-
this.
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
this.
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
this.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
971
|
-
|
|
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
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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 (
|
|
1594
|
-
this.screen.write(0, y, promptSymbol, PRIMARY_COLOR);
|
|
1595
|
-
|
|
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
|
|
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
|
|
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
|
-
? `${
|
|
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
|
-
|
|
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
|
-
?
|
|
2268
|
-
:
|
|
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 =
|
|
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
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
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
|
}
|