moderado 0.4.28 → 0.4.33
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/README.md +82 -67
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +6 -2
- package/dist/args.js.map +1 -1
- package/dist/commands/chat.d.ts +6 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +135 -56
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +14 -12
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/run.d.ts +11 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +25 -8
- package/dist/commands/run.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +50 -19
- package/dist/config.js.map +1 -1
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +6 -1
- package/dist/sessions.js.map +1 -1
- package/dist/ui/box.d.ts +6 -0
- package/dist/ui/box.d.ts.map +1 -1
- package/dist/ui/box.js +33 -1
- package/dist/ui/box.js.map +1 -1
- package/dist/ui/file_mentions.js +18 -9
- package/dist/ui/file_mentions.js.map +1 -1
- package/dist/ui/model_selector.d.ts.map +1 -1
- package/dist/ui/model_selector.js +6 -4
- package/dist/ui/model_selector.js.map +1 -1
- package/dist/ui/popup.d.ts.map +1 -1
- package/dist/ui/popup.js +4 -3
- package/dist/ui/popup.js.map +1 -1
- package/dist/ui/provider_connect.d.ts +13 -4
- package/dist/ui/provider_connect.d.ts.map +1 -1
- package/dist/ui/provider_connect.js +37 -29
- package/dist/ui/provider_connect.js.map +1 -1
- package/dist/ui/welcome.d.ts +14 -2
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +43 -17
- package/dist/ui/welcome.js.map +1 -1
- package/dist/vendor/contracts/provider.js +8 -8
- package/dist/vendor/contracts/provider.js.map +1 -1
- package/dist/vendor/core/agent.d.ts.map +1 -1
- package/dist/vendor/core/agent.js +6 -3
- package/dist/vendor/core/agent.js.map +1 -1
- package/dist/vendor/providers/model_discovery.js +1 -1
- package/dist/vendor/providers/model_discovery.js.map +1 -1
- package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts +1 -1
- package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts.map +1 -1
- package/dist/vendor/providers/nvidia/nvidia_adapter.js +4 -4
- package/dist/vendor/providers/nvidia/nvidia_adapter.js.map +1 -1
- package/dist/vendor/providers/nvidia/sse_parser.d.ts +1 -0
- package/dist/vendor/providers/nvidia/sse_parser.d.ts.map +1 -1
- package/dist/vendor/providers/nvidia/sse_parser.js +2 -1
- package/dist/vendor/providers/nvidia/sse_parser.js.map +1 -1
- package/dist/vendor/providers/presets.d.ts +12 -4
- package/dist/vendor/providers/presets.d.ts.map +1 -1
- package/dist/vendor/providers/presets.js +24 -15
- package/dist/vendor/providers/presets.js.map +1 -1
- package/package.json +3 -2
- package/dist/commands/interactive_menu.d.ts +0 -3
- package/dist/commands/interactive_menu.d.ts.map +0 -1
- package/dist/commands/interactive_menu.js +0 -98
- package/dist/commands/interactive_menu.js.map +0 -1
package/dist/commands/chat.js
CHANGED
|
@@ -10,7 +10,7 @@ import { MemoryCredentialStore } from '../credentials.js';
|
|
|
10
10
|
import { WindowsCredentialStore } from '../windows_credentials.js';
|
|
11
11
|
import { TerminalApprovalHandler } from '../ui/terminal_approval.js';
|
|
12
12
|
import { selectCompatibleModelOverlay, selectModelOverlay, showModelConnectionRequired } from '../ui/model_selector.js';
|
|
13
|
-
import { connectProviderInteractive, isAuthenticationFailure,
|
|
13
|
+
import { connectProviderInteractive, isAuthenticationFailure, isModeradoOAuthConnection, loginModeradoInteractive, replaceProviderKeyInteractive } from '../ui/provider_connect.js';
|
|
14
14
|
import { initWorkspace } from './init.js';
|
|
15
15
|
import { expandMentions, createWorkspaceFileSource } from '../ui/file_mentions.js';
|
|
16
16
|
import { listFiles } from '../vendor/tools/index.js';
|
|
@@ -27,6 +27,11 @@ import { getHelpCommandLines } from '../ui/help_content.js';
|
|
|
27
27
|
export function createConnectionProvider(connection) {
|
|
28
28
|
return new NvidiaAdapter({ apiKey: connection.apiKey, baseUrl: connection.baseUrl, providerId: connection.id, providerName: connection.displayName });
|
|
29
29
|
}
|
|
30
|
+
export function getProviderStatusName(connection) {
|
|
31
|
+
return connection?.id === 'moderado' || connection?.id === 'moderado-cloud'
|
|
32
|
+
? 'Moderado'
|
|
33
|
+
: connection?.displayName;
|
|
34
|
+
}
|
|
30
35
|
export function formatSessionExitSummary(session, modelId) {
|
|
31
36
|
const usage = session.usage;
|
|
32
37
|
const tokens = usage.available
|
|
@@ -173,8 +178,18 @@ export function formatTurnFailureAnswer(status, errorEvent, modelId, otherConnec
|
|
|
173
178
|
const cleanDetail = rawDetail
|
|
174
179
|
.replace(/^(?:NVIDIA NIM|OpenAI|OpenRouter|[a-zA-Z0-9_-]+)\s*(?:request failed|rate limit exceeded|service or model unavailable)?\s*(?:\([^)]*\))?\s*(?:during\s*streamChat)?:\s*/i, '')
|
|
175
180
|
.trim();
|
|
181
|
+
if (errorEvent?.code === 'ERR_GATEWAY_UNAUTHORIZED') {
|
|
182
|
+
return `⚠️ **Gateway Authorization (${errorEvent.code}):**\n${cleanDetail}`;
|
|
183
|
+
}
|
|
184
|
+
if (errorEvent?.code?.toUpperCase().includes('RATE_LIMIT')) {
|
|
185
|
+
return `⚠️ **Model Error (${errorEvent.code}):**\n${cleanDetail || 'Provider rate limit reached.'}\n\n👉 **Suggestion:** The provider temporarily limited requests for \`${modelId}\`. Please retry shortly or choose another model with \`/model\`.`;
|
|
186
|
+
}
|
|
176
187
|
return `⚠️ **Model Error (${errorEvent?.code || 'ERR_INFERENCE_FAILED'}):**\n${cleanDetail || 'Model returned no response.'}\n\n👉 **Suggestion:** The model \`${modelId}\` encountered an error. Switch models with \`/model\` or choose a provider with \`/connect\`${altSuggestion}.`;
|
|
177
188
|
}
|
|
189
|
+
/** A turn whose provider rejected its credential may recover by re-entering a key once. */
|
|
190
|
+
export function turnFailedAuthentication(status, errorEvent) {
|
|
191
|
+
return status === 'failed' && isAuthenticationFailure(errorEvent);
|
|
192
|
+
}
|
|
178
193
|
export function resolveTurnAssistantAnswer(result, previousHistoryLength, lastErrorEvent, streamedAnswer = '', currentModel = 'unknown', otherConnections = []) {
|
|
179
194
|
if (result.status === 'cancelled') {
|
|
180
195
|
const partial = cleanConversationalFiller(streamedAnswer);
|
|
@@ -203,6 +218,8 @@ export function saveFailedTurnPrompt(history, prompt, partialResponse = '') {
|
|
|
203
218
|
return [...history, { role: 'user', content: prompt }, { role: 'assistant', content: interruptedResponse }];
|
|
204
219
|
}
|
|
205
220
|
const CONTINUE_INTERRUPTED_TASK_PROMPT = 'Continue the interrupted task from the saved conversation. Start from the unfinished work and avoid repeating completed steps.';
|
|
221
|
+
/** Braille spinner frames for the live "still running" status row. */
|
|
222
|
+
const ACTIVITY_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
206
223
|
function hasInterruptedTurn(messages) {
|
|
207
224
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
208
225
|
const message = messages[index];
|
|
@@ -466,7 +483,8 @@ export function formatDirectWebSearchAnswer(result, modelConnected = false) {
|
|
|
466
483
|
.filter((line) => line && line !== '...' && line !== '---' && !SEARCH_BLOCK_METADATA.test(line) && !/^https?:\/\//i.test(line));
|
|
467
484
|
if (!lines.length)
|
|
468
485
|
return 'Web search returned no readable content.';
|
|
469
|
-
|
|
486
|
+
const excerpt = lines.slice(0, 10).join('\n');
|
|
487
|
+
return `${excerpt}\n\n${modelConnected ? 'Live web search excerpt.' : 'Live web search excerpt. Connect a model for a summary.'}`;
|
|
470
488
|
}
|
|
471
489
|
/** Environment configuration overrides the persistent search provider preference. */
|
|
472
490
|
export function resolveWebSearchProvider(config) {
|
|
@@ -838,6 +856,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
838
856
|
else {
|
|
839
857
|
let autoContinueRequested = false;
|
|
840
858
|
const turn = await promptInteractiveTurn({
|
|
859
|
+
getProviderName: () => getProviderStatusName(activeConnection),
|
|
841
860
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace, version,
|
|
842
861
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, initialMode: activeMode, initialAutoApprove: activeAutoApprove, isFirstTurn: isFirst, signal, chatQuestion: lastQuestion || undefined, chatAnswer: lastAnswer || undefined, chatThoughtTime: lastThoughtTime, chatTaskDone: lastTurnDone, outputTokenRate: lastOutputTokenRate, tokenUsage: lastTokenUsage,
|
|
843
862
|
queuedCommands: commandQueue.items,
|
|
@@ -856,7 +875,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
856
875
|
}
|
|
857
876
|
if (activeConnection.kind === 'openai-compatible') {
|
|
858
877
|
const modelId = await selectCompatibleModelOverlay({
|
|
859
|
-
apiKey: activeConnection.id === 'moderado
|
|
878
|
+
apiKey: activeConnection.id === 'moderado' ? undefined : activeConnection.apiKey,
|
|
860
879
|
baseUrl: activeConnection.baseUrl,
|
|
861
880
|
providerId: activeConnection.id,
|
|
862
881
|
providerName: activeConnection.displayName,
|
|
@@ -877,7 +896,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
877
896
|
}
|
|
878
897
|
return modelId;
|
|
879
898
|
}
|
|
880
|
-
const selection = await selectModelOverlay({ apiKey: activeConnection.id === 'moderado
|
|
899
|
+
const selection = await selectModelOverlay({ apiKey: activeConnection.id === 'moderado' ? undefined : activeConnection.apiKey, currentModel, signal, saveSelectionByDefault: true, drawFrame });
|
|
881
900
|
if (selection.modelId && selection.modelId !== currentModel) {
|
|
882
901
|
currentModel = selection.modelId;
|
|
883
902
|
activeConnection = { ...activeConnection, defaultModel: selection.modelId };
|
|
@@ -895,7 +914,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
895
914
|
const connection = await connectProviderInteractive({
|
|
896
915
|
signal,
|
|
897
916
|
drawFrame,
|
|
898
|
-
gatewayUrl: config.connections?.
|
|
917
|
+
gatewayUrl: config.connections?.moderado?.baseUrl,
|
|
899
918
|
activeConnection,
|
|
900
919
|
savedConnections: config.connections,
|
|
901
920
|
connectProviders: config.connectProviders,
|
|
@@ -1160,7 +1179,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1160
1179
|
lastTokenUsage = undefined;
|
|
1161
1180
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1162
1181
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1163
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1182
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1164
1183
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1165
1184
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1166
1185
|
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
@@ -1176,7 +1195,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1176
1195
|
lastTokenUsage = undefined;
|
|
1177
1196
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1178
1197
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1179
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1198
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1180
1199
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1181
1200
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1182
1201
|
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
@@ -1211,7 +1230,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1211
1230
|
}
|
|
1212
1231
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1213
1232
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1214
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1233
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1215
1234
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1216
1235
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1217
1236
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1250,7 +1269,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1250
1269
|
lastTokenUsage = undefined;
|
|
1251
1270
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1252
1271
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1253
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1272
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1254
1273
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1255
1274
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1256
1275
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1289,7 +1308,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1289
1308
|
lastOutputTokenRate = undefined;
|
|
1290
1309
|
lastTokenUsage = undefined;
|
|
1291
1310
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1292
|
-
process.stdout.write(renderFullWelcomeScreen({ model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode, autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer, chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items }, process.stdout.rows));
|
|
1311
|
+
process.stdout.write(renderFullWelcomeScreen({ provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode, autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer, chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items }, process.stdout.rows));
|
|
1293
1312
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1294
1313
|
continue;
|
|
1295
1314
|
}
|
|
@@ -1304,7 +1323,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1304
1323
|
lastTokenUsage = undefined;
|
|
1305
1324
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1306
1325
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1307
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1326
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1308
1327
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1309
1328
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1310
1329
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1336,7 +1355,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1336
1355
|
sessionStore.save(activeSession);
|
|
1337
1356
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1338
1357
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1339
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1358
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1340
1359
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1341
1360
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1342
1361
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1356,7 +1375,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1356
1375
|
sessionStore.save(activeSession);
|
|
1357
1376
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1358
1377
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1359
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1378
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1360
1379
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1361
1380
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1362
1381
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1381,7 +1400,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1381
1400
|
lastTokenUsage = undefined;
|
|
1382
1401
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1383
1402
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1384
|
-
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1403
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1385
1404
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1386
1405
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1387
1406
|
}, process.stdout.rows));
|
|
@@ -1402,7 +1421,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1402
1421
|
sessionStore.save(activeSession);
|
|
1403
1422
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1404
1423
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1405
|
-
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1424
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1406
1425
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1407
1426
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1408
1427
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1418,13 +1437,13 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1418
1437
|
drawFrame: (popupLines) => {
|
|
1419
1438
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1420
1439
|
process.stdout.write(renderWelcomePopupLayer({
|
|
1421
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1440
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1422
1441
|
tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace,
|
|
1423
1442
|
mode: activeMode, autoApprove: activeAutoApprove,
|
|
1424
1443
|
chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1425
1444
|
}, popupLines, process.stdout.columns, process.stdout.rows));
|
|
1426
1445
|
},
|
|
1427
|
-
gatewayUrl: config.connections?.
|
|
1446
|
+
gatewayUrl: config.connections?.moderado?.baseUrl,
|
|
1428
1447
|
activeConnection,
|
|
1429
1448
|
savedConnections: config.connections,
|
|
1430
1449
|
connectProviders: config.connectProviders,
|
|
@@ -1472,7 +1491,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1472
1491
|
lastAnswer = '';
|
|
1473
1492
|
lastTurnDone = false;
|
|
1474
1493
|
let taskStep;
|
|
1475
|
-
let
|
|
1494
|
+
let activityLabel;
|
|
1495
|
+
let activityFrame = 0;
|
|
1496
|
+
let turnActive = true;
|
|
1476
1497
|
let outputTokenRate;
|
|
1477
1498
|
let budgetWarningIssued = false;
|
|
1478
1499
|
let budgetStopped = false;
|
|
@@ -1480,12 +1501,32 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1480
1501
|
const usedModels = new Set();
|
|
1481
1502
|
let answerPosition = { row: 15, column: getChatAnswerArea(process.stdout.columns).startColumn };
|
|
1482
1503
|
let lastErrorEvent;
|
|
1504
|
+
// Keep a live spinner on the status row for the whole turn: streaming
|
|
1505
|
+
// text and silent tool execution both leave the screen looking settled.
|
|
1506
|
+
const stopActivity = () => {
|
|
1507
|
+
if (thinkingTimer) {
|
|
1508
|
+
clearInterval(thinkingTimer);
|
|
1509
|
+
thinkingTimer = undefined;
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
const paintActivity = (glyph, label) => {
|
|
1513
|
+
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, taskStep, process.stdout.columns, { glyph, label }));
|
|
1514
|
+
};
|
|
1515
|
+
const startActivity = () => {
|
|
1516
|
+
stopActivity();
|
|
1517
|
+
if (!turnActive)
|
|
1518
|
+
return;
|
|
1519
|
+
activityLabel = undefined;
|
|
1520
|
+
thinkingTimer = setInterval(() => {
|
|
1521
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel ?? 'Working');
|
|
1522
|
+
}, 200);
|
|
1523
|
+
};
|
|
1483
1524
|
const redrawChatFrame = () => {
|
|
1484
1525
|
const thoughtTime = Math.max(0.001, (Date.now() - startedAt) / 1000);
|
|
1485
1526
|
const displayAnswer = streamedAnswer || lastAnswer;
|
|
1486
1527
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1487
1528
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1488
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1529
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1489
1530
|
tokens: Math.round(sessionTokens + (usageRecorded ? 0 : turnUsage?.usage.totalTokens ?? 0)),
|
|
1490
1531
|
cost: costLabel(),
|
|
1491
1532
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
@@ -1507,6 +1548,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1507
1548
|
const area = getChatAnswerArea(process.stdout.columns);
|
|
1508
1549
|
answerPosition = renderChatAnswerDelta(displayAnswer, { row: 15, column: area.startColumn }, area.wrapWidth, area.startColumn);
|
|
1509
1550
|
}
|
|
1551
|
+
// The frame paints a static status; keep the live row authoritative while the turn runs.
|
|
1552
|
+
if (thinkingTimer)
|
|
1553
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel ?? 'Working');
|
|
1510
1554
|
};
|
|
1511
1555
|
const onGenerationKeypress = (str, key) => {
|
|
1512
1556
|
handleGenerationKeypress(str, key, commandQueue, {
|
|
@@ -1522,6 +1566,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1522
1566
|
},
|
|
1523
1567
|
onBtw: async (command) => {
|
|
1524
1568
|
const resume = suspendGenerationInput?.();
|
|
1569
|
+
stopActivity();
|
|
1525
1570
|
const ac = new AbortController();
|
|
1526
1571
|
const onBtwKey = (s, k) => {
|
|
1527
1572
|
if ((k && k.name === 'escape') || s === '\x1b') {
|
|
@@ -1533,7 +1578,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1533
1578
|
await executeBtwQuery(command, async (popupLines, frameOpts) => {
|
|
1534
1579
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1535
1580
|
process.stdout.write(renderWelcomePopupLayer({
|
|
1536
|
-
model: currentModel ?? 'No model connected',
|
|
1581
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected',
|
|
1537
1582
|
tokens: Math.round(sessionTokens),
|
|
1538
1583
|
cost: costLabel(),
|
|
1539
1584
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
@@ -1562,6 +1607,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1562
1607
|
}
|
|
1563
1608
|
finally {
|
|
1564
1609
|
process.stdin.removeListener('keypress', onBtwKey);
|
|
1610
|
+
startActivity();
|
|
1565
1611
|
redrawChatFrame();
|
|
1566
1612
|
resume?.();
|
|
1567
1613
|
}
|
|
@@ -1569,6 +1615,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1569
1615
|
});
|
|
1570
1616
|
};
|
|
1571
1617
|
if (process.stdin.isTTY) {
|
|
1618
|
+
const onGenerationResize = () => redrawChatFrame();
|
|
1619
|
+
process.stdout.on('resize', onGenerationResize);
|
|
1572
1620
|
const attachGenerationListener = () => {
|
|
1573
1621
|
readline.emitKeypressEvents(process.stdin);
|
|
1574
1622
|
process.stdin.resume();
|
|
@@ -1588,16 +1636,13 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1588
1636
|
};
|
|
1589
1637
|
removeGenerationListener = () => {
|
|
1590
1638
|
process.stdin.removeListener('keypress', onGenerationKeypress);
|
|
1639
|
+
process.stdout.removeListener('resize', onGenerationResize);
|
|
1591
1640
|
suspendGenerationInput = undefined;
|
|
1592
1641
|
};
|
|
1593
1642
|
}
|
|
1594
1643
|
// Move to the chat layout before the provider can emit its first event.
|
|
1595
1644
|
redrawChatFrame();
|
|
1596
|
-
|
|
1597
|
-
if (!firstAssistantDeltaAt && !turnUsage) {
|
|
1598
|
-
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, taskStep, process.stdout.columns));
|
|
1599
|
-
}
|
|
1600
|
-
}, 400);
|
|
1645
|
+
startActivity();
|
|
1601
1646
|
const evidenceTask = liveSearch ? buildSearchAnswerTask(effectivePrompt, liveSearch) : undefined;
|
|
1602
1647
|
const runAgent = () => loop.run(isReview ? createReviewTask(reviewFiles) : evidenceTask ?? createAgentTask(effectivePrompt, activeMode), {
|
|
1603
1648
|
workspaceRoot: canonicalWorkspace, provider: provider, tools: isReview ? createReviewToolRegistry() : tools, approvalHandler, router, policy,
|
|
@@ -1609,9 +1654,21 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1609
1654
|
usedModels.add(event.newModelId);
|
|
1610
1655
|
if (event.type === 'progress') {
|
|
1611
1656
|
taskStep = { step: event.step, maxSteps: event.maxSteps ?? 0 };
|
|
1612
|
-
|
|
1613
|
-
|
|
1657
|
+
activityLabel = event.status;
|
|
1658
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel ?? 'Working');
|
|
1659
|
+
}
|
|
1660
|
+
if (event.type === 'tool_call_initiated') {
|
|
1661
|
+
activityLabel = `Running ${event.toolName}`;
|
|
1662
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel);
|
|
1663
|
+
}
|
|
1664
|
+
if (event.type === 'tool_result') {
|
|
1665
|
+
activityLabel = undefined;
|
|
1666
|
+
if (!thinkingTimer)
|
|
1667
|
+
startActivity();
|
|
1614
1668
|
}
|
|
1669
|
+
// The approval prompt owns the screen until the tool result arrives.
|
|
1670
|
+
if (event.type === 'approval_request')
|
|
1671
|
+
stopActivity();
|
|
1615
1672
|
if (event.type === 'gateway_status')
|
|
1616
1673
|
process.stdout.write(renderGatewayFallbackUpdate(event, process.stdout.columns || 80));
|
|
1617
1674
|
if (event.type === 'usage') {
|
|
@@ -1632,11 +1689,6 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1632
1689
|
}
|
|
1633
1690
|
}
|
|
1634
1691
|
if (event.type === 'assistant_delta') {
|
|
1635
|
-
if (!firstAssistantDeltaAt && event.delta.trim().length > 0) {
|
|
1636
|
-
firstAssistantDeltaAt = Date.now();
|
|
1637
|
-
if (thinkingTimer)
|
|
1638
|
-
clearInterval(thinkingTimer);
|
|
1639
|
-
}
|
|
1640
1692
|
streamedAnswer += event.delta;
|
|
1641
1693
|
if (Date.now() - lastSessionCheckpointAt >= 1_000)
|
|
1642
1694
|
checkpointInterruptedTurn();
|
|
@@ -1661,28 +1713,24 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1661
1713
|
checkpoints.recordPostWrite(canonicalWorkspace, paths);
|
|
1662
1714
|
},
|
|
1663
1715
|
});
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
clearInterval(thinkingTimer);
|
|
1674
|
-
const cloudOAuth = isModeradoCloudOAuthConnection(activeConnection);
|
|
1675
|
-
const cloudWithoutKey = activeConnection.id === 'moderado-cloud' && !activeConnection.apiKey;
|
|
1676
|
-
process.stdout.write(cloudWithoutKey
|
|
1716
|
+
/** Prompt for a replacement credential once, persist it, and activate it. Falsy when the user cancels. */
|
|
1717
|
+
const recoverAuthenticationFailure = async () => {
|
|
1718
|
+
if (!activeConnection)
|
|
1719
|
+
return undefined;
|
|
1720
|
+
stopActivity();
|
|
1721
|
+
const connection = activeConnection;
|
|
1722
|
+
const moderadoOAuth = isModeradoOAuthConnection(connection);
|
|
1723
|
+
const moderadoWithoutKey = connection.id === 'moderado' && !connection.apiKey;
|
|
1724
|
+
process.stdout.write(moderadoWithoutKey
|
|
1677
1725
|
? '\nThis free Gateway route requires a Moderado website API key. Sign in or enter a key to retry once.\n'
|
|
1678
|
-
:
|
|
1679
|
-
? '\nModerado
|
|
1680
|
-
: `\nThe saved ${
|
|
1681
|
-
const replacement =
|
|
1682
|
-
? await
|
|
1683
|
-
: await replaceProviderKeyInteractive(
|
|
1726
|
+
: moderadoOAuth
|
|
1727
|
+
? '\nModerado rejected this browser login. Sign in again to retry once.\n'
|
|
1728
|
+
: `\nThe saved ${connection.displayName} API key was rejected. Enter a replacement key to retry once.\n`);
|
|
1729
|
+
const replacement = moderadoOAuth || moderadoWithoutKey
|
|
1730
|
+
? await loginModeradoInteractive({ signal, gatewayUrl: connection.baseUrl })
|
|
1731
|
+
: await replaceProviderKeyInteractive(connection, { signal });
|
|
1684
1732
|
if (!replacement)
|
|
1685
|
-
|
|
1733
|
+
return undefined;
|
|
1686
1734
|
recordTurnUsage();
|
|
1687
1735
|
turnUsage = undefined;
|
|
1688
1736
|
usageRecorded = false;
|
|
@@ -1690,8 +1738,37 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1690
1738
|
saveConnection(secured);
|
|
1691
1739
|
config = loadConfig();
|
|
1692
1740
|
activateConnection(secured);
|
|
1741
|
+
startActivity();
|
|
1742
|
+
return secured;
|
|
1743
|
+
};
|
|
1744
|
+
let result;
|
|
1745
|
+
try {
|
|
1746
|
+
checkpointInterruptedTurn();
|
|
1747
|
+
result = await runAgent();
|
|
1748
|
+
// The agent loop reports provider failures as error events and resolves
|
|
1749
|
+
// with a failed result instead of throwing, so key re-entry must be
|
|
1750
|
+
// offered here too. Retry once; a second failure falls through to the
|
|
1751
|
+
// formatted error answer.
|
|
1752
|
+
if (turnFailedAuthentication(result.status, lastErrorEvent) && activeConnection && await recoverAuthenticationFailure()) {
|
|
1753
|
+
lastErrorEvent = undefined;
|
|
1754
|
+
result = await runAgent();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
catch (error) {
|
|
1758
|
+
if (!activeConnection || !isAuthenticationFailure(error))
|
|
1759
|
+
throw error;
|
|
1760
|
+
if (!(await recoverAuthenticationFailure()))
|
|
1761
|
+
throw error;
|
|
1693
1762
|
result = await runAgent();
|
|
1694
1763
|
}
|
|
1764
|
+
// The turn is over: replace the spinner with the settled state while
|
|
1765
|
+
// usage accounting and catalog refresh finish.
|
|
1766
|
+
turnActive = false;
|
|
1767
|
+
stopActivity();
|
|
1768
|
+
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, undefined, process.stdout.columns, {
|
|
1769
|
+
glyph: result.status === 'completed' ? '✔' : '✖',
|
|
1770
|
+
label: result.status === 'completed' ? 'Done' : result.status === 'cancelled' ? 'Stopped' : 'Failed',
|
|
1771
|
+
}));
|
|
1695
1772
|
if (turnUsage) {
|
|
1696
1773
|
let pricing;
|
|
1697
1774
|
const modelIds = new Set([...usedModels, result.selectedModel.id]);
|
|
@@ -1763,14 +1840,16 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1763
1840
|
lastQuestion = trimmed;
|
|
1764
1841
|
lastThoughtTime = Math.max(0.001, (Date.now() - startedAt) / 1000);
|
|
1765
1842
|
lastOutputTokenRate = outputTokenRate;
|
|
1766
|
-
|
|
1767
|
-
|
|
1843
|
+
turnActive = false;
|
|
1844
|
+
stopActivity();
|
|
1768
1845
|
removeGenerationListener?.();
|
|
1769
1846
|
redrawChatFrame();
|
|
1770
1847
|
}
|
|
1771
1848
|
catch (err) {
|
|
1772
|
-
if (thinkingTimer)
|
|
1849
|
+
if (thinkingTimer) {
|
|
1773
1850
|
clearInterval(thinkingTimer);
|
|
1851
|
+
thinkingTimer = undefined;
|
|
1852
|
+
}
|
|
1774
1853
|
removeGenerationListener?.();
|
|
1775
1854
|
recordTurnUsage();
|
|
1776
1855
|
if (turnUsage)
|
|
@@ -1796,7 +1875,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1796
1875
|
lastOutputTokenRate = undefined;
|
|
1797
1876
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1798
1877
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1799
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1878
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1800
1879
|
tokens: Math.round(sessionTokens),
|
|
1801
1880
|
cost: costLabel(),
|
|
1802
1881
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|