@trenchwork/coder 1.3.0 → 1.5.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/dist/bin/deepseek.js +2 -2
- 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/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/diffPanel.js +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.js +4 -4
- 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 +21 -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 +90 -137
- 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 +16 -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 +21 -3
- package/dist/ui/ink/App.js.map +1 -1
- package/dist/ui/ink/ChatStatic.d.ts +1 -1
- package/dist/ui/ink/ChatStatic.d.ts.map +1 -1
- package/dist/ui/ink/ChatStatic.js +19 -12
- package/dist/ui/ink/ChatStatic.js.map +1 -1
- package/dist/ui/ink/InkPromptController.d.ts +8 -8
- package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
- package/dist/ui/ink/InkPromptController.js +19 -11
- package/dist/ui/ink/InkPromptController.js.map +1 -1
- package/dist/ui/ink/Menu.js +1 -1
- package/dist/ui/ink/Menu.js.map +1 -1
- package/dist/ui/ink/Prompt.js +3 -3
- 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 +11 -11
- 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/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +56 -57
- package/dist/ui/theme.js.map +1 -1
- package/package.json +3 -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
|
@@ -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
|
|
@@ -342,7 +342,7 @@ class InteractiveShell {
|
|
|
342
342
|
const secret = secrets.find(s => s.id === first);
|
|
343
343
|
if (secret && this.promptController.supportsInlinePanel()) {
|
|
344
344
|
const lines = [
|
|
345
|
-
chalk.bold.hex('#
|
|
345
|
+
chalk.bold.hex('#e8e9ed')(`Set ${secret.label}`),
|
|
346
346
|
chalk.dim(secret.description),
|
|
347
347
|
'',
|
|
348
348
|
chalk.dim('Enter value (or press Enter to skip)'),
|
|
@@ -457,16 +457,7 @@ class InteractiveShell {
|
|
|
457
457
|
// even though the fake "run" had no controller.send). Pending items
|
|
458
458
|
// will hit the normal early guard + error path, but the queue/dequeue
|
|
459
459
|
// 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
|
-
}
|
|
460
|
+
void this.drainNextQueuedPrompt().catch(() => { });
|
|
470
461
|
}, forceBusyMs);
|
|
471
462
|
}
|
|
472
463
|
// Process any queued prompts
|
|
@@ -517,13 +508,13 @@ class InteractiveShell {
|
|
|
517
508
|
chalk.yellow(`v${updateInfo.current}`) +
|
|
518
509
|
chalk.dim(' → ') +
|
|
519
510
|
chalk.green(`v${updateInfo.latest}`) +
|
|
520
|
-
chalk.dim(' · type ') + chalk.hex('#
|
|
511
|
+
chalk.dim(' · type ') + chalk.hex('#ffd666')('/update') + chalk.dim(' to upgrade'));
|
|
521
512
|
}
|
|
522
513
|
// Clean, minimal welcome — a sparkle + the essentials in a rounded box,
|
|
523
514
|
// mirroring Claude Code. The pure composeWelcomeLines() is the contract for
|
|
524
515
|
// WHICH lines appear; here we draw the same box with brand colour.
|
|
525
|
-
const
|
|
526
|
-
const wire = chalk.hex('#
|
|
516
|
+
const starlight = chalk.hex('#82aaff');
|
|
517
|
+
const wire = chalk.hex('#30303a');
|
|
527
518
|
const keyStatus = resolveKeyMode();
|
|
528
519
|
const body = welcomeBodyLines({
|
|
529
520
|
hasApiKey,
|
|
@@ -535,7 +526,7 @@ class InteractiveShell {
|
|
|
535
526
|
keyModeLine: keyModeLine(keyStatus),
|
|
536
527
|
version: `v${version}`,
|
|
537
528
|
});
|
|
538
|
-
const boxed = roundedBox(body, (cell) => cell.replace('✻',
|
|
529
|
+
const boxed = roundedBox(body, (cell) => cell.replace('✻', starlight('✻')), (s) => wire(s));
|
|
539
530
|
const welcomeContent = ['', ...updateLines, ...boxed, ''].join('\n');
|
|
540
531
|
// Use renderer event system instead of direct stdout writes
|
|
541
532
|
renderer.addEvent('banner', welcomeContent);
|
|
@@ -840,6 +831,12 @@ class InteractiveShell {
|
|
|
840
831
|
setSecretValue(entry.id, entry.value);
|
|
841
832
|
const label = getSecretDefinition(entry.id)?.label ?? entry.id;
|
|
842
833
|
renderer?.addEvent('system', chalk.green(`✓ ${label} saved`));
|
|
834
|
+
// Re-render the welcome banner so it reflects the now-saved DeepSeek
|
|
835
|
+
// key (masked key + model) instead of still showing "No DeepSeek API
|
|
836
|
+
// key configured". Tavily-only saves don't appear in the banner.
|
|
837
|
+
if (entry.id === 'DEEPSEEK_API_KEY') {
|
|
838
|
+
void this.showWelcome();
|
|
839
|
+
}
|
|
843
840
|
}
|
|
844
841
|
catch (error) {
|
|
845
842
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -851,34 +848,6 @@ class InteractiveShell {
|
|
|
851
848
|
}
|
|
852
849
|
return true;
|
|
853
850
|
}
|
|
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
851
|
// /update — check npm for a newer version and upgrade in-shell.
|
|
883
852
|
if (lower === '/update' || lower === '/upgrade') {
|
|
884
853
|
void this.handleUpdateCommand();
|
|
@@ -923,8 +892,7 @@ class InteractiveShell {
|
|
|
923
892
|
return true;
|
|
924
893
|
}
|
|
925
894
|
// /cost — DeepSeek tokens + Tavily searches consumed (this session + all
|
|
926
|
-
// time), and the
|
|
927
|
-
// backend number shown in the ero.solar portal.
|
|
895
|
+
// time), and the Tavily shared-proxy free-pool reference.
|
|
928
896
|
if (lower === '/cost' || lower === '/spend') {
|
|
929
897
|
this.showUsage();
|
|
930
898
|
return true;
|
|
@@ -1301,7 +1269,7 @@ class InteractiveShell {
|
|
|
1301
1269
|
// Show in inline panel (no chat output)
|
|
1302
1270
|
if (this.promptController?.supportsInlinePanel()) {
|
|
1303
1271
|
const lines = [
|
|
1304
|
-
chalk.bold.hex('#
|
|
1272
|
+
chalk.bold.hex('#e8e9ed')(`Set ${secret.label}`),
|
|
1305
1273
|
chalk.dim(secret.description),
|
|
1306
1274
|
'',
|
|
1307
1275
|
chalk.dim('Enter value (or press Enter to skip)'),
|
|
@@ -1449,16 +1417,16 @@ class InteractiveShell {
|
|
|
1449
1417
|
const model = this.profileConfig.model;
|
|
1450
1418
|
const windowTokens = getModelContextInfo(model).contextWindow;
|
|
1451
1419
|
const usage = computeContextUsage(this.controller.getHistory(), windowTokens, this.lastInputTokens);
|
|
1452
|
-
const label = (s) => chalk.hex('#
|
|
1420
|
+
const label = (s) => chalk.hex('#ffd666')(s.padEnd(8));
|
|
1453
1421
|
const dim = (s) => chalk.dim(s);
|
|
1454
1422
|
const approx = usage.estimated ? '~' : '';
|
|
1455
1423
|
const barWidth = 24;
|
|
1456
1424
|
const filled = Math.min(barWidth, Math.round((usage.percentUsed / 100) * barWidth));
|
|
1457
1425
|
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
1458
1426
|
const lines = [
|
|
1459
|
-
chalk.bold.hex('#
|
|
1427
|
+
chalk.bold.hex('#e8e9ed')('Context') + dim(' (press any key to dismiss)'),
|
|
1460
1428
|
'',
|
|
1461
|
-
dim(bar) + ' ' + chalk.hex('#
|
|
1429
|
+
dim(bar) + ' ' + chalk.hex('#e8e9ed')(`${usage.percentLeft}% context left`),
|
|
1462
1430
|
'',
|
|
1463
1431
|
label('Window') + dim(`${formatTokenCount(windowTokens)} tokens · ${model}`),
|
|
1464
1432
|
label('Used') + dim(`${approx}${formatTokenCount(usage.usedTokens)} tokens (${usage.percentUsed}%)`),
|
|
@@ -1477,17 +1445,16 @@ class InteractiveShell {
|
|
|
1477
1445
|
return;
|
|
1478
1446
|
}
|
|
1479
1447
|
const { session, cumulative } = getUsage();
|
|
1480
|
-
const label = (s) => chalk.hex('#
|
|
1448
|
+
const label = (s) => chalk.hex('#ffd666')(s.padEnd(9));
|
|
1481
1449
|
const dim = (s) => chalk.dim(s);
|
|
1482
1450
|
const ds = (u) => `${formatTokenCount(u.deepseekInputTokens)} in · ${formatTokenCount(u.deepseekOutputTokens)} out`;
|
|
1483
1451
|
const lines = [
|
|
1484
|
-
chalk.bold.hex('#
|
|
1452
|
+
chalk.bold.hex('#e8e9ed')('Usage') + dim(' (press any key to dismiss)'),
|
|
1485
1453
|
'',
|
|
1486
1454
|
label('DeepSeek') + dim(`${ds(cumulative)} · this session ${ds(session)}`),
|
|
1487
1455
|
label('Tavily') + dim(`${cumulative.tavilySearches} searches · this session ${session.tavilySearches}`),
|
|
1488
1456
|
'',
|
|
1489
|
-
dim(`
|
|
1490
|
-
dim('Account-wide totals + remaining show in the ero.solar portal after sign-in.'),
|
|
1457
|
+
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
1458
|
];
|
|
1492
1459
|
this.promptController.setInlinePanel(lines);
|
|
1493
1460
|
this.scheduleInlinePanelDismiss();
|
|
@@ -1531,7 +1498,7 @@ class InteractiveShell {
|
|
|
1531
1498
|
const panel = renderChangePanel(items);
|
|
1532
1499
|
const dim = (s) => chalk.dim(s);
|
|
1533
1500
|
const lines = [
|
|
1534
|
-
chalk.bold.hex('#
|
|
1501
|
+
chalk.bold.hex('#e8e9ed')('Changes') + dim(' (press any key to dismiss)'),
|
|
1535
1502
|
'',
|
|
1536
1503
|
...panel.lines,
|
|
1537
1504
|
];
|
|
@@ -1560,7 +1527,7 @@ class InteractiveShell {
|
|
|
1560
1527
|
const lines = rewindPreviewLines(items);
|
|
1561
1528
|
lines.forEach((line, i) => {
|
|
1562
1529
|
const last = i === lines.length - 1;
|
|
1563
|
-
renderer?.addEvent('system', last ? chalk.hex('#
|
|
1530
|
+
renderer?.addEvent('system', last ? chalk.hex('#ffd666')(line) : chalk.dim(line));
|
|
1564
1531
|
});
|
|
1565
1532
|
return;
|
|
1566
1533
|
}
|
|
@@ -1575,72 +1542,22 @@ class InteractiveShell {
|
|
|
1575
1542
|
revertAllChanges(this.workingDir); // restores/deletes on disk + clears tracking
|
|
1576
1543
|
renderer?.addEvent('system', chalk.green('✓ ' + rewindResultLine(restored, deleted)));
|
|
1577
1544
|
}
|
|
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
1545
|
showHelp() {
|
|
1627
1546
|
if (!this.promptController?.supportsInlinePanel()) {
|
|
1628
1547
|
this.promptController?.setStatusMessage('Help: /key sk-… (everything else is automatic)');
|
|
1629
1548
|
setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
|
|
1630
1549
|
return;
|
|
1631
1550
|
}
|
|
1632
|
-
const cmd = (s) => chalk.hex('#
|
|
1551
|
+
const cmd = (s) => chalk.hex('#ffd666')(s);
|
|
1633
1552
|
const dim = (s) => chalk.dim(s);
|
|
1634
1553
|
// One knob. Everything else (ultracode, max thought, the adversarial
|
|
1635
1554
|
// verifier, auto-continue) is on by default for max performance — there
|
|
1636
1555
|
// are no toggles to tune.
|
|
1637
1556
|
const lines = [
|
|
1638
|
-
chalk.bold.hex('#
|
|
1557
|
+
chalk.bold.hex('#e8e9ed')('Trenchwork Coder') + dim(' (press any key to dismiss)'),
|
|
1639
1558
|
'',
|
|
1640
|
-
cmd('/login') + dim(' Sign in with Google (ero.solar) to use hosted keys'),
|
|
1641
1559
|
cmd('/key sk-…') + dim(' Set your DeepSeek API key (required)'),
|
|
1642
1560
|
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
1561
|
cmd('/update') + dim(' Check npm and upgrade to the latest version'),
|
|
1645
1562
|
cmd('/resume') + dim(' Restore a previous conversation'),
|
|
1646
1563
|
cmd('/context') + dim(' Show context-window usage'),
|
|
@@ -1663,35 +1580,35 @@ class InteractiveShell {
|
|
|
1663
1580
|
setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
|
|
1664
1581
|
return;
|
|
1665
1582
|
}
|
|
1666
|
-
const kb = (key) => chalk.hex('#
|
|
1583
|
+
const kb = (key) => chalk.hex('#ffd666')(key);
|
|
1667
1584
|
const desc = (text) => chalk.dim(text);
|
|
1668
1585
|
// Only shortcuts the Ink Prompt (src/ui/ink/Prompt.tsx) actually
|
|
1669
1586
|
// implements are listed — advertising keys the input handler ignores
|
|
1670
1587
|
// would be a deceptive panel (Glasswing transparency).
|
|
1671
1588
|
const lines = [
|
|
1672
|
-
chalk.bold.hex('#
|
|
1589
|
+
chalk.bold.hex('#e8e9ed')('Keyboard Shortcuts') + chalk.dim(' (press any key to dismiss)'),
|
|
1673
1590
|
'',
|
|
1674
|
-
chalk.hex('#
|
|
1591
|
+
chalk.hex('#64d2ff')('Navigation'),
|
|
1675
1592
|
` ${kb('Ctrl+A')} / ${kb('Home')} ${desc('Move to start of line')}`,
|
|
1676
1593
|
` ${kb('Ctrl+E')} / ${kb('End')} ${desc('Move to end of line')}`,
|
|
1677
1594
|
` ${kb('←')} / ${kb('→')} ${desc('Move cursor')}`,
|
|
1678
1595
|
` ${kb('↑')} / ${kb('↓')} ${desc('Prompt history (older / newer)')}`,
|
|
1679
1596
|
` ${kb('Ctrl+R')} ${desc('Reverse-search prompt history')}`,
|
|
1680
1597
|
'',
|
|
1681
|
-
chalk.hex('#
|
|
1598
|
+
chalk.hex('#64d2ff')('Editing'),
|
|
1682
1599
|
` ${kb('Ctrl+U')} ${desc('Delete to start of line')}`,
|
|
1683
1600
|
` ${kb('Ctrl+W')} ${desc('Delete word backward')}`,
|
|
1684
1601
|
` ${kb('Ctrl+K')} ${desc('Delete to end of line')}`,
|
|
1685
1602
|
'',
|
|
1686
|
-
chalk.hex('#
|
|
1603
|
+
chalk.hex('#64d2ff')('Modes'),
|
|
1687
1604
|
` ${kb('Shift+Tab')} ${desc('Cycle permission mode (default · accept edits · plan)')}`,
|
|
1688
1605
|
` ${kb('Ctrl+O')} ${desc('Expand the last truncated tool result')}`,
|
|
1689
1606
|
'',
|
|
1690
|
-
chalk.hex('#
|
|
1607
|
+
chalk.hex('#64d2ff')('Completion'),
|
|
1691
1608
|
` ${kb('@')} ${desc('Autocomplete a file (↑/↓ · Tab/Enter); its content is inlined for the agent')}`,
|
|
1692
1609
|
` ${kb('/')} ${desc('Autocomplete a command (↑/↓ · Tab to complete; Enter runs it)')}`,
|
|
1693
1610
|
'',
|
|
1694
|
-
chalk.hex('#
|
|
1611
|
+
chalk.hex('#64d2ff')('Control'),
|
|
1695
1612
|
` ${kb('Ctrl+C')} ${desc('Clear input / interrupt')}`,
|
|
1696
1613
|
` ${kb('Ctrl+D')} ${desc('Exit (when empty)')}`,
|
|
1697
1614
|
];
|
|
@@ -1778,6 +1695,27 @@ class InteractiveShell {
|
|
|
1778
1695
|
}
|
|
1779
1696
|
void this.processPrompt(trimmed);
|
|
1780
1697
|
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Dequeue and run the next live follow-up, if any: commit its user line to
|
|
1700
|
+
* history, refresh the transient queue UI, then process it. Single source of
|
|
1701
|
+
* truth for the dequeue so the per-turn drain and the test seam can't drift —
|
|
1702
|
+
* that drift is exactly how a queued-prompt UX goes subtly wrong.
|
|
1703
|
+
*/
|
|
1704
|
+
async drainNextQueuedPrompt() {
|
|
1705
|
+
if (this.pendingPrompts.length === 0 || this.shouldExit) {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
const next = this.pendingPrompts.shift();
|
|
1709
|
+
if (!next) {
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
const r = this.promptController?.getRenderer();
|
|
1713
|
+
r?.setFollowUpQueueMode(false);
|
|
1714
|
+
r?.addUserHistoryItem(next);
|
|
1715
|
+
r?.setQueuedPrompts(this.pendingPrompts.slice());
|
|
1716
|
+
await this.processPrompt(next);
|
|
1717
|
+
return true;
|
|
1718
|
+
}
|
|
1781
1719
|
async processPrompt(prompt) {
|
|
1782
1720
|
if (this.isProcessing) {
|
|
1783
1721
|
return;
|
|
@@ -1802,6 +1740,10 @@ class InteractiveShell {
|
|
|
1802
1740
|
this.autoGovernor.reset();
|
|
1803
1741
|
this.failureRegistry.reset();
|
|
1804
1742
|
this.adversarialCorrectionCount = 0;
|
|
1743
|
+
// New user turn → `↑ N tokens` restarts from zero. Continuations
|
|
1744
|
+
// ('continue' / IMPORTANT:-prefixed) keep accumulating into the same turn.
|
|
1745
|
+
this.turnTokenMeter.reset();
|
|
1746
|
+
this.promptController?.setMetaStatus({ outputTokens: 0 });
|
|
1805
1747
|
// Pinned-prompt persistence removed per request — no longer
|
|
1806
1748
|
// displayed above the chat box.
|
|
1807
1749
|
}
|
|
@@ -1886,6 +1828,14 @@ class InteractiveShell {
|
|
|
1886
1828
|
// Stream content as it arrives
|
|
1887
1829
|
this.currentResponseBuffer += event.content ?? '';
|
|
1888
1830
|
this.finalResponseText += event.content ?? '';
|
|
1831
|
+
// Live `↑ N tokens`: estimate from streamed chars until the
|
|
1832
|
+
// provider's usage event snaps the exact count. Synthetic deltas
|
|
1833
|
+
// (already-streamed narration replays, retry notices) were never
|
|
1834
|
+
// provider output — metering them double-counts.
|
|
1835
|
+
if (!event.synthetic) {
|
|
1836
|
+
this.turnTokenMeter.addStreamedChars((event.content ?? '').length);
|
|
1837
|
+
this.promptController?.setMetaStatus({ outputTokens: this.turnTokenMeter.current() });
|
|
1838
|
+
}
|
|
1889
1839
|
if (renderer) {
|
|
1890
1840
|
renderer.addEvent('stream', event.content);
|
|
1891
1841
|
}
|
|
@@ -1899,6 +1849,10 @@ class InteractiveShell {
|
|
|
1899
1849
|
case 'reasoning':
|
|
1900
1850
|
// Accumulate reasoning for potential fallback synthesis
|
|
1901
1851
|
reasoningBuffer += event.content ?? '';
|
|
1852
|
+
// Reasoning streams count toward completion_tokens too (DeepSeek
|
|
1853
|
+
// thinking) — meter them so the live `↑` doesn't sit at zero.
|
|
1854
|
+
this.turnTokenMeter.addStreamedChars((event.content ?? '').length);
|
|
1855
|
+
this.promptController?.setMetaStatus({ outputTokens: this.turnTokenMeter.current() });
|
|
1902
1856
|
// Update status to show reasoning is actively streaming
|
|
1903
1857
|
this.promptController?.setActivityMessage('Thinking');
|
|
1904
1858
|
// Start the reasoning timer on first reasoning event
|
|
@@ -2033,6 +1987,10 @@ class InteractiveShell {
|
|
|
2033
1987
|
case 'usage': {
|
|
2034
1988
|
// Meter cumulative DeepSeek consumption for /usage + the portal.
|
|
2035
1989
|
recordDeepSeekUsage(event.inputTokens, event.outputTokens);
|
|
1990
|
+
// Snap the live `↑` estimate to the provider-exact output count
|
|
1991
|
+
// for this request; the meter keeps accumulating across the
|
|
1992
|
+
// turn's tool-loop requests.
|
|
1993
|
+
this.turnTokenMeter.recordExactOutput(event.outputTokens ?? 0);
|
|
2036
1994
|
// inputTokens = exactly what occupies the context window this turn.
|
|
2037
1995
|
// The real model window (not a hardcoded guess) is the denominator
|
|
2038
1996
|
// so "% context left" reflects the actual model.
|
|
@@ -2042,7 +2000,8 @@ class InteractiveShell {
|
|
|
2042
2000
|
}
|
|
2043
2001
|
const windowTokens = getModelContextInfo(this.profileConfig.model).contextWindow;
|
|
2044
2002
|
this.promptController?.setMetaStatus({
|
|
2045
|
-
|
|
2003
|
+
outputTokens: this.turnTokenMeter.current(),
|
|
2004
|
+
contextTokens,
|
|
2046
2005
|
tokenLimit: windowTokens,
|
|
2047
2006
|
});
|
|
2048
2007
|
break;
|
|
@@ -2134,7 +2093,7 @@ class InteractiveShell {
|
|
|
2134
2093
|
// This handles models like deepseek-v4-pro that output thinking but empty response
|
|
2135
2094
|
// Also handles step timeouts where the model was stuck
|
|
2136
2095
|
// IMPORTANT: Don't add "Next steps" when only reasoning occurred - only after real work
|
|
2137
|
-
if ((
|
|
2096
|
+
if (shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
2138
2097
|
const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
|
|
2139
2098
|
if (synthesized && renderer) {
|
|
2140
2099
|
renderer.addEvent('stream', '\n' + synthesized);
|
|
@@ -2156,7 +2115,7 @@ class InteractiveShell {
|
|
|
2156
2115
|
renderer.addEvent('error', message);
|
|
2157
2116
|
}
|
|
2158
2117
|
// Fallback: If we have reasoning content but no response was generated, synthesize one
|
|
2159
|
-
if (!episodeSuccess &&
|
|
2118
|
+
if (!episodeSuccess && shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
2160
2119
|
const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
|
|
2161
2120
|
if (synthesized && renderer) {
|
|
2162
2121
|
renderer.addEvent('stream', '\n' + synthesized);
|
|
@@ -2169,7 +2128,7 @@ class InteractiveShell {
|
|
|
2169
2128
|
// Exit critical section - allow termination again
|
|
2170
2129
|
exitCriticalSection();
|
|
2171
2130
|
// Final fallback: If stream ended without message.complete but we have reasoning
|
|
2172
|
-
if (!episodeSuccess &&
|
|
2131
|
+
if (!episodeSuccess && shouldSynthesizeFromReasoning({ hasReceivedResponseContent, finalResponseText: this.finalResponseText, currentResponseBuffer: this.currentResponseBuffer, reasoningBuffer })) {
|
|
2173
2132
|
const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
|
|
2174
2133
|
if (synthesized && renderer) {
|
|
2175
2134
|
renderer.addEvent('stream', '\n' + synthesized);
|
|
@@ -2217,17 +2176,11 @@ class InteractiveShell {
|
|
|
2217
2176
|
// Autosave the conversation so /resume has something to restore. Each
|
|
2218
2177
|
// turn updates the same snapshot in place (keyed by this.sessionId).
|
|
2219
2178
|
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
|
-
}
|
|
2179
|
+
// Process any queued follow-up — single source of truth (drainNextQueuedPrompt).
|
|
2180
|
+
// This takes priority over auto-continue: a user's explicit follow-up runs
|
|
2181
|
+
// before the loop decides the original task is "complete".
|
|
2182
|
+
if (await this.drainNextQueuedPrompt()) {
|
|
2183
|
+
// handled
|
|
2231
2184
|
}
|
|
2232
2185
|
else if (refusedTurn) {
|
|
2233
2186
|
// Refusal terminates the turn. Don't re-prompt the model — the
|