codeep 2.14.0 → 2.16.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 +47 -27
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +13 -2
- package/dist/acp/session.js +22 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +35 -24
- package/dist/renderer/App.d.ts +77 -30
- package/dist/renderer/App.js +429 -659
- package/dist/renderer/agentExecution.d.ts +1 -0
- package/dist/renderer/agentExecution.js +3 -2
- package/dist/renderer/commands/helpers.d.ts +251 -0
- package/dist/renderer/commands/helpers.js +450 -0
- package/dist/renderer/commands/registry.js +7 -1
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +363 -318
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +58 -0
- package/dist/renderer/components/Autocomplete.js +75 -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 +219 -0
- package/dist/renderer/layout.js +338 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +79 -11
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +23 -1
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- 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/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- 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.js +21 -5
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- package/dist/utils/webFetch.d.ts +101 -0
- package/dist/utils/webFetch.js +375 -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,18 @@
|
|
|
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, detectMentionQuery } from './components/Autocomplete.js';
|
|
13
|
+
import { suggestMentions } from '../utils/mentions.js';
|
|
9
14
|
import { handleInlineStatusKey, handleInlineHelpKey, handleMenuKey, handleInlinePermissionKey, handleInlineSessionPickerKey, handleInlineConfirmKey, handleLoginKey, } from './handlers.js';
|
|
10
15
|
import clipboardy from 'clipboardy';
|
|
11
16
|
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;
|
|
17
|
+
// (PRIMARY_COLOR, SPINNER_FRAMES, LOGO_LINES, LOGO_HEIGHT moved to
|
|
18
|
+
// ./components/uiConstants — imported above.)
|
|
26
19
|
// ─── Command metadata ────────────────────────────────────────────────────────
|
|
27
20
|
//
|
|
28
21
|
// `COMMAND_DESCRIPTIONS` used to be hand-maintained here in App.ts and kept in
|
|
@@ -34,6 +27,7 @@ import { handleSettingsKey, SETTINGS } from './components/Settings.js';
|
|
|
34
27
|
import { renderExportPanel, handleExportKey as handleExportKeyComponent } from './components/Export.js';
|
|
35
28
|
import { renderLogoutPanel, handleLogoutKey as handleLogoutKeyComponent } from './components/Logout.js';
|
|
36
29
|
import { renderSearchPanel, handleSearchKey as handleSearchKeyComponent } from './components/Search.js';
|
|
30
|
+
import { formatMessage as formatMessageFn, } from './components/MessageFormatter.js';
|
|
37
31
|
export class App {
|
|
38
32
|
screen;
|
|
39
33
|
input;
|
|
@@ -66,7 +60,7 @@ export class App {
|
|
|
66
60
|
// Paste detection state
|
|
67
61
|
pasteInfo = null;
|
|
68
62
|
pasteInfoOpen = false;
|
|
69
|
-
codeBlockCounter = 0; // Global code block counter for /copy numbering
|
|
63
|
+
codeBlockCounter = { current: 0 }; // Global code block counter for /copy numbering
|
|
70
64
|
// Message render cache: index → { lines, width, startBlock, blockCount }
|
|
71
65
|
messageCache = [];
|
|
72
66
|
// Inline help state
|
|
@@ -84,10 +78,26 @@ export class App {
|
|
|
84
78
|
showAutocomplete = false;
|
|
85
79
|
autocompleteIndex = 0;
|
|
86
80
|
autocompleteItems = [];
|
|
81
|
+
// `@mention` autocomplete state — separate from the `/command` picker
|
|
82
|
+
// because mentions appear mid-sentence (not just at the start) and
|
|
83
|
+
// insert a file path (not a slash command). `mentionAtStart` is the
|
|
84
|
+
// index of the `@` in the editor value, used to replace `@query` with
|
|
85
|
+
// `@selectedPath` on Tab/Enter.
|
|
86
|
+
showMentionAutocomplete = false;
|
|
87
|
+
mentionIndex = 0;
|
|
88
|
+
mentionItems = [];
|
|
89
|
+
mentionAtStart = 0;
|
|
90
|
+
/** Project root for resolving `suggestMentions`. Cached per update. */
|
|
91
|
+
mentionRoot = '';
|
|
87
92
|
// Inline confirmation dialog state
|
|
88
93
|
confirmOpen = false;
|
|
89
94
|
confirmOptions = null;
|
|
90
95
|
confirmSelection = 'no';
|
|
96
|
+
// Inline hunk-picker state (`/apply --interactive`)
|
|
97
|
+
hunkPickerOpen = false;
|
|
98
|
+
hunkPickerOptions = null;
|
|
99
|
+
hunkPickerIndex = 0;
|
|
100
|
+
hunkPickerAccepted = [];
|
|
91
101
|
// Inline menu state (renders below input/status)
|
|
92
102
|
menuOpen = false;
|
|
93
103
|
menuTitle = '';
|
|
@@ -228,26 +238,10 @@ export class App {
|
|
|
228
238
|
scrollToMessage(messageIndex) {
|
|
229
239
|
const { width, height } = this.screen.getSize();
|
|
230
240
|
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
|
-
}
|
|
241
|
+
const { totalLines, targetStartLine } = messageOffsets(this.messages.map((m) => m.content), maxWidth, messageIndex);
|
|
248
242
|
const visibleLines = height - 12; // Approximate visible area
|
|
249
243
|
// Set scroll offset to show the target message near the top
|
|
250
|
-
this.scrollOffset =
|
|
244
|
+
this.scrollOffset = scrollOffsetForTarget(totalLines, targetStartLine, visibleLines);
|
|
251
245
|
this.scheduleRender();
|
|
252
246
|
this.notify(`Jumped to message #${messageIndex + 1}`);
|
|
253
247
|
}
|
|
@@ -409,10 +403,8 @@ export class App {
|
|
|
409
403
|
* Handle paste detection - call this when large text is pasted
|
|
410
404
|
*/
|
|
411
405
|
handlePaste(text) {
|
|
412
|
-
const lines = text.split('\n');
|
|
413
|
-
const chars = text.length;
|
|
414
406
|
// Only show paste info for significant pastes (>100 chars or >3 lines)
|
|
415
|
-
if (
|
|
407
|
+
if (!shouldShowPasteDialog(text)) {
|
|
416
408
|
// Small paste - just add to input directly
|
|
417
409
|
this.editor.insert(text);
|
|
418
410
|
this.updateAutocomplete();
|
|
@@ -420,13 +412,7 @@ export class App {
|
|
|
420
412
|
return;
|
|
421
413
|
}
|
|
422
414
|
// Large paste - show info box
|
|
423
|
-
|
|
424
|
-
this.pasteInfo = {
|
|
425
|
-
chars,
|
|
426
|
-
lines: lines.length,
|
|
427
|
-
preview,
|
|
428
|
-
fullText: text,
|
|
429
|
-
};
|
|
415
|
+
this.pasteInfo = buildPasteInfo(text);
|
|
430
416
|
this.pasteInfoOpen = true;
|
|
431
417
|
this.scheduleRender();
|
|
432
418
|
}
|
|
@@ -538,6 +524,17 @@ export class App {
|
|
|
538
524
|
this.confirmOpen = true;
|
|
539
525
|
this.scheduleRender();
|
|
540
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Show the interactive hunk picker (`/apply --interactive`).
|
|
529
|
+
* The caller passes pre-built items + an `onComplete` callback.
|
|
530
|
+
*/
|
|
531
|
+
showHunkPicker(options) {
|
|
532
|
+
this.hunkPickerOptions = options;
|
|
533
|
+
this.hunkPickerIndex = 0;
|
|
534
|
+
this.hunkPickerAccepted = [];
|
|
535
|
+
this.hunkPickerOpen = true;
|
|
536
|
+
this.scheduleRender();
|
|
537
|
+
}
|
|
541
538
|
/**
|
|
542
539
|
* Show permission dialog (inline, below status bar)
|
|
543
540
|
*/
|
|
@@ -720,73 +717,75 @@ export class App {
|
|
|
720
717
|
* Handle chat screen keys
|
|
721
718
|
*/
|
|
722
719
|
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
|
-
|
|
776
|
-
|
|
720
|
+
// Dispatch to whichever inline panel currently owns focus.
|
|
721
|
+
switch (activePanel({
|
|
722
|
+
pasteInfoOpen: this.pasteInfoOpen,
|
|
723
|
+
permissionOpen: this.permissionOpen,
|
|
724
|
+
sessionPickerOpen: this.sessionPickerOpen,
|
|
725
|
+
confirmOpen: this.confirmOpen,
|
|
726
|
+
statusOpen: this.statusOpen,
|
|
727
|
+
helpOpen: this.helpOpen,
|
|
728
|
+
settingsOpen: this.settingsOpen,
|
|
729
|
+
searchOpen: this.searchOpen,
|
|
730
|
+
exportOpen: this.exportOpen,
|
|
731
|
+
logoutOpen: this.logoutOpen,
|
|
732
|
+
loginOpen: this.loginOpen,
|
|
733
|
+
menuOpen: this.menuOpen,
|
|
734
|
+
showAutocomplete: this.showAutocomplete,
|
|
735
|
+
hunkPickerOpen: this.hunkPickerOpen,
|
|
736
|
+
})) {
|
|
737
|
+
case 'pasteInfo':
|
|
738
|
+
this.handlePasteInfoKey(event);
|
|
739
|
+
return;
|
|
740
|
+
case 'permission':
|
|
741
|
+
this.handleInlinePermissionKey(event);
|
|
742
|
+
return;
|
|
743
|
+
case 'sessionPicker':
|
|
744
|
+
this.handleInlineSessionPickerKey(event);
|
|
745
|
+
return;
|
|
746
|
+
case 'confirm':
|
|
747
|
+
this.handleInlineConfirmKey(event);
|
|
748
|
+
return;
|
|
749
|
+
case 'status':
|
|
750
|
+
this.handleInlineStatusKey(event);
|
|
751
|
+
return;
|
|
752
|
+
case 'help':
|
|
753
|
+
this.handleInlineHelpKey(event);
|
|
754
|
+
return;
|
|
755
|
+
case 'settings':
|
|
756
|
+
this.handleInlineSettingsKey(event);
|
|
757
|
+
return;
|
|
758
|
+
case 'search':
|
|
759
|
+
this.handleSearchKey(event);
|
|
760
|
+
return;
|
|
761
|
+
case 'export':
|
|
762
|
+
this.handleExportKey(event);
|
|
763
|
+
return;
|
|
764
|
+
case 'logout':
|
|
765
|
+
this.handleLogoutKey(event);
|
|
766
|
+
return;
|
|
767
|
+
case 'login':
|
|
768
|
+
this.handleLoginKey(event);
|
|
769
|
+
return;
|
|
770
|
+
case 'menu':
|
|
771
|
+
this.handleMenuKey(event);
|
|
772
|
+
return;
|
|
773
|
+
case 'hunkPicker':
|
|
774
|
+
this.handleHunkPickerKey(event);
|
|
775
|
+
return;
|
|
777
776
|
}
|
|
778
|
-
// If intro is playing, skip on any key
|
|
777
|
+
// If intro is playing, skip on any key.
|
|
779
778
|
if (this.showIntro) {
|
|
780
779
|
this.skipIntro();
|
|
781
780
|
return;
|
|
782
781
|
}
|
|
783
|
-
// If menu is open, handle menu keys first
|
|
784
|
-
if (this.menuOpen) {
|
|
785
|
-
this.handleMenuKey(event);
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
782
|
// Escape to cancel streaming/loading/agent or close autocomplete
|
|
789
783
|
if (event.key === 'escape') {
|
|
784
|
+
if (this.showMentionAutocomplete) {
|
|
785
|
+
this.showMentionAutocomplete = false;
|
|
786
|
+
this.scheduleRender();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
790
789
|
if (this.showAutocomplete) {
|
|
791
790
|
this.showAutocomplete = false;
|
|
792
791
|
this.scheduleRender();
|
|
@@ -808,7 +807,7 @@ export class App {
|
|
|
808
807
|
}
|
|
809
808
|
return;
|
|
810
809
|
}
|
|
811
|
-
// Handle autocomplete navigation
|
|
810
|
+
// Handle autocomplete navigation (`/command` picker)
|
|
812
811
|
if (this.showAutocomplete) {
|
|
813
812
|
if (event.key === 'up') {
|
|
814
813
|
this.autocompleteIndex = Math.max(0, this.autocompleteIndex - 1);
|
|
@@ -831,6 +830,28 @@ export class App {
|
|
|
831
830
|
}
|
|
832
831
|
}
|
|
833
832
|
}
|
|
833
|
+
// Handle `@mention` autocomplete navigation
|
|
834
|
+
if (this.showMentionAutocomplete) {
|
|
835
|
+
if (event.key === 'up') {
|
|
836
|
+
this.mentionIndex = Math.max(0, this.mentionIndex - 1);
|
|
837
|
+
this.scheduleRender();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (event.key === 'down') {
|
|
841
|
+
this.mentionIndex = Math.min(this.mentionItems.length - 1, this.mentionIndex + 1);
|
|
842
|
+
this.scheduleRender();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (event.key === 'tab') {
|
|
846
|
+
// Replace `@query` with `@selectedPath` in the editor.
|
|
847
|
+
if (this.mentionItems.length > 0) {
|
|
848
|
+
this.applyMentionSelection();
|
|
849
|
+
this.showMentionAutocomplete = false;
|
|
850
|
+
this.scheduleRender();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
834
855
|
// Ctrl+L to clear
|
|
835
856
|
if (event.ctrl && event.key === 'l') {
|
|
836
857
|
this.clearMessages();
|
|
@@ -968,17 +989,60 @@ export class App {
|
|
|
968
989
|
*/
|
|
969
990
|
updateAutocomplete() {
|
|
970
991
|
const value = this.editor.getValue();
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
this.autocompleteIndex = 0;
|
|
977
|
-
}
|
|
978
|
-
else {
|
|
992
|
+
const cursorPos = this.editor.getCursorPos();
|
|
993
|
+
// `/command` picker — only when the input starts with `/` and the
|
|
994
|
+
// cursor is in the command-name segment (no space yet).
|
|
995
|
+
const result = filterCommands(value, App.COMMANDS);
|
|
996
|
+
if (result === null) {
|
|
979
997
|
this.showAutocomplete = false;
|
|
980
998
|
this.autocompleteItems = [];
|
|
981
999
|
}
|
|
1000
|
+
else {
|
|
1001
|
+
this.autocompleteItems = result.items;
|
|
1002
|
+
this.showAutocomplete = result.items.length > 0;
|
|
1003
|
+
this.autocompleteIndex = result.index;
|
|
1004
|
+
}
|
|
1005
|
+
// `@mention` picker — detect an in-progress mention at the cursor.
|
|
1006
|
+
// Independent of the `/` picker so the two never compete.
|
|
1007
|
+
const mention = detectMentionQuery(value, cursorPos);
|
|
1008
|
+
if (mention) {
|
|
1009
|
+
const root = this.options.getProjectRoot?.() ?? process.cwd();
|
|
1010
|
+
this.mentionRoot = root;
|
|
1011
|
+
this.mentionAtStart = mention.atStart;
|
|
1012
|
+
this.mentionItems = suggestMentions({ root, query: mention.query, limit: 10 });
|
|
1013
|
+
this.showMentionAutocomplete = this.mentionItems.length > 0;
|
|
1014
|
+
this.mentionIndex = 0;
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
this.showMentionAutocomplete = false;
|
|
1018
|
+
this.mentionItems = [];
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Replace the in-progress `@query` (from `mentionAtStart` to the
|
|
1023
|
+
* cursor) with the selected mention's path. Keeps the `@` prefix and
|
|
1024
|
+
* positions the cursor right after the inserted path so the user can
|
|
1025
|
+
* keep typing the rest of the message.
|
|
1026
|
+
*/
|
|
1027
|
+
applyMentionSelection() {
|
|
1028
|
+
if (this.mentionItems.length === 0)
|
|
1029
|
+
return;
|
|
1030
|
+
const selected = this.mentionItems[this.mentionIndex];
|
|
1031
|
+
const value = this.editor.getValue();
|
|
1032
|
+
const cursor = this.editor.getCursorPos();
|
|
1033
|
+
if (this.mentionAtStart >= value.length)
|
|
1034
|
+
return;
|
|
1035
|
+
// `mentionAtStart` is the index OF the `@`, so this slice EXCLUDES it —
|
|
1036
|
+
// re-add the sigil or the completed path is no longer a mention and the
|
|
1037
|
+
// file never gets attached.
|
|
1038
|
+
const before = value.slice(0, this.mentionAtStart) + '@';
|
|
1039
|
+
const after = value.slice(cursor);
|
|
1040
|
+
const next = before + selected.insertPath + ' ' + after;
|
|
1041
|
+
this.editor.setValue(next);
|
|
1042
|
+
const newCursor = (before + selected.insertPath + ' ').length;
|
|
1043
|
+
this.editor.setCursorPos(newCursor);
|
|
1044
|
+
// The picker may still have matches for the new prefix — refresh.
|
|
1045
|
+
this.updateAutocomplete();
|
|
982
1046
|
}
|
|
983
1047
|
/**
|
|
984
1048
|
* Handle inline status keys
|
|
@@ -1202,6 +1266,85 @@ export class App {
|
|
|
1202
1266
|
render: () => this.scheduleRender(),
|
|
1203
1267
|
});
|
|
1204
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Handle keys in the interactive hunk picker.
|
|
1271
|
+
* y / Enter / → accept this hunk, advance
|
|
1272
|
+
* n / ← skip this hunk, advance
|
|
1273
|
+
* a accept this + all remaining, finish
|
|
1274
|
+
* q / Esc finish without accepting this hunk
|
|
1275
|
+
* ↑ / ↓ navigate (preview only — no decision)
|
|
1276
|
+
*/
|
|
1277
|
+
handleHunkPickerKey(event) {
|
|
1278
|
+
const opts = this.hunkPickerOptions;
|
|
1279
|
+
if (!opts) {
|
|
1280
|
+
this.hunkPickerOpen = false;
|
|
1281
|
+
this.scheduleRender();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const finish = () => {
|
|
1285
|
+
const accepted = this.hunkPickerAccepted;
|
|
1286
|
+
const cb = opts.onComplete;
|
|
1287
|
+
this.hunkPickerOptions = null;
|
|
1288
|
+
this.hunkPickerOpen = false;
|
|
1289
|
+
this.hunkPickerAccepted = [];
|
|
1290
|
+
this.hunkPickerIndex = 0;
|
|
1291
|
+
cb(accepted);
|
|
1292
|
+
this.scheduleRender();
|
|
1293
|
+
};
|
|
1294
|
+
const advance = () => {
|
|
1295
|
+
if (this.hunkPickerIndex >= opts.items.length - 1) {
|
|
1296
|
+
finish();
|
|
1297
|
+
}
|
|
1298
|
+
else {
|
|
1299
|
+
this.hunkPickerIndex++;
|
|
1300
|
+
this.scheduleRender();
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
const acceptCurrent = () => {
|
|
1304
|
+
const item = opts.items[this.hunkPickerIndex];
|
|
1305
|
+
if (item) {
|
|
1306
|
+
this.hunkPickerAccepted.push({ path: item.path, hunkIndex: item.hunkIndex });
|
|
1307
|
+
}
|
|
1308
|
+
advance();
|
|
1309
|
+
};
|
|
1310
|
+
switch (event.key) {
|
|
1311
|
+
case 'y':
|
|
1312
|
+
case 'enter':
|
|
1313
|
+
case 'right':
|
|
1314
|
+
acceptCurrent();
|
|
1315
|
+
return;
|
|
1316
|
+
case 'n':
|
|
1317
|
+
case 'left':
|
|
1318
|
+
advance();
|
|
1319
|
+
return;
|
|
1320
|
+
case 'a':
|
|
1321
|
+
// Accept current + all remaining.
|
|
1322
|
+
for (let i = this.hunkPickerIndex; i < opts.items.length; i++) {
|
|
1323
|
+
const item = opts.items[i];
|
|
1324
|
+
this.hunkPickerAccepted.push({ path: item.path, hunkIndex: item.hunkIndex });
|
|
1325
|
+
}
|
|
1326
|
+
finish();
|
|
1327
|
+
return;
|
|
1328
|
+
case 'q':
|
|
1329
|
+
case 'escape':
|
|
1330
|
+
finish();
|
|
1331
|
+
return;
|
|
1332
|
+
case 'up':
|
|
1333
|
+
if (this.hunkPickerIndex > 0) {
|
|
1334
|
+
this.hunkPickerIndex--;
|
|
1335
|
+
this.scheduleRender();
|
|
1336
|
+
}
|
|
1337
|
+
return;
|
|
1338
|
+
case 'down':
|
|
1339
|
+
if (this.hunkPickerIndex < opts.items.length - 1) {
|
|
1340
|
+
this.hunkPickerIndex++;
|
|
1341
|
+
this.scheduleRender();
|
|
1342
|
+
}
|
|
1343
|
+
return;
|
|
1344
|
+
default:
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1205
1348
|
/**
|
|
1206
1349
|
* Submit the current input buffer (used by Enter and Escape-in-multiline)
|
|
1207
1350
|
*/
|
|
@@ -1228,9 +1371,10 @@ export class App {
|
|
|
1228
1371
|
* Handle command
|
|
1229
1372
|
*/
|
|
1230
1373
|
handleCommand(input) {
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1374
|
+
const parsed = parseCommandInput(input);
|
|
1375
|
+
if (!parsed)
|
|
1376
|
+
return;
|
|
1377
|
+
const { command, args } = parsed;
|
|
1234
1378
|
switch (command) {
|
|
1235
1379
|
case 'help':
|
|
1236
1380
|
this.helpOpen = true;
|
|
@@ -1297,59 +1441,43 @@ export class App {
|
|
|
1297
1441
|
}
|
|
1298
1442
|
this.screen.clear();
|
|
1299
1443
|
// 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
|
-
|
|
1335
|
-
|
|
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);
|
|
1444
|
+
const panelHeight = bottomPanelHeight({
|
|
1445
|
+
height,
|
|
1446
|
+
pasteInfoOpen: this.pasteInfoOpen,
|
|
1447
|
+
pasteInfoPreviewLines: this.pasteInfo ? this.pasteInfo.preview.split('\n').length : 0,
|
|
1448
|
+
isAgentRunning: this.isAgentRunning,
|
|
1449
|
+
confirmOpen: this.confirmOpen && !!this.confirmOptions,
|
|
1450
|
+
hunkPickerOpen: this.hunkPickerOpen && !!this.hunkPickerOptions,
|
|
1451
|
+
permissionOpen: this.permissionOpen,
|
|
1452
|
+
sessionPickerOpen: this.sessionPickerOpen,
|
|
1453
|
+
sessionPickerItemCount: this.sessionPickerItems.length,
|
|
1454
|
+
confirmMessageCount: this.confirmOptions?.message.length ?? 0,
|
|
1455
|
+
statusOpen: this.statusOpen,
|
|
1456
|
+
helpOpen: this.helpOpen,
|
|
1457
|
+
searchOpen: this.searchOpen,
|
|
1458
|
+
searchResultCount: this.searchResults.length,
|
|
1459
|
+
exportOpen: this.exportOpen,
|
|
1460
|
+
logoutOpen: this.logoutOpen,
|
|
1461
|
+
logoutProviderCount: this.logoutProviders.length,
|
|
1462
|
+
loginOpen: this.loginOpen,
|
|
1463
|
+
loginStep: this.loginStep,
|
|
1464
|
+
loginProviderCount: this.loginProviders.length,
|
|
1465
|
+
menuOpen: this.menuOpen,
|
|
1466
|
+
menuItemCount: this.menuItems.length,
|
|
1467
|
+
settingsOpen: this.settingsOpen,
|
|
1468
|
+
settingsCount: SETTINGS.length,
|
|
1469
|
+
showAutocomplete: this.showAutocomplete,
|
|
1470
|
+
autocompleteItemCount: this.autocompleteItems.length,
|
|
1471
|
+
mentionPickerOpen: this.showMentionAutocomplete,
|
|
1472
|
+
mentionItemCount: this.mentionItems.length,
|
|
1473
|
+
});
|
|
1474
|
+
const layout = chatLayout(height, panelHeight);
|
|
1475
|
+
const mainHeight = layout.mainHeight;
|
|
1476
|
+
const messagesStart = layout.messagesStart;
|
|
1477
|
+
const messagesEnd = layout.messagesEnd;
|
|
1478
|
+
const separatorLine = layout.separatorLine;
|
|
1479
|
+
const inputLine = layout.inputLine;
|
|
1480
|
+
const statusLine = layout.statusLine;
|
|
1353
1481
|
// Messages
|
|
1354
1482
|
const messagesHeight = Math.max(1, messagesEnd - messagesStart + 1);
|
|
1355
1483
|
const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
|
|
@@ -1408,10 +1536,17 @@ export class App {
|
|
|
1408
1536
|
if (this.confirmOpen && this.confirmOptions) {
|
|
1409
1537
|
this.renderInlineConfirm(statusLine + 1, width);
|
|
1410
1538
|
}
|
|
1539
|
+
// Inline hunk picker renders BELOW status bar
|
|
1540
|
+
if (this.hunkPickerOpen && this.hunkPickerOptions) {
|
|
1541
|
+
this.renderInlineHunkPicker(statusLine + 1, width);
|
|
1542
|
+
}
|
|
1411
1543
|
// Inline autocomplete renders BELOW status bar
|
|
1412
1544
|
if (this.showAutocomplete && this.autocompleteItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
|
|
1413
1545
|
this.renderInlineAutocomplete(statusLine + 1, width);
|
|
1414
1546
|
}
|
|
1547
|
+
else if (this.showMentionAutocomplete && this.mentionItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
|
|
1548
|
+
this.renderInlineMentionPicker(statusLine + 1, width);
|
|
1549
|
+
}
|
|
1415
1550
|
// Inline permission renders BELOW status bar
|
|
1416
1551
|
if (this.permissionOpen) {
|
|
1417
1552
|
this.renderInlinePermission(statusLine + 1, width);
|
|
@@ -1467,6 +1602,53 @@ export class App {
|
|
|
1467
1602
|
// Footer
|
|
1468
1603
|
this.screen.writeLine(y, '←/→ select • y/n quick • Enter confirm • Esc cancel', fg.gray);
|
|
1469
1604
|
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Render inline hunk picker (`/apply --interactive`).
|
|
1607
|
+
* Shows the current hunk's diff + the y/n/a/q key legend.
|
|
1608
|
+
*/
|
|
1609
|
+
renderInlineHunkPicker(startY, width) {
|
|
1610
|
+
const opts = this.hunkPickerOptions;
|
|
1611
|
+
if (!opts)
|
|
1612
|
+
return;
|
|
1613
|
+
const item = opts.items[this.hunkPickerIndex];
|
|
1614
|
+
let y = startY;
|
|
1615
|
+
this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
|
|
1616
|
+
// Title + progress
|
|
1617
|
+
const progress = opts.items.length > 0
|
|
1618
|
+
? ` (${this.hunkPickerIndex + 1}/${opts.items.length})`
|
|
1619
|
+
: '';
|
|
1620
|
+
this.screen.writeLine(y++, `${opts.title}${progress}`, PRIMARY_COLOR + style.bold);
|
|
1621
|
+
if (!item) {
|
|
1622
|
+
this.screen.writeLine(y++, 'No hunks to review.', fg.gray);
|
|
1623
|
+
this.screen.writeLine(y, 'Press any key to close.', fg.gray);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
// File path + hunk header
|
|
1627
|
+
this.screen.writeLine(y++, `File: ${item.path}`, fg.cyan);
|
|
1628
|
+
this.screen.writeLine(y++, `Hunk: ${item.header}`, fg.gray);
|
|
1629
|
+
// Diff lines (capped to available vertical space; show up to 12)
|
|
1630
|
+
const maxDiffLines = 12;
|
|
1631
|
+
const lines = item.lines.slice(0, maxDiffLines);
|
|
1632
|
+
for (const line of lines) {
|
|
1633
|
+
const prefix = line.charAt(0);
|
|
1634
|
+
let color = fg.white;
|
|
1635
|
+
if (prefix === '+')
|
|
1636
|
+
color = fg.green;
|
|
1637
|
+
else if (prefix === '-')
|
|
1638
|
+
color = fg.red;
|
|
1639
|
+
else if (prefix === '@')
|
|
1640
|
+
color = fg.cyan;
|
|
1641
|
+
// Truncate long lines to terminal width.
|
|
1642
|
+
const truncated = line.length > width - 2 ? line.slice(0, width - 5) + '...' : line;
|
|
1643
|
+
this.screen.writeLine(y++, ` ${truncated}`, color);
|
|
1644
|
+
}
|
|
1645
|
+
if (item.lines.length > maxDiffLines) {
|
|
1646
|
+
this.screen.writeLine(y++, ` … (${item.lines.length - maxDiffLines} more lines)`, fg.gray);
|
|
1647
|
+
}
|
|
1648
|
+
y++;
|
|
1649
|
+
// Key legend
|
|
1650
|
+
this.screen.writeLine(y, 'y/Enter accept • n skip • a accept all • q/Esc quit • ↑/↓ navigate', fg.gray);
|
|
1651
|
+
}
|
|
1470
1652
|
/**
|
|
1471
1653
|
* Render input line
|
|
1472
1654
|
*/
|
|
@@ -1584,20 +1766,18 @@ export class App {
|
|
|
1584
1766
|
return;
|
|
1585
1767
|
}
|
|
1586
1768
|
// Build prompt prefix
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1769
|
+
const display = computeInputDisplay({
|
|
1770
|
+
value: inputValue,
|
|
1771
|
+
cursorPos,
|
|
1772
|
+
width,
|
|
1773
|
+
isMultilineMode: this.isMultilineMode,
|
|
1774
|
+
});
|
|
1592
1775
|
// 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);
|
|
1776
|
+
if (display.isEmpty) {
|
|
1777
|
+
this.screen.write(0, y, display.promptSymbol, PRIMARY_COLOR);
|
|
1778
|
+
this.screen.write(display.promptSymbol.length, y, display.placeholder, fg.gray);
|
|
1599
1779
|
if (!hideCursor) {
|
|
1600
|
-
this.screen.setCursor(promptSymbol.length, y);
|
|
1780
|
+
this.screen.setCursor(display.promptSymbol.length, y);
|
|
1601
1781
|
this.screen.showCursor(true);
|
|
1602
1782
|
}
|
|
1603
1783
|
else {
|
|
@@ -1605,38 +1785,15 @@ export class App {
|
|
|
1605
1785
|
}
|
|
1606
1786
|
return;
|
|
1607
1787
|
}
|
|
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
1788
|
// 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);
|
|
1789
|
+
this.screen.write(0, y, display.promptSymbol, PRIMARY_COLOR);
|
|
1790
|
+
this.screen.write(display.promptSymbol.length, y, display.displayValue, fg.white);
|
|
1634
1791
|
// Hide cursor when menu/settings is open
|
|
1635
1792
|
if (hideCursor) {
|
|
1636
1793
|
this.screen.showCursor(false);
|
|
1637
1794
|
}
|
|
1638
1795
|
else {
|
|
1639
|
-
this.screen.setCursor(Math.min(cursorX, width - 1), y);
|
|
1796
|
+
this.screen.setCursor(Math.min(display.cursorX, width - 1), y);
|
|
1640
1797
|
this.screen.showCursor(true);
|
|
1641
1798
|
}
|
|
1642
1799
|
}
|
|
@@ -1854,6 +2011,46 @@ export class App {
|
|
|
1854
2011
|
const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
|
|
1855
2012
|
this.screen.writeLine(y, `↑↓ navigate • Tab/Enter select • Esc cancel${scrollInfo}`, fg.gray);
|
|
1856
2013
|
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Render inline `@mention` file picker below the status bar.
|
|
2016
|
+
*
|
|
2017
|
+
* Mirrors the layout of `renderInlineAutocomplete` (separator → title →
|
|
2018
|
+
* items → footer) but shows file paths with their parent directory as
|
|
2019
|
+
* the description, and a `@` prefix instead of `/`.
|
|
2020
|
+
*/
|
|
2021
|
+
renderInlineMentionPicker(startY, width) {
|
|
2022
|
+
const items = this.mentionItems;
|
|
2023
|
+
const maxVisible = Math.min(items.length, 8);
|
|
2024
|
+
let y = startY;
|
|
2025
|
+
// Separator line
|
|
2026
|
+
this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
|
|
2027
|
+
// Title
|
|
2028
|
+
this.screen.writeLine(y++, 'Add file to context (@mention)', PRIMARY_COLOR + style.bold);
|
|
2029
|
+
// Items: `path` + directory detail
|
|
2030
|
+
const visibleStart = Math.max(0, this.mentionIndex - maxVisible + 1);
|
|
2031
|
+
const visibleItems = items.slice(visibleStart, visibleStart + maxVisible);
|
|
2032
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
2033
|
+
const item = visibleItems[i];
|
|
2034
|
+
const actualIndex = visibleStart + i;
|
|
2035
|
+
const isSelected = actualIndex === this.mentionIndex;
|
|
2036
|
+
const prefix = isSelected ? '► ' : ' ';
|
|
2037
|
+
const pathText = ('@' + item.label).padEnd(40);
|
|
2038
|
+
if (isSelected) {
|
|
2039
|
+
this.screen.write(0, y, prefix, PRIMARY_COLOR);
|
|
2040
|
+
this.screen.write(prefix.length, y, pathText, PRIMARY_COLOR + style.bold);
|
|
2041
|
+
this.screen.write(prefix.length + pathText.length, y, item.detail, fg.white);
|
|
2042
|
+
}
|
|
2043
|
+
else {
|
|
2044
|
+
this.screen.write(0, y, prefix, '');
|
|
2045
|
+
this.screen.write(prefix.length, y, pathText, fg.cyan);
|
|
2046
|
+
this.screen.write(prefix.length + pathText.length, y, item.detail, fg.gray);
|
|
2047
|
+
}
|
|
2048
|
+
y++;
|
|
2049
|
+
}
|
|
2050
|
+
// Footer
|
|
2051
|
+
const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
|
|
2052
|
+
this.screen.writeLine(y, `↑↓ navigate • Tab select • Esc cancel${scrollInfo}`, fg.gray);
|
|
2053
|
+
}
|
|
1857
2054
|
/**
|
|
1858
2055
|
* Render inline permission dialog
|
|
1859
2056
|
*/
|
|
@@ -2091,21 +2288,7 @@ export class App {
|
|
|
2091
2288
|
// 8-bit gradient progress bar (right side, if max iterations known)
|
|
2092
2289
|
if (this.agentMaxIterations > 0) {
|
|
2093
2290
|
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
|
-
}
|
|
2291
|
+
const bar = agentProgressBar(this.agentIteration, this.agentMaxIterations, barWidth);
|
|
2109
2292
|
const barColored = PRIMARY_COLOR + bar + style.reset;
|
|
2110
2293
|
const stepText = `${this.agentIteration}/${this.agentMaxIterations}`;
|
|
2111
2294
|
const barX = width - barWidth - stepText.length - 3;
|
|
@@ -2128,51 +2311,6 @@ export class App {
|
|
|
2128
2311
|
/**
|
|
2129
2312
|
* Get color for action type
|
|
2130
2313
|
*/
|
|
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
2314
|
/**
|
|
2177
2315
|
* Render status bar
|
|
2178
2316
|
*/
|
|
@@ -2182,7 +2320,7 @@ export class App {
|
|
|
2182
2320
|
if (this.notification) {
|
|
2183
2321
|
const notifColor = this.notificationIsWarn ? '\x1b[38;5;208m' : PRIMARY_COLOR; // orange for warn
|
|
2184
2322
|
const maxLen = width - 2;
|
|
2185
|
-
const msg = this.notification
|
|
2323
|
+
const msg = truncateNotification(this.notification, maxLen);
|
|
2186
2324
|
this.screen.write(0, y, notifColor + ' ' + msg + style.reset);
|
|
2187
2325
|
return;
|
|
2188
2326
|
}
|
|
@@ -2192,7 +2330,7 @@ export class App {
|
|
|
2192
2330
|
const modelName = status.model || '';
|
|
2193
2331
|
const msgCount = `${this.messages.length} msg`;
|
|
2194
2332
|
const tokenStr = stats && stats.totalTokens > 0
|
|
2195
|
-
? `${
|
|
2333
|
+
? `${formatTokenCount(stats.totalTokens)} tok`
|
|
2196
2334
|
: '';
|
|
2197
2335
|
let leftX = 1;
|
|
2198
2336
|
if (modelName) {
|
|
@@ -2218,18 +2356,16 @@ export class App {
|
|
|
2218
2356
|
// Right: context-sensitive hints. While scrolled up, the "new
|
|
2219
2357
|
// messages below" badge takes priority — it's the only signal that
|
|
2220
2358
|
// the conversation moved on (addMessage no longer yanks the view).
|
|
2359
|
+
const rightText = statusBarRightHint({
|
|
2360
|
+
scrollOffset: this.scrollOffset,
|
|
2361
|
+
unseenWhileScrolled: this.unseenWhileScrolled,
|
|
2362
|
+
isStreaming: this.isStreaming,
|
|
2363
|
+
isLoading: this.isLoading,
|
|
2364
|
+
});
|
|
2221
2365
|
if (this.scrollOffset > 0 && this.unseenWhileScrolled > 0) {
|
|
2222
|
-
|
|
2223
|
-
this.screen.write(width - badge.length, y, badge, PRIMARY_COLOR);
|
|
2366
|
+
this.screen.write(width - rightText.length, y, rightText, PRIMARY_COLOR);
|
|
2224
2367
|
return;
|
|
2225
2368
|
}
|
|
2226
|
-
let rightText;
|
|
2227
|
-
if (this.isStreaming || this.isLoading) {
|
|
2228
|
-
rightText = 'Esc to stop ';
|
|
2229
|
-
}
|
|
2230
|
-
else {
|
|
2231
|
-
rightText = '/help · ↑↓ history ';
|
|
2232
|
-
}
|
|
2233
2369
|
this.screen.write(width - rightText.length, y, rightText, fg.gray);
|
|
2234
2370
|
}
|
|
2235
2371
|
/**
|
|
@@ -2237,7 +2373,7 @@ export class App {
|
|
|
2237
2373
|
*/
|
|
2238
2374
|
getVisibleMessages(height, width) {
|
|
2239
2375
|
const allLines = [];
|
|
2240
|
-
this.codeBlockCounter = 0; // Reset block counter for each render pass
|
|
2376
|
+
this.codeBlockCounter.current = 0; // Reset block counter for each render pass
|
|
2241
2377
|
// Logo at the top, scrolls with content
|
|
2242
2378
|
if (height >= 20) {
|
|
2243
2379
|
const logoWidth = LOGO_LINES[0].length;
|
|
@@ -2255,365 +2391,36 @@ export class App {
|
|
|
2255
2391
|
for (let i = 0; i < this.messages.length; i++) {
|
|
2256
2392
|
const msg = this.messages[i];
|
|
2257
2393
|
const cached = this.messageCache[i];
|
|
2258
|
-
if (cached && cached.width === width && cached.startBlock === this.codeBlockCounter) {
|
|
2394
|
+
if (cached && cached.width === width && cached.startBlock === this.codeBlockCounter.current) {
|
|
2259
2395
|
// Cache hit — preskoči formatiranje
|
|
2260
|
-
this.codeBlockCounter += cached.blockCount;
|
|
2396
|
+
this.codeBlockCounter.current += cached.blockCount;
|
|
2261
2397
|
allLines.push(...cached.lines);
|
|
2262
2398
|
}
|
|
2263
2399
|
else {
|
|
2264
2400
|
// Cache miss — formatiraj i spremi
|
|
2265
|
-
const startBlock = this.codeBlockCounter;
|
|
2401
|
+
const startBlock = this.codeBlockCounter.current;
|
|
2266
2402
|
const msgLines = msg.role === 'welcome'
|
|
2267
|
-
?
|
|
2268
|
-
:
|
|
2269
|
-
const blockCount = this.codeBlockCounter - startBlock;
|
|
2403
|
+
? formatWelcomeMessage(msg.content)
|
|
2404
|
+
: formatMessageFn(msg.role, msg.content, width, this.codeBlockCounter);
|
|
2405
|
+
const blockCount = this.codeBlockCounter.current - startBlock;
|
|
2270
2406
|
this.messageCache[i] = { lines: msgLines, width, startBlock, blockCount };
|
|
2271
2407
|
allLines.push(...msgLines);
|
|
2272
2408
|
}
|
|
2273
2409
|
}
|
|
2274
2410
|
if (this.isStreaming && this.streamingContent) {
|
|
2275
|
-
const streamLines =
|
|
2411
|
+
const streamLines = formatMessageFn('assistant', this.streamingContent + '▊', width, this.codeBlockCounter);
|
|
2276
2412
|
allLines.push(...streamLines);
|
|
2277
2413
|
}
|
|
2278
2414
|
// Calculate visible window based on scroll offset
|
|
2279
2415
|
const totalLines = allLines.length;
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2416
|
+
const { startIndex, endIndex, clampedScrollOffset } = scrollWindow({
|
|
2417
|
+
totalLines,
|
|
2418
|
+
height,
|
|
2419
|
+
scrollOffset: this.scrollOffset,
|
|
2420
|
+
});
|
|
2421
|
+
this.scrollOffset = clampedScrollOffset;
|
|
2286
2422
|
return allLines.slice(startIndex, endIndex);
|
|
2287
2423
|
}
|
|
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
2424
|
/**
|
|
2618
2425
|
* Render inline search screen
|
|
2619
2426
|
*/
|
|
@@ -2772,41 +2579,4 @@ export class App {
|
|
|
2772
2579
|
return resultLine;
|
|
2773
2580
|
}).join('\n');
|
|
2774
2581
|
}
|
|
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
2582
|
}
|