@trenchwork/coder 1.5.6 → 1.5.8
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/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +31 -9
- package/dist/core/agent.js.map +1 -1
- package/dist/core/contextManager.d.ts.map +1 -1
- package/dist/core/contextManager.js +27 -8
- package/dist/core/contextManager.js.map +1 -1
- package/dist/core/contextWindow.d.ts.map +1 -1
- package/dist/core/contextWindow.js +8 -5
- package/dist/core/contextWindow.js.map +1 -1
- package/dist/core/errorDisplay.d.ts +17 -0
- package/dist/core/errorDisplay.d.ts.map +1 -0
- package/dist/core/errorDisplay.js +47 -0
- package/dist/core/errorDisplay.js.map +1 -0
- package/dist/core/sessionStore.d.ts.map +1 -1
- package/dist/core/sessionStore.js +14 -2
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/updateChecker.d.ts.map +1 -1
- package/dist/core/updateChecker.js +39 -0
- package/dist/core/updateChecker.js.map +1 -1
- package/dist/headless/interactiveShell.d.ts.map +1 -1
- package/dist/headless/interactiveShell.js +220 -138
- package/dist/headless/interactiveShell.js.map +1 -1
- package/dist/leanAgent.d.ts.map +1 -1
- package/dist/leanAgent.js +13 -3
- package/dist/leanAgent.js.map +1 -1
- package/dist/plugins/providers/deepseek/index.d.ts.map +1 -1
- package/dist/plugins/providers/deepseek/index.js +14 -2
- package/dist/plugins/providers/deepseek/index.js.map +1 -1
- package/dist/providers/openaiChatCompletionsProvider.d.ts +6 -0
- package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -1
- package/dist/providers/openaiChatCompletionsProvider.js +42 -12
- package/dist/providers/openaiChatCompletionsProvider.js.map +1 -1
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +22 -8
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/ui/ink/App.d.ts.map +1 -1
- package/dist/ui/ink/App.js +10 -1
- package/dist/ui/ink/App.js.map +1 -1
- package/dist/ui/ink/ChatStatic.d.ts.map +1 -1
- package/dist/ui/ink/ChatStatic.js +4 -2
- package/dist/ui/ink/ChatStatic.js.map +1 -1
- package/dist/ui/ink/InkPromptController.d.ts +15 -0
- package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
- package/dist/ui/ink/InkPromptController.js +92 -14
- package/dist/ui/ink/InkPromptController.js.map +1 -1
- package/dist/ui/ink/Menu.d.ts +5 -0
- package/dist/ui/ink/Menu.d.ts.map +1 -1
- package/dist/ui/ink/Menu.js +7 -3
- package/dist/ui/ink/Menu.js.map +1 -1
- package/dist/ui/ink/Prompt.d.ts +2 -0
- package/dist/ui/ink/Prompt.d.ts.map +1 -1
- package/dist/ui/ink/Prompt.js +23 -1
- package/dist/ui/ink/Prompt.js.map +1 -1
- package/dist/ui/ink/StatusLine.js +1 -1
- package/dist/ui/ink/StatusLine.js.map +1 -1
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ import { exec as childExec } from 'node:child_process';
|
|
|
20
20
|
import { promisify } from 'node:util';
|
|
21
21
|
import chalk from 'chalk';
|
|
22
22
|
import { getHITL, hitlEvents, setDecisionPresenter } from '../core/hitl.js';
|
|
23
|
+
import { formatErrorForDisplay } from '../core/errorDisplay.js';
|
|
23
24
|
// Connector imports removed — CLI is local-only, no GitHub gate.
|
|
24
25
|
// Stub functions (antiTermination removed)
|
|
25
26
|
const initializeProtection = (_config) => { };
|
|
@@ -105,9 +106,21 @@ async function* iterateWithTimeout(iterator, timeoutMs, onTimeout) {
|
|
|
105
106
|
result = await pending;
|
|
106
107
|
}
|
|
107
108
|
else {
|
|
108
|
-
// Race between pending result and timeout
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// Race between pending result and timeout. The timer MUST be cleared
|
|
110
|
+
// once the race settles: Promise.race does not cancel losers, so the
|
|
111
|
+
// old discarded timer id left one live 10-minute timer PER CONSUMED
|
|
112
|
+
// EVENT — tens of thousands of armed timers (and ~15MB of pinned
|
|
113
|
+
// closures) per fast-streaming turn, holding the event loop open.
|
|
114
|
+
let timeoutId;
|
|
115
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
116
|
+
timeoutId = setTimeout(() => resolve({ __timeout: true }), timeoutMs);
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
result = await Promise.race([pending, timeoutPromise]);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
clearTimeout(timeoutId);
|
|
123
|
+
}
|
|
111
124
|
}
|
|
112
125
|
if ('__timeout' in result) {
|
|
113
126
|
onTimeout?.();
|
|
@@ -155,7 +168,10 @@ function welcomeBodyLines(input) {
|
|
|
155
168
|
const body = [title, ''];
|
|
156
169
|
const mode = input.keyMode ?? (input.hasApiKey ? 'own' : 'none');
|
|
157
170
|
if (mode === 'own') {
|
|
158
|
-
|
|
171
|
+
// §7 shape: model + /help, then cwd. No provider chip (redundant with
|
|
172
|
+
// the model name) and no key material in chrome — Claude Code never
|
|
173
|
+
// surfaces credentials in the banner; /keys still shows the masked key.
|
|
174
|
+
body.push(`${input.model} · /help for commands`);
|
|
159
175
|
}
|
|
160
176
|
else {
|
|
161
177
|
body.push('⚠ No DeepSeek API key configured', '', ' /key sk-… DeepSeek (required) · platform.deepseek.com', ' /key tvly-… Tavily web search (optional) · tavily.com');
|
|
@@ -190,7 +206,9 @@ function roundedBox(content, paint = (s) => s, border = (s) => s) {
|
|
|
190
206
|
* WHICH lines appear.
|
|
191
207
|
*/
|
|
192
208
|
export function composeWelcomeLines(input) {
|
|
193
|
-
|
|
209
|
+
// No trailing blank: ChatStatic already adds the §1 one-line gap before the
|
|
210
|
+
// next block — a built-in trailing blank made it a double gap.
|
|
211
|
+
return ['', ...(input.updateLines ?? []), ...roundedBox(welcomeBodyLines(input))];
|
|
194
212
|
}
|
|
195
213
|
/**
|
|
196
214
|
* Run the fully interactive shell with rich UI.
|
|
@@ -285,6 +303,11 @@ class InteractiveShell {
|
|
|
285
303
|
};
|
|
286
304
|
pendingModelSwitch = null;
|
|
287
305
|
currentResponseBuffer = '';
|
|
306
|
+
// What this turn already rendered as an 'error' — dedupes the event-vs-
|
|
307
|
+
// rejection double print of the same provider failure.
|
|
308
|
+
lastShownTurnError = null;
|
|
309
|
+
// One-time per session: real prompt_tokens exceeded the configured window.
|
|
310
|
+
warnedWindowDrift = false;
|
|
288
311
|
// The turn's final assistant text, captured BEFORE currentResponseBuffer is
|
|
289
312
|
// cleared on message.complete. The auto-continue refusal/completion/governor
|
|
290
313
|
// reads run in the `finally`, AFTER that clear, so reading the buffer there saw
|
|
@@ -491,31 +514,25 @@ class InteractiveShell {
|
|
|
491
514
|
return key.slice(0, 3) + '...' + key.slice(-3);
|
|
492
515
|
return key.slice(0, 6) + '...' + key.slice(-4);
|
|
493
516
|
};
|
|
494
|
-
// Update check
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
// previous order built welcomeLines with `...updateLines` (the array
|
|
503
|
-
// was empty at that point) and only populated updateLines afterwards,
|
|
504
|
-
// so the upgrade banner literally never rendered. Bug shipped before
|
|
505
|
-
// the scoped-package rename made the check return wrong data anyway.
|
|
506
|
-
const updateInfo = await updatePromise;
|
|
507
|
-
if (updateInfo?.updateAvailable) {
|
|
517
|
+
// Update check: NEVER gates the banner. The old order awaited an
|
|
518
|
+
// `npm view` subprocess (a full network round trip, up to the 2s cap, on
|
|
519
|
+
// EVERY launch) before the welcome box — and before any queued startup
|
|
520
|
+
// prompt — rendered. Now the banner renders immediately and the update
|
|
521
|
+
// offer arrives as a follow-up system line when the check resolves.
|
|
522
|
+
void checkForUpdates(version).then((updateInfo) => {
|
|
523
|
+
if (!updateInfo?.updateAvailable || this.shouldExit)
|
|
524
|
+
return;
|
|
508
525
|
// Detect + OFFER (don't force) — the user applies it in-shell with
|
|
509
526
|
// /update. Auto-installing on every startup ran `npm i -g` without
|
|
510
527
|
// consent and could fail silently; making it user-initiated is clearer.
|
|
511
528
|
this.pendingUpdate = updateInfo;
|
|
512
|
-
|
|
529
|
+
this.promptController?.getRenderer()?.addEvent('system', chalk.cyan('⬆ ') +
|
|
513
530
|
chalk.dim('Update available: ') +
|
|
514
531
|
chalk.yellow(`v${updateInfo.current}`) +
|
|
515
532
|
chalk.dim(' → ') +
|
|
516
533
|
chalk.green(`v${updateInfo.latest}`) +
|
|
517
534
|
chalk.dim(' · type ') + chalk.hex('#ffd666')('/update') + chalk.dim(' to upgrade'));
|
|
518
|
-
}
|
|
535
|
+
}).catch(() => { });
|
|
519
536
|
// Clean, minimal welcome — a sparkle + the essentials in a rounded box,
|
|
520
537
|
// mirroring Claude Code. The pure composeWelcomeLines() is the contract for
|
|
521
538
|
// WHICH lines appear; here we draw the same box with brand colour.
|
|
@@ -533,7 +550,10 @@ class InteractiveShell {
|
|
|
533
550
|
version: `v${version}`,
|
|
534
551
|
});
|
|
535
552
|
const boxed = roundedBox(body, (cell) => cell.replace('✻', starlight('✻')), (s) => wire(s));
|
|
536
|
-
|
|
553
|
+
// No leading/trailing '' sentinels: the banner string used to embed its
|
|
554
|
+
// own blank lines AND ChatStatic adds marginTop on the next block — a
|
|
555
|
+
// double-blank gap (§1 violation) around every banner.
|
|
556
|
+
const welcomeContent = boxed.join('\n');
|
|
537
557
|
// Use renderer event system instead of direct stdout writes
|
|
538
558
|
renderer.addEvent('banner', welcomeContent);
|
|
539
559
|
// Update renderer meta with model info
|
|
@@ -808,12 +828,16 @@ class InteractiveShell {
|
|
|
808
828
|
maxBuffer: 4 * 1024 * 1024,
|
|
809
829
|
});
|
|
810
830
|
const output = [out, stderr].filter(Boolean).join('').trim() || '(no output)';
|
|
811
|
-
|
|
831
|
+
// §2/§3: header and result are separate blocks — one combined 'tool'
|
|
832
|
+
// event rendered the whole output bold as if it were the tool name.
|
|
833
|
+
renderer?.addEvent('tool', `$ ${command}`);
|
|
834
|
+
renderer?.addEvent('tool-result', formatToolResult('bash', output, { command }));
|
|
812
835
|
}
|
|
813
836
|
catch (error) {
|
|
814
837
|
const err = error;
|
|
815
838
|
const output = [err.stdout, err.stderr, err.message].filter(Boolean).join('\n').trim();
|
|
816
|
-
renderer?.addEvent('
|
|
839
|
+
renderer?.addEvent('tool', `$ ${command}`);
|
|
840
|
+
renderer?.addEvent('error', formatToolError(output || 'command failed'));
|
|
817
841
|
}
|
|
818
842
|
finally {
|
|
819
843
|
this.promptController?.setStatusMessage(null);
|
|
@@ -1441,7 +1465,6 @@ class InteractiveShell {
|
|
|
1441
1465
|
dim(`System prompt ~${formatTokenCount(usage.systemTokens)} · conversation ~${formatTokenCount(usage.conversationTokens)} · ${usage.messageCount} messages`),
|
|
1442
1466
|
];
|
|
1443
1467
|
this.promptController.setInlinePanel(lines);
|
|
1444
|
-
this.scheduleInlinePanelDismiss();
|
|
1445
1468
|
}
|
|
1446
1469
|
/** /cost — DeepSeek tokens + Tavily searches consumed (this install). */
|
|
1447
1470
|
showUsage() {
|
|
@@ -1460,10 +1483,12 @@ class InteractiveShell {
|
|
|
1460
1483
|
label('DeepSeek') + dim(`${ds(cumulative)} · this session ${ds(session)}`),
|
|
1461
1484
|
label('Tavily') + dim(`${cumulative.tavilySearches} searches · this session ${session.tavilySearches}`),
|
|
1462
1485
|
'',
|
|
1463
|
-
|
|
1486
|
+
// Two ≤78-col lines — the single-sentence version measured 97 cols and
|
|
1487
|
+
// word-wrapped onto an unindented row at the default 80-col terminal.
|
|
1488
|
+
dim(`Tavily free pool: ${TAVILY_MONTHLY_FREE.toLocaleString('en-US')}/mo + ${TAVILY_ONE_TIME_BONUS.toLocaleString('en-US')} one-time bonus (shared proxy).`),
|
|
1489
|
+
dim('Set your own key for unlimited: /key tvly-…'),
|
|
1464
1490
|
];
|
|
1465
1491
|
this.promptController.setInlinePanel(lines);
|
|
1466
|
-
this.scheduleInlinePanelDismiss();
|
|
1467
1492
|
}
|
|
1468
1493
|
/**
|
|
1469
1494
|
* /diff — review every file the agent changed this run as a colored diff,
|
|
@@ -1509,7 +1534,6 @@ class InteractiveShell {
|
|
|
1509
1534
|
...panel.lines,
|
|
1510
1535
|
];
|
|
1511
1536
|
this.promptController.setInlinePanel(lines);
|
|
1512
|
-
this.scheduleInlinePanelDismiss();
|
|
1513
1537
|
}
|
|
1514
1538
|
/**
|
|
1515
1539
|
* /rewind — restore the files changed this run. Two-step: bare `/rewind`
|
|
@@ -1578,7 +1602,6 @@ class InteractiveShell {
|
|
|
1578
1602
|
dim('Shift+Tab cycles permission mode · Ctrl+D exits · ? for shortcuts'),
|
|
1579
1603
|
];
|
|
1580
1604
|
this.promptController.setInlinePanel(lines);
|
|
1581
|
-
this.scheduleInlinePanelDismiss();
|
|
1582
1605
|
}
|
|
1583
1606
|
showKeyboardShortcuts() {
|
|
1584
1607
|
if (!this.promptController?.supportsInlinePanel()) {
|
|
@@ -1588,6 +1611,10 @@ class InteractiveShell {
|
|
|
1588
1611
|
}
|
|
1589
1612
|
const kb = (key) => chalk.hex('#ffd666')(key);
|
|
1590
1613
|
const desc = (text) => chalk.dim(text);
|
|
1614
|
+
// Pad the PLAIN key text before colouring so the description column is
|
|
1615
|
+
// grid-aligned — hand-counted spaces inside coloured templates drifted
|
|
1616
|
+
// by 1-2 cols per row.
|
|
1617
|
+
const row = (keys, text) => ` ${kb(keys.padEnd(14))}${desc(text)}`;
|
|
1591
1618
|
// Only shortcuts the Ink Prompt (src/ui/ink/Prompt.tsx) actually
|
|
1592
1619
|
// implements are listed — advertising keys the input handler ignores
|
|
1593
1620
|
// would be a deceptive panel (Glasswing transparency).
|
|
@@ -1595,52 +1622,35 @@ class InteractiveShell {
|
|
|
1595
1622
|
chalk.bold.hex('#e8e9ed')('Keyboard Shortcuts') + chalk.dim(' (press any key to dismiss)'),
|
|
1596
1623
|
'',
|
|
1597
1624
|
chalk.hex('#64d2ff')('Navigation'),
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1625
|
+
row('Ctrl+A / Home', 'Move to start of line'),
|
|
1626
|
+
row('Ctrl+E / End', 'Move to end of line'),
|
|
1627
|
+
row('← / →', 'Move cursor'),
|
|
1628
|
+
row('↑ / ↓', 'Prompt history (older / newer)'),
|
|
1629
|
+
row('Ctrl+R', 'Reverse-search prompt history'),
|
|
1603
1630
|
'',
|
|
1604
1631
|
chalk.hex('#64d2ff')('Editing'),
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1632
|
+
row('Ctrl+U', 'Delete to start of line'),
|
|
1633
|
+
row('Ctrl+W', 'Delete word backward'),
|
|
1634
|
+
row('Ctrl+K', 'Delete to end of line'),
|
|
1608
1635
|
'',
|
|
1609
1636
|
chalk.hex('#64d2ff')('Modes'),
|
|
1610
|
-
|
|
1611
|
-
|
|
1637
|
+
row('Shift+Tab', 'Cycle permission mode (default · accept edits · plan)'),
|
|
1638
|
+
row('Ctrl+O', 'Expand the last truncated tool result'),
|
|
1612
1639
|
'',
|
|
1613
1640
|
chalk.hex('#64d2ff')('Completion'),
|
|
1614
|
-
|
|
1615
|
-
|
|
1641
|
+
row('@', 'Autocomplete a file; its content is inlined for the agent'),
|
|
1642
|
+
row('/', 'Autocomplete a command (↑/↓ · Tab to complete; Enter runs it)'),
|
|
1616
1643
|
'',
|
|
1617
1644
|
chalk.hex('#64d2ff')('Control'),
|
|
1618
|
-
|
|
1619
|
-
|
|
1645
|
+
row('Ctrl+C', 'Clear input / interrupt'),
|
|
1646
|
+
row('Ctrl+D', 'Exit (when empty)'),
|
|
1620
1647
|
];
|
|
1621
1648
|
this.promptController.setInlinePanel(lines);
|
|
1622
|
-
this.scheduleInlinePanelDismiss();
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* Auto-dismiss inline panel after timeout or on next input.
|
|
1626
|
-
*/
|
|
1627
|
-
inlinePanelDismissTimer = null;
|
|
1628
|
-
scheduleInlinePanelDismiss() {
|
|
1629
|
-
// Clear any existing timer
|
|
1630
|
-
if (this.inlinePanelDismissTimer) {
|
|
1631
|
-
clearTimeout(this.inlinePanelDismissTimer);
|
|
1632
|
-
}
|
|
1633
|
-
// Auto-dismiss after 8 seconds
|
|
1634
|
-
this.inlinePanelDismissTimer = setTimeout(() => {
|
|
1635
|
-
this.promptController?.clearInlinePanel();
|
|
1636
|
-
this.inlinePanelDismissTimer = null;
|
|
1637
|
-
}, 8000);
|
|
1638
1649
|
}
|
|
1650
|
+
// Panels dismiss on the next keypress (Prompt → onDismissPanel), never on a
|
|
1651
|
+
// timer: the old 8s auto-dismiss yanked /context and /help mid-read, which
|
|
1652
|
+
// Claude Code never does.
|
|
1639
1653
|
dismissInlinePanel() {
|
|
1640
|
-
if (this.inlinePanelDismissTimer) {
|
|
1641
|
-
clearTimeout(this.inlinePanelDismissTimer);
|
|
1642
|
-
this.inlinePanelDismissTimer = null;
|
|
1643
|
-
}
|
|
1644
1654
|
this.promptController?.clearInlinePanel();
|
|
1645
1655
|
}
|
|
1646
1656
|
handleSubmit(text) {
|
|
@@ -1699,7 +1709,15 @@ class InteractiveShell {
|
|
|
1699
1709
|
renderer?.setQueuedPrompts(this.pendingPrompts.slice());
|
|
1700
1710
|
return;
|
|
1701
1711
|
}
|
|
1702
|
-
void this.processPrompt(trimmed)
|
|
1712
|
+
void this.processPrompt(trimmed).catch((e) => {
|
|
1713
|
+
// processPrompt handles its own errors; this is the last net so a
|
|
1714
|
+
// rejection can't reach the global unhandledRejection handler (which
|
|
1715
|
+
// exits the CLI with code 1).
|
|
1716
|
+
try {
|
|
1717
|
+
this.promptController?.getRenderer()?.addEvent('error', formatErrorForDisplay(e instanceof Error ? e.message : String(e)));
|
|
1718
|
+
}
|
|
1719
|
+
catch { /* ignore */ }
|
|
1720
|
+
});
|
|
1703
1721
|
}
|
|
1704
1722
|
/**
|
|
1705
1723
|
* Dequeue and run the next live follow-up, if any: commit its user line to
|
|
@@ -1754,6 +1772,10 @@ class InteractiveShell {
|
|
|
1754
1772
|
// displayed above the chat box.
|
|
1755
1773
|
}
|
|
1756
1774
|
enterCriticalSection();
|
|
1775
|
+
// Per-turn dedupe latch for error display: a provider failure arrives
|
|
1776
|
+
// both as an 'error' event AND as the sink rejection thrown out of the
|
|
1777
|
+
// event loop — without the latch the same message printed twice.
|
|
1778
|
+
this.lastShownTurnError = null;
|
|
1757
1779
|
this.isProcessing = true;
|
|
1758
1780
|
this.currentResponseBuffer = '';
|
|
1759
1781
|
this.finalResponseText = '';
|
|
@@ -1776,6 +1798,12 @@ class InteractiveShell {
|
|
|
1776
1798
|
let reasoningTimedOut = false;
|
|
1777
1799
|
let stepTimedOut = false;
|
|
1778
1800
|
let hitlDepth = 0;
|
|
1801
|
+
// The `⏺ Tool(arg)` header most recently emitted into history, or null
|
|
1802
|
+
// once any other event rendered after it. With PARALLEL tools the
|
|
1803
|
+
// start/start/complete/complete interleave glued tool B's result under
|
|
1804
|
+
// tool A's header (§3); tool.complete re-emits its own header when it
|
|
1805
|
+
// isn't the last thing on screen.
|
|
1806
|
+
let lastToolHeaderEmitted = null;
|
|
1779
1807
|
// Track total prompt processing time to prevent infinite loops
|
|
1780
1808
|
const promptStartTime = Date.now();
|
|
1781
1809
|
const TOTAL_PROMPT_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 hours max for entire prompt without meaningful content
|
|
@@ -1910,6 +1938,7 @@ class InteractiveShell {
|
|
|
1910
1938
|
}
|
|
1911
1939
|
}
|
|
1912
1940
|
renderer.addEvent('response', '\n');
|
|
1941
|
+
lastToolHeaderEmitted = null; // prose rendered since the last header
|
|
1913
1942
|
// Capture the authoritative final text BEFORE the buffer is cleared
|
|
1914
1943
|
// (the finally's auto-continue reads run after this clear).
|
|
1915
1944
|
this.finalResponseText = sourceText || this.finalResponseText;
|
|
@@ -1940,7 +1969,9 @@ class InteractiveShell {
|
|
|
1940
1969
|
// (subagent.start/complete) are the visible surface instead of a
|
|
1941
1970
|
// raw `parallel_agents({"tasks":…})` JSON dump.
|
|
1942
1971
|
if (renderer && toolName !== 'parallel_agents') {
|
|
1943
|
-
|
|
1972
|
+
const header = formatToolCall(toolName, args, this.workingDir);
|
|
1973
|
+
renderer.addEvent('tool', header);
|
|
1974
|
+
lastToolHeaderEmitted = header;
|
|
1944
1975
|
}
|
|
1945
1976
|
this.promptController?.setStatusMessage(toolActivityLabel(toolName, args, this.workingDir));
|
|
1946
1977
|
break;
|
|
@@ -1964,6 +1995,16 @@ class InteractiveShell {
|
|
|
1964
1995
|
if (event.result && typeof event.result === 'string' && event.result.trim() && renderer) {
|
|
1965
1996
|
const params = event.parameters;
|
|
1966
1997
|
const summary = formatToolResult(event.toolName, event.result, params);
|
|
1998
|
+
// Pair the ⎿ result with ITS call: if another tool's header (or
|
|
1999
|
+
// any other event) rendered since this tool's start, re-emit
|
|
2000
|
+
// this call's header so the result lands under the right one.
|
|
2001
|
+
if (event.toolName !== 'parallel_agents') {
|
|
2002
|
+
const ownHeader = formatToolCall(event.toolName, params, this.workingDir);
|
|
2003
|
+
if (lastToolHeaderEmitted !== ownHeader) {
|
|
2004
|
+
renderer.addEvent('tool', ownHeader);
|
|
2005
|
+
}
|
|
2006
|
+
lastToolHeaderEmitted = null; // a result now sits below the header
|
|
2007
|
+
}
|
|
1967
2008
|
renderer.addEvent('tool-result', summary);
|
|
1968
2009
|
// Remember the full result so Ctrl+O can expand it — but only
|
|
1969
2010
|
// when the summary actually truncated (the `(ctrl+o to expand)`
|
|
@@ -1983,11 +2024,18 @@ class InteractiveShell {
|
|
|
1983
2024
|
if (renderer) {
|
|
1984
2025
|
// Red ` ⎿ Error: …` line, mirroring a failed tool result.
|
|
1985
2026
|
renderer.addEvent('error', formatToolError(event.error));
|
|
2027
|
+
lastToolHeaderEmitted = null;
|
|
1986
2028
|
}
|
|
1987
2029
|
break;
|
|
1988
2030
|
case 'error':
|
|
1989
2031
|
if (renderer) {
|
|
1990
|
-
|
|
2032
|
+
// Compact display (no multi-KB HTML/JSON walls) + remember what
|
|
2033
|
+
// was shown so the catch below doesn't print it a second time —
|
|
2034
|
+
// the same failure also arrives as the sink rejection.
|
|
2035
|
+
const shown = formatErrorForDisplay(event.error);
|
|
2036
|
+
this.lastShownTurnError = shown;
|
|
2037
|
+
renderer.addEvent('error', shown);
|
|
2038
|
+
lastToolHeaderEmitted = null;
|
|
1991
2039
|
}
|
|
1992
2040
|
break;
|
|
1993
2041
|
case 'usage': {
|
|
@@ -2005,6 +2053,17 @@ class InteractiveShell {
|
|
|
2005
2053
|
this.lastInputTokens = contextTokens;
|
|
2006
2054
|
}
|
|
2007
2055
|
const windowTokens = getModelContextInfo(this.profileConfig.model).contextWindow;
|
|
2056
|
+
// Window-drift self-report: the provider's prompt_tokens is REAL
|
|
2057
|
+
// API data — if it ever exceeds the configured window, the static
|
|
2058
|
+
// context table is provably stale (this exact drift hid the
|
|
2059
|
+
// 131k-vs-1M bug; the meter clamps to 100% so nothing else
|
|
2060
|
+
// surfaces it). DeepSeek's /models returns no window metadata
|
|
2061
|
+
// (probed), so this is the only runtime verification available.
|
|
2062
|
+
if (!this.warnedWindowDrift &&
|
|
2063
|
+
typeof contextTokens === 'number' && contextTokens > windowTokens) {
|
|
2064
|
+
this.warnedWindowDrift = true;
|
|
2065
|
+
renderer?.addEvent('system', chalk.dim(`Note: the provider reports ${contextTokens.toLocaleString('en-US')} input tokens — more than the configured ${windowTokens.toLocaleString('en-US')}-token window for ${this.profileConfig.model}. The context table is likely stale; context % may be wrong.`));
|
|
2066
|
+
}
|
|
2008
2067
|
this.promptController?.setMetaStatus({
|
|
2009
2068
|
outputTokens: this.turnTokenMeter.current(),
|
|
2010
2069
|
contextTokens,
|
|
@@ -2118,7 +2177,14 @@ class InteractiveShell {
|
|
|
2118
2177
|
catch (error) {
|
|
2119
2178
|
const message = error instanceof Error ? error.message : String(error);
|
|
2120
2179
|
if (renderer) {
|
|
2121
|
-
|
|
2180
|
+
// Same failure usually already arrived (and rendered) as the turn's
|
|
2181
|
+
// 'error' event — the sink queues the event BEFORE rejecting. Skip
|
|
2182
|
+
// the duplicate; render compactly when it really is new.
|
|
2183
|
+
const shown = formatErrorForDisplay(message);
|
|
2184
|
+
if (shown !== this.lastShownTurnError) {
|
|
2185
|
+
this.lastShownTurnError = shown;
|
|
2186
|
+
renderer.addEvent('error', shown);
|
|
2187
|
+
}
|
|
2122
2188
|
}
|
|
2123
2189
|
// Fallback: If we have reasoning content but no response was generated, synthesize one
|
|
2124
2190
|
if (!episodeSuccess && shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
@@ -2185,80 +2251,96 @@ class InteractiveShell {
|
|
|
2185
2251
|
// Process any queued follow-up — single source of truth (drainNextQueuedPrompt).
|
|
2186
2252
|
// This takes priority over auto-continue: a user's explicit follow-up runs
|
|
2187
2253
|
// before the loop decides the original task is "complete".
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
//
|
|
2208
|
-
//
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2254
|
+
//
|
|
2255
|
+
// GUARDED: processPrompt is launched fire-and-forget (void) and the
|
|
2256
|
+
// global unhandledRejection handler exits the whole CLI with code 1 —
|
|
2257
|
+
// an exception anywhere in this post-turn pipeline (drain, completion
|
|
2258
|
+
// heuristics, governor, renderer calls) must degrade to an error line,
|
|
2259
|
+
// never kill the live session.
|
|
2260
|
+
try {
|
|
2261
|
+
if (await this.drainNextQueuedPrompt()) {
|
|
2262
|
+
// handled
|
|
2263
|
+
}
|
|
2264
|
+
else if (refusedTurn) {
|
|
2265
|
+
// Refusal terminates the turn. Don't re-prompt the model — the
|
|
2266
|
+
// user's request is finished from the agent's side. Clear the
|
|
2267
|
+
// stored "original prompt" so a stray Alt+G later doesn't pick
|
|
2268
|
+
// up where this turn left off.
|
|
2269
|
+
this.originalPromptForAutoContinue = null;
|
|
2270
|
+
}
|
|
2271
|
+
else if (!this.shouldExit && !this.userInterruptedRun) {
|
|
2272
|
+
// Auto mode: keep running until user's prompt is fully completed.
|
|
2273
|
+
// Skipped after a Ctrl+C interrupt so we don't immediately resume
|
|
2274
|
+
// the work the user just cancelled.
|
|
2275
|
+
const autoMode = this.promptController?.getAutoMode() ?? 'off';
|
|
2276
|
+
if (autoMode !== 'off') {
|
|
2277
|
+
// Check if original user prompt is fully completed
|
|
2278
|
+
const detector = getTaskCompletionDetector();
|
|
2279
|
+
const analysis = detector.analyzeCompletion(this.finalResponseText, toolsUsed);
|
|
2280
|
+
// Record this turn with the governor (bounds the loop + detects a
|
|
2281
|
+
// stall: the same tools/files/failure repeating with no new progress)
|
|
2282
|
+
// and the failure registry (catches the same error recurring across
|
|
2283
|
+
// NON-consecutive turns — a thrash the stall check would miss).
|
|
2284
|
+
this.autoGovernor.recordTurn({
|
|
2285
|
+
toolsUsed,
|
|
2286
|
+
filesModified,
|
|
2287
|
+
failingSignal: detectFailingTestOrBuild(combinedTurnOutput),
|
|
2288
|
+
});
|
|
2289
|
+
this.failureRegistry.trackTurn(combinedTurnOutput);
|
|
2290
|
+
const gov = this.autoGovernor.check();
|
|
2291
|
+
const failureNudge = this.failureRegistry.nudge();
|
|
2292
|
+
const todos = getCurrentTodos();
|
|
2293
|
+
const pending = pendingTodos(todos);
|
|
2294
|
+
if (gov.stop) {
|
|
2295
|
+
// Yield to the user WITH state instead of thrashing forever.
|
|
2296
|
+
const note = gov.reason === 'limit'
|
|
2297
|
+
? `Paused after ${gov.turn} auto-continue turns (turn limit).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} still pending` : ''} — say "continue" to keep going.`
|
|
2298
|
+
: `Paused: no new progress over the last few turns (same actions repeating).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} pending` : ''} — tell me how to proceed.`;
|
|
2299
|
+
this.promptController?.getRenderer()?.addEvent('system', chalk.dim(note));
|
|
2300
|
+
this.promptController?.setStatusMessage(null);
|
|
2301
|
+
this.originalPromptForAutoContinue = null;
|
|
2302
|
+
}
|
|
2303
|
+
else if (turnAdversarialFindings && this.adversarialCorrectionCount < MAX_ADVERSARIAL_CORRECTIONS) {
|
|
2304
|
+
// The reviewer refuted this turn's draft — re-run the FULL tool loop
|
|
2305
|
+
// to actually fix the findings (not just show the caveat), bounded
|
|
2306
|
+
// by the governor + this per-request cap.
|
|
2307
|
+
this.adversarialCorrectionCount += 1;
|
|
2308
|
+
this.promptController?.setStatusMessage('Addressing reviewer findings…');
|
|
2309
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
2310
|
+
await this.processPrompt(buildAdversarialCorrectionPrompt(turnAdversarialFindings));
|
|
2311
|
+
}
|
|
2312
|
+
else if (!analysis.isComplete || pending.length > 0) {
|
|
2313
|
+
// Continue — but only stop when the LIVE PLAN is also clear: pending
|
|
2314
|
+
// todos force a continue even if the response sounded "done".
|
|
2315
|
+
this.promptController?.setStatusMessage('Continuing...');
|
|
2316
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2317
|
+
// Prefer the plan's next task; fall back to the response heuristic.
|
|
2318
|
+
const base = nextTodoPrompt(todos)
|
|
2319
|
+
?? this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', combinedTurnOutput, toolsUsed)
|
|
2320
|
+
?? 'continue';
|
|
2321
|
+
// When a failure keeps recurring, lead with the change-approach nudge.
|
|
2322
|
+
// Keep an IMPORTANT: prefix so this counts as an auto-continue (not a
|
|
2323
|
+
// fresh user prompt, which would reset the governor).
|
|
2324
|
+
const autoPrompt = failureNudge
|
|
2325
|
+
? `IMPORTANT: ${failureNudge}\n\n${base.replace(/^IMPORTANT:\s*/, '')}`
|
|
2326
|
+
: base;
|
|
2327
|
+
await this.processPrompt(autoPrompt);
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
this.promptController?.setStatusMessage('Task complete');
|
|
2331
|
+
setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
|
|
2332
|
+
}
|
|
2259
2333
|
}
|
|
2260
2334
|
}
|
|
2261
2335
|
}
|
|
2336
|
+
catch (postTurnError) {
|
|
2337
|
+
const msg = postTurnError instanceof Error ? postTurnError.message : String(postTurnError);
|
|
2338
|
+
try {
|
|
2339
|
+
this.promptController?.getRenderer()?.addEvent('error', formatErrorForDisplay(msg));
|
|
2340
|
+
this.promptController?.setStatusMessage(null);
|
|
2341
|
+
}
|
|
2342
|
+
catch { /* renderer down — nothing more to do */ }
|
|
2343
|
+
}
|
|
2262
2344
|
}
|
|
2263
2345
|
}
|
|
2264
2346
|
generateAutoContinuePrompt(originalPrompt, response, toolsUsed) {
|