codeep 2.15.0 → 2.17.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 +41 -7
- package/dist/acp/serverHandlers.js +1 -1
- package/dist/acp/session.js +22 -1
- package/dist/config/index.js +20 -4
- package/dist/config/providers.d.ts +3 -2
- package/dist/config/providers.js +163 -69
- package/dist/renderer/App.d.ts +89 -0
- package/dist/renderer/App.js +637 -43
- package/dist/renderer/Screen.d.ts +1 -0
- package/dist/renderer/Screen.js +8 -3
- package/dist/renderer/commands/helpers.d.ts +189 -0
- package/dist/renderer/commands/helpers.js +345 -0
- package/dist/renderer/commands/registry.js +2 -1
- package/dist/renderer/commands.js +218 -267
- package/dist/renderer/components/AgentTimeline.d.ts +44 -0
- package/dist/renderer/components/AgentTimeline.js +157 -0
- package/dist/renderer/components/Autocomplete.d.ts +25 -0
- package/dist/renderer/components/Autocomplete.js +35 -0
- package/dist/renderer/components/Status.d.ts +2 -0
- package/dist/renderer/layout.d.ts +5 -1
- package/dist/renderer/layout.js +12 -0
- package/dist/renderer/main.js +110 -30
- package/dist/utils/agent.js +1 -1
- package/dist/utils/agents.d.ts +1 -1
- package/dist/utils/agents.js +1 -1
- package/dist/utils/checkpoints.d.ts +1 -1
- package/dist/utils/checkpoints.js +1 -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/resourceImpact.d.ts +25 -0
- package/dist/utils/resourceImpact.js +54 -0
- package/dist/utils/tokenTracker.js +52 -37
- 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 -1
package/dist/renderer/App.js
CHANGED
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
import { Screen } from './Screen.js';
|
|
6
6
|
import { Input, LineEditor } from './Input.js';
|
|
7
7
|
import { fg, style } from './ansi.js';
|
|
8
|
+
import { getActionColor, formatActionTarget, getActionLabel } from './components/ActionFormatting.js';
|
|
8
9
|
import { PRIMARY_COLOR, SPINNER_FRAMES, LOGO_LINES } from './components/uiConstants.js';
|
|
9
10
|
import { bottomPanelHeight, chatLayout, messageOffsets, scrollOffsetForTarget, scrollWindow, formatTokenCount, statusBarRightHint, activePanel, computeInputDisplay, agentProgressBar, truncateNotification, shouldShowPasteDialog, buildPasteInfo } from './layout.js';
|
|
11
|
+
import { buildAgentTimelineModel, formatElapsed, truncateMiddle, } from './components/AgentTimeline.js';
|
|
10
12
|
import { parseCommandInput } from './inputParsing.js';
|
|
11
13
|
import { formatWelcomeMessage } from './components/WelcomeFormatter.js';
|
|
12
|
-
import { filterCommands } from './components/Autocomplete.js';
|
|
14
|
+
import { filterCommands, detectMentionQuery } from './components/Autocomplete.js';
|
|
15
|
+
import { suggestMentions } from '../utils/mentions.js';
|
|
13
16
|
import { handleInlineStatusKey, handleInlineHelpKey, handleMenuKey, handleInlinePermissionKey, handleInlineSessionPickerKey, handleInlineConfirmKey, handleLoginKey, } from './handlers.js';
|
|
14
17
|
import clipboardy from 'clipboardy';
|
|
15
18
|
import { readImageFromClipboard } from '../utils/clipboard.js';
|
|
19
|
+
import { estimateResourceImpact, formatResourceImpact } from '../utils/resourceImpact.js';
|
|
16
20
|
// (PRIMARY_COLOR, SPINNER_FRAMES, LOGO_LINES, LOGO_HEIGHT moved to
|
|
17
21
|
// ./components/uiConstants — imported above.)
|
|
18
22
|
// ─── Command metadata ────────────────────────────────────────────────────────
|
|
@@ -56,6 +60,10 @@ export class App {
|
|
|
56
60
|
agentThinking = '';
|
|
57
61
|
agentWaitingForAI = false;
|
|
58
62
|
agentLog = [];
|
|
63
|
+
/** Process uptime shown in the persistent footer. */
|
|
64
|
+
appStartedAt = Date.now();
|
|
65
|
+
/** Start of the current agent run; unlike app uptime, resets per task. */
|
|
66
|
+
agentStartedAt = null;
|
|
59
67
|
// Paste detection state
|
|
60
68
|
pasteInfo = null;
|
|
61
69
|
pasteInfoOpen = false;
|
|
@@ -77,10 +85,26 @@ export class App {
|
|
|
77
85
|
showAutocomplete = false;
|
|
78
86
|
autocompleteIndex = 0;
|
|
79
87
|
autocompleteItems = [];
|
|
88
|
+
// `@mention` autocomplete state — separate from the `/command` picker
|
|
89
|
+
// because mentions appear mid-sentence (not just at the start) and
|
|
90
|
+
// insert a file path (not a slash command). `mentionAtStart` is the
|
|
91
|
+
// index of the `@` in the editor value, used to replace `@query` with
|
|
92
|
+
// `@selectedPath` on Tab/Enter.
|
|
93
|
+
showMentionAutocomplete = false;
|
|
94
|
+
mentionIndex = 0;
|
|
95
|
+
mentionItems = [];
|
|
96
|
+
mentionAtStart = 0;
|
|
97
|
+
/** Project root for resolving `suggestMentions`. Cached per update. */
|
|
98
|
+
mentionRoot = '';
|
|
80
99
|
// Inline confirmation dialog state
|
|
81
100
|
confirmOpen = false;
|
|
82
101
|
confirmOptions = null;
|
|
83
102
|
confirmSelection = 'no';
|
|
103
|
+
// Inline hunk-picker state (`/apply --interactive`)
|
|
104
|
+
hunkPickerOpen = false;
|
|
105
|
+
hunkPickerOptions = null;
|
|
106
|
+
hunkPickerIndex = 0;
|
|
107
|
+
hunkPickerAccepted = [];
|
|
84
108
|
// Inline menu state (renders below input/status)
|
|
85
109
|
menuOpen = false;
|
|
86
110
|
menuTitle = '';
|
|
@@ -306,8 +330,11 @@ export class App {
|
|
|
306
330
|
* Set agent running state
|
|
307
331
|
*/
|
|
308
332
|
setAgentRunning(running) {
|
|
333
|
+
const wasRunning = this.isAgentRunning;
|
|
309
334
|
this.isAgentRunning = running;
|
|
310
335
|
if (running) {
|
|
336
|
+
if (!wasRunning)
|
|
337
|
+
this.agentStartedAt = Date.now();
|
|
311
338
|
this.agentIteration = 0;
|
|
312
339
|
this.agentMaxIterations = 0;
|
|
313
340
|
this.agentActions = [];
|
|
@@ -318,6 +345,7 @@ export class App {
|
|
|
318
345
|
this.startSpinner();
|
|
319
346
|
}
|
|
320
347
|
else {
|
|
348
|
+
this.agentStartedAt = null;
|
|
321
349
|
this.isLoading = false; // Ensure loading is cleared when agent finishes
|
|
322
350
|
this.stopSpinner();
|
|
323
351
|
}
|
|
@@ -507,6 +535,17 @@ export class App {
|
|
|
507
535
|
this.confirmOpen = true;
|
|
508
536
|
this.scheduleRender();
|
|
509
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Show the interactive hunk picker (`/apply --interactive`).
|
|
540
|
+
* The caller passes pre-built items + an `onComplete` callback.
|
|
541
|
+
*/
|
|
542
|
+
showHunkPicker(options) {
|
|
543
|
+
this.hunkPickerOptions = options;
|
|
544
|
+
this.hunkPickerIndex = 0;
|
|
545
|
+
this.hunkPickerAccepted = [];
|
|
546
|
+
this.hunkPickerOpen = true;
|
|
547
|
+
this.scheduleRender();
|
|
548
|
+
}
|
|
510
549
|
/**
|
|
511
550
|
* Show permission dialog (inline, below status bar)
|
|
512
551
|
*/
|
|
@@ -704,6 +743,7 @@ export class App {
|
|
|
704
743
|
loginOpen: this.loginOpen,
|
|
705
744
|
menuOpen: this.menuOpen,
|
|
706
745
|
showAutocomplete: this.showAutocomplete,
|
|
746
|
+
hunkPickerOpen: this.hunkPickerOpen,
|
|
707
747
|
})) {
|
|
708
748
|
case 'pasteInfo':
|
|
709
749
|
this.handlePasteInfoKey(event);
|
|
@@ -741,6 +781,9 @@ export class App {
|
|
|
741
781
|
case 'menu':
|
|
742
782
|
this.handleMenuKey(event);
|
|
743
783
|
return;
|
|
784
|
+
case 'hunkPicker':
|
|
785
|
+
this.handleHunkPickerKey(event);
|
|
786
|
+
return;
|
|
744
787
|
}
|
|
745
788
|
// If intro is playing, skip on any key.
|
|
746
789
|
if (this.showIntro) {
|
|
@@ -749,6 +792,11 @@ export class App {
|
|
|
749
792
|
}
|
|
750
793
|
// Escape to cancel streaming/loading/agent or close autocomplete
|
|
751
794
|
if (event.key === 'escape') {
|
|
795
|
+
if (this.showMentionAutocomplete) {
|
|
796
|
+
this.showMentionAutocomplete = false;
|
|
797
|
+
this.scheduleRender();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
752
800
|
if (this.showAutocomplete) {
|
|
753
801
|
this.showAutocomplete = false;
|
|
754
802
|
this.scheduleRender();
|
|
@@ -770,7 +818,7 @@ export class App {
|
|
|
770
818
|
}
|
|
771
819
|
return;
|
|
772
820
|
}
|
|
773
|
-
// Handle autocomplete navigation
|
|
821
|
+
// Handle autocomplete navigation (`/command` picker)
|
|
774
822
|
if (this.showAutocomplete) {
|
|
775
823
|
if (event.key === 'up') {
|
|
776
824
|
this.autocompleteIndex = Math.max(0, this.autocompleteIndex - 1);
|
|
@@ -793,6 +841,28 @@ export class App {
|
|
|
793
841
|
}
|
|
794
842
|
}
|
|
795
843
|
}
|
|
844
|
+
// Handle `@mention` autocomplete navigation
|
|
845
|
+
if (this.showMentionAutocomplete) {
|
|
846
|
+
if (event.key === 'up') {
|
|
847
|
+
this.mentionIndex = Math.max(0, this.mentionIndex - 1);
|
|
848
|
+
this.scheduleRender();
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (event.key === 'down') {
|
|
852
|
+
this.mentionIndex = Math.min(this.mentionItems.length - 1, this.mentionIndex + 1);
|
|
853
|
+
this.scheduleRender();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (event.key === 'tab') {
|
|
857
|
+
// Replace `@query` with `@selectedPath` in the editor.
|
|
858
|
+
if (this.mentionItems.length > 0) {
|
|
859
|
+
this.applyMentionSelection();
|
|
860
|
+
this.showMentionAutocomplete = false;
|
|
861
|
+
this.scheduleRender();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
796
866
|
// Ctrl+L to clear
|
|
797
867
|
if (event.ctrl && event.key === 'l') {
|
|
798
868
|
this.clearMessages();
|
|
@@ -929,7 +999,11 @@ export class App {
|
|
|
929
999
|
* Update autocomplete suggestions
|
|
930
1000
|
*/
|
|
931
1001
|
updateAutocomplete() {
|
|
932
|
-
const
|
|
1002
|
+
const value = this.editor.getValue();
|
|
1003
|
+
const cursorPos = this.editor.getCursorPos();
|
|
1004
|
+
// `/command` picker — only when the input starts with `/` and the
|
|
1005
|
+
// cursor is in the command-name segment (no space yet).
|
|
1006
|
+
const result = filterCommands(value, App.COMMANDS);
|
|
933
1007
|
if (result === null) {
|
|
934
1008
|
this.showAutocomplete = false;
|
|
935
1009
|
this.autocompleteItems = [];
|
|
@@ -939,6 +1013,47 @@ export class App {
|
|
|
939
1013
|
this.showAutocomplete = result.items.length > 0;
|
|
940
1014
|
this.autocompleteIndex = result.index;
|
|
941
1015
|
}
|
|
1016
|
+
// `@mention` picker — detect an in-progress mention at the cursor.
|
|
1017
|
+
// Independent of the `/` picker so the two never compete.
|
|
1018
|
+
const mention = detectMentionQuery(value, cursorPos);
|
|
1019
|
+
if (mention) {
|
|
1020
|
+
const root = this.options.getProjectRoot?.() ?? process.cwd();
|
|
1021
|
+
this.mentionRoot = root;
|
|
1022
|
+
this.mentionAtStart = mention.atStart;
|
|
1023
|
+
this.mentionItems = suggestMentions({ root, query: mention.query, limit: 10 });
|
|
1024
|
+
this.showMentionAutocomplete = this.mentionItems.length > 0;
|
|
1025
|
+
this.mentionIndex = 0;
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
this.showMentionAutocomplete = false;
|
|
1029
|
+
this.mentionItems = [];
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Replace the in-progress `@query` (from `mentionAtStart` to the
|
|
1034
|
+
* cursor) with the selected mention's path. Keeps the `@` prefix and
|
|
1035
|
+
* positions the cursor right after the inserted path so the user can
|
|
1036
|
+
* keep typing the rest of the message.
|
|
1037
|
+
*/
|
|
1038
|
+
applyMentionSelection() {
|
|
1039
|
+
if (this.mentionItems.length === 0)
|
|
1040
|
+
return;
|
|
1041
|
+
const selected = this.mentionItems[this.mentionIndex];
|
|
1042
|
+
const value = this.editor.getValue();
|
|
1043
|
+
const cursor = this.editor.getCursorPos();
|
|
1044
|
+
if (this.mentionAtStart >= value.length)
|
|
1045
|
+
return;
|
|
1046
|
+
// `mentionAtStart` is the index OF the `@`, so this slice EXCLUDES it —
|
|
1047
|
+
// re-add the sigil or the completed path is no longer a mention and the
|
|
1048
|
+
// file never gets attached.
|
|
1049
|
+
const before = value.slice(0, this.mentionAtStart) + '@';
|
|
1050
|
+
const after = value.slice(cursor);
|
|
1051
|
+
const next = before + selected.insertPath + ' ' + after;
|
|
1052
|
+
this.editor.setValue(next);
|
|
1053
|
+
const newCursor = (before + selected.insertPath + ' ').length;
|
|
1054
|
+
this.editor.setCursorPos(newCursor);
|
|
1055
|
+
// The picker may still have matches for the new prefix — refresh.
|
|
1056
|
+
this.updateAutocomplete();
|
|
942
1057
|
}
|
|
943
1058
|
/**
|
|
944
1059
|
* Handle inline status keys
|
|
@@ -1162,6 +1277,85 @@ export class App {
|
|
|
1162
1277
|
render: () => this.scheduleRender(),
|
|
1163
1278
|
});
|
|
1164
1279
|
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Handle keys in the interactive hunk picker.
|
|
1282
|
+
* y / Enter / → accept this hunk, advance
|
|
1283
|
+
* n / ← skip this hunk, advance
|
|
1284
|
+
* a accept this + all remaining, finish
|
|
1285
|
+
* q / Esc finish without accepting this hunk
|
|
1286
|
+
* ↑ / ↓ navigate (preview only — no decision)
|
|
1287
|
+
*/
|
|
1288
|
+
handleHunkPickerKey(event) {
|
|
1289
|
+
const opts = this.hunkPickerOptions;
|
|
1290
|
+
if (!opts) {
|
|
1291
|
+
this.hunkPickerOpen = false;
|
|
1292
|
+
this.scheduleRender();
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const finish = () => {
|
|
1296
|
+
const accepted = this.hunkPickerAccepted;
|
|
1297
|
+
const cb = opts.onComplete;
|
|
1298
|
+
this.hunkPickerOptions = null;
|
|
1299
|
+
this.hunkPickerOpen = false;
|
|
1300
|
+
this.hunkPickerAccepted = [];
|
|
1301
|
+
this.hunkPickerIndex = 0;
|
|
1302
|
+
cb(accepted);
|
|
1303
|
+
this.scheduleRender();
|
|
1304
|
+
};
|
|
1305
|
+
const advance = () => {
|
|
1306
|
+
if (this.hunkPickerIndex >= opts.items.length - 1) {
|
|
1307
|
+
finish();
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
this.hunkPickerIndex++;
|
|
1311
|
+
this.scheduleRender();
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
const acceptCurrent = () => {
|
|
1315
|
+
const item = opts.items[this.hunkPickerIndex];
|
|
1316
|
+
if (item) {
|
|
1317
|
+
this.hunkPickerAccepted.push({ path: item.path, hunkIndex: item.hunkIndex });
|
|
1318
|
+
}
|
|
1319
|
+
advance();
|
|
1320
|
+
};
|
|
1321
|
+
switch (event.key) {
|
|
1322
|
+
case 'y':
|
|
1323
|
+
case 'enter':
|
|
1324
|
+
case 'right':
|
|
1325
|
+
acceptCurrent();
|
|
1326
|
+
return;
|
|
1327
|
+
case 'n':
|
|
1328
|
+
case 'left':
|
|
1329
|
+
advance();
|
|
1330
|
+
return;
|
|
1331
|
+
case 'a':
|
|
1332
|
+
// Accept current + all remaining.
|
|
1333
|
+
for (let i = this.hunkPickerIndex; i < opts.items.length; i++) {
|
|
1334
|
+
const item = opts.items[i];
|
|
1335
|
+
this.hunkPickerAccepted.push({ path: item.path, hunkIndex: item.hunkIndex });
|
|
1336
|
+
}
|
|
1337
|
+
finish();
|
|
1338
|
+
return;
|
|
1339
|
+
case 'q':
|
|
1340
|
+
case 'escape':
|
|
1341
|
+
finish();
|
|
1342
|
+
return;
|
|
1343
|
+
case 'up':
|
|
1344
|
+
if (this.hunkPickerIndex > 0) {
|
|
1345
|
+
this.hunkPickerIndex--;
|
|
1346
|
+
this.scheduleRender();
|
|
1347
|
+
}
|
|
1348
|
+
return;
|
|
1349
|
+
case 'down':
|
|
1350
|
+
if (this.hunkPickerIndex < opts.items.length - 1) {
|
|
1351
|
+
this.hunkPickerIndex++;
|
|
1352
|
+
this.scheduleRender();
|
|
1353
|
+
}
|
|
1354
|
+
return;
|
|
1355
|
+
default:
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1165
1359
|
/**
|
|
1166
1360
|
* Submit the current input buffer (used by Enter and Escape-in-multiline)
|
|
1167
1361
|
*/
|
|
@@ -1257,6 +1451,10 @@ export class App {
|
|
|
1257
1451
|
return;
|
|
1258
1452
|
}
|
|
1259
1453
|
this.screen.clear();
|
|
1454
|
+
if (this.shouldRenderAgentTimeline(width, height)) {
|
|
1455
|
+
this.renderAgentTimelineScreen(width, height);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1260
1458
|
// If menu or settings is open, reserve space for it at bottom
|
|
1261
1459
|
const panelHeight = bottomPanelHeight({
|
|
1262
1460
|
height,
|
|
@@ -1264,6 +1462,7 @@ export class App {
|
|
|
1264
1462
|
pasteInfoPreviewLines: this.pasteInfo ? this.pasteInfo.preview.split('\n').length : 0,
|
|
1265
1463
|
isAgentRunning: this.isAgentRunning,
|
|
1266
1464
|
confirmOpen: this.confirmOpen && !!this.confirmOptions,
|
|
1465
|
+
hunkPickerOpen: this.hunkPickerOpen && !!this.hunkPickerOptions,
|
|
1267
1466
|
permissionOpen: this.permissionOpen,
|
|
1268
1467
|
sessionPickerOpen: this.sessionPickerOpen,
|
|
1269
1468
|
sessionPickerItemCount: this.sessionPickerItems.length,
|
|
@@ -1284,14 +1483,20 @@ export class App {
|
|
|
1284
1483
|
settingsCount: SETTINGS.length,
|
|
1285
1484
|
showAutocomplete: this.showAutocomplete,
|
|
1286
1485
|
autocompleteItemCount: this.autocompleteItems.length,
|
|
1486
|
+
mentionPickerOpen: this.showMentionAutocomplete,
|
|
1487
|
+
mentionItemCount: this.mentionItems.length,
|
|
1287
1488
|
});
|
|
1288
1489
|
const layout = chatLayout(height, panelHeight);
|
|
1289
1490
|
const mainHeight = layout.mainHeight;
|
|
1290
|
-
const
|
|
1491
|
+
const headerHeight = width >= 60 && height >= 16 ? 2 : 0;
|
|
1492
|
+
const messagesStart = Math.min(layout.messagesEnd, headerHeight);
|
|
1291
1493
|
const messagesEnd = layout.messagesEnd;
|
|
1292
1494
|
const separatorLine = layout.separatorLine;
|
|
1293
1495
|
const inputLine = layout.inputLine;
|
|
1294
1496
|
const statusLine = layout.statusLine;
|
|
1497
|
+
if (headerHeight > 0) {
|
|
1498
|
+
this.renderPersistentHeader(width);
|
|
1499
|
+
}
|
|
1295
1500
|
// Messages
|
|
1296
1501
|
const messagesHeight = Math.max(1, messagesEnd - messagesStart + 1);
|
|
1297
1502
|
const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
|
|
@@ -1350,10 +1555,17 @@ export class App {
|
|
|
1350
1555
|
if (this.confirmOpen && this.confirmOptions) {
|
|
1351
1556
|
this.renderInlineConfirm(statusLine + 1, width);
|
|
1352
1557
|
}
|
|
1558
|
+
// Inline hunk picker renders BELOW status bar
|
|
1559
|
+
if (this.hunkPickerOpen && this.hunkPickerOptions) {
|
|
1560
|
+
this.renderInlineHunkPicker(statusLine + 1, width);
|
|
1561
|
+
}
|
|
1353
1562
|
// Inline autocomplete renders BELOW status bar
|
|
1354
1563
|
if (this.showAutocomplete && this.autocompleteItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
|
|
1355
1564
|
this.renderInlineAutocomplete(statusLine + 1, width);
|
|
1356
1565
|
}
|
|
1566
|
+
else if (this.showMentionAutocomplete && this.mentionItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
|
|
1567
|
+
this.renderInlineMentionPicker(statusLine + 1, width);
|
|
1568
|
+
}
|
|
1357
1569
|
// Inline permission renders BELOW status bar
|
|
1358
1570
|
if (this.permissionOpen) {
|
|
1359
1571
|
this.renderInlinePermission(statusLine + 1, width);
|
|
@@ -1372,6 +1584,278 @@ export class App {
|
|
|
1372
1584
|
}
|
|
1373
1585
|
this.screen.render();
|
|
1374
1586
|
}
|
|
1587
|
+
shouldRenderAgentTimeline(width, height) {
|
|
1588
|
+
if (!this.isAgentRunning || width < 92 || height < 26)
|
|
1589
|
+
return false;
|
|
1590
|
+
return !(this.pasteInfoOpen ||
|
|
1591
|
+
this.permissionOpen ||
|
|
1592
|
+
this.sessionPickerOpen ||
|
|
1593
|
+
this.confirmOpen ||
|
|
1594
|
+
this.hunkPickerOpen ||
|
|
1595
|
+
this.statusOpen ||
|
|
1596
|
+
this.helpOpen ||
|
|
1597
|
+
this.settingsOpen ||
|
|
1598
|
+
this.searchOpen ||
|
|
1599
|
+
this.exportOpen ||
|
|
1600
|
+
this.logoutOpen ||
|
|
1601
|
+
this.loginOpen ||
|
|
1602
|
+
this.menuOpen ||
|
|
1603
|
+
this.showAutocomplete ||
|
|
1604
|
+
this.showMentionAutocomplete);
|
|
1605
|
+
}
|
|
1606
|
+
renderPersistentHeader(width) {
|
|
1607
|
+
const status = this.options.getStatus();
|
|
1608
|
+
const project = status.projectPath.split('/').filter(Boolean).pop() || status.projectPath || 'no project';
|
|
1609
|
+
const session = status.sessionId ? status.sessionId.slice(0, 8) : 'new';
|
|
1610
|
+
const branch = status.branch || '';
|
|
1611
|
+
this.screen.writeLine(0, '');
|
|
1612
|
+
let x = 1;
|
|
1613
|
+
const writeSegment = (label, value, valueStyle = fg.white) => {
|
|
1614
|
+
const separator = x > 1 ? ' │ ' : '';
|
|
1615
|
+
const required = separator.length + label.length + value.length;
|
|
1616
|
+
if (x + required >= width - 1)
|
|
1617
|
+
return false;
|
|
1618
|
+
if (separator) {
|
|
1619
|
+
this.screen.write(x, 0, separator, fg.gray);
|
|
1620
|
+
x += separator.length;
|
|
1621
|
+
}
|
|
1622
|
+
this.screen.write(x, 0, label, fg.gray);
|
|
1623
|
+
x += label.length;
|
|
1624
|
+
this.screen.write(x, 0, value, valueStyle);
|
|
1625
|
+
x += value.length;
|
|
1626
|
+
return true;
|
|
1627
|
+
};
|
|
1628
|
+
const wordmark = 'CODEEP';
|
|
1629
|
+
this.screen.write(x, 0, wordmark, PRIMARY_COLOR + style.bold);
|
|
1630
|
+
x += wordmark.length;
|
|
1631
|
+
writeSegment('', `v${status.version}`);
|
|
1632
|
+
if (width >= 86)
|
|
1633
|
+
writeSegment('session: ', session);
|
|
1634
|
+
writeSegment('model: ', truncateMiddle(status.model || 'unknown', 22));
|
|
1635
|
+
if (width >= 112)
|
|
1636
|
+
writeSegment('provider: ', truncateMiddle(status.provider, 14));
|
|
1637
|
+
if (width >= 128)
|
|
1638
|
+
writeSegment('project: ', truncateMiddle(project, 18));
|
|
1639
|
+
if (width >= 148 && branch) {
|
|
1640
|
+
// Budget the separator and label before truncating. Sizing the value off
|
|
1641
|
+
// `width - x` alone always overshoots writeSegment's fit guard, so a
|
|
1642
|
+
// branch long enough to need truncating used to render nothing at all.
|
|
1643
|
+
const branchBudget = width - x - ' │ '.length - 'branch: '.length - 2;
|
|
1644
|
+
if (branchBudget >= 10) {
|
|
1645
|
+
writeSegment('branch: ', truncateMiddle(branch, branchBudget), PRIMARY_COLOR);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
this.screen.horizontalLine(1, '─', PRIMARY_COLOR);
|
|
1649
|
+
}
|
|
1650
|
+
renderAgentTimelineScreen(width, height) {
|
|
1651
|
+
this.screen.clear();
|
|
1652
|
+
this.renderPersistentHeader(width);
|
|
1653
|
+
const inputY = height - 4;
|
|
1654
|
+
const hintsY = height - 3;
|
|
1655
|
+
const footerDividerY = height - 2;
|
|
1656
|
+
const statusY = height - 1;
|
|
1657
|
+
const workspaceTop = 2;
|
|
1658
|
+
const workspaceBottom = inputY - 2;
|
|
1659
|
+
const railWidth = width >= 132 ? Math.min(44, Math.floor(width * 0.28)) : 0;
|
|
1660
|
+
const dividerX = railWidth > 0 ? width - railWidth : width;
|
|
1661
|
+
const leftWidth = railWidth > 0 ? dividerX - 1 : width;
|
|
1662
|
+
const timeline = buildAgentTimelineModel({
|
|
1663
|
+
actions: this.agentActions,
|
|
1664
|
+
thinking: this.agentThinking,
|
|
1665
|
+
waitingForAI: this.agentWaitingForAI,
|
|
1666
|
+
iteration: this.agentIteration,
|
|
1667
|
+
maxIterations: this.agentMaxIterations,
|
|
1668
|
+
});
|
|
1669
|
+
const task = this.currentAgentTask();
|
|
1670
|
+
this.screen.writeLine(workspaceTop, '');
|
|
1671
|
+
this.screen.write(1, workspaceTop, 'YOU', PRIMARY_COLOR + style.bold);
|
|
1672
|
+
this.screen.write(7, workspaceTop, 'Task:', PRIMARY_COLOR + style.bold);
|
|
1673
|
+
this.screen.write(13, workspaceTop, truncateMiddle(task, Math.max(8, leftWidth - 15)), fg.white);
|
|
1674
|
+
this.screen.horizontalLine(workspaceTop + 1, '─', fg.gray);
|
|
1675
|
+
this.screen.writeLine(workspaceTop + 2, '');
|
|
1676
|
+
this.screen.write(1, workspaceTop + 2, 'AGENT', PRIMARY_COLOR + style.bold);
|
|
1677
|
+
const runLabel = this.agentWaitingForAI ? 'Choosing the next step' : 'Executing a tool';
|
|
1678
|
+
this.screen.write(9, workspaceTop + 2, runLabel, fg.white);
|
|
1679
|
+
const stepLabel = this.agentMaxIterations > 0
|
|
1680
|
+
? `step ${this.agentIteration}/${this.agentMaxIterations}`
|
|
1681
|
+
: `step ${this.agentIteration}`;
|
|
1682
|
+
if (stepLabel.length + 2 < leftWidth) {
|
|
1683
|
+
this.screen.write(leftWidth - stepLabel.length - 1, workspaceTop + 2, stepLabel, fg.gray);
|
|
1684
|
+
}
|
|
1685
|
+
const expandedTimeline = workspaceBottom - workspaceTop >= 30;
|
|
1686
|
+
let y = workspaceTop + 4;
|
|
1687
|
+
if (expandedTimeline) {
|
|
1688
|
+
this.screen.write(4, y, 'PLAN (HIGH LEVEL)', PRIMARY_COLOR + style.bold);
|
|
1689
|
+
this.screen.write(22, y++, 'Inspect the relevant project context', fg.white);
|
|
1690
|
+
this.screen.write(22, y++, 'Apply focused changes with permission checks', fg.white);
|
|
1691
|
+
this.screen.write(22, y++, 'Run verification and summarize the result', fg.white);
|
|
1692
|
+
this.screen.write(4, y, '─'.repeat(Math.max(8, leftWidth - 6)), fg.gray);
|
|
1693
|
+
y += 2;
|
|
1694
|
+
}
|
|
1695
|
+
for (const stage of timeline.stages) {
|
|
1696
|
+
if (y + 1 > workspaceBottom)
|
|
1697
|
+
break;
|
|
1698
|
+
const marker = stage.status === 'done' ? '●' : stage.status === 'active' ? '◆' : '○';
|
|
1699
|
+
const markerStyle = this.timelineStatusStyle(stage.status);
|
|
1700
|
+
this.screen.write(1, y, marker, markerStyle);
|
|
1701
|
+
this.screen.write(2, y + 1, '│', stage.status === 'pending' ? fg.gray : markerStyle);
|
|
1702
|
+
if (expandedTimeline && y + 2 <= workspaceBottom) {
|
|
1703
|
+
this.screen.write(2, y + 2, '│', stage.status === 'pending' ? fg.gray : markerStyle);
|
|
1704
|
+
}
|
|
1705
|
+
this.screen.write(4, y, stage.id.padEnd(8), markerStyle + (stage.status === 'active' ? style.bold : ''));
|
|
1706
|
+
this.screen.write(13, y, truncateMiddle(stage.summary, Math.max(8, leftWidth - 24)), stage.status === 'pending' ? fg.gray : fg.white);
|
|
1707
|
+
const stateLabel = stage.status === 'done' ? 'done' : stage.status === 'active' ? 'active' : 'pending';
|
|
1708
|
+
if (leftWidth > 50) {
|
|
1709
|
+
this.screen.write(leftWidth - stateLabel.length - 1, y, stateLabel, markerStyle);
|
|
1710
|
+
}
|
|
1711
|
+
this.screen.write(13, y + 1, truncateMiddle(stage.detail, Math.max(8, leftWidth - 16)), fg.gray);
|
|
1712
|
+
y += expandedTimeline ? 3 : 2;
|
|
1713
|
+
if (stage.status === 'active' && y + 2 <= workspaceBottom) {
|
|
1714
|
+
if (timeline.currentTarget) {
|
|
1715
|
+
const actionType = this.currentActionType();
|
|
1716
|
+
const actionLabel = actionType ? getActionLabel(actionType) : 'Working';
|
|
1717
|
+
this.screen.write(13, y, `${actionLabel}:`, PRIMARY_COLOR);
|
|
1718
|
+
this.screen.write(13 + actionLabel.length + 2, y, formatActionTarget(timeline.currentTarget, Math.max(12, leftWidth - actionLabel.length - 18)), fg.white);
|
|
1719
|
+
y++;
|
|
1720
|
+
}
|
|
1721
|
+
if (this.agentMaxIterations > 0) {
|
|
1722
|
+
const barWidth = Math.max(8, Math.min(36, leftWidth - 28));
|
|
1723
|
+
const bar = agentProgressBar(this.agentIteration, this.agentMaxIterations, barWidth);
|
|
1724
|
+
const percent = `${Math.round(timeline.progress * 100)}%`;
|
|
1725
|
+
this.screen.write(13, y, percent.padStart(4), PRIMARY_COLOR);
|
|
1726
|
+
this.screen.write(19, y, bar, PRIMARY_COLOR);
|
|
1727
|
+
y++;
|
|
1728
|
+
}
|
|
1729
|
+
if (expandedTimeline && y + 3 <= workspaceBottom) {
|
|
1730
|
+
this.screen.write(13, y, 'RECENT ACTIVITY', PRIMARY_COLOR + style.bold);
|
|
1731
|
+
this.screen.write(29, y, '─'.repeat(Math.max(4, leftWidth - 31)), fg.gray);
|
|
1732
|
+
y++;
|
|
1733
|
+
const recentActivity = this.agentLog.slice(-3);
|
|
1734
|
+
if (recentActivity.length === 0) {
|
|
1735
|
+
this.screen.write(13, y++, 'Waiting for the first completed action', fg.gray);
|
|
1736
|
+
}
|
|
1737
|
+
else {
|
|
1738
|
+
for (const entry of recentActivity) {
|
|
1739
|
+
if (y > workspaceBottom)
|
|
1740
|
+
break;
|
|
1741
|
+
const isError = entry.startsWith('✗') || entry.startsWith('!');
|
|
1742
|
+
const isActive = entry.startsWith('◆');
|
|
1743
|
+
const entryStyle = isError ? fg.red : isActive ? PRIMARY_COLOR : fg.green;
|
|
1744
|
+
this.screen.write(13, y, entry.slice(0, 1), entryStyle + style.bold);
|
|
1745
|
+
this.screen.write(15, y++, truncateMiddle(entry.slice(2), Math.max(8, leftWidth - 17)), isActive ? fg.white : fg.gray);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
y++;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (railWidth > 0) {
|
|
1753
|
+
this.renderAgentContextRail(dividerX, workspaceTop, workspaceBottom, railWidth, timeline);
|
|
1754
|
+
}
|
|
1755
|
+
this.screen.horizontalLine(inputY - 1, '─', PRIMARY_COLOR);
|
|
1756
|
+
this.renderInput(inputY, width, false);
|
|
1757
|
+
this.renderAgentKeyHints(hintsY, width);
|
|
1758
|
+
this.screen.horizontalLine(footerDividerY, '─', fg.gray);
|
|
1759
|
+
this.renderStatusBar(statusY, width);
|
|
1760
|
+
this.screen.render();
|
|
1761
|
+
}
|
|
1762
|
+
renderAgentContextRail(dividerX, top, bottom, railWidth, timeline) {
|
|
1763
|
+
for (let y = top; y <= bottom; y++) {
|
|
1764
|
+
this.screen.write(dividerX, y, '│', PRIMARY_COLOR);
|
|
1765
|
+
}
|
|
1766
|
+
const x = dividerX + 2;
|
|
1767
|
+
const contentWidth = Math.max(8, railWidth - 3);
|
|
1768
|
+
let y = top + 1;
|
|
1769
|
+
this.screen.write(x, y++, `CURRENT: ${timeline.currentStage}`, PRIMARY_COLOR + style.bold);
|
|
1770
|
+
y++;
|
|
1771
|
+
this.screen.write(x, y++, `FILES (${timeline.files.length})`, PRIMARY_COLOR + style.bold);
|
|
1772
|
+
if (timeline.files.length === 0) {
|
|
1773
|
+
this.screen.write(x, y++, 'No file changes yet', fg.gray);
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1776
|
+
for (const file of timeline.files.slice(-6)) {
|
|
1777
|
+
if (y > bottom - 7)
|
|
1778
|
+
break;
|
|
1779
|
+
const marker = file.type === 'delete' ? 'D' : file.type === 'write' ? 'A' : 'M';
|
|
1780
|
+
const color = file.result === 'error' ? fg.red : getActionColor(file.type);
|
|
1781
|
+
this.screen.write(x, y, marker, color + style.bold);
|
|
1782
|
+
this.screen.write(x + 2, y++, formatActionTarget(file.target, contentWidth - 2), file.result === 'error' ? fg.red : fg.white);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
y++;
|
|
1786
|
+
if (y <= bottom - 5) {
|
|
1787
|
+
this.screen.write(x, y++, 'CHECKS', PRIMARY_COLOR + style.bold);
|
|
1788
|
+
if (timeline.checks.length === 0) {
|
|
1789
|
+
const activeCheck = timeline.currentStage === 'VERIFY' && timeline.currentTarget
|
|
1790
|
+
? formatActionTarget(timeline.currentTarget, contentWidth)
|
|
1791
|
+
: 'Pending';
|
|
1792
|
+
this.screen.write(x, y++, activeCheck, timeline.currentStage === 'VERIFY' ? fg.yellow : fg.gray);
|
|
1793
|
+
}
|
|
1794
|
+
else {
|
|
1795
|
+
for (const check of timeline.checks.slice(-3)) {
|
|
1796
|
+
if (y > bottom - 3)
|
|
1797
|
+
break;
|
|
1798
|
+
const symbol = check.result === 'success' ? '✓' : '!';
|
|
1799
|
+
const color = check.result === 'success' ? fg.green : fg.red;
|
|
1800
|
+
this.screen.write(x, y, symbol, color + style.bold);
|
|
1801
|
+
this.screen.write(x + 2, y++, formatActionTarget(check.target, contentWidth - 2), fg.white);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const status = this.options.getStatus();
|
|
1806
|
+
const contextY = Math.max(y + 1, bottom - 3);
|
|
1807
|
+
if (contextY <= bottom) {
|
|
1808
|
+
this.screen.write(x, contextY, 'CONTEXT', PRIMARY_COLOR + style.bold);
|
|
1809
|
+
if (contextY + 1 <= bottom) {
|
|
1810
|
+
const project = status.projectPath.split('/').filter(Boolean).pop() || status.projectPath;
|
|
1811
|
+
this.screen.write(x, contextY + 1, truncateMiddle(project, contentWidth), fg.white);
|
|
1812
|
+
}
|
|
1813
|
+
if (contextY + 2 <= bottom) {
|
|
1814
|
+
const branch = status.branch ? `branch ${status.branch}` : `${status.provider} · ${status.model}`;
|
|
1815
|
+
this.screen.write(x, contextY + 2, truncateMiddle(branch, contentWidth), fg.gray);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
renderAgentKeyHints(y, width) {
|
|
1820
|
+
this.screen.writeLine(y, '');
|
|
1821
|
+
const left = 'Keys: Enter reply · / commands · Esc stop · ↑↓ history';
|
|
1822
|
+
const runStartedAt = this.agentStartedAt ?? Date.now();
|
|
1823
|
+
const right = `agent: working · ${formatElapsed(Date.now() - runStartedAt)}`;
|
|
1824
|
+
this.screen.write(1, y, truncateMiddle(left, Math.max(12, width - right.length - 4)), fg.gray);
|
|
1825
|
+
if (right.length + 2 < width) {
|
|
1826
|
+
this.screen.write(width - right.length - 1, y, right, fg.gray);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
timelineStatusStyle(status) {
|
|
1830
|
+
if (status === 'done')
|
|
1831
|
+
return fg.green;
|
|
1832
|
+
if (status === 'active')
|
|
1833
|
+
return PRIMARY_COLOR;
|
|
1834
|
+
return fg.gray;
|
|
1835
|
+
}
|
|
1836
|
+
currentActionType() {
|
|
1837
|
+
const separator = this.agentThinking.indexOf(':');
|
|
1838
|
+
if (separator < 0)
|
|
1839
|
+
return '';
|
|
1840
|
+
const type = this.agentThinking.slice(0, separator).trim().toLowerCase();
|
|
1841
|
+
return ['read', 'search', 'list', 'fetch', 'write', 'edit', 'delete', 'mkdir', 'command'].includes(type)
|
|
1842
|
+
? type
|
|
1843
|
+
: '';
|
|
1844
|
+
}
|
|
1845
|
+
currentAgentTask() {
|
|
1846
|
+
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
1847
|
+
const message = this.messages[index];
|
|
1848
|
+
if (message.role !== 'user')
|
|
1849
|
+
continue;
|
|
1850
|
+
const task = message.content
|
|
1851
|
+
.replace(/^\[DRY RUN]\s*/i, '')
|
|
1852
|
+
.replace(/^\[AGENT]\s*/i, '')
|
|
1853
|
+
.trim();
|
|
1854
|
+
if (task)
|
|
1855
|
+
return task;
|
|
1856
|
+
}
|
|
1857
|
+
return 'Autonomous coding task';
|
|
1858
|
+
}
|
|
1375
1859
|
/**
|
|
1376
1860
|
* Render inline confirmation dialog below status bar
|
|
1377
1861
|
*/
|
|
@@ -1409,6 +1893,53 @@ export class App {
|
|
|
1409
1893
|
// Footer
|
|
1410
1894
|
this.screen.writeLine(y, '←/→ select • y/n quick • Enter confirm • Esc cancel', fg.gray);
|
|
1411
1895
|
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Render inline hunk picker (`/apply --interactive`).
|
|
1898
|
+
* Shows the current hunk's diff + the y/n/a/q key legend.
|
|
1899
|
+
*/
|
|
1900
|
+
renderInlineHunkPicker(startY, width) {
|
|
1901
|
+
const opts = this.hunkPickerOptions;
|
|
1902
|
+
if (!opts)
|
|
1903
|
+
return;
|
|
1904
|
+
const item = opts.items[this.hunkPickerIndex];
|
|
1905
|
+
let y = startY;
|
|
1906
|
+
this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
|
|
1907
|
+
// Title + progress
|
|
1908
|
+
const progress = opts.items.length > 0
|
|
1909
|
+
? ` (${this.hunkPickerIndex + 1}/${opts.items.length})`
|
|
1910
|
+
: '';
|
|
1911
|
+
this.screen.writeLine(y++, `${opts.title}${progress}`, PRIMARY_COLOR + style.bold);
|
|
1912
|
+
if (!item) {
|
|
1913
|
+
this.screen.writeLine(y++, 'No hunks to review.', fg.gray);
|
|
1914
|
+
this.screen.writeLine(y, 'Press any key to close.', fg.gray);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
// File path + hunk header
|
|
1918
|
+
this.screen.writeLine(y++, `File: ${item.path}`, fg.cyan);
|
|
1919
|
+
this.screen.writeLine(y++, `Hunk: ${item.header}`, fg.gray);
|
|
1920
|
+
// Diff lines (capped to available vertical space; show up to 12)
|
|
1921
|
+
const maxDiffLines = 12;
|
|
1922
|
+
const lines = item.lines.slice(0, maxDiffLines);
|
|
1923
|
+
for (const line of lines) {
|
|
1924
|
+
const prefix = line.charAt(0);
|
|
1925
|
+
let color = fg.white;
|
|
1926
|
+
if (prefix === '+')
|
|
1927
|
+
color = fg.green;
|
|
1928
|
+
else if (prefix === '-')
|
|
1929
|
+
color = fg.red;
|
|
1930
|
+
else if (prefix === '@')
|
|
1931
|
+
color = fg.cyan;
|
|
1932
|
+
// Truncate long lines to terminal width.
|
|
1933
|
+
const truncated = line.length > width - 2 ? line.slice(0, width - 5) + '...' : line;
|
|
1934
|
+
this.screen.writeLine(y++, ` ${truncated}`, color);
|
|
1935
|
+
}
|
|
1936
|
+
if (item.lines.length > maxDiffLines) {
|
|
1937
|
+
this.screen.writeLine(y++, ` … (${item.lines.length - maxDiffLines} more lines)`, fg.gray);
|
|
1938
|
+
}
|
|
1939
|
+
y++;
|
|
1940
|
+
// Key legend
|
|
1941
|
+
this.screen.writeLine(y, 'y/Enter accept • n skip • a accept all • q/Esc quit • ↑/↓ navigate', fg.gray);
|
|
1942
|
+
}
|
|
1412
1943
|
/**
|
|
1413
1944
|
* Render input line
|
|
1414
1945
|
*/
|
|
@@ -1477,12 +2008,12 @@ export class App {
|
|
|
1477
2008
|
this.screen.showCursor(false);
|
|
1478
2009
|
return;
|
|
1479
2010
|
}
|
|
1480
|
-
//
|
|
2011
|
+
// Keep the composer available while the agent works so the user can steer
|
|
2012
|
+
// the run without losing context or waiting for the current tool to finish.
|
|
1481
2013
|
if (this.isAgentRunning) {
|
|
2014
|
+
const promptSymbol = '❯ ';
|
|
2015
|
+
const maxInputWidth = Math.max(1, width - promptSymbol.length - 1);
|
|
1482
2016
|
if (inputValue) {
|
|
1483
|
-
// User is typing a reply — show their input with a prompt
|
|
1484
|
-
const promptSymbol = '❯ ';
|
|
1485
|
-
const maxInputWidth = width - promptSymbol.length - 1;
|
|
1486
2017
|
const displayInput = inputValue.length <= maxInputWidth ? inputValue : '…' + inputValue.slice(-(maxInputWidth - 1));
|
|
1487
2018
|
this.screen.write(0, y, promptSymbol, PRIMARY_COLOR);
|
|
1488
2019
|
this.screen.write(promptSymbol.length, y, displayInput + ' ');
|
|
@@ -1493,13 +2024,12 @@ export class App {
|
|
|
1493
2024
|
}
|
|
1494
2025
|
}
|
|
1495
2026
|
else {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
this.screen.showCursor(false);
|
|
2027
|
+
this.screen.write(0, y, promptSymbol, PRIMARY_COLOR + style.bold);
|
|
2028
|
+
this.screen.write(promptSymbol.length, y, 'Reply to agent…', fg.gray);
|
|
2029
|
+
if (!hideCursor) {
|
|
2030
|
+
this.screen.setCursor(promptSymbol.length, y);
|
|
2031
|
+
this.screen.showCursor(true);
|
|
2032
|
+
}
|
|
1503
2033
|
}
|
|
1504
2034
|
return;
|
|
1505
2035
|
}
|
|
@@ -1771,6 +2301,46 @@ export class App {
|
|
|
1771
2301
|
const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
|
|
1772
2302
|
this.screen.writeLine(y, `↑↓ navigate • Tab/Enter select • Esc cancel${scrollInfo}`, fg.gray);
|
|
1773
2303
|
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Render inline `@mention` file picker below the status bar.
|
|
2306
|
+
*
|
|
2307
|
+
* Mirrors the layout of `renderInlineAutocomplete` (separator → title →
|
|
2308
|
+
* items → footer) but shows file paths with their parent directory as
|
|
2309
|
+
* the description, and a `@` prefix instead of `/`.
|
|
2310
|
+
*/
|
|
2311
|
+
renderInlineMentionPicker(startY, width) {
|
|
2312
|
+
const items = this.mentionItems;
|
|
2313
|
+
const maxVisible = Math.min(items.length, 8);
|
|
2314
|
+
let y = startY;
|
|
2315
|
+
// Separator line
|
|
2316
|
+
this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
|
|
2317
|
+
// Title
|
|
2318
|
+
this.screen.writeLine(y++, 'Add file to context (@mention)', PRIMARY_COLOR + style.bold);
|
|
2319
|
+
// Items: `path` + directory detail
|
|
2320
|
+
const visibleStart = Math.max(0, this.mentionIndex - maxVisible + 1);
|
|
2321
|
+
const visibleItems = items.slice(visibleStart, visibleStart + maxVisible);
|
|
2322
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
2323
|
+
const item = visibleItems[i];
|
|
2324
|
+
const actualIndex = visibleStart + i;
|
|
2325
|
+
const isSelected = actualIndex === this.mentionIndex;
|
|
2326
|
+
const prefix = isSelected ? '► ' : ' ';
|
|
2327
|
+
const pathText = ('@' + item.label).padEnd(40);
|
|
2328
|
+
if (isSelected) {
|
|
2329
|
+
this.screen.write(0, y, prefix, PRIMARY_COLOR);
|
|
2330
|
+
this.screen.write(prefix.length, y, pathText, PRIMARY_COLOR + style.bold);
|
|
2331
|
+
this.screen.write(prefix.length + pathText.length, y, item.detail, fg.white);
|
|
2332
|
+
}
|
|
2333
|
+
else {
|
|
2334
|
+
this.screen.write(0, y, prefix, '');
|
|
2335
|
+
this.screen.write(prefix.length, y, pathText, fg.cyan);
|
|
2336
|
+
this.screen.write(prefix.length + pathText.length, y, item.detail, fg.gray);
|
|
2337
|
+
}
|
|
2338
|
+
y++;
|
|
2339
|
+
}
|
|
2340
|
+
// Footer
|
|
2341
|
+
const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
|
|
2342
|
+
this.screen.writeLine(y, `↑↓ navigate • Tab select • Esc cancel${scrollInfo}`, fg.gray);
|
|
2343
|
+
}
|
|
1774
2344
|
/**
|
|
1775
2345
|
* Render inline permission dialog
|
|
1776
2346
|
*/
|
|
@@ -2046,7 +2616,58 @@ export class App {
|
|
|
2046
2616
|
}
|
|
2047
2617
|
const status = this.options.getStatus();
|
|
2048
2618
|
const stats = status.tokenStats;
|
|
2049
|
-
|
|
2619
|
+
const rightText = statusBarRightHint({
|
|
2620
|
+
scrollOffset: this.scrollOffset,
|
|
2621
|
+
unseenWhileScrolled: this.unseenWhileScrolled,
|
|
2622
|
+
isStreaming: this.isStreaming,
|
|
2623
|
+
isLoading: this.isLoading,
|
|
2624
|
+
});
|
|
2625
|
+
if (this.scrollOffset > 0 && this.unseenWhileScrolled > 0) {
|
|
2626
|
+
this.screen.write(width - rightText.length, y, rightText, PRIMARY_COLOR);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
// Wide terminals get an operational footer: elapsed time and honest,
|
|
2630
|
+
// explicitly-labelled resource ranges. These are estimates, never implied
|
|
2631
|
+
// to be provider measurements.
|
|
2632
|
+
if (width >= 110 && stats) {
|
|
2633
|
+
const totalTokens = Math.max(0, stats.totalTokens);
|
|
2634
|
+
const elapsed = formatElapsed(Date.now() - this.appStartedAt);
|
|
2635
|
+
const leftParts = [
|
|
2636
|
+
`runtime ${elapsed}`,
|
|
2637
|
+
`tokens ${formatTokenCount(totalTokens)}`,
|
|
2638
|
+
];
|
|
2639
|
+
if (typeof stats.estimatedCost === 'number' && stats.estimatedCost > 0) {
|
|
2640
|
+
leftParts.push(`cost $${stats.estimatedCost < 0.01 ? stats.estimatedCost.toFixed(4) : stats.estimatedCost.toFixed(2)}`);
|
|
2641
|
+
}
|
|
2642
|
+
// Thinking-effort tier, same chip the compact fallback shows beside the
|
|
2643
|
+
// model. Only present when set + supported — see getStatus.
|
|
2644
|
+
if (status.reasoningEffort) {
|
|
2645
|
+
leftParts.push(`effort ${status.reasoningEffort}`);
|
|
2646
|
+
}
|
|
2647
|
+
const leftText = leftParts.join(' · ');
|
|
2648
|
+
this.screen.write(1, y, leftText, fg.gray);
|
|
2649
|
+
// The right edge belongs to the hint: while streaming it reads
|
|
2650
|
+
// "Esc to stop", the only on-screen affordance for interrupting a run.
|
|
2651
|
+
// Claim it first, then spend whatever gap is left on the (decorative)
|
|
2652
|
+
// resource estimate — never the other way around.
|
|
2653
|
+
// Same right-edge column as the compact fallback and the scroll badge, so
|
|
2654
|
+
// the hint doesn't shift by one when the terminal crosses 110 columns.
|
|
2655
|
+
let rightEdge = width;
|
|
2656
|
+
if (rightText && width - rightText.length > leftText.length + 3) {
|
|
2657
|
+
rightEdge = width - rightText.length;
|
|
2658
|
+
this.screen.write(rightEdge, y, rightText, fg.gray);
|
|
2659
|
+
}
|
|
2660
|
+
if (totalTokens > 0 && width >= 138) {
|
|
2661
|
+
const impact = formatResourceImpact(estimateResourceImpact(totalTokens));
|
|
2662
|
+
const impactText = `energy ${impact.energy} est · water ${impact.water} est`;
|
|
2663
|
+
const impactX = rightEdge - impactText.length - 3;
|
|
2664
|
+
if (impactX > leftText.length + 3) {
|
|
2665
|
+
this.screen.write(impactX, y, impactText, fg.gray);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
// Compact fallback: model · messages · token count.
|
|
2050
2671
|
const modelName = status.model || '';
|
|
2051
2672
|
const msgCount = `${this.messages.length} msg`;
|
|
2052
2673
|
const tokenStr = stats && stats.totalTokens > 0
|
|
@@ -2073,19 +2694,6 @@ export class App {
|
|
|
2073
2694
|
this.screen.write(leftX, y, ' · ', fg.gray);
|
|
2074
2695
|
this.screen.write(leftX + 3, y, tokenStr, fg.gray);
|
|
2075
2696
|
}
|
|
2076
|
-
// Right: context-sensitive hints. While scrolled up, the "new
|
|
2077
|
-
// messages below" badge takes priority — it's the only signal that
|
|
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
|
-
});
|
|
2085
|
-
if (this.scrollOffset > 0 && this.unseenWhileScrolled > 0) {
|
|
2086
|
-
this.screen.write(width - rightText.length, y, rightText, PRIMARY_COLOR);
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
2697
|
this.screen.write(width - rightText.length, y, rightText, fg.gray);
|
|
2090
2698
|
}
|
|
2091
2699
|
/**
|
|
@@ -2094,20 +2702,6 @@ export class App {
|
|
|
2094
2702
|
getVisibleMessages(height, width) {
|
|
2095
2703
|
const allLines = [];
|
|
2096
2704
|
this.codeBlockCounter.current = 0; // Reset block counter for each render pass
|
|
2097
|
-
// Logo at the top, scrolls with content
|
|
2098
|
-
if (height >= 20) {
|
|
2099
|
-
const logoWidth = LOGO_LINES[0].length;
|
|
2100
|
-
const logoX = Math.max(0, Math.floor((width - logoWidth) / 2));
|
|
2101
|
-
const pad = ' '.repeat(logoX);
|
|
2102
|
-
for (const line of LOGO_LINES) {
|
|
2103
|
-
allLines.push({ text: pad + line, style: PRIMARY_COLOR, raw: false });
|
|
2104
|
-
}
|
|
2105
|
-
allLines.push({ text: '', style: '' });
|
|
2106
|
-
}
|
|
2107
|
-
else {
|
|
2108
|
-
allLines.push({ text: ' Codeep', style: PRIMARY_COLOR, raw: false });
|
|
2109
|
-
allLines.push({ text: '', style: '' });
|
|
2110
|
-
}
|
|
2111
2705
|
for (let i = 0; i < this.messages.length; i++) {
|
|
2112
2706
|
const msg = this.messages[i];
|
|
2113
2707
|
const cached = this.messageCache[i];
|