@trenchwork/coder 1.4.0 → 1.5.1
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/contracts/v1/agent.d.ts +2 -0
- package/dist/contracts/v1/agent.d.ts.map +1 -1
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +77 -2
- package/dist/core/agent.js.map +1 -1
- package/dist/core/bashCommandGuidance.d.ts.map +1 -1
- package/dist/core/bashCommandGuidance.js +19 -0
- package/dist/core/bashCommandGuidance.js.map +1 -1
- package/dist/core/contextManager.d.ts +10 -137
- package/dist/core/contextManager.d.ts.map +1 -1
- package/dist/core/contextManager.js +74 -540
- package/dist/core/contextManager.js.map +1 -1
- package/dist/core/errorClassification.d.ts.map +1 -1
- package/dist/core/errorClassification.js +8 -0
- package/dist/core/errorClassification.js.map +1 -1
- package/dist/core/hitl.d.ts +15 -0
- package/dist/core/hitl.d.ts.map +1 -1
- package/dist/core/hitl.js +20 -0
- package/dist/core/hitl.js.map +1 -1
- package/dist/core/hooks.d.ts.map +1 -1
- package/dist/core/hooks.js +42 -19
- package/dist/core/hooks.js.map +1 -1
- package/dist/core/keyResolution.d.ts +30 -0
- package/dist/core/keyResolution.d.ts.map +1 -0
- package/dist/core/keyResolution.js +38 -0
- package/dist/core/keyResolution.js.map +1 -0
- package/dist/core/permissionMode.d.ts +17 -2
- package/dist/core/permissionMode.d.ts.map +1 -1
- package/dist/core/permissionMode.js +20 -7
- package/dist/core/permissionMode.js.map +1 -1
- package/dist/core/reasoningFallback.d.ts +22 -0
- package/dist/core/reasoningFallback.d.ts.map +1 -0
- package/dist/core/reasoningFallback.js +22 -0
- package/dist/core/reasoningFallback.js.map +1 -0
- package/dist/core/sessionStore.js +34 -8
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/slashCommands.d.ts.map +1 -1
- package/dist/core/slashCommands.js +0 -3
- package/dist/core/slashCommands.js.map +1 -1
- package/dist/core/taskCompletionDetector.d.ts.map +1 -1
- package/dist/core/taskCompletionDetector.js +10 -3
- package/dist/core/taskCompletionDetector.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +1 -0
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +25 -2
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/turnTokenMeter.d.ts +19 -0
- package/dist/core/turnTokenMeter.d.ts.map +1 -0
- package/dist/core/turnTokenMeter.js +36 -0
- package/dist/core/turnTokenMeter.js.map +1 -0
- package/dist/headless/interactiveShell.d.ts +3 -3
- package/dist/headless/interactiveShell.d.ts.map +1 -1
- package/dist/headless/interactiveShell.js +125 -116
- package/dist/headless/interactiveShell.js.map +1 -1
- package/dist/plugins/providers/deepseek/index.js +4 -6
- package/dist/plugins/providers/deepseek/index.js.map +1 -1
- package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -1
- package/dist/providers/openaiChatCompletionsProvider.js +33 -26
- package/dist/providers/openaiChatCompletionsProvider.js.map +1 -1
- package/dist/runtime/agentController.d.ts.map +1 -1
- package/dist/runtime/agentController.js +23 -7
- package/dist/runtime/agentController.js.map +1 -1
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +10 -3
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/tools/bashTools.d.ts +63 -0
- package/dist/tools/bashTools.d.ts.map +1 -1
- package/dist/tools/bashTools.js +186 -77
- package/dist/tools/bashTools.js.map +1 -1
- package/dist/tools/grepTools.d.ts.map +1 -1
- package/dist/tools/grepTools.js +41 -23
- package/dist/tools/grepTools.js.map +1 -1
- package/dist/tools/searchTools.d.ts.map +1 -1
- package/dist/tools/searchTools.js +18 -8
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/tools/webTools.d.ts.map +1 -1
- package/dist/tools/webTools.js +10 -2
- package/dist/tools/webTools.js.map +1 -1
- package/dist/ui/ink/App.d.ts +6 -1
- package/dist/ui/ink/App.d.ts.map +1 -1
- package/dist/ui/ink/App.js +20 -2
- 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 +9 -2
- package/dist/ui/ink/ChatStatic.js.map +1 -1
- package/dist/ui/ink/InkPromptController.d.ts +17 -8
- package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
- package/dist/ui/ink/InkPromptController.js +48 -21
- package/dist/ui/ink/InkPromptController.js.map +1 -1
- package/dist/ui/ink/StatusLine.d.ts +6 -7
- package/dist/ui/ink/StatusLine.d.ts.map +1 -1
- package/dist/ui/ink/StatusLine.js +9 -9
- package/dist/ui/ink/StatusLine.js.map +1 -1
- package/dist/ui/ink/markdownRender.d.ts +2 -0
- package/dist/ui/ink/markdownRender.d.ts.map +1 -0
- package/dist/ui/ink/markdownRender.js +76 -0
- package/dist/ui/ink/markdownRender.js.map +1 -0
- package/dist/ui/ink/narrationDedup.d.ts +28 -0
- package/dist/ui/ink/narrationDedup.d.ts.map +1 -0
- package/dist/ui/ink/narrationDedup.js +41 -0
- package/dist/ui/ink/narrationDedup.js.map +1 -0
- package/package.json +2 -1
- package/dist/core/hostedAuth.d.ts +0 -88
- package/dist/core/hostedAuth.d.ts.map +0 -1
- package/dist/core/hostedAuth.js +0 -219
- package/dist/core/hostedAuth.js.map +0 -1
|
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
19
19
|
import { exec as childExec } from 'node:child_process';
|
|
20
20
|
import { promisify } from 'node:util';
|
|
21
21
|
import chalk from 'chalk';
|
|
22
|
-
import { getHITL, hitlEvents } from '../core/hitl.js';
|
|
22
|
+
import { getHITL, hitlEvents, setDecisionPresenter } from '../core/hitl.js';
|
|
23
23
|
// Connector imports removed — CLI is local-only, no GitHub gate.
|
|
24
24
|
// Stub functions (antiTermination removed)
|
|
25
25
|
const initializeProtection = (_config) => { };
|
|
@@ -32,9 +32,10 @@ import { createAgentController } from '../runtime/agentController.js';
|
|
|
32
32
|
import { expandFileMentions, listWorkspaceFiles } from '../core/fileMentions.js';
|
|
33
33
|
import { resolveWorkspaceCaptureOptions, buildWorkspaceContext } from '../workspace.js';
|
|
34
34
|
import { loadAllSecrets, listSecretDefinitions, setSecretValue, getSecretValue, getSecretDefinition, classifyKeyEntry } from '../core/secretStore.js';
|
|
35
|
-
import { resolveKeyMode, keyModeLine
|
|
35
|
+
import { resolveKeyMode, keyModeLine } from '../core/keyResolution.js';
|
|
36
36
|
import { appendMemoryNote } from '../tools/memoryTools.js';
|
|
37
37
|
import { recordDeepSeekUsage, getUsage, TAVILY_MONTHLY_FREE, TAVILY_ONE_TIME_BONUS } from '../core/usage.js';
|
|
38
|
+
import { TurnTokenMeter } from '../core/turnTokenMeter.js';
|
|
38
39
|
import { listSessions, loadSessionById, saveSessionSnapshot } from '../core/sessionStore.js';
|
|
39
40
|
import { relativeTime } from '../core/relativeTime.js';
|
|
40
41
|
import { getModelContextInfo } from '../core/contextWindow.js';
|
|
@@ -59,6 +60,7 @@ import { startNewRun } from '../tools/fileChangeTracker.js';
|
|
|
59
60
|
import { onSudoPasswordNeeded, offSudoPasswordNeeded, provideSudoPassword } from '../core/sudoPasswordManager.js';
|
|
60
61
|
import { reportStatus, setStatusSink } from '../utils/statusReporter.js';
|
|
61
62
|
import { isSafetyRefusal } from '../core/refusalDetection.js';
|
|
63
|
+
import { shouldSynthesizeFromReasoning } from '../core/reasoningFallback.js';
|
|
62
64
|
import { formatToolCall, toolActivityLabel, formatToolResult, formatToolError } from '../shell/toolPresentation.js';
|
|
63
65
|
// Tool-result display (ANSI stripping, summarisation, the `⎿` block) now lives
|
|
64
66
|
// in ../shell/toolPresentation.ts — the shell just emits the formatted strings.
|
|
@@ -152,16 +154,11 @@ function welcomeBodyLines(input) {
|
|
|
152
154
|
const title = input.version ? `✻ Welcome to Trenchwork Coder ${input.version}` : '✻ Welcome to Trenchwork Coder';
|
|
153
155
|
const body = [title, ''];
|
|
154
156
|
const mode = input.keyMode ?? (input.hasApiKey ? 'own' : 'none');
|
|
155
|
-
if (mode === '
|
|
156
|
-
// Signed in — running on hosted keys. The mode line names the account so
|
|
157
|
-
// it's unmistakable this is NOT the user's own key.
|
|
158
|
-
body.push(input.keyModeLine ?? 'Signed in · using hosted keys');
|
|
159
|
-
}
|
|
160
|
-
else if (mode === 'own') {
|
|
157
|
+
if (mode === 'own') {
|
|
161
158
|
body.push(`${input.model} · ${input.provider}`, `Key: ${input.maskedKey} · /help for commands`);
|
|
162
159
|
}
|
|
163
160
|
else {
|
|
164
|
-
body.push('⚠ No DeepSeek API key configured', '', '
|
|
161
|
+
body.push('⚠ No DeepSeek API key configured', '', ' /key sk-… DeepSeek (required) · platform.deepseek.com', ' /key tvly-… Tavily web search (optional) · tavily.com');
|
|
165
162
|
}
|
|
166
163
|
if (input.cwd)
|
|
167
164
|
body.push(`cwd: ${input.cwd}`);
|
|
@@ -270,6 +267,9 @@ class InteractiveShell {
|
|
|
270
267
|
// currently occupying the context window). Drives the accurate "% context
|
|
271
268
|
// left" chrome indicator and the /context view.
|
|
272
269
|
lastInputTokens = null;
|
|
270
|
+
// Live `↑ N tokens` source: estimates from streamed chars, snaps to the
|
|
271
|
+
// provider-exact count on each usage event; resets per user turn.
|
|
272
|
+
turnTokenMeter = new TurnTokenMeter();
|
|
273
273
|
ctrlCCount = 0;
|
|
274
274
|
lastCtrlCTime = 0;
|
|
275
275
|
// Set when the user Ctrl+C interrupts a run; suppresses the auto-continue
|
|
@@ -421,6 +421,12 @@ class InteractiveShell {
|
|
|
421
421
|
hitlEvents.removeListener('prompt-open', onHitlOpen);
|
|
422
422
|
hitlEvents.removeListener('prompt-close', onHitlClose);
|
|
423
423
|
});
|
|
424
|
+
// Render HITL decisions through the in-app menu (below the prompt, same
|
|
425
|
+
// arrow+Enter UX as the slash palette) instead of the screen-clearing
|
|
426
|
+
// raw-mode fallback. The above suspend wiring is bypassed for this path —
|
|
427
|
+
// Ink's own input routing owns the menu, so the terminal is never handed off.
|
|
428
|
+
setDecisionPresenter((request) => this.presentHitlDecision(request));
|
|
429
|
+
onShutdown(() => setDecisionPresenter(null));
|
|
424
430
|
// Start the UI
|
|
425
431
|
this.promptController.start();
|
|
426
432
|
this.applyDebugState(this.debugEnabled);
|
|
@@ -457,16 +463,7 @@ class InteractiveShell {
|
|
|
457
463
|
// even though the fake "run" had no controller.send). Pending items
|
|
458
464
|
// will hit the normal early guard + error path, but the queue/dequeue
|
|
459
465
|
// logic itself runs for the test assertions.
|
|
460
|
-
|
|
461
|
-
const next = this.pendingPrompts.shift();
|
|
462
|
-
if (next) {
|
|
463
|
-
const r = this.promptController?.getRenderer();
|
|
464
|
-
r?.setFollowUpQueueMode(false);
|
|
465
|
-
r?.addUserHistoryItem(next);
|
|
466
|
-
r?.setQueuedPrompts(this.pendingPrompts.slice());
|
|
467
|
-
void this.processPrompt(next).catch(() => { });
|
|
468
|
-
}
|
|
469
|
-
}
|
|
466
|
+
void this.drainNextQueuedPrompt().catch(() => { });
|
|
470
467
|
}, forceBusyMs);
|
|
471
468
|
}
|
|
472
469
|
// Process any queued prompts
|
|
@@ -840,6 +837,12 @@ class InteractiveShell {
|
|
|
840
837
|
setSecretValue(entry.id, entry.value);
|
|
841
838
|
const label = getSecretDefinition(entry.id)?.label ?? entry.id;
|
|
842
839
|
renderer?.addEvent('system', chalk.green(`✓ ${label} saved`));
|
|
840
|
+
// Re-render the welcome banner so it reflects the now-saved DeepSeek
|
|
841
|
+
// key (masked key + model) instead of still showing "No DeepSeek API
|
|
842
|
+
// key configured". Tavily-only saves don't appear in the banner.
|
|
843
|
+
if (entry.id === 'DEEPSEEK_API_KEY') {
|
|
844
|
+
void this.showWelcome();
|
|
845
|
+
}
|
|
843
846
|
}
|
|
844
847
|
catch (error) {
|
|
845
848
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -851,34 +854,6 @@ class InteractiveShell {
|
|
|
851
854
|
}
|
|
852
855
|
return true;
|
|
853
856
|
}
|
|
854
|
-
// /account — show the active key source (hosted vs your own) and switch
|
|
855
|
-
// between them. `/account own` forces your own keys even while signed in;
|
|
856
|
-
// `/account hosted` returns to hosted. Hosted keys come from sign-in
|
|
857
|
-
// (server-side, never baked into this client) — see core/hostedAuth.ts.
|
|
858
|
-
if (lower === '/account' || lower.startsWith('/account ')) {
|
|
859
|
-
const r = this.promptController?.getRenderer();
|
|
860
|
-
const arg = trimmed.slice('/account'.length).trim().toLowerCase();
|
|
861
|
-
if (arg === 'own')
|
|
862
|
-
setPreferOwnKeys(true);
|
|
863
|
-
else if (arg === 'hosted')
|
|
864
|
-
setPreferOwnKeys(false);
|
|
865
|
-
if (arg === 'own' || arg === 'hosted')
|
|
866
|
-
void this.showWelcome(); // banner reflects the switch
|
|
867
|
-
r?.addEvent('system', this.accountStatusText(resolveKeyMode()));
|
|
868
|
-
return true;
|
|
869
|
-
}
|
|
870
|
-
// /login — Google sign-in via ero.solar (loopback OAuth) to unlock hosted keys.
|
|
871
|
-
if (lower === '/login' || lower === '/signin') {
|
|
872
|
-
void this.handleLogin();
|
|
873
|
-
return true;
|
|
874
|
-
}
|
|
875
|
-
// /logout — drop the hosted session (back to your own keys, or none).
|
|
876
|
-
if (lower === '/logout' || lower === '/signout') {
|
|
877
|
-
clearHostedSession();
|
|
878
|
-
this.promptController?.getRenderer()?.addEvent('system', chalk.green('✓ Signed out — using your own keys.'));
|
|
879
|
-
void this.showWelcome();
|
|
880
|
-
return true;
|
|
881
|
-
}
|
|
882
857
|
// /update — check npm for a newer version and upgrade in-shell.
|
|
883
858
|
if (lower === '/update' || lower === '/upgrade') {
|
|
884
859
|
void this.handleUpdateCommand();
|
|
@@ -923,8 +898,7 @@ class InteractiveShell {
|
|
|
923
898
|
return true;
|
|
924
899
|
}
|
|
925
900
|
// /cost — DeepSeek tokens + Tavily searches consumed (this session + all
|
|
926
|
-
// time), and the
|
|
927
|
-
// backend number shown in the ero.solar portal.
|
|
901
|
+
// time), and the Tavily shared-proxy free-pool reference.
|
|
928
902
|
if (lower === '/cost' || lower === '/spend') {
|
|
929
903
|
this.showUsage();
|
|
930
904
|
return true;
|
|
@@ -1486,8 +1460,7 @@ class InteractiveShell {
|
|
|
1486
1460
|
label('DeepSeek') + dim(`${ds(cumulative)} · this session ${ds(session)}`),
|
|
1487
1461
|
label('Tavily') + dim(`${cumulative.tavilySearches} searches · this session ${session.tavilySearches}`),
|
|
1488
1462
|
'',
|
|
1489
|
-
dim(`
|
|
1490
|
-
dim('Account-wide totals + remaining show in the ero.solar portal after sign-in.'),
|
|
1463
|
+
dim(`Tavily free pool: ${TAVILY_MONTHLY_FREE.toLocaleString('en-US')}/mo + ${TAVILY_ONE_TIME_BONUS.toLocaleString('en-US')} one-time bonus (shared proxy; set your own key for unlimited).`),
|
|
1491
1464
|
];
|
|
1492
1465
|
this.promptController.setInlinePanel(lines);
|
|
1493
1466
|
this.scheduleInlinePanelDismiss();
|
|
@@ -1575,54 +1548,6 @@ class InteractiveShell {
|
|
|
1575
1548
|
revertAllChanges(this.workingDir); // restores/deletes on disk + clears tracking
|
|
1576
1549
|
renderer?.addEvent('system', chalk.green('✓ ' + rewindResultLine(restored, deleted)));
|
|
1577
1550
|
}
|
|
1578
|
-
/** One-line summary of the active key source for /account. */
|
|
1579
|
-
accountStatusText(s) {
|
|
1580
|
-
if (s.mode === 'hosted') {
|
|
1581
|
-
return chalk.green(`Hosted keys · signed in as ${s.email}.`) +
|
|
1582
|
-
chalk.dim(` /account own to use your own · /logout to sign out.`);
|
|
1583
|
-
}
|
|
1584
|
-
if (s.mode === 'own') {
|
|
1585
|
-
return chalk.green(`Your own keys · DeepSeek${s.ownTavily ? ' + Tavily' : ''}.`) +
|
|
1586
|
-
chalk.dim(s.signedIn ? ` /account hosted to use hosted keys.` : ` /login to use hosted keys.`);
|
|
1587
|
-
}
|
|
1588
|
-
return chalk.yellow('No keys configured.') +
|
|
1589
|
-
chalk.dim(' /login for hosted keys, or set your own: /key sk-… (and /key tvly-…).');
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* /login — Google sign-in via ero.solar. Opens the browser to the SSO URL and
|
|
1593
|
-
* runs a one-shot 127.0.0.1 loopback server that captures the redirect with
|
|
1594
|
-
* the short-lived token (see core/hostedAuth.ts). On success the CLI is on
|
|
1595
|
-
* hosted keys; no key ever touches this client.
|
|
1596
|
-
*/
|
|
1597
|
-
async handleLogin() {
|
|
1598
|
-
const r = this.promptController?.getRenderer();
|
|
1599
|
-
const status = resolveKeyMode();
|
|
1600
|
-
if (status.signedIn) {
|
|
1601
|
-
r?.addEvent('system', chalk.green(`Already signed in as ${status.email}.`) +
|
|
1602
|
-
chalk.dim(' /logout to sign out · /account to switch key source.'));
|
|
1603
|
-
return;
|
|
1604
|
-
}
|
|
1605
|
-
r?.addEvent('system', chalk.dim('Opening ero.solar sign-in in your browser — finish there, then return here…'));
|
|
1606
|
-
const result = await loginViaLoopback({ open: (url) => this.openInBrowser(url) });
|
|
1607
|
-
if (result.ok && result.session) {
|
|
1608
|
-
r?.addEvent('system', chalk.green(`✓ Signed in as ${result.session.email} — using hosted keys.`));
|
|
1609
|
-
void this.showWelcome();
|
|
1610
|
-
}
|
|
1611
|
-
else {
|
|
1612
|
-
r?.addEvent('system', chalk.yellow(`Sign-in didn't complete: ${result.error ?? 'unknown error'}.`) +
|
|
1613
|
-
chalk.dim(' Retry /login, or use /key sk-… for your own key.'));
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
/** Best-effort open a URL in the OS browser; also prints it as a fallback. */
|
|
1617
|
-
openInBrowser(url) {
|
|
1618
|
-
const opener = process.platform === 'darwin' ? 'open'
|
|
1619
|
-
: process.platform === 'win32' ? 'start ""'
|
|
1620
|
-
: 'xdg-open';
|
|
1621
|
-
// url is built by loginViaLoopback (no user input) and JSON-quoted, so the
|
|
1622
|
-
// `&` in the query string can't break out of the argument.
|
|
1623
|
-
childExec(`${opener} ${JSON.stringify(url)}`, () => { });
|
|
1624
|
-
this.promptController?.getRenderer()?.addEvent('system', chalk.dim(`If the browser didn't open: ${url}`));
|
|
1625
|
-
}
|
|
1626
1551
|
showHelp() {
|
|
1627
1552
|
if (!this.promptController?.supportsInlinePanel()) {
|
|
1628
1553
|
this.promptController?.setStatusMessage('Help: /key sk-… (everything else is automatic)');
|
|
@@ -1637,10 +1562,8 @@ class InteractiveShell {
|
|
|
1637
1562
|
const lines = [
|
|
1638
1563
|
chalk.bold.hex('#e8e9ed')('Trenchwork Coder') + dim(' (press any key to dismiss)'),
|
|
1639
1564
|
'',
|
|
1640
|
-
cmd('/login') + dim(' Sign in with Google (ero.solar) to use hosted keys'),
|
|
1641
1565
|
cmd('/key sk-…') + dim(' Set your DeepSeek API key (required)'),
|
|
1642
1566
|
cmd('/key tvly-…') + dim(' Set your Tavily key for web search (optional)'),
|
|
1643
|
-
cmd('/account') + dim(' Show / switch key source (hosted vs your own)'),
|
|
1644
1567
|
cmd('/update') + dim(' Check npm and upgrade to the latest version'),
|
|
1645
1568
|
cmd('/resume') + dim(' Restore a previous conversation'),
|
|
1646
1569
|
cmd('/context') + dim(' Show context-window usage'),
|
|
@@ -1778,6 +1701,27 @@ class InteractiveShell {
|
|
|
1778
1701
|
}
|
|
1779
1702
|
void this.processPrompt(trimmed);
|
|
1780
1703
|
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Dequeue and run the next live follow-up, if any: commit its user line to
|
|
1706
|
+
* history, refresh the transient queue UI, then process it. Single source of
|
|
1707
|
+
* truth for the dequeue so the per-turn drain and the test seam can't drift —
|
|
1708
|
+
* that drift is exactly how a queued-prompt UX goes subtly wrong.
|
|
1709
|
+
*/
|
|
1710
|
+
async drainNextQueuedPrompt() {
|
|
1711
|
+
if (this.pendingPrompts.length === 0 || this.shouldExit) {
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
const next = this.pendingPrompts.shift();
|
|
1715
|
+
if (!next) {
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
const r = this.promptController?.getRenderer();
|
|
1719
|
+
r?.setFollowUpQueueMode(false);
|
|
1720
|
+
r?.addUserHistoryItem(next);
|
|
1721
|
+
r?.setQueuedPrompts(this.pendingPrompts.slice());
|
|
1722
|
+
await this.processPrompt(next);
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1781
1725
|
async processPrompt(prompt) {
|
|
1782
1726
|
if (this.isProcessing) {
|
|
1783
1727
|
return;
|
|
@@ -1802,6 +1746,10 @@ class InteractiveShell {
|
|
|
1802
1746
|
this.autoGovernor.reset();
|
|
1803
1747
|
this.failureRegistry.reset();
|
|
1804
1748
|
this.adversarialCorrectionCount = 0;
|
|
1749
|
+
// New user turn → `↑ N tokens` restarts from zero. Continuations
|
|
1750
|
+
// ('continue' / IMPORTANT:-prefixed) keep accumulating into the same turn.
|
|
1751
|
+
this.turnTokenMeter.reset();
|
|
1752
|
+
this.promptController?.setMetaStatus({ outputTokens: 0 });
|
|
1805
1753
|
// Pinned-prompt persistence removed per request — no longer
|
|
1806
1754
|
// displayed above the chat box.
|
|
1807
1755
|
}
|
|
@@ -1886,6 +1834,14 @@ class InteractiveShell {
|
|
|
1886
1834
|
// Stream content as it arrives
|
|
1887
1835
|
this.currentResponseBuffer += event.content ?? '';
|
|
1888
1836
|
this.finalResponseText += event.content ?? '';
|
|
1837
|
+
// Live `↑ N tokens`: estimate from streamed chars until the
|
|
1838
|
+
// provider's usage event snaps the exact count. Synthetic deltas
|
|
1839
|
+
// (already-streamed narration replays, retry notices) were never
|
|
1840
|
+
// provider output — metering them double-counts.
|
|
1841
|
+
if (!event.synthetic) {
|
|
1842
|
+
this.turnTokenMeter.addStreamedChars((event.content ?? '').length);
|
|
1843
|
+
this.promptController?.setMetaStatus({ outputTokens: this.turnTokenMeter.current() });
|
|
1844
|
+
}
|
|
1889
1845
|
if (renderer) {
|
|
1890
1846
|
renderer.addEvent('stream', event.content);
|
|
1891
1847
|
}
|
|
@@ -1899,6 +1855,10 @@ class InteractiveShell {
|
|
|
1899
1855
|
case 'reasoning':
|
|
1900
1856
|
// Accumulate reasoning for potential fallback synthesis
|
|
1901
1857
|
reasoningBuffer += event.content ?? '';
|
|
1858
|
+
// Reasoning streams count toward completion_tokens too (DeepSeek
|
|
1859
|
+
// thinking) — meter them so the live `↑` doesn't sit at zero.
|
|
1860
|
+
this.turnTokenMeter.addStreamedChars((event.content ?? '').length);
|
|
1861
|
+
this.promptController?.setMetaStatus({ outputTokens: this.turnTokenMeter.current() });
|
|
1902
1862
|
// Update status to show reasoning is actively streaming
|
|
1903
1863
|
this.promptController?.setActivityMessage('Thinking');
|
|
1904
1864
|
// Start the reasoning timer on first reasoning event
|
|
@@ -2033,6 +1993,10 @@ class InteractiveShell {
|
|
|
2033
1993
|
case 'usage': {
|
|
2034
1994
|
// Meter cumulative DeepSeek consumption for /usage + the portal.
|
|
2035
1995
|
recordDeepSeekUsage(event.inputTokens, event.outputTokens);
|
|
1996
|
+
// Snap the live `↑` estimate to the provider-exact output count
|
|
1997
|
+
// for this request; the meter keeps accumulating across the
|
|
1998
|
+
// turn's tool-loop requests.
|
|
1999
|
+
this.turnTokenMeter.recordExactOutput(event.outputTokens ?? 0);
|
|
2036
2000
|
// inputTokens = exactly what occupies the context window this turn.
|
|
2037
2001
|
// The real model window (not a hardcoded guess) is the denominator
|
|
2038
2002
|
// so "% context left" reflects the actual model.
|
|
@@ -2042,7 +2006,8 @@ class InteractiveShell {
|
|
|
2042
2006
|
}
|
|
2043
2007
|
const windowTokens = getModelContextInfo(this.profileConfig.model).contextWindow;
|
|
2044
2008
|
this.promptController?.setMetaStatus({
|
|
2045
|
-
|
|
2009
|
+
outputTokens: this.turnTokenMeter.current(),
|
|
2010
|
+
contextTokens,
|
|
2046
2011
|
tokenLimit: windowTokens,
|
|
2047
2012
|
});
|
|
2048
2013
|
break;
|
|
@@ -2134,7 +2099,7 @@ class InteractiveShell {
|
|
|
2134
2099
|
// This handles models like deepseek-v4-pro that output thinking but empty response
|
|
2135
2100
|
// Also handles step timeouts where the model was stuck
|
|
2136
2101
|
// IMPORTANT: Don't add "Next steps" when only reasoning occurred - only after real work
|
|
2137
|
-
if ((
|
|
2102
|
+
if (shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
2138
2103
|
const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
|
|
2139
2104
|
if (synthesized && renderer) {
|
|
2140
2105
|
renderer.addEvent('stream', '\n' + synthesized);
|
|
@@ -2156,7 +2121,7 @@ class InteractiveShell {
|
|
|
2156
2121
|
renderer.addEvent('error', message);
|
|
2157
2122
|
}
|
|
2158
2123
|
// Fallback: If we have reasoning content but no response was generated, synthesize one
|
|
2159
|
-
if (!episodeSuccess &&
|
|
2124
|
+
if (!episodeSuccess && shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
2160
2125
|
const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
|
|
2161
2126
|
if (synthesized && renderer) {
|
|
2162
2127
|
renderer.addEvent('stream', '\n' + synthesized);
|
|
@@ -2169,7 +2134,7 @@ class InteractiveShell {
|
|
|
2169
2134
|
// Exit critical section - allow termination again
|
|
2170
2135
|
exitCriticalSection();
|
|
2171
2136
|
// Final fallback: If stream ended without message.complete but we have reasoning
|
|
2172
|
-
if (!episodeSuccess &&
|
|
2137
|
+
if (!episodeSuccess && shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
2173
2138
|
const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
|
|
2174
2139
|
if (synthesized && renderer) {
|
|
2175
2140
|
renderer.addEvent('stream', '\n' + synthesized);
|
|
@@ -2217,17 +2182,11 @@ class InteractiveShell {
|
|
|
2217
2182
|
// Autosave the conversation so /resume has something to restore. Each
|
|
2218
2183
|
// turn updates the same snapshot in place (keyed by this.sessionId).
|
|
2219
2184
|
this.persistSessionSnapshot();
|
|
2220
|
-
// Process any queued
|
|
2221
|
-
//
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
const r = this.promptController?.getRenderer();
|
|
2226
|
-
r?.setFollowUpQueueMode(false);
|
|
2227
|
-
r?.addUserHistoryItem(next);
|
|
2228
|
-
r?.setQueuedPrompts(this.pendingPrompts.slice());
|
|
2229
|
-
await this.processPrompt(next);
|
|
2230
|
-
}
|
|
2185
|
+
// Process any queued follow-up — single source of truth (drainNextQueuedPrompt).
|
|
2186
|
+
// This takes priority over auto-continue: a user's explicit follow-up runs
|
|
2187
|
+
// before the loop decides the original task is "complete".
|
|
2188
|
+
if (await this.drainNextQueuedPrompt()) {
|
|
2189
|
+
// handled
|
|
2231
2190
|
}
|
|
2232
2191
|
else if (refusedTurn) {
|
|
2233
2192
|
// Refusal terminates the turn. Don't re-prompt the model — the
|
|
@@ -2395,6 +2354,56 @@ class InteractiveShell {
|
|
|
2395
2354
|
this.promptController?.setStatusMessage(`HITL: ${mode}`);
|
|
2396
2355
|
setTimeout(() => this.promptController?.setStatusMessage(null), 1500);
|
|
2397
2356
|
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Render a HITL decision as an in-app menu BELOW the prompt: the question and
|
|
2359
|
+
* any context as a block, then the model's options plus an "Enter your own"
|
|
2360
|
+
* write-in — navigable with ↑↓ and Enter, the same surface as the slash
|
|
2361
|
+
* palette. No screen clear, no terminal handoff. Resolves with the chosen
|
|
2362
|
+
* option id (or the typed custom plan).
|
|
2363
|
+
*/
|
|
2364
|
+
presentHitlDecision(request) {
|
|
2365
|
+
return new Promise((resolve) => {
|
|
2366
|
+
const controller = this.promptController;
|
|
2367
|
+
const r = controller?.getRenderer();
|
|
2368
|
+
const fallbackId = request.defaultOptionId ?? request.options[0]?.id ?? '__custom__';
|
|
2369
|
+
if (!controller || !r) {
|
|
2370
|
+
resolve({ optionId: fallbackId });
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
// The question block, so the choice has its context right above it.
|
|
2374
|
+
const block = [chalk.bold.hex('#82aaff')(`◆ ${request.title}`)];
|
|
2375
|
+
if (request.description?.trim())
|
|
2376
|
+
block.push(chalk.hex('#e8e9ed')(request.description.trim()));
|
|
2377
|
+
if (request.context?.trim())
|
|
2378
|
+
block.push(chalk.dim(request.context.trim()));
|
|
2379
|
+
r.addEvent('system', block.join('\n'));
|
|
2380
|
+
const items = [
|
|
2381
|
+
...request.options.map((o) => ({ id: o.id, label: o.label, description: o.description })),
|
|
2382
|
+
{ id: '__custom__', label: 'Enter your own', description: 'Type a custom plan, instruction, or alternative approach' },
|
|
2383
|
+
];
|
|
2384
|
+
// Default the cursor to the model's recommended option, if it named one.
|
|
2385
|
+
const initialIndex = request.defaultOptionId
|
|
2386
|
+
? Math.max(0, items.findIndex((i) => i.id === request.defaultOptionId))
|
|
2387
|
+
: 0;
|
|
2388
|
+
controller.setMenu(items, { title: 'Choose — ↑↓ then Enter', initialIndex }, (selected) => {
|
|
2389
|
+
if (!selected) {
|
|
2390
|
+
resolve({ optionId: fallbackId }); // Esc → the model's default
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
if (selected.id === '__custom__') {
|
|
2394
|
+
r.addEvent('system', chalk.cyan('Type your own plan or instruction, then Enter:'));
|
|
2395
|
+
void r.captureInput({ allowEmpty: false, trim: true, resetBuffer: true })
|
|
2396
|
+
.then((text) => {
|
|
2397
|
+
const t = (text || '').trim();
|
|
2398
|
+
resolve(t ? { optionId: '__custom__', customInput: t } : { optionId: fallbackId });
|
|
2399
|
+
})
|
|
2400
|
+
.catch(() => resolve({ optionId: fallbackId }));
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
resolve({ optionId: selected.id });
|
|
2404
|
+
});
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2398
2407
|
/**
|
|
2399
2408
|
* Shift+Tab cycled the permission mode. The hint line under the input box
|
|
2400
2409
|
* already shows the active mode; this surfaces a brief one-line note in
|