@trenchwork/coder 1.5.6 → 1.5.7
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/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 +207 -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/providers/openaiChatCompletionsProvider.d.ts +6 -0
- package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -1
- package/dist/providers/openaiChatCompletionsProvider.js +31 -11
- 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,9 @@ 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;
|
|
288
309
|
// The turn's final assistant text, captured BEFORE currentResponseBuffer is
|
|
289
310
|
// cleared on message.complete. The auto-continue refusal/completion/governor
|
|
290
311
|
// reads run in the `finally`, AFTER that clear, so reading the buffer there saw
|
|
@@ -491,31 +512,25 @@ class InteractiveShell {
|
|
|
491
512
|
return key.slice(0, 3) + '...' + key.slice(-3);
|
|
492
513
|
return key.slice(0, 6) + '...' + key.slice(-4);
|
|
493
514
|
};
|
|
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) {
|
|
515
|
+
// Update check: NEVER gates the banner. The old order awaited an
|
|
516
|
+
// `npm view` subprocess (a full network round trip, up to the 2s cap, on
|
|
517
|
+
// EVERY launch) before the welcome box — and before any queued startup
|
|
518
|
+
// prompt — rendered. Now the banner renders immediately and the update
|
|
519
|
+
// offer arrives as a follow-up system line when the check resolves.
|
|
520
|
+
void checkForUpdates(version).then((updateInfo) => {
|
|
521
|
+
if (!updateInfo?.updateAvailable || this.shouldExit)
|
|
522
|
+
return;
|
|
508
523
|
// Detect + OFFER (don't force) — the user applies it in-shell with
|
|
509
524
|
// /update. Auto-installing on every startup ran `npm i -g` without
|
|
510
525
|
// consent and could fail silently; making it user-initiated is clearer.
|
|
511
526
|
this.pendingUpdate = updateInfo;
|
|
512
|
-
|
|
527
|
+
this.promptController?.getRenderer()?.addEvent('system', chalk.cyan('⬆ ') +
|
|
513
528
|
chalk.dim('Update available: ') +
|
|
514
529
|
chalk.yellow(`v${updateInfo.current}`) +
|
|
515
530
|
chalk.dim(' → ') +
|
|
516
531
|
chalk.green(`v${updateInfo.latest}`) +
|
|
517
532
|
chalk.dim(' · type ') + chalk.hex('#ffd666')('/update') + chalk.dim(' to upgrade'));
|
|
518
|
-
}
|
|
533
|
+
}).catch(() => { });
|
|
519
534
|
// Clean, minimal welcome — a sparkle + the essentials in a rounded box,
|
|
520
535
|
// mirroring Claude Code. The pure composeWelcomeLines() is the contract for
|
|
521
536
|
// WHICH lines appear; here we draw the same box with brand colour.
|
|
@@ -533,7 +548,10 @@ class InteractiveShell {
|
|
|
533
548
|
version: `v${version}`,
|
|
534
549
|
});
|
|
535
550
|
const boxed = roundedBox(body, (cell) => cell.replace('✻', starlight('✻')), (s) => wire(s));
|
|
536
|
-
|
|
551
|
+
// No leading/trailing '' sentinels: the banner string used to embed its
|
|
552
|
+
// own blank lines AND ChatStatic adds marginTop on the next block — a
|
|
553
|
+
// double-blank gap (§1 violation) around every banner.
|
|
554
|
+
const welcomeContent = boxed.join('\n');
|
|
537
555
|
// Use renderer event system instead of direct stdout writes
|
|
538
556
|
renderer.addEvent('banner', welcomeContent);
|
|
539
557
|
// Update renderer meta with model info
|
|
@@ -808,12 +826,16 @@ class InteractiveShell {
|
|
|
808
826
|
maxBuffer: 4 * 1024 * 1024,
|
|
809
827
|
});
|
|
810
828
|
const output = [out, stderr].filter(Boolean).join('').trim() || '(no output)';
|
|
811
|
-
|
|
829
|
+
// §2/§3: header and result are separate blocks — one combined 'tool'
|
|
830
|
+
// event rendered the whole output bold as if it were the tool name.
|
|
831
|
+
renderer?.addEvent('tool', `$ ${command}`);
|
|
832
|
+
renderer?.addEvent('tool-result', formatToolResult('bash', output, { command }));
|
|
812
833
|
}
|
|
813
834
|
catch (error) {
|
|
814
835
|
const err = error;
|
|
815
836
|
const output = [err.stdout, err.stderr, err.message].filter(Boolean).join('\n').trim();
|
|
816
|
-
renderer?.addEvent('
|
|
837
|
+
renderer?.addEvent('tool', `$ ${command}`);
|
|
838
|
+
renderer?.addEvent('error', formatToolError(output || 'command failed'));
|
|
817
839
|
}
|
|
818
840
|
finally {
|
|
819
841
|
this.promptController?.setStatusMessage(null);
|
|
@@ -1441,7 +1463,6 @@ class InteractiveShell {
|
|
|
1441
1463
|
dim(`System prompt ~${formatTokenCount(usage.systemTokens)} · conversation ~${formatTokenCount(usage.conversationTokens)} · ${usage.messageCount} messages`),
|
|
1442
1464
|
];
|
|
1443
1465
|
this.promptController.setInlinePanel(lines);
|
|
1444
|
-
this.scheduleInlinePanelDismiss();
|
|
1445
1466
|
}
|
|
1446
1467
|
/** /cost — DeepSeek tokens + Tavily searches consumed (this install). */
|
|
1447
1468
|
showUsage() {
|
|
@@ -1460,10 +1481,12 @@ class InteractiveShell {
|
|
|
1460
1481
|
label('DeepSeek') + dim(`${ds(cumulative)} · this session ${ds(session)}`),
|
|
1461
1482
|
label('Tavily') + dim(`${cumulative.tavilySearches} searches · this session ${session.tavilySearches}`),
|
|
1462
1483
|
'',
|
|
1463
|
-
|
|
1484
|
+
// Two ≤78-col lines — the single-sentence version measured 97 cols and
|
|
1485
|
+
// word-wrapped onto an unindented row at the default 80-col terminal.
|
|
1486
|
+
dim(`Tavily free pool: ${TAVILY_MONTHLY_FREE.toLocaleString('en-US')}/mo + ${TAVILY_ONE_TIME_BONUS.toLocaleString('en-US')} one-time bonus (shared proxy).`),
|
|
1487
|
+
dim('Set your own key for unlimited: /key tvly-…'),
|
|
1464
1488
|
];
|
|
1465
1489
|
this.promptController.setInlinePanel(lines);
|
|
1466
|
-
this.scheduleInlinePanelDismiss();
|
|
1467
1490
|
}
|
|
1468
1491
|
/**
|
|
1469
1492
|
* /diff — review every file the agent changed this run as a colored diff,
|
|
@@ -1509,7 +1532,6 @@ class InteractiveShell {
|
|
|
1509
1532
|
...panel.lines,
|
|
1510
1533
|
];
|
|
1511
1534
|
this.promptController.setInlinePanel(lines);
|
|
1512
|
-
this.scheduleInlinePanelDismiss();
|
|
1513
1535
|
}
|
|
1514
1536
|
/**
|
|
1515
1537
|
* /rewind — restore the files changed this run. Two-step: bare `/rewind`
|
|
@@ -1578,7 +1600,6 @@ class InteractiveShell {
|
|
|
1578
1600
|
dim('Shift+Tab cycles permission mode · Ctrl+D exits · ? for shortcuts'),
|
|
1579
1601
|
];
|
|
1580
1602
|
this.promptController.setInlinePanel(lines);
|
|
1581
|
-
this.scheduleInlinePanelDismiss();
|
|
1582
1603
|
}
|
|
1583
1604
|
showKeyboardShortcuts() {
|
|
1584
1605
|
if (!this.promptController?.supportsInlinePanel()) {
|
|
@@ -1588,6 +1609,10 @@ class InteractiveShell {
|
|
|
1588
1609
|
}
|
|
1589
1610
|
const kb = (key) => chalk.hex('#ffd666')(key);
|
|
1590
1611
|
const desc = (text) => chalk.dim(text);
|
|
1612
|
+
// Pad the PLAIN key text before colouring so the description column is
|
|
1613
|
+
// grid-aligned — hand-counted spaces inside coloured templates drifted
|
|
1614
|
+
// by 1-2 cols per row.
|
|
1615
|
+
const row = (keys, text) => ` ${kb(keys.padEnd(14))}${desc(text)}`;
|
|
1591
1616
|
// Only shortcuts the Ink Prompt (src/ui/ink/Prompt.tsx) actually
|
|
1592
1617
|
// implements are listed — advertising keys the input handler ignores
|
|
1593
1618
|
// would be a deceptive panel (Glasswing transparency).
|
|
@@ -1595,52 +1620,35 @@ class InteractiveShell {
|
|
|
1595
1620
|
chalk.bold.hex('#e8e9ed')('Keyboard Shortcuts') + chalk.dim(' (press any key to dismiss)'),
|
|
1596
1621
|
'',
|
|
1597
1622
|
chalk.hex('#64d2ff')('Navigation'),
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1623
|
+
row('Ctrl+A / Home', 'Move to start of line'),
|
|
1624
|
+
row('Ctrl+E / End', 'Move to end of line'),
|
|
1625
|
+
row('← / →', 'Move cursor'),
|
|
1626
|
+
row('↑ / ↓', 'Prompt history (older / newer)'),
|
|
1627
|
+
row('Ctrl+R', 'Reverse-search prompt history'),
|
|
1603
1628
|
'',
|
|
1604
1629
|
chalk.hex('#64d2ff')('Editing'),
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1630
|
+
row('Ctrl+U', 'Delete to start of line'),
|
|
1631
|
+
row('Ctrl+W', 'Delete word backward'),
|
|
1632
|
+
row('Ctrl+K', 'Delete to end of line'),
|
|
1608
1633
|
'',
|
|
1609
1634
|
chalk.hex('#64d2ff')('Modes'),
|
|
1610
|
-
|
|
1611
|
-
|
|
1635
|
+
row('Shift+Tab', 'Cycle permission mode (default · accept edits · plan)'),
|
|
1636
|
+
row('Ctrl+O', 'Expand the last truncated tool result'),
|
|
1612
1637
|
'',
|
|
1613
1638
|
chalk.hex('#64d2ff')('Completion'),
|
|
1614
|
-
|
|
1615
|
-
|
|
1639
|
+
row('@', 'Autocomplete a file; its content is inlined for the agent'),
|
|
1640
|
+
row('/', 'Autocomplete a command (↑/↓ · Tab to complete; Enter runs it)'),
|
|
1616
1641
|
'',
|
|
1617
1642
|
chalk.hex('#64d2ff')('Control'),
|
|
1618
|
-
|
|
1619
|
-
|
|
1643
|
+
row('Ctrl+C', 'Clear input / interrupt'),
|
|
1644
|
+
row('Ctrl+D', 'Exit (when empty)'),
|
|
1620
1645
|
];
|
|
1621
1646
|
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
1647
|
}
|
|
1648
|
+
// Panels dismiss on the next keypress (Prompt → onDismissPanel), never on a
|
|
1649
|
+
// timer: the old 8s auto-dismiss yanked /context and /help mid-read, which
|
|
1650
|
+
// Claude Code never does.
|
|
1639
1651
|
dismissInlinePanel() {
|
|
1640
|
-
if (this.inlinePanelDismissTimer) {
|
|
1641
|
-
clearTimeout(this.inlinePanelDismissTimer);
|
|
1642
|
-
this.inlinePanelDismissTimer = null;
|
|
1643
|
-
}
|
|
1644
1652
|
this.promptController?.clearInlinePanel();
|
|
1645
1653
|
}
|
|
1646
1654
|
handleSubmit(text) {
|
|
@@ -1699,7 +1707,15 @@ class InteractiveShell {
|
|
|
1699
1707
|
renderer?.setQueuedPrompts(this.pendingPrompts.slice());
|
|
1700
1708
|
return;
|
|
1701
1709
|
}
|
|
1702
|
-
void this.processPrompt(trimmed)
|
|
1710
|
+
void this.processPrompt(trimmed).catch((e) => {
|
|
1711
|
+
// processPrompt handles its own errors; this is the last net so a
|
|
1712
|
+
// rejection can't reach the global unhandledRejection handler (which
|
|
1713
|
+
// exits the CLI with code 1).
|
|
1714
|
+
try {
|
|
1715
|
+
this.promptController?.getRenderer()?.addEvent('error', formatErrorForDisplay(e instanceof Error ? e.message : String(e)));
|
|
1716
|
+
}
|
|
1717
|
+
catch { /* ignore */ }
|
|
1718
|
+
});
|
|
1703
1719
|
}
|
|
1704
1720
|
/**
|
|
1705
1721
|
* Dequeue and run the next live follow-up, if any: commit its user line to
|
|
@@ -1754,6 +1770,10 @@ class InteractiveShell {
|
|
|
1754
1770
|
// displayed above the chat box.
|
|
1755
1771
|
}
|
|
1756
1772
|
enterCriticalSection();
|
|
1773
|
+
// Per-turn dedupe latch for error display: a provider failure arrives
|
|
1774
|
+
// both as an 'error' event AND as the sink rejection thrown out of the
|
|
1775
|
+
// event loop — without the latch the same message printed twice.
|
|
1776
|
+
this.lastShownTurnError = null;
|
|
1757
1777
|
this.isProcessing = true;
|
|
1758
1778
|
this.currentResponseBuffer = '';
|
|
1759
1779
|
this.finalResponseText = '';
|
|
@@ -1776,6 +1796,12 @@ class InteractiveShell {
|
|
|
1776
1796
|
let reasoningTimedOut = false;
|
|
1777
1797
|
let stepTimedOut = false;
|
|
1778
1798
|
let hitlDepth = 0;
|
|
1799
|
+
// The `⏺ Tool(arg)` header most recently emitted into history, or null
|
|
1800
|
+
// once any other event rendered after it. With PARALLEL tools the
|
|
1801
|
+
// start/start/complete/complete interleave glued tool B's result under
|
|
1802
|
+
// tool A's header (§3); tool.complete re-emits its own header when it
|
|
1803
|
+
// isn't the last thing on screen.
|
|
1804
|
+
let lastToolHeaderEmitted = null;
|
|
1779
1805
|
// Track total prompt processing time to prevent infinite loops
|
|
1780
1806
|
const promptStartTime = Date.now();
|
|
1781
1807
|
const TOTAL_PROMPT_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 hours max for entire prompt without meaningful content
|
|
@@ -1910,6 +1936,7 @@ class InteractiveShell {
|
|
|
1910
1936
|
}
|
|
1911
1937
|
}
|
|
1912
1938
|
renderer.addEvent('response', '\n');
|
|
1939
|
+
lastToolHeaderEmitted = null; // prose rendered since the last header
|
|
1913
1940
|
// Capture the authoritative final text BEFORE the buffer is cleared
|
|
1914
1941
|
// (the finally's auto-continue reads run after this clear).
|
|
1915
1942
|
this.finalResponseText = sourceText || this.finalResponseText;
|
|
@@ -1940,7 +1967,9 @@ class InteractiveShell {
|
|
|
1940
1967
|
// (subagent.start/complete) are the visible surface instead of a
|
|
1941
1968
|
// raw `parallel_agents({"tasks":…})` JSON dump.
|
|
1942
1969
|
if (renderer && toolName !== 'parallel_agents') {
|
|
1943
|
-
|
|
1970
|
+
const header = formatToolCall(toolName, args, this.workingDir);
|
|
1971
|
+
renderer.addEvent('tool', header);
|
|
1972
|
+
lastToolHeaderEmitted = header;
|
|
1944
1973
|
}
|
|
1945
1974
|
this.promptController?.setStatusMessage(toolActivityLabel(toolName, args, this.workingDir));
|
|
1946
1975
|
break;
|
|
@@ -1964,6 +1993,16 @@ class InteractiveShell {
|
|
|
1964
1993
|
if (event.result && typeof event.result === 'string' && event.result.trim() && renderer) {
|
|
1965
1994
|
const params = event.parameters;
|
|
1966
1995
|
const summary = formatToolResult(event.toolName, event.result, params);
|
|
1996
|
+
// Pair the ⎿ result with ITS call: if another tool's header (or
|
|
1997
|
+
// any other event) rendered since this tool's start, re-emit
|
|
1998
|
+
// this call's header so the result lands under the right one.
|
|
1999
|
+
if (event.toolName !== 'parallel_agents') {
|
|
2000
|
+
const ownHeader = formatToolCall(event.toolName, params, this.workingDir);
|
|
2001
|
+
if (lastToolHeaderEmitted !== ownHeader) {
|
|
2002
|
+
renderer.addEvent('tool', ownHeader);
|
|
2003
|
+
}
|
|
2004
|
+
lastToolHeaderEmitted = null; // a result now sits below the header
|
|
2005
|
+
}
|
|
1967
2006
|
renderer.addEvent('tool-result', summary);
|
|
1968
2007
|
// Remember the full result so Ctrl+O can expand it — but only
|
|
1969
2008
|
// when the summary actually truncated (the `(ctrl+o to expand)`
|
|
@@ -1983,11 +2022,18 @@ class InteractiveShell {
|
|
|
1983
2022
|
if (renderer) {
|
|
1984
2023
|
// Red ` ⎿ Error: …` line, mirroring a failed tool result.
|
|
1985
2024
|
renderer.addEvent('error', formatToolError(event.error));
|
|
2025
|
+
lastToolHeaderEmitted = null;
|
|
1986
2026
|
}
|
|
1987
2027
|
break;
|
|
1988
2028
|
case 'error':
|
|
1989
2029
|
if (renderer) {
|
|
1990
|
-
|
|
2030
|
+
// Compact display (no multi-KB HTML/JSON walls) + remember what
|
|
2031
|
+
// was shown so the catch below doesn't print it a second time —
|
|
2032
|
+
// the same failure also arrives as the sink rejection.
|
|
2033
|
+
const shown = formatErrorForDisplay(event.error);
|
|
2034
|
+
this.lastShownTurnError = shown;
|
|
2035
|
+
renderer.addEvent('error', shown);
|
|
2036
|
+
lastToolHeaderEmitted = null;
|
|
1991
2037
|
}
|
|
1992
2038
|
break;
|
|
1993
2039
|
case 'usage': {
|
|
@@ -2118,7 +2164,14 @@ class InteractiveShell {
|
|
|
2118
2164
|
catch (error) {
|
|
2119
2165
|
const message = error instanceof Error ? error.message : String(error);
|
|
2120
2166
|
if (renderer) {
|
|
2121
|
-
|
|
2167
|
+
// Same failure usually already arrived (and rendered) as the turn's
|
|
2168
|
+
// 'error' event — the sink queues the event BEFORE rejecting. Skip
|
|
2169
|
+
// the duplicate; render compactly when it really is new.
|
|
2170
|
+
const shown = formatErrorForDisplay(message);
|
|
2171
|
+
if (shown !== this.lastShownTurnError) {
|
|
2172
|
+
this.lastShownTurnError = shown;
|
|
2173
|
+
renderer.addEvent('error', shown);
|
|
2174
|
+
}
|
|
2122
2175
|
}
|
|
2123
2176
|
// Fallback: If we have reasoning content but no response was generated, synthesize one
|
|
2124
2177
|
if (!episodeSuccess && shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
@@ -2185,80 +2238,96 @@ class InteractiveShell {
|
|
|
2185
2238
|
// Process any queued follow-up — single source of truth (drainNextQueuedPrompt).
|
|
2186
2239
|
// This takes priority over auto-continue: a user's explicit follow-up runs
|
|
2187
2240
|
// 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
|
-
|
|
2241
|
+
//
|
|
2242
|
+
// GUARDED: processPrompt is launched fire-and-forget (void) and the
|
|
2243
|
+
// global unhandledRejection handler exits the whole CLI with code 1 —
|
|
2244
|
+
// an exception anywhere in this post-turn pipeline (drain, completion
|
|
2245
|
+
// heuristics, governor, renderer calls) must degrade to an error line,
|
|
2246
|
+
// never kill the live session.
|
|
2247
|
+
try {
|
|
2248
|
+
if (await this.drainNextQueuedPrompt()) {
|
|
2249
|
+
// handled
|
|
2250
|
+
}
|
|
2251
|
+
else if (refusedTurn) {
|
|
2252
|
+
// Refusal terminates the turn. Don't re-prompt the model — the
|
|
2253
|
+
// user's request is finished from the agent's side. Clear the
|
|
2254
|
+
// stored "original prompt" so a stray Alt+G later doesn't pick
|
|
2255
|
+
// up where this turn left off.
|
|
2256
|
+
this.originalPromptForAutoContinue = null;
|
|
2257
|
+
}
|
|
2258
|
+
else if (!this.shouldExit && !this.userInterruptedRun) {
|
|
2259
|
+
// Auto mode: keep running until user's prompt is fully completed.
|
|
2260
|
+
// Skipped after a Ctrl+C interrupt so we don't immediately resume
|
|
2261
|
+
// the work the user just cancelled.
|
|
2262
|
+
const autoMode = this.promptController?.getAutoMode() ?? 'off';
|
|
2263
|
+
if (autoMode !== 'off') {
|
|
2264
|
+
// Check if original user prompt is fully completed
|
|
2265
|
+
const detector = getTaskCompletionDetector();
|
|
2266
|
+
const analysis = detector.analyzeCompletion(this.finalResponseText, toolsUsed);
|
|
2267
|
+
// Record this turn with the governor (bounds the loop + detects a
|
|
2268
|
+
// stall: the same tools/files/failure repeating with no new progress)
|
|
2269
|
+
// and the failure registry (catches the same error recurring across
|
|
2270
|
+
// NON-consecutive turns — a thrash the stall check would miss).
|
|
2271
|
+
this.autoGovernor.recordTurn({
|
|
2272
|
+
toolsUsed,
|
|
2273
|
+
filesModified,
|
|
2274
|
+
failingSignal: detectFailingTestOrBuild(combinedTurnOutput),
|
|
2275
|
+
});
|
|
2276
|
+
this.failureRegistry.trackTurn(combinedTurnOutput);
|
|
2277
|
+
const gov = this.autoGovernor.check();
|
|
2278
|
+
const failureNudge = this.failureRegistry.nudge();
|
|
2279
|
+
const todos = getCurrentTodos();
|
|
2280
|
+
const pending = pendingTodos(todos);
|
|
2281
|
+
if (gov.stop) {
|
|
2282
|
+
// Yield to the user WITH state instead of thrashing forever.
|
|
2283
|
+
const note = gov.reason === 'limit'
|
|
2284
|
+
? `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.`
|
|
2285
|
+
: `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.`;
|
|
2286
|
+
this.promptController?.getRenderer()?.addEvent('system', chalk.dim(note));
|
|
2287
|
+
this.promptController?.setStatusMessage(null);
|
|
2288
|
+
this.originalPromptForAutoContinue = null;
|
|
2289
|
+
}
|
|
2290
|
+
else if (turnAdversarialFindings && this.adversarialCorrectionCount < MAX_ADVERSARIAL_CORRECTIONS) {
|
|
2291
|
+
// The reviewer refuted this turn's draft — re-run the FULL tool loop
|
|
2292
|
+
// to actually fix the findings (not just show the caveat), bounded
|
|
2293
|
+
// by the governor + this per-request cap.
|
|
2294
|
+
this.adversarialCorrectionCount += 1;
|
|
2295
|
+
this.promptController?.setStatusMessage('Addressing reviewer findings…');
|
|
2296
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
2297
|
+
await this.processPrompt(buildAdversarialCorrectionPrompt(turnAdversarialFindings));
|
|
2298
|
+
}
|
|
2299
|
+
else if (!analysis.isComplete || pending.length > 0) {
|
|
2300
|
+
// Continue — but only stop when the LIVE PLAN is also clear: pending
|
|
2301
|
+
// todos force a continue even if the response sounded "done".
|
|
2302
|
+
this.promptController?.setStatusMessage('Continuing...');
|
|
2303
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2304
|
+
// Prefer the plan's next task; fall back to the response heuristic.
|
|
2305
|
+
const base = nextTodoPrompt(todos)
|
|
2306
|
+
?? this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', combinedTurnOutput, toolsUsed)
|
|
2307
|
+
?? 'continue';
|
|
2308
|
+
// When a failure keeps recurring, lead with the change-approach nudge.
|
|
2309
|
+
// Keep an IMPORTANT: prefix so this counts as an auto-continue (not a
|
|
2310
|
+
// fresh user prompt, which would reset the governor).
|
|
2311
|
+
const autoPrompt = failureNudge
|
|
2312
|
+
? `IMPORTANT: ${failureNudge}\n\n${base.replace(/^IMPORTANT:\s*/, '')}`
|
|
2313
|
+
: base;
|
|
2314
|
+
await this.processPrompt(autoPrompt);
|
|
2315
|
+
}
|
|
2316
|
+
else {
|
|
2317
|
+
this.promptController?.setStatusMessage('Task complete');
|
|
2318
|
+
setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
|
|
2319
|
+
}
|
|
2259
2320
|
}
|
|
2260
2321
|
}
|
|
2261
2322
|
}
|
|
2323
|
+
catch (postTurnError) {
|
|
2324
|
+
const msg = postTurnError instanceof Error ? postTurnError.message : String(postTurnError);
|
|
2325
|
+
try {
|
|
2326
|
+
this.promptController?.getRenderer()?.addEvent('error', formatErrorForDisplay(msg));
|
|
2327
|
+
this.promptController?.setStatusMessage(null);
|
|
2328
|
+
}
|
|
2329
|
+
catch { /* renderer down — nothing more to do */ }
|
|
2330
|
+
}
|
|
2262
2331
|
}
|
|
2263
2332
|
}
|
|
2264
2333
|
generateAutoContinuePrompt(originalPrompt, response, toolsUsed) {
|