codeep 2.15.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 +12 -3
- package/dist/acp/session.js +22 -1
- package/dist/config/providers.js +20 -14
- package/dist/renderer/App.d.ts +77 -0
- package/dist/renderer/App.js +283 -3
- package/dist/renderer/commands/helpers.d.ts +188 -0
- package/dist/renderer/commands/helpers.js +342 -0
- package/dist/renderer/commands/registry.js +2 -1
- package/dist/renderer/commands.js +193 -264
- package/dist/renderer/components/Autocomplete.d.ts +25 -0
- package/dist/renderer/components/Autocomplete.js +35 -0
- package/dist/renderer/layout.d.ts +5 -1
- package/dist/renderer/layout.js +12 -0
- package/dist/renderer/main.js +34 -1
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/tokenTracker.js +9 -3
- 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 +1 -1
package/dist/renderer/App.js
CHANGED
|
@@ -9,7 +9,8 @@ import { PRIMARY_COLOR, SPINNER_FRAMES, LOGO_LINES } from './components/uiConsta
|
|
|
9
9
|
import { bottomPanelHeight, chatLayout, messageOffsets, scrollOffsetForTarget, scrollWindow, formatTokenCount, statusBarRightHint, activePanel, computeInputDisplay, agentProgressBar, truncateNotification, shouldShowPasteDialog, buildPasteInfo } from './layout.js';
|
|
10
10
|
import { parseCommandInput } from './inputParsing.js';
|
|
11
11
|
import { formatWelcomeMessage } from './components/WelcomeFormatter.js';
|
|
12
|
-
import { filterCommands } from './components/Autocomplete.js';
|
|
12
|
+
import { filterCommands, detectMentionQuery } from './components/Autocomplete.js';
|
|
13
|
+
import { suggestMentions } from '../utils/mentions.js';
|
|
13
14
|
import { handleInlineStatusKey, handleInlineHelpKey, handleMenuKey, handleInlinePermissionKey, handleInlineSessionPickerKey, handleInlineConfirmKey, handleLoginKey, } from './handlers.js';
|
|
14
15
|
import clipboardy from 'clipboardy';
|
|
15
16
|
import { readImageFromClipboard } from '../utils/clipboard.js';
|
|
@@ -77,10 +78,26 @@ export class App {
|
|
|
77
78
|
showAutocomplete = false;
|
|
78
79
|
autocompleteIndex = 0;
|
|
79
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 = '';
|
|
80
92
|
// Inline confirmation dialog state
|
|
81
93
|
confirmOpen = false;
|
|
82
94
|
confirmOptions = null;
|
|
83
95
|
confirmSelection = 'no';
|
|
96
|
+
// Inline hunk-picker state (`/apply --interactive`)
|
|
97
|
+
hunkPickerOpen = false;
|
|
98
|
+
hunkPickerOptions = null;
|
|
99
|
+
hunkPickerIndex = 0;
|
|
100
|
+
hunkPickerAccepted = [];
|
|
84
101
|
// Inline menu state (renders below input/status)
|
|
85
102
|
menuOpen = false;
|
|
86
103
|
menuTitle = '';
|
|
@@ -507,6 +524,17 @@ export class App {
|
|
|
507
524
|
this.confirmOpen = true;
|
|
508
525
|
this.scheduleRender();
|
|
509
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
|
+
}
|
|
510
538
|
/**
|
|
511
539
|
* Show permission dialog (inline, below status bar)
|
|
512
540
|
*/
|
|
@@ -704,6 +732,7 @@ export class App {
|
|
|
704
732
|
loginOpen: this.loginOpen,
|
|
705
733
|
menuOpen: this.menuOpen,
|
|
706
734
|
showAutocomplete: this.showAutocomplete,
|
|
735
|
+
hunkPickerOpen: this.hunkPickerOpen,
|
|
707
736
|
})) {
|
|
708
737
|
case 'pasteInfo':
|
|
709
738
|
this.handlePasteInfoKey(event);
|
|
@@ -741,6 +770,9 @@ export class App {
|
|
|
741
770
|
case 'menu':
|
|
742
771
|
this.handleMenuKey(event);
|
|
743
772
|
return;
|
|
773
|
+
case 'hunkPicker':
|
|
774
|
+
this.handleHunkPickerKey(event);
|
|
775
|
+
return;
|
|
744
776
|
}
|
|
745
777
|
// If intro is playing, skip on any key.
|
|
746
778
|
if (this.showIntro) {
|
|
@@ -749,6 +781,11 @@ export class App {
|
|
|
749
781
|
}
|
|
750
782
|
// Escape to cancel streaming/loading/agent or close autocomplete
|
|
751
783
|
if (event.key === 'escape') {
|
|
784
|
+
if (this.showMentionAutocomplete) {
|
|
785
|
+
this.showMentionAutocomplete = false;
|
|
786
|
+
this.scheduleRender();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
752
789
|
if (this.showAutocomplete) {
|
|
753
790
|
this.showAutocomplete = false;
|
|
754
791
|
this.scheduleRender();
|
|
@@ -770,7 +807,7 @@ export class App {
|
|
|
770
807
|
}
|
|
771
808
|
return;
|
|
772
809
|
}
|
|
773
|
-
// Handle autocomplete navigation
|
|
810
|
+
// Handle autocomplete navigation (`/command` picker)
|
|
774
811
|
if (this.showAutocomplete) {
|
|
775
812
|
if (event.key === 'up') {
|
|
776
813
|
this.autocompleteIndex = Math.max(0, this.autocompleteIndex - 1);
|
|
@@ -793,6 +830,28 @@ export class App {
|
|
|
793
830
|
}
|
|
794
831
|
}
|
|
795
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
|
+
}
|
|
796
855
|
// Ctrl+L to clear
|
|
797
856
|
if (event.ctrl && event.key === 'l') {
|
|
798
857
|
this.clearMessages();
|
|
@@ -929,7 +988,11 @@ export class App {
|
|
|
929
988
|
* Update autocomplete suggestions
|
|
930
989
|
*/
|
|
931
990
|
updateAutocomplete() {
|
|
932
|
-
const
|
|
991
|
+
const value = this.editor.getValue();
|
|
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);
|
|
933
996
|
if (result === null) {
|
|
934
997
|
this.showAutocomplete = false;
|
|
935
998
|
this.autocompleteItems = [];
|
|
@@ -939,6 +1002,47 @@ export class App {
|
|
|
939
1002
|
this.showAutocomplete = result.items.length > 0;
|
|
940
1003
|
this.autocompleteIndex = result.index;
|
|
941
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();
|
|
942
1046
|
}
|
|
943
1047
|
/**
|
|
944
1048
|
* Handle inline status keys
|
|
@@ -1162,6 +1266,85 @@ export class App {
|
|
|
1162
1266
|
render: () => this.scheduleRender(),
|
|
1163
1267
|
});
|
|
1164
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
|
+
}
|
|
1165
1348
|
/**
|
|
1166
1349
|
* Submit the current input buffer (used by Enter and Escape-in-multiline)
|
|
1167
1350
|
*/
|
|
@@ -1264,6 +1447,7 @@ export class App {
|
|
|
1264
1447
|
pasteInfoPreviewLines: this.pasteInfo ? this.pasteInfo.preview.split('\n').length : 0,
|
|
1265
1448
|
isAgentRunning: this.isAgentRunning,
|
|
1266
1449
|
confirmOpen: this.confirmOpen && !!this.confirmOptions,
|
|
1450
|
+
hunkPickerOpen: this.hunkPickerOpen && !!this.hunkPickerOptions,
|
|
1267
1451
|
permissionOpen: this.permissionOpen,
|
|
1268
1452
|
sessionPickerOpen: this.sessionPickerOpen,
|
|
1269
1453
|
sessionPickerItemCount: this.sessionPickerItems.length,
|
|
@@ -1284,6 +1468,8 @@ export class App {
|
|
|
1284
1468
|
settingsCount: SETTINGS.length,
|
|
1285
1469
|
showAutocomplete: this.showAutocomplete,
|
|
1286
1470
|
autocompleteItemCount: this.autocompleteItems.length,
|
|
1471
|
+
mentionPickerOpen: this.showMentionAutocomplete,
|
|
1472
|
+
mentionItemCount: this.mentionItems.length,
|
|
1287
1473
|
});
|
|
1288
1474
|
const layout = chatLayout(height, panelHeight);
|
|
1289
1475
|
const mainHeight = layout.mainHeight;
|
|
@@ -1350,10 +1536,17 @@ export class App {
|
|
|
1350
1536
|
if (this.confirmOpen && this.confirmOptions) {
|
|
1351
1537
|
this.renderInlineConfirm(statusLine + 1, width);
|
|
1352
1538
|
}
|
|
1539
|
+
// Inline hunk picker renders BELOW status bar
|
|
1540
|
+
if (this.hunkPickerOpen && this.hunkPickerOptions) {
|
|
1541
|
+
this.renderInlineHunkPicker(statusLine + 1, width);
|
|
1542
|
+
}
|
|
1353
1543
|
// Inline autocomplete renders BELOW status bar
|
|
1354
1544
|
if (this.showAutocomplete && this.autocompleteItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
|
|
1355
1545
|
this.renderInlineAutocomplete(statusLine + 1, width);
|
|
1356
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
|
+
}
|
|
1357
1550
|
// Inline permission renders BELOW status bar
|
|
1358
1551
|
if (this.permissionOpen) {
|
|
1359
1552
|
this.renderInlinePermission(statusLine + 1, width);
|
|
@@ -1409,6 +1602,53 @@ export class App {
|
|
|
1409
1602
|
// Footer
|
|
1410
1603
|
this.screen.writeLine(y, '←/→ select • y/n quick • Enter confirm • Esc cancel', fg.gray);
|
|
1411
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
|
+
}
|
|
1412
1652
|
/**
|
|
1413
1653
|
* Render input line
|
|
1414
1654
|
*/
|
|
@@ -1771,6 +2011,46 @@ export class App {
|
|
|
1771
2011
|
const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
|
|
1772
2012
|
this.screen.writeLine(y, `↑↓ navigate • Tab/Enter select • Esc cancel${scrollInfo}`, fg.gray);
|
|
1773
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
|
+
}
|
|
1774
2054
|
/**
|
|
1775
2055
|
* Render inline permission dialog
|
|
1776
2056
|
*/
|
|
@@ -61,3 +61,191 @@ export declare function formatTaskList(tasks: Array<{
|
|
|
61
61
|
description?: string | null;
|
|
62
62
|
project_name?: string | null;
|
|
63
63
|
}>, scopeProjectName?: string): string;
|
|
64
|
+
/** Render the `/profile list` Markdown message from saved profile names. */
|
|
65
|
+
export declare function formatProfileList(profiles: string[]): string;
|
|
66
|
+
/** Render the `/memory list` Markdown message from saved notes. */
|
|
67
|
+
export declare function formatMemoryList(notes: string[]): string;
|
|
68
|
+
export interface StatsModelRow {
|
|
69
|
+
model: string;
|
|
70
|
+
provider: string;
|
|
71
|
+
promptTokens: number;
|
|
72
|
+
completionTokens: number;
|
|
73
|
+
estimatedCost: number;
|
|
74
|
+
}
|
|
75
|
+
export interface StatsTotals {
|
|
76
|
+
requestCount: number;
|
|
77
|
+
totalTokens: number;
|
|
78
|
+
totalPromptTokens: number;
|
|
79
|
+
totalCompletionTokens: number;
|
|
80
|
+
estimatedCost: number;
|
|
81
|
+
}
|
|
82
|
+
export interface StatsCache {
|
|
83
|
+
cacheReadTokens: number;
|
|
84
|
+
cacheCreationTokens: number;
|
|
85
|
+
estimatedSavingsUsd: number;
|
|
86
|
+
}
|
|
87
|
+
export interface PricingRow {
|
|
88
|
+
model: string;
|
|
89
|
+
inputPer1M: number;
|
|
90
|
+
outputPer1M: number;
|
|
91
|
+
}
|
|
92
|
+
/** A formatter for token counts (injected so this module stays pure). */
|
|
93
|
+
export type TokenFormatter = (n: number) => string;
|
|
94
|
+
/** Format a single model-row's cost string, mirroring the inline logic. */
|
|
95
|
+
export declare function formatModelCost(provider: string, estimatedCost: number): string;
|
|
96
|
+
/**
|
|
97
|
+
* Build the full `/stats` Markdown report. `currentProvider` controls
|
|
98
|
+
* whether the total shows "free" (ollama) or a dollar figure.
|
|
99
|
+
*/
|
|
100
|
+
export declare function formatStatsReport(args: {
|
|
101
|
+
totals: StatsTotals;
|
|
102
|
+
breakdown: StatsModelRow[];
|
|
103
|
+
cache: StatsCache;
|
|
104
|
+
pricing: PricingRow[];
|
|
105
|
+
currentProvider: string;
|
|
106
|
+
fmt: TokenFormatter;
|
|
107
|
+
}): string;
|
|
108
|
+
/**
|
|
109
|
+
* Extract every fenced code block body (the text inside ```…```) from a
|
|
110
|
+
* string, mirroring the `/copy` loop. Language fences (```ts) are ignored —
|
|
111
|
+
* only the body is captured.
|
|
112
|
+
*/
|
|
113
|
+
export declare function extractCodeBlocks(text: string): string[];
|
|
114
|
+
/**
|
|
115
|
+
* Validate a 1-based block index against the available block list, as used
|
|
116
|
+
* by `/copy <n>`. Returns the 0-based index, or `null` when the index is
|
|
117
|
+
* out of range (the caller shows an error).
|
|
118
|
+
*/
|
|
119
|
+
export declare function resolveBlockIndex(blockNum: number, blockCount: number): number | null;
|
|
120
|
+
export interface FileChange {
|
|
121
|
+
path: string;
|
|
122
|
+
content: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Extract file-change pairs from an assistant message, mirroring `/apply`.
|
|
126
|
+
* Two patterns are tried in order:
|
|
127
|
+
* 1. fence with a filename header: ```ts\nsrc/foo.ts\n<body>```
|
|
128
|
+
* 2. fence with a `// File:` / `# Path:` comment header.
|
|
129
|
+
* A path is only accepted when it contains a dot and no spaces.
|
|
130
|
+
*/
|
|
131
|
+
export declare function extractFileChanges(text: string): FileChange[];
|
|
132
|
+
/** Truncate a path for display: keep the last 37 chars, prefixed with “…”. */
|
|
133
|
+
export declare function shortPathForDisplay(path: string, max?: number): string;
|
|
134
|
+
/**
|
|
135
|
+
* Build a single diff-line summary for a file change, mirroring `/apply`.
|
|
136
|
+
* Returns `null` when `existingContent` is empty (a CREATE), otherwise a
|
|
137
|
+
* MODIFY line with the line-count delta.
|
|
138
|
+
*/
|
|
139
|
+
export declare function formatApplyDiffLine(change: {
|
|
140
|
+
path: string;
|
|
141
|
+
content: string;
|
|
142
|
+
}, existingContent: string): string[];
|
|
143
|
+
/**
|
|
144
|
+
* Parse `key=value` tokens (as used by `/mcp prompt <server> <name> [k=v...]`)
|
|
145
|
+
* into a record. Tokens without an `=` (or with `=` at position 0) are
|
|
146
|
+
* skipped. Mirrors the inline loop.
|
|
147
|
+
*/
|
|
148
|
+
export declare function parsePromptArgs(tokens: string[]): Record<string, string>;
|
|
149
|
+
/** Pluralise "tool"/"tools" based on the count. */
|
|
150
|
+
export declare function pluralTools(n: number): string;
|
|
151
|
+
/**
|
|
152
|
+
* Group a flat list of tools by `serverName`, preserving first-seen order.
|
|
153
|
+
* Used by `/mcp` (default), `/mcp reload`, and the install report.
|
|
154
|
+
*/
|
|
155
|
+
export declare function groupToolsByServer<T extends {
|
|
156
|
+
serverName: string;
|
|
157
|
+
}>(tools: T[]): Array<{
|
|
158
|
+
serverName: string;
|
|
159
|
+
serverTools: T[];
|
|
160
|
+
}>;
|
|
161
|
+
/** Format the `/mcp` default server/tool listing. */
|
|
162
|
+
export declare function formatMcpServerList<T extends {
|
|
163
|
+
serverName: string;
|
|
164
|
+
agentName: string;
|
|
165
|
+
description?: string;
|
|
166
|
+
}>(tools: T[], errors: Array<{
|
|
167
|
+
server: string;
|
|
168
|
+
error: string;
|
|
169
|
+
}>): string;
|
|
170
|
+
/** Format the `/mcp reload` report. */
|
|
171
|
+
export declare function formatMcpReloadReport(toolCount: number, serverCount: number, errors: Array<{
|
|
172
|
+
server: string;
|
|
173
|
+
error: string;
|
|
174
|
+
}>): string;
|
|
175
|
+
/** Format the `/mcp resources` listing. */
|
|
176
|
+
export declare function formatMcpResourcesList(groups: Array<{
|
|
177
|
+
serverName: string;
|
|
178
|
+
resources: Array<{
|
|
179
|
+
uri: string;
|
|
180
|
+
name?: string;
|
|
181
|
+
mimeType?: string;
|
|
182
|
+
description?: string;
|
|
183
|
+
}>;
|
|
184
|
+
}>): string;
|
|
185
|
+
/** Format the `/mcp read` output for a list of resource contents. */
|
|
186
|
+
export declare function formatMcpResourceRead(uri: string, contents: Array<{
|
|
187
|
+
text?: string;
|
|
188
|
+
blob?: string;
|
|
189
|
+
mimeType?: string;
|
|
190
|
+
}>): string;
|
|
191
|
+
/** Format the `/mcp prompts` listing. */
|
|
192
|
+
export declare function formatMcpPromptsList(groups: Array<{
|
|
193
|
+
serverName: string;
|
|
194
|
+
prompts: Array<{
|
|
195
|
+
name: string;
|
|
196
|
+
description?: string;
|
|
197
|
+
arguments?: Array<{
|
|
198
|
+
name: string;
|
|
199
|
+
required?: boolean;
|
|
200
|
+
}>;
|
|
201
|
+
}>;
|
|
202
|
+
}>): string;
|
|
203
|
+
/** Format the `/mcp prompt` materialised output. */
|
|
204
|
+
export declare function formatMcpPromptResult(serverName: string, name: string, description: string | undefined, messages: Array<{
|
|
205
|
+
role: string;
|
|
206
|
+
content?: {
|
|
207
|
+
text?: string;
|
|
208
|
+
};
|
|
209
|
+
}>): string;
|
|
210
|
+
/**
|
|
211
|
+
* Parse the `--days N` / `--days=N` flag from `/insights` args. Returns the
|
|
212
|
+
* default (7) when absent or unparseable; clamps negatives to 0.
|
|
213
|
+
*/
|
|
214
|
+
export declare function parseInsightsDays(args: string[], fallback?: number): number;
|
|
215
|
+
/**
|
|
216
|
+
* Format a single cloud-session row for the `/cloud` picker, mirroring the
|
|
217
|
+
* inline template: `title · date · N msg · [project?]`.
|
|
218
|
+
*/
|
|
219
|
+
export declare function formatCloudSessionLabel(s: {
|
|
220
|
+
sessionId: string;
|
|
221
|
+
sessionName?: string | null;
|
|
222
|
+
updatedAt: string;
|
|
223
|
+
messageCount: number;
|
|
224
|
+
projectName?: string | null;
|
|
225
|
+
}): string;
|
|
226
|
+
/** Format the `/me sync` result list. `pulled` is `1` on success, `0` or `null` otherwise. */
|
|
227
|
+
export declare function formatMeSyncReport(pushed: boolean, pulled: number | null): string;
|
|
228
|
+
/**
|
|
229
|
+
* Format the `/me learn` result. `updated` distinguishes "new facts written"
|
|
230
|
+
* from "already covered"; `file` is the human-readable path.
|
|
231
|
+
*/
|
|
232
|
+
export declare function formatMeLearnResult(scope: 'global' | 'project', file: string, res: {
|
|
233
|
+
updated: boolean;
|
|
234
|
+
facts: string;
|
|
235
|
+
}): string;
|
|
236
|
+
/** Format the `/me init` result. */
|
|
237
|
+
export declare function formatMeInitResult(scope: 'global' | 'project', res: {
|
|
238
|
+
created: boolean;
|
|
239
|
+
path: string;
|
|
240
|
+
}): string;
|
|
241
|
+
/** Format the `/skills show` detail view from a skill bundle. */
|
|
242
|
+
export declare function formatSkillsShow(bundle: {
|
|
243
|
+
name: string;
|
|
244
|
+
description: string;
|
|
245
|
+
source: string;
|
|
246
|
+
body: string;
|
|
247
|
+
}): string;
|
|
248
|
+
/** Format the `/skills browse` empty-state message. */
|
|
249
|
+
export declare function formatSkillsBrowseEmpty(query: string): string;
|
|
250
|
+
/** Format the `/skills publish` success message. */
|
|
251
|
+
export declare function formatSkillsPublishResult(slug: string, isPublic: boolean, owner: string | null | undefined): string;
|