banana-code 1.3.1 → 1.4.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/banana.js +219 -31
- package/lib/agenticRunner.js +236 -10
- package/lib/claudeCodeProvider.js +540 -0
- package/lib/config.js +49 -15
- package/lib/contextBuilder.js +11 -4
- package/lib/fileManager.js +9 -11
- package/lib/fsUtils.js +30 -0
- package/lib/historyManager.js +3 -5
- package/lib/interactivePicker.js +2 -2
- package/lib/modelRegistry.js +3 -2
- package/lib/providerManager.js +7 -1
- package/lib/providerStore.js +38 -4
- package/lib/streamHandler.js +25 -4
- package/package.json +48 -43
- package/prompts/base.md +33 -23
- package/prompts/code-agent-qwen.md +1 -0
- package/prompts/code-agent.md +157 -70
package/banana.js
CHANGED
|
@@ -81,7 +81,7 @@ let pendingHumanQuestion = null; // { resolve, question }
|
|
|
81
81
|
// CONFIGURATION
|
|
82
82
|
// =============================================================================
|
|
83
83
|
|
|
84
|
-
const VERSION = '1.
|
|
84
|
+
const VERSION = '1.4.0';
|
|
85
85
|
const { PAD } = require('./lib/borderRenderer'); // Single source of truth for left padding
|
|
86
86
|
const DEBUG_DISABLED_VALUES = new Set(['0', 'false', 'off', 'no']);
|
|
87
87
|
const NEXT_TURN_RESERVE_TOKENS = 1200;
|
|
@@ -389,7 +389,7 @@ ${P}${c.yellow}/hooks${c.reset} Manage lifecycle hooks (add, edit,
|
|
|
389
389
|
${P}${c.yellow}/steer <text>${c.reset} Steer next turn (or interrupt + redirect current turn)
|
|
390
390
|
${P}${c.yellow}/model [name]${c.reset} Show/switch model
|
|
391
391
|
${P}${c.yellow}/model search <query>${c.reset} Search OpenRouter models and add one
|
|
392
|
-
${P}${c.yellow}/connect [provider]${c.reset} Connect
|
|
392
|
+
${P}${c.yellow}/connect [provider]${c.reset} Connect/disconnect providers (Anthropic, OpenAI, OpenRouter, Claude Code)
|
|
393
393
|
${P}${c.yellow}/prompt [name]${c.reset} Show/switch prompt (base, code-agent, or any .md)
|
|
394
394
|
|
|
395
395
|
${P}${c.banana}${c.dim}Config Commands:${c.reset}
|
|
@@ -442,6 +442,15 @@ function initProject() {
|
|
|
442
442
|
tokenCounter = new TokenCounter(config);
|
|
443
443
|
imageHandler = new ImageHandler(projectDir);
|
|
444
444
|
|
|
445
|
+
const lastRunSnapshot = config.getRunSnapshot();
|
|
446
|
+
if (lastRunSnapshot && lastRunSnapshot.completed === false) {
|
|
447
|
+
const when = lastRunSnapshot.savedAt ? new Date(lastRunSnapshot.savedAt).toLocaleString() : 'recently';
|
|
448
|
+
console.log(`${PAD}${c.yellow}⚠ Previous run appears to have ended unexpectedly.${c.reset} ${c.dim}(${when})${c.reset}`);
|
|
449
|
+
if (lastRunSnapshot.userMessage) {
|
|
450
|
+
console.log(`${PAD}${c.dim} Last request: ${String(lastRunSnapshot.userMessage).slice(0, 120)}${c.reset}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
445
454
|
// Initialize LM Studio + provider manager
|
|
446
455
|
const lmStudioUrl = config.get('lmStudioUrl') || 'http://localhost:1234';
|
|
447
456
|
lmStudio = new LmStudio({ baseUrl: lmStudioUrl });
|
|
@@ -913,6 +922,7 @@ function normalizeProviderKey(raw) {
|
|
|
913
922
|
const key = String(raw || '').trim().toLowerCase();
|
|
914
923
|
if (!key) return null;
|
|
915
924
|
if (key === 'lmstudio' || key === 'local') return 'local';
|
|
925
|
+
if (key === 'claude' || key === 'claudecode' || key === 'claude-code') return 'claude-code';
|
|
916
926
|
if (PROVIDERS.includes(key)) return key;
|
|
917
927
|
return null;
|
|
918
928
|
}
|
|
@@ -958,7 +968,11 @@ function buildFullSystemPrompt(promptMode = resolveActivePromptMode()) {
|
|
|
958
968
|
// Build cache key from prompt mode + instruction file modification times
|
|
959
969
|
const globalInstructions = globalConfig ? globalConfig.getInstructions() : null;
|
|
960
970
|
const instructions = config ? config.getInstructions() : null;
|
|
961
|
-
|
|
971
|
+
// Include file mtimes so edits mid-session invalidate the cache
|
|
972
|
+
function safeMtime(filePath) {
|
|
973
|
+
try { return filePath ? fs.statSync(filePath).mtimeMs : 0; } catch { return 0; }
|
|
974
|
+
}
|
|
975
|
+
const cacheKey = `${promptMode}|${globalInstructions?.source || ''}:${safeMtime(globalInstructions?.source)}|${instructions?.source || ''}:${safeMtime(instructions?.source)}`;
|
|
962
976
|
|
|
963
977
|
if (_systemPromptCache.key === cacheKey) {
|
|
964
978
|
return _systemPromptCache.value;
|
|
@@ -966,11 +980,23 @@ function buildFullSystemPrompt(promptMode = resolveActivePromptMode()) {
|
|
|
966
980
|
|
|
967
981
|
const systemPrompt = promptManager ? promptManager.get(promptMode) : '';
|
|
968
982
|
let fullSystemPrompt = systemPrompt || '';
|
|
983
|
+
|
|
984
|
+
// Strip HTML comments and collapse blank lines from instruction files to avoid wasting tokens
|
|
985
|
+
// on placeholder templates that users haven't edited yet
|
|
986
|
+
function cleanInstructions(raw) {
|
|
987
|
+
const stripped = raw.replace(/<!--[\s\S]*?-->/g, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
988
|
+
// If only headings and whitespace remain, treat as empty (unedited template)
|
|
989
|
+
const withoutHeadings = stripped.replace(/^#+\s+.*$/gm, '').trim();
|
|
990
|
+
return withoutHeadings ? stripped : '';
|
|
991
|
+
}
|
|
992
|
+
|
|
969
993
|
if (globalInstructions) {
|
|
970
|
-
|
|
994
|
+
const cleaned = cleanInstructions(globalInstructions.content);
|
|
995
|
+
if (cleaned) fullSystemPrompt += `\n\n## Global Instructions\n\n${cleaned}`;
|
|
971
996
|
}
|
|
972
997
|
if (instructions) {
|
|
973
|
-
|
|
998
|
+
const cleaned = cleanInstructions(instructions.content);
|
|
999
|
+
if (cleaned) fullSystemPrompt += `\n\n## Project Instructions\n\n${cleaned}`;
|
|
974
1000
|
}
|
|
975
1001
|
|
|
976
1002
|
_systemPromptCache = { key: cacheKey, value: fullSystemPrompt };
|
|
@@ -1301,24 +1327,85 @@ function listProviderStatus() {
|
|
|
1301
1327
|
|
|
1302
1328
|
async function connectProviderInteractive(provider) {
|
|
1303
1329
|
if (!provider) {
|
|
1304
|
-
const providerItems = PROVIDERS.map(p =>
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1330
|
+
const providerItems = PROVIDERS.filter(p => p !== 'monkey').map(p => {
|
|
1331
|
+
const connected = providerStore.isConnected(p);
|
|
1332
|
+
let description;
|
|
1333
|
+
if (connected) {
|
|
1334
|
+
description = 'Connected';
|
|
1335
|
+
} else if (p === 'openai') {
|
|
1336
|
+
description = 'OAuth device login for Codex subscription';
|
|
1337
|
+
} else if (p === 'claude-code') {
|
|
1338
|
+
description = 'Use your Claude Code CLI subscription (no API key needed)';
|
|
1339
|
+
} else {
|
|
1340
|
+
description = 'Connect with API key';
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
key: p,
|
|
1344
|
+
label: PROVIDER_LABELS[p] || p,
|
|
1345
|
+
description,
|
|
1346
|
+
tags: [],
|
|
1347
|
+
active: connected
|
|
1348
|
+
};
|
|
1349
|
+
});
|
|
1350
|
+
const selected = await pick(providerItems, { title: 'Manage Providers' });
|
|
1314
1351
|
if (!selected) {
|
|
1315
1352
|
console.log(`${PAD}${c.dim}Cancelled${c.reset}\n`);
|
|
1316
1353
|
return;
|
|
1317
1354
|
}
|
|
1318
1355
|
provider = selected.key;
|
|
1356
|
+
|
|
1357
|
+
// If already connected, offer to disconnect
|
|
1358
|
+
if (providerStore.isConnected(provider)) {
|
|
1359
|
+
const providerLabel = PROVIDER_LABELS[provider] || provider;
|
|
1360
|
+
const answer = await askQuestion(`${PAD}${c.yellow}Disconnect ${providerLabel}? (y/N): ${c.reset}`);
|
|
1361
|
+
if (answer && answer.trim().toLowerCase() === 'y') {
|
|
1362
|
+
const wasActiveProvider = (modelRegistry.getCurrentModel()?.provider || 'local') === provider;
|
|
1363
|
+
providerStore.disconnect(provider);
|
|
1364
|
+
modelRegistry.refreshRemoteModels();
|
|
1365
|
+
if (wasActiveProvider) {
|
|
1366
|
+
const fallback = modelRegistry.getDefault();
|
|
1367
|
+
if (fallback) {
|
|
1368
|
+
await switchModel(fallback);
|
|
1369
|
+
} else {
|
|
1370
|
+
console.log(`${PAD}${c.yellow}No fallback model available.${c.reset}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
console.log(`${PAD}${c.green}✓ Disconnected ${providerLabel}${c.reset}\n`);
|
|
1374
|
+
} else {
|
|
1375
|
+
console.log(`${PAD}${c.dim}Cancelled${c.reset}\n`);
|
|
1376
|
+
}
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1319
1379
|
}
|
|
1320
1380
|
const providerLabel = PROVIDER_LABELS[provider] || provider;
|
|
1321
1381
|
|
|
1382
|
+
// If called with an explicit provider arg and it's already connected, offer disconnect or reconnect
|
|
1383
|
+
if (providerStore.isConnected(provider)) {
|
|
1384
|
+
console.log(`\n${PAD}${c.cyan}${providerLabel} is already connected.${c.reset}`);
|
|
1385
|
+
const answer = await askQuestion(`${PAD}${c.yellow}Disconnect (d), reconnect with new key (r), or cancel (Enter): ${c.reset}`);
|
|
1386
|
+
const choice = (answer || '').trim().toLowerCase();
|
|
1387
|
+
if (choice === 'd') {
|
|
1388
|
+
const wasActiveProvider = (modelRegistry.getCurrentModel()?.provider || 'local') === provider;
|
|
1389
|
+
providerStore.disconnect(provider);
|
|
1390
|
+
modelRegistry.refreshRemoteModels();
|
|
1391
|
+
if (wasActiveProvider) {
|
|
1392
|
+
const fallback = modelRegistry.getDefault();
|
|
1393
|
+
if (fallback) {
|
|
1394
|
+
await switchModel(fallback);
|
|
1395
|
+
} else {
|
|
1396
|
+
console.log(`${PAD}${c.yellow}No fallback model available.${c.reset}`);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
console.log(`${PAD}${c.green}✓ Disconnected ${providerLabel}${c.reset}\n`);
|
|
1400
|
+
return;
|
|
1401
|
+
} else if (choice === 'r') {
|
|
1402
|
+
// Fall through to the connect flow below to re-enter credentials
|
|
1403
|
+
} else {
|
|
1404
|
+
console.log(`${PAD}${c.dim}Cancelled${c.reset}\n`);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1322
1409
|
if (provider === 'anthropic' || provider === 'openrouter') {
|
|
1323
1410
|
const label = provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENROUTER_API_KEY';
|
|
1324
1411
|
console.log(`\n${PAD}${c.cyan}${providerLabel} Connection${c.reset}`);
|
|
@@ -1351,6 +1438,29 @@ async function connectProviderInteractive(provider) {
|
|
|
1351
1438
|
return;
|
|
1352
1439
|
}
|
|
1353
1440
|
|
|
1441
|
+
if (provider === 'claude-code') {
|
|
1442
|
+
console.log(`\n${PAD}${c.cyan}Claude Code CLI Connection${c.reset}`);
|
|
1443
|
+
console.log(`${PAD}${c.dim}Checking for Claude Code CLI...${c.reset}`);
|
|
1444
|
+
|
|
1445
|
+
const { ClaudeCodeClient } = require('./lib/claudeCodeProvider');
|
|
1446
|
+
const claudeClient = new ClaudeCodeClient();
|
|
1447
|
+
const connected = await claudeClient.isConnected();
|
|
1448
|
+
|
|
1449
|
+
if (!connected) {
|
|
1450
|
+
console.log(`${PAD}${c.red}✗ Claude Code CLI not found.${c.reset}`);
|
|
1451
|
+
console.log(`${PAD}${c.dim}Install it: npm install -g @anthropic-ai/claude-code${c.reset}`);
|
|
1452
|
+
console.log(`${PAD}${c.dim}Then run: claude login${c.reset}\n`);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
providerStore.connectClaudeCode();
|
|
1457
|
+
modelRegistry.refreshRemoteModels();
|
|
1458
|
+
console.log(`${PAD}${c.green}✓ Connected Claude Code CLI${c.reset}`);
|
|
1459
|
+
console.log(`${PAD}${c.dim}Uses your existing Claude subscription (no API key needed).${c.reset}`);
|
|
1460
|
+
console.log(`${PAD}${c.dim}Use /model to switch to Claude Code models.${c.reset}\n`);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1354
1464
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
1355
1465
|
}
|
|
1356
1466
|
|
|
@@ -1589,9 +1699,15 @@ async function handleCommand(input) {
|
|
|
1589
1699
|
tokenCounter.resetSession();
|
|
1590
1700
|
imageHandler.clearPending();
|
|
1591
1701
|
setContextEstimate(0);
|
|
1702
|
+
promptDuringWork = false;
|
|
1703
|
+
renderWorkingPrompt = null;
|
|
1704
|
+
if (rl) {
|
|
1705
|
+
rl.write(null, { ctrl: true, name: 'u' });
|
|
1706
|
+
}
|
|
1592
1707
|
if (statusBar) {
|
|
1708
|
+
statusBar.setInputHint('');
|
|
1593
1709
|
statusBar.update({ sessionIn: 0, sessionOut: 0 });
|
|
1594
|
-
statusBar.
|
|
1710
|
+
statusBar.uninstall();
|
|
1595
1711
|
}
|
|
1596
1712
|
console.clear();
|
|
1597
1713
|
// Push cursor to bottom of scroll region so prompt isn't stranded at the top
|
|
@@ -1603,6 +1719,9 @@ async function handleCommand(input) {
|
|
|
1603
1719
|
if (padding > 0) process.stdout.write('\n'.repeat(padding));
|
|
1604
1720
|
}
|
|
1605
1721
|
refreshIdleContextEstimate();
|
|
1722
|
+
if (statusBar) {
|
|
1723
|
+
statusBar.reinstall();
|
|
1724
|
+
}
|
|
1606
1725
|
if (rl) {
|
|
1607
1726
|
rl.setPrompt(buildPromptPrefix());
|
|
1608
1727
|
rl.prompt(false);
|
|
@@ -2009,13 +2128,52 @@ async function handleCommand(input) {
|
|
|
2009
2128
|
await modelRegistry.refreshLmStudio();
|
|
2010
2129
|
const models = modelRegistry.list();
|
|
2011
2130
|
const current = modelRegistry.getCurrent();
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2131
|
+
|
|
2132
|
+
// Group models by provider for clarity
|
|
2133
|
+
const providerOrder = ['monkey', 'claude-code', 'anthropic', 'openai', 'openrouter', 'local'];
|
|
2134
|
+
const providerHints = {
|
|
2135
|
+
monkey: 'Banana Cloud',
|
|
2136
|
+
'claude-code': 'Claude Code CLI',
|
|
2137
|
+
anthropic: 'your API key',
|
|
2138
|
+
openai: 'your API key',
|
|
2139
|
+
openrouter: 'your API key',
|
|
2140
|
+
local: 'local'
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
// Sort models by provider group order, then by name within each group
|
|
2144
|
+
const sorted = [...models].sort((a, b) => {
|
|
2145
|
+
const aIdx = providerOrder.indexOf(a.provider || 'local');
|
|
2146
|
+
const bIdx = providerOrder.indexOf(b.provider || 'local');
|
|
2147
|
+
const aOrder = aIdx >= 0 ? aIdx : providerOrder.length;
|
|
2148
|
+
const bOrder = bIdx >= 0 ? bIdx : providerOrder.length;
|
|
2149
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
2150
|
+
return (a.name || '').localeCompare(b.name || '');
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
// Build picker items with provider context in description
|
|
2154
|
+
const pickerItems = sorted.map(m => {
|
|
2155
|
+
const provider = m.provider || 'local';
|
|
2156
|
+
const providerLabel = PROVIDER_LABELS[provider] || provider;
|
|
2157
|
+
const hint = providerHints[provider] || provider;
|
|
2158
|
+
const via = `${providerLabel} (${hint})`;
|
|
2159
|
+
const hasProviderInfo = m.description && m.description.includes(' via ');
|
|
2160
|
+
let baseDesc = (m.description && m.description !== m.name) ? m.description : '';
|
|
2161
|
+
if (baseDesc.length > 30) baseDesc = baseDesc.slice(0, 28) + '..';
|
|
2162
|
+
const desc = hasProviderInfo
|
|
2163
|
+
? `${m.description} (${hint})`
|
|
2164
|
+
: baseDesc
|
|
2165
|
+
? `${baseDesc} [${hint}]`
|
|
2166
|
+
: `[${hint}]`;
|
|
2167
|
+
// Pass vision tag through for the V indicator column, but drop the rest to keep lines short
|
|
2168
|
+
const visionTags = (m.tags || []).includes('vision') ? ['vision'] : [];
|
|
2169
|
+
return {
|
|
2170
|
+
key: m.key,
|
|
2171
|
+
label: m.name,
|
|
2172
|
+
description: desc,
|
|
2173
|
+
tags: visionTags,
|
|
2174
|
+
active: m.key === current
|
|
2175
|
+
};
|
|
2176
|
+
});
|
|
2019
2177
|
|
|
2020
2178
|
const selected = await pick(pickerItems, {
|
|
2021
2179
|
title: 'Switch Model',
|
|
@@ -2117,8 +2275,8 @@ async function handleCommand(input) {
|
|
|
2117
2275
|
|
|
2118
2276
|
if (normalizedSub === 'disconnect') {
|
|
2119
2277
|
const provider = normalizeProviderKey(secondArg);
|
|
2120
|
-
if (!provider || provider === 'local') {
|
|
2121
|
-
console.log(`\n${PAD}${c.yellow}Usage: /connect disconnect <anthropic|openai|openrouter>${c.reset}\n`);
|
|
2278
|
+
if (!provider || provider === 'local' || provider === 'monkey') {
|
|
2279
|
+
console.log(`\n${PAD}${c.yellow}Usage: /connect disconnect <anthropic|openai|openrouter|claude-code>${c.reset}\n`);
|
|
2122
2280
|
return true;
|
|
2123
2281
|
}
|
|
2124
2282
|
const wasActiveProvider = (modelRegistry.getCurrentModel()?.provider || 'local') === provider;
|
|
@@ -2135,7 +2293,7 @@ async function handleCommand(input) {
|
|
|
2135
2293
|
if (normalizedSub === 'use') {
|
|
2136
2294
|
const provider = normalizeProviderKey(secondArg);
|
|
2137
2295
|
if (!provider) {
|
|
2138
|
-
console.log(`\n${PAD}${c.yellow}Usage: /connect use <local|anthropic|openai|openrouter>${c.reset}\n`);
|
|
2296
|
+
console.log(`\n${PAD}${c.yellow}Usage: /connect use <local|anthropic|openai|openrouter|claude-code>${c.reset}\n`);
|
|
2139
2297
|
return true;
|
|
2140
2298
|
}
|
|
2141
2299
|
|
|
@@ -2160,7 +2318,7 @@ async function handleCommand(input) {
|
|
|
2160
2318
|
if (!provider || provider === 'local') {
|
|
2161
2319
|
console.log(`\n${PAD}${c.yellow}Usage:${c.reset}`);
|
|
2162
2320
|
console.log(`${PAD}${c.dim} /connect${c.reset}`);
|
|
2163
|
-
console.log(`${PAD}${c.dim} /connect <anthropic|openai|openrouter>${c.reset}`);
|
|
2321
|
+
console.log(`${PAD}${c.dim} /connect <anthropic|openai|openrouter|claude-code>${c.reset}`);
|
|
2164
2322
|
console.log(`${PAD}${c.dim} /connect status${c.reset}`);
|
|
2165
2323
|
console.log(`${PAD}${c.dim} /connect disconnect <provider>${c.reset}`);
|
|
2166
2324
|
console.log(`${PAD}${c.dim} /connect use <local|provider>${c.reset}\n`);
|
|
@@ -2684,9 +2842,19 @@ async function sendMessage(message) {
|
|
|
2684
2842
|
fullMessage += '\n\n[Image analysis above is primary source of truth. Focus on image content, not file listing.]';
|
|
2685
2843
|
}
|
|
2686
2844
|
|
|
2845
|
+
config.saveRunSnapshot({
|
|
2846
|
+
projectDir,
|
|
2847
|
+
activeModel: modelRegistry.getCurrent(),
|
|
2848
|
+
userMessage: message,
|
|
2849
|
+
fullMessagePreview: fullMessage.slice(0, 2000),
|
|
2850
|
+
conversationLength: conversationHistory.length
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2687
2853
|
try {
|
|
2688
2854
|
await sendAgenticMessage(fullMessage, pendingImages, message);
|
|
2855
|
+
config.completeRunSnapshot({ status: 'completed' });
|
|
2689
2856
|
} catch (error) {
|
|
2857
|
+
config.completeRunSnapshot({ status: 'failed', error: error.message });
|
|
2690
2858
|
const provider = activeProviderKey();
|
|
2691
2859
|
const providerLabel = providerManager.getProviderLabel(provider);
|
|
2692
2860
|
console.log(`\n${PAD}${c.red}✗ Error: ${error.message}${c.reset}`);
|
|
@@ -2985,6 +3153,11 @@ async function sendStreamingMessage(message, images = [], rawMessage = '') {
|
|
|
2985
3153
|
|
|
2986
3154
|
try {
|
|
2987
3155
|
await streamHandler.handleStream(response);
|
|
3156
|
+
const streamResult = streamHandler.getResult();
|
|
3157
|
+
if (!streamResult.completed && streamResult.warning) {
|
|
3158
|
+
fullResponse = `${streamResult.warning}\n\n${fullResponse}`.trim();
|
|
3159
|
+
console.log(`\n${PAD}${c.yellow}⚠ ${streamResult.warning}${c.reset}`);
|
|
3160
|
+
}
|
|
2988
3161
|
} catch (error) {
|
|
2989
3162
|
stopStatus();
|
|
2990
3163
|
// Check if this was an abort
|
|
@@ -5172,10 +5345,12 @@ Examples:
|
|
|
5172
5345
|
// then subsequent lines arrive as new 'line' events. We detect paste by
|
|
5173
5346
|
// buffering lines that arrive within PASTE_DELAY_MS of each other.
|
|
5174
5347
|
const PASTE_DELAY_MS = 400; // 400ms to handle large pastes and Windows Terminal dialog latency
|
|
5348
|
+
const PASTE_STRAGGLER_WINDOW_MS = 1200; // Late lines can arrive after submit on Windows Terminal
|
|
5175
5349
|
let pasteBuffer = [];
|
|
5176
5350
|
let pasteTimer = null;
|
|
5177
5351
|
let waitingForInput = false;
|
|
5178
5352
|
let lastFlushTime = 0; // Track when paste buffer last flushed (to catch stragglers)
|
|
5353
|
+
let lastPasteStragglerWarningAt = 0;
|
|
5179
5354
|
|
|
5180
5355
|
showGeminiKeyPrompt = (callback) => {
|
|
5181
5356
|
awaitingGeminiKey = true;
|
|
@@ -5342,13 +5517,17 @@ Examples:
|
|
|
5342
5517
|
return;
|
|
5343
5518
|
}
|
|
5344
5519
|
|
|
5345
|
-
// Straggler paste lines: arrived after flush but
|
|
5346
|
-
//
|
|
5347
|
-
|
|
5520
|
+
// Straggler paste lines: arrived after flush but before the paste has fully settled.
|
|
5521
|
+
// On Windows Terminal, delayed lines can arrive after the first chunk was submitted,
|
|
5522
|
+
// and without this guard they'd be misread as mid-turn steering.
|
|
5523
|
+
if (lastFlushTime && (Date.now() - lastFlushTime) < PASTE_STRAGGLER_WINDOW_MS) {
|
|
5348
5524
|
const trimmed = String(input || '').trim();
|
|
5349
5525
|
if (trimmed) {
|
|
5350
5526
|
appendDebugLog(`[paste-straggler] Dropped line arrived ${Date.now() - lastFlushTime}ms after flush: ${trimmed.slice(0, 60)}\n`);
|
|
5351
|
-
|
|
5527
|
+
if (lastPasteStragglerWarningAt !== lastFlushTime) {
|
|
5528
|
+
lastPasteStragglerWarningAt = lastFlushTime;
|
|
5529
|
+
console.log(`${PAD}${c.yellow}Ignored delayed paste lines from the previous submission.${c.reset} ${c.dim}If this keeps happening, disable the Windows Terminal paste warning or paste again after the prompt settles.${c.reset}`);
|
|
5530
|
+
}
|
|
5352
5531
|
}
|
|
5353
5532
|
return;
|
|
5354
5533
|
}
|
|
@@ -5476,6 +5655,9 @@ process.on('SIGINT', () => {
|
|
|
5476
5655
|
if (config && config.get('autoSaveHistory') && conversationHistory.length > 0) {
|
|
5477
5656
|
config.saveConversation('autosave', conversationHistory);
|
|
5478
5657
|
}
|
|
5658
|
+
if (config) {
|
|
5659
|
+
config.completeRunSnapshot({ status: 'cancelled' });
|
|
5660
|
+
}
|
|
5479
5661
|
if (watcher) watcher.stop();
|
|
5480
5662
|
console.log(`\n${PAD}${c.cyan}👋 See you later!${c.reset}\n`);
|
|
5481
5663
|
process.exit(0);
|
|
@@ -5484,6 +5666,12 @@ process.on('SIGINT', () => {
|
|
|
5484
5666
|
main().catch(error => {
|
|
5485
5667
|
logSessionEnd('crash', ` error=${error.message}`);
|
|
5486
5668
|
if (statusBar) statusBar.uninstall();
|
|
5669
|
+
if (config && config.get('autoSaveHistory') && conversationHistory.length > 0) {
|
|
5670
|
+
config.saveConversation('autosave-crash', conversationHistory);
|
|
5671
|
+
}
|
|
5672
|
+
if (config) {
|
|
5673
|
+
config.completeRunSnapshot({ status: 'crashed', error: error.message });
|
|
5674
|
+
}
|
|
5487
5675
|
console.error(`${c.red}Fatal error: ${error.message}${c.reset}`);
|
|
5488
5676
|
if (watcher) watcher.stop();
|
|
5489
5677
|
process.exit(1);
|