codeep 2.16.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 +29 -4
- package/dist/acp/serverHandlers.js +1 -1
- package/dist/config/index.js +20 -4
- package/dist/config/providers.d.ts +3 -2
- package/dist/config/providers.js +158 -70
- package/dist/renderer/App.d.ts +12 -0
- package/dist/renderer/App.js +354 -40
- package/dist/renderer/Screen.d.ts +1 -0
- package/dist/renderer/Screen.js +8 -3
- package/dist/renderer/commands/helpers.d.ts +1 -0
- package/dist/renderer/commands/helpers.js +4 -1
- package/dist/renderer/commands.js +26 -4
- package/dist/renderer/components/AgentTimeline.d.ts +44 -0
- package/dist/renderer/components/AgentTimeline.js +157 -0
- package/dist/renderer/components/Status.d.ts +2 -0
- package/dist/renderer/main.js +76 -29
- 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/resourceImpact.d.ts +25 -0
- package/dist/utils/resourceImpact.js +54 -0
- package/dist/utils/tokenTracker.js +50 -41
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/dist/renderer/App.d.ts
CHANGED
|
@@ -89,6 +89,10 @@ export declare class App {
|
|
|
89
89
|
private agentThinking;
|
|
90
90
|
private agentWaitingForAI;
|
|
91
91
|
private agentLog;
|
|
92
|
+
/** Process uptime shown in the persistent footer. */
|
|
93
|
+
private appStartedAt;
|
|
94
|
+
/** Start of the current agent run; unlike app uptime, resets per task. */
|
|
95
|
+
private agentStartedAt;
|
|
92
96
|
private pasteInfo;
|
|
93
97
|
private pasteInfoOpen;
|
|
94
98
|
private codeBlockCounter;
|
|
@@ -433,6 +437,14 @@ export declare class App {
|
|
|
433
437
|
* Render chat screen
|
|
434
438
|
*/
|
|
435
439
|
private renderChat;
|
|
440
|
+
private shouldRenderAgentTimeline;
|
|
441
|
+
private renderPersistentHeader;
|
|
442
|
+
private renderAgentTimelineScreen;
|
|
443
|
+
private renderAgentContextRail;
|
|
444
|
+
private renderAgentKeyHints;
|
|
445
|
+
private timelineStatusStyle;
|
|
446
|
+
private currentActionType;
|
|
447
|
+
private currentAgentTask;
|
|
436
448
|
/**
|
|
437
449
|
* Render inline confirmation dialog below status bar
|
|
438
450
|
*/
|
package/dist/renderer/App.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
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
14
|
import { filterCommands, detectMentionQuery } from './components/Autocomplete.js';
|
|
@@ -14,6 +16,7 @@ import { suggestMentions } from '../utils/mentions.js';
|
|
|
14
16
|
import { handleInlineStatusKey, handleInlineHelpKey, handleMenuKey, handleInlinePermissionKey, handleInlineSessionPickerKey, handleInlineConfirmKey, handleLoginKey, } from './handlers.js';
|
|
15
17
|
import clipboardy from 'clipboardy';
|
|
16
18
|
import { readImageFromClipboard } from '../utils/clipboard.js';
|
|
19
|
+
import { estimateResourceImpact, formatResourceImpact } from '../utils/resourceImpact.js';
|
|
17
20
|
// (PRIMARY_COLOR, SPINNER_FRAMES, LOGO_LINES, LOGO_HEIGHT moved to
|
|
18
21
|
// ./components/uiConstants — imported above.)
|
|
19
22
|
// ─── Command metadata ────────────────────────────────────────────────────────
|
|
@@ -57,6 +60,10 @@ export class App {
|
|
|
57
60
|
agentThinking = '';
|
|
58
61
|
agentWaitingForAI = false;
|
|
59
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;
|
|
60
67
|
// Paste detection state
|
|
61
68
|
pasteInfo = null;
|
|
62
69
|
pasteInfoOpen = false;
|
|
@@ -323,8 +330,11 @@ export class App {
|
|
|
323
330
|
* Set agent running state
|
|
324
331
|
*/
|
|
325
332
|
setAgentRunning(running) {
|
|
333
|
+
const wasRunning = this.isAgentRunning;
|
|
326
334
|
this.isAgentRunning = running;
|
|
327
335
|
if (running) {
|
|
336
|
+
if (!wasRunning)
|
|
337
|
+
this.agentStartedAt = Date.now();
|
|
328
338
|
this.agentIteration = 0;
|
|
329
339
|
this.agentMaxIterations = 0;
|
|
330
340
|
this.agentActions = [];
|
|
@@ -335,6 +345,7 @@ export class App {
|
|
|
335
345
|
this.startSpinner();
|
|
336
346
|
}
|
|
337
347
|
else {
|
|
348
|
+
this.agentStartedAt = null;
|
|
338
349
|
this.isLoading = false; // Ensure loading is cleared when agent finishes
|
|
339
350
|
this.stopSpinner();
|
|
340
351
|
}
|
|
@@ -1440,6 +1451,10 @@ export class App {
|
|
|
1440
1451
|
return;
|
|
1441
1452
|
}
|
|
1442
1453
|
this.screen.clear();
|
|
1454
|
+
if (this.shouldRenderAgentTimeline(width, height)) {
|
|
1455
|
+
this.renderAgentTimelineScreen(width, height);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1443
1458
|
// If menu or settings is open, reserve space for it at bottom
|
|
1444
1459
|
const panelHeight = bottomPanelHeight({
|
|
1445
1460
|
height,
|
|
@@ -1473,11 +1488,15 @@ export class App {
|
|
|
1473
1488
|
});
|
|
1474
1489
|
const layout = chatLayout(height, panelHeight);
|
|
1475
1490
|
const mainHeight = layout.mainHeight;
|
|
1476
|
-
const
|
|
1491
|
+
const headerHeight = width >= 60 && height >= 16 ? 2 : 0;
|
|
1492
|
+
const messagesStart = Math.min(layout.messagesEnd, headerHeight);
|
|
1477
1493
|
const messagesEnd = layout.messagesEnd;
|
|
1478
1494
|
const separatorLine = layout.separatorLine;
|
|
1479
1495
|
const inputLine = layout.inputLine;
|
|
1480
1496
|
const statusLine = layout.statusLine;
|
|
1497
|
+
if (headerHeight > 0) {
|
|
1498
|
+
this.renderPersistentHeader(width);
|
|
1499
|
+
}
|
|
1481
1500
|
// Messages
|
|
1482
1501
|
const messagesHeight = Math.max(1, messagesEnd - messagesStart + 1);
|
|
1483
1502
|
const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
|
|
@@ -1565,6 +1584,278 @@ export class App {
|
|
|
1565
1584
|
}
|
|
1566
1585
|
this.screen.render();
|
|
1567
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
|
+
}
|
|
1568
1859
|
/**
|
|
1569
1860
|
* Render inline confirmation dialog below status bar
|
|
1570
1861
|
*/
|
|
@@ -1717,12 +2008,12 @@ export class App {
|
|
|
1717
2008
|
this.screen.showCursor(false);
|
|
1718
2009
|
return;
|
|
1719
2010
|
}
|
|
1720
|
-
//
|
|
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.
|
|
1721
2013
|
if (this.isAgentRunning) {
|
|
2014
|
+
const promptSymbol = '❯ ';
|
|
2015
|
+
const maxInputWidth = Math.max(1, width - promptSymbol.length - 1);
|
|
1722
2016
|
if (inputValue) {
|
|
1723
|
-
// User is typing a reply — show their input with a prompt
|
|
1724
|
-
const promptSymbol = '❯ ';
|
|
1725
|
-
const maxInputWidth = width - promptSymbol.length - 1;
|
|
1726
2017
|
const displayInput = inputValue.length <= maxInputWidth ? inputValue : '…' + inputValue.slice(-(maxInputWidth - 1));
|
|
1727
2018
|
this.screen.write(0, y, promptSymbol, PRIMARY_COLOR);
|
|
1728
2019
|
this.screen.write(promptSymbol.length, y, displayInput + ' ');
|
|
@@ -1733,13 +2024,12 @@ export class App {
|
|
|
1733
2024
|
}
|
|
1734
2025
|
}
|
|
1735
2026
|
else {
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
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
|
+
}
|
|
1743
2033
|
}
|
|
1744
2034
|
return;
|
|
1745
2035
|
}
|
|
@@ -2326,7 +2616,58 @@ export class App {
|
|
|
2326
2616
|
}
|
|
2327
2617
|
const status = this.options.getStatus();
|
|
2328
2618
|
const stats = status.tokenStats;
|
|
2329
|
-
|
|
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.
|
|
2330
2671
|
const modelName = status.model || '';
|
|
2331
2672
|
const msgCount = `${this.messages.length} msg`;
|
|
2332
2673
|
const tokenStr = stats && stats.totalTokens > 0
|
|
@@ -2353,19 +2694,6 @@ export class App {
|
|
|
2353
2694
|
this.screen.write(leftX, y, ' · ', fg.gray);
|
|
2354
2695
|
this.screen.write(leftX + 3, y, tokenStr, fg.gray);
|
|
2355
2696
|
}
|
|
2356
|
-
// Right: context-sensitive hints. While scrolled up, the "new
|
|
2357
|
-
// messages below" badge takes priority — it's the only signal that
|
|
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
|
-
});
|
|
2365
|
-
if (this.scrollOffset > 0 && this.unseenWhileScrolled > 0) {
|
|
2366
|
-
this.screen.write(width - rightText.length, y, rightText, PRIMARY_COLOR);
|
|
2367
|
-
return;
|
|
2368
|
-
}
|
|
2369
2697
|
this.screen.write(width - rightText.length, y, rightText, fg.gray);
|
|
2370
2698
|
}
|
|
2371
2699
|
/**
|
|
@@ -2374,20 +2702,6 @@ export class App {
|
|
|
2374
2702
|
getVisibleMessages(height, width) {
|
|
2375
2703
|
const allLines = [];
|
|
2376
2704
|
this.codeBlockCounter.current = 0; // Reset block counter for each render pass
|
|
2377
|
-
// Logo at the top, scrolls with content
|
|
2378
|
-
if (height >= 20) {
|
|
2379
|
-
const logoWidth = LOGO_LINES[0].length;
|
|
2380
|
-
const logoX = Math.max(0, Math.floor((width - logoWidth) / 2));
|
|
2381
|
-
const pad = ' '.repeat(logoX);
|
|
2382
|
-
for (const line of LOGO_LINES) {
|
|
2383
|
-
allLines.push({ text: pad + line, style: PRIMARY_COLOR, raw: false });
|
|
2384
|
-
}
|
|
2385
|
-
allLines.push({ text: '', style: '' });
|
|
2386
|
-
}
|
|
2387
|
-
else {
|
|
2388
|
-
allLines.push({ text: ' Codeep', style: PRIMARY_COLOR, raw: false });
|
|
2389
|
-
allLines.push({ text: '', style: '' });
|
|
2390
|
-
}
|
|
2391
2705
|
for (let i = 0; i < this.messages.length; i++) {
|
|
2392
2706
|
const msg = this.messages[i];
|
|
2393
2707
|
const cached = this.messageCache[i];
|
package/dist/renderer/Screen.js
CHANGED
|
@@ -12,13 +12,15 @@ export class Screen {
|
|
|
12
12
|
cursorY = 0;
|
|
13
13
|
cursorVisible = true;
|
|
14
14
|
resizeCallback = null;
|
|
15
|
+
resizeHandler;
|
|
15
16
|
constructor() {
|
|
16
17
|
this.width = process.stdout.columns || 80;
|
|
17
18
|
this.height = process.stdout.rows || 24;
|
|
18
19
|
this.buffer = this.createEmptyBuffer();
|
|
19
20
|
this.rendered = this.createEmptyBuffer();
|
|
20
|
-
//
|
|
21
|
-
|
|
21
|
+
// Keep the exact handler so cleanup can detach it. This matters for
|
|
22
|
+
// embedders/tests that create more than one Screen in the same process.
|
|
23
|
+
this.resizeHandler = () => {
|
|
22
24
|
this.width = process.stdout.columns || 80;
|
|
23
25
|
this.height = process.stdout.rows || 24;
|
|
24
26
|
this.buffer = this.createEmptyBuffer();
|
|
@@ -28,7 +30,8 @@ export class Screen {
|
|
|
28
30
|
if (this.resizeCallback) {
|
|
29
31
|
this.resizeCallback();
|
|
30
32
|
}
|
|
31
|
-
}
|
|
33
|
+
};
|
|
34
|
+
process.stdout.on('resize', this.resizeHandler);
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
34
37
|
* Register a callback to be called on terminal resize
|
|
@@ -318,6 +321,8 @@ export class Screen {
|
|
|
318
321
|
* Cleanup (show cursor, clear)
|
|
319
322
|
*/
|
|
320
323
|
cleanup() {
|
|
324
|
+
process.stdout.removeListener('resize', this.resizeHandler);
|
|
325
|
+
this.resizeCallback = null;
|
|
321
326
|
process.stdout.write(style.reset + screen.clear + cursor.home + cursor.show);
|
|
322
327
|
}
|
|
323
328
|
}
|
|
@@ -104,6 +104,7 @@ export declare function formatStatsReport(args: {
|
|
|
104
104
|
pricing: PricingRow[];
|
|
105
105
|
currentProvider: string;
|
|
106
106
|
fmt: TokenFormatter;
|
|
107
|
+
impactLines?: string[];
|
|
107
108
|
}): string;
|
|
108
109
|
/**
|
|
109
110
|
* Extract every fenced code block body (the text inside ```…```) from a
|
|
@@ -128,7 +128,7 @@ export function formatModelCost(provider, estimatedCost) {
|
|
|
128
128
|
* whether the total shows "free" (ollama) or a dollar figure.
|
|
129
129
|
*/
|
|
130
130
|
export function formatStatsReport(args) {
|
|
131
|
-
const { totals, breakdown, cache, pricing, currentProvider, fmt } = args;
|
|
131
|
+
const { totals, breakdown, cache, pricing, currentProvider, fmt, impactLines = [] } = args;
|
|
132
132
|
const lines = ['## Session Cost', ''];
|
|
133
133
|
if (totals.requestCount === 0) {
|
|
134
134
|
lines.push('*No API calls made yet this session.*', '');
|
|
@@ -160,6 +160,9 @@ export function formatStatsReport(args) {
|
|
|
160
160
|
lines.push(`Estimated savings vs no caching: $${cache.estimatedSavingsUsd.toFixed(4)}`);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
if (impactLines.length > 0) {
|
|
164
|
+
lines.push('', ...impactLines);
|
|
165
|
+
}
|
|
163
166
|
lines.push('');
|
|
164
167
|
}
|
|
165
168
|
lines.push('### Pricing (per 1M tokens)');
|
|
@@ -166,6 +166,26 @@ export async function handleCommand(command, args, ctx) {
|
|
|
166
166
|
});
|
|
167
167
|
break;
|
|
168
168
|
}
|
|
169
|
+
if (providerId === 'modelscope') {
|
|
170
|
+
ctx.app.notify('Fetching ModelScope catalog…');
|
|
171
|
+
const { fetchOpenAiCompatibleModels, getApiKey: _getKey } = await import('../config/index.js');
|
|
172
|
+
const base = 'https://api-inference.modelscope.cn/v1';
|
|
173
|
+
const models = await fetchOpenAiCompatibleModels(base, _getKey('modelscope') || undefined);
|
|
174
|
+
const fallback = getModelsForCurrentProvider();
|
|
175
|
+
const available = models && models.length > 0
|
|
176
|
+
? models
|
|
177
|
+
: Object.keys(fallback).map(id => ({ id, name: id, description: 'Built-in fallback' }));
|
|
178
|
+
if (!models || models.length === 0) {
|
|
179
|
+
ctx.app.notify('Could not fetch the ModelScope catalog. Using the built-in fallback model.');
|
|
180
|
+
}
|
|
181
|
+
const modelItems = available.map(m => ({ key: m.id, label: m.name, description: m.description }));
|
|
182
|
+
const currentModel = config.get('model');
|
|
183
|
+
ctx.app.showSelect(`Select ModelScope Model (${available.length})`, modelItems, currentModel, (item) => {
|
|
184
|
+
config.set('model', item.key);
|
|
185
|
+
ctx.app.notify(`Model: ${item.key}`);
|
|
186
|
+
});
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
169
189
|
if (providerId === 'custom') {
|
|
170
190
|
const base = config.get('customBaseUrl') || 'http://localhost:8000/v1';
|
|
171
191
|
ctx.app.notify(`Fetching models from ${base}…`);
|
|
@@ -304,7 +324,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
304
324
|
const providerId = config.get('provider');
|
|
305
325
|
const model = config.get('model');
|
|
306
326
|
const supported = modelSupportsReasoningEffort(providerId, model);
|
|
307
|
-
// Tiers THIS model actually distinguishes (e.g.
|
|
327
|
+
// Tiers THIS model actually distinguishes (e.g. Kimi K3 → auto/low/high/max).
|
|
308
328
|
const available = availableReasoningTiers(providerId, model);
|
|
309
329
|
const sub = args[0]?.toLowerCase();
|
|
310
330
|
if (sub && REASONING_TIERS.includes(sub)) {
|
|
@@ -313,11 +333,11 @@ export async function handleCommand(command, args, ctx) {
|
|
|
313
333
|
ctx.app.notify('Thinking effort: auto — each model uses its own default.');
|
|
314
334
|
}
|
|
315
335
|
else if (!supported) {
|
|
316
|
-
ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x, Gemini 3, DeepSeek V4,
|
|
336
|
+
ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x, Gemini 3, DeepSeek V4, Kimi K3).`);
|
|
317
337
|
}
|
|
318
338
|
else {
|
|
319
339
|
// Tell the user what THIS model will actually run (the tier may
|
|
320
|
-
// collapse onto a level the model distinguishes, e.g.
|
|
340
|
+
// collapse onto a level the model distinguishes, e.g. medium→high on Kimi K3).
|
|
321
341
|
const resolved = resolveReasoningTier(providerId, model, sub);
|
|
322
342
|
const note = resolved === sub ? '' : ` (${model} runs this as "${resolved}")`;
|
|
323
343
|
ctx.app.notify(`Thinking effort: ${sub}${note} — sending ${JSON.stringify(reasoningParamsFor(providerId, model, sub))}.`);
|
|
@@ -346,7 +366,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
346
366
|
if (supported)
|
|
347
367
|
tLines.push(`**Available** ${available.join(' · ')}`);
|
|
348
368
|
tLines.push('');
|
|
349
|
-
tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (
|
|
369
|
+
tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (DeepSeek → high · max; Kimi K3 → low · high · max; Gemini → low · high; Opus/Sonnet & GPT-5.x → the full set). The setting is global and clamps to the active model, so it never sends a value the API rejects. `/effort` is an alias.');
|
|
350
370
|
ctx.app.addMessage({ role: 'system', content: tLines.join('\n') });
|
|
351
371
|
break;
|
|
352
372
|
}
|
|
@@ -2261,6 +2281,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2261
2281
|
// case no longer also claims 'cost' (which always hit the handler above).
|
|
2262
2282
|
case 'stats': {
|
|
2263
2283
|
const { getCostBreakdown, getSessionStats, formatTokenCount, getPricingTable, getCacheStats } = await import('../utils/tokenTracker.js');
|
|
2284
|
+
const { formatResourceImpactReport } = await import('../utils/resourceImpact.js');
|
|
2264
2285
|
const stats = getSessionStats();
|
|
2265
2286
|
const content = formatStatsReport({
|
|
2266
2287
|
totals: stats,
|
|
@@ -2269,6 +2290,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2269
2290
|
pricing: getPricingTable(),
|
|
2270
2291
|
currentProvider: config.get('provider'),
|
|
2271
2292
|
fmt: formatTokenCount,
|
|
2293
|
+
impactLines: formatResourceImpactReport(stats.totalTokens),
|
|
2272
2294
|
});
|
|
2273
2295
|
ctx.app.addMessage({ role: 'system', content });
|
|
2274
2296
|
break;
|