moderado 0.4.28 → 0.4.34
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 +88 -68
- 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 +161 -59
- 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/http_shutdown.d.ts +22 -0
- package/dist/http_shutdown.d.ts.map +1 -0
- package/dist/http_shutdown.js +36 -0
- package/dist/http_shutdown.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/self_update.d.ts +6 -0
- package/dist/self_update.d.ts.map +1 -1
- package/dist/self_update.js +17 -1
- package/dist/self_update.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 +10 -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 +25 -3
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +69 -25
- 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 +26 -8
- 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';
|
|
@@ -24,9 +24,15 @@ import { buildWorkspaceMap } from '../repo_map.js';
|
|
|
24
24
|
import { evaluateTokenBudget, parseBudgetCommand } from '../budget.js';
|
|
25
25
|
import { discoverSkills, formatSkillContext, formatSkillsList, getEnabledSkillNames, selectEnabledSkills, setSkillEnabled } from '../skills.js';
|
|
26
26
|
import { getHelpCommandLines } from '../ui/help_content.js';
|
|
27
|
+
import { forceExit } from '../http_shutdown.js';
|
|
27
28
|
export function createConnectionProvider(connection) {
|
|
28
29
|
return new NvidiaAdapter({ apiKey: connection.apiKey, baseUrl: connection.baseUrl, providerId: connection.id, providerName: connection.displayName });
|
|
29
30
|
}
|
|
31
|
+
export function getProviderStatusName(connection) {
|
|
32
|
+
return connection?.id === 'moderado' || connection?.id === 'moderado-cloud'
|
|
33
|
+
? 'Moderado'
|
|
34
|
+
: connection?.displayName;
|
|
35
|
+
}
|
|
30
36
|
export function formatSessionExitSummary(session, modelId) {
|
|
31
37
|
const usage = session.usage;
|
|
32
38
|
const tokens = usage.available
|
|
@@ -173,8 +179,18 @@ export function formatTurnFailureAnswer(status, errorEvent, modelId, otherConnec
|
|
|
173
179
|
const cleanDetail = rawDetail
|
|
174
180
|
.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
181
|
.trim();
|
|
182
|
+
if (errorEvent?.code === 'ERR_GATEWAY_UNAUTHORIZED') {
|
|
183
|
+
return `⚠️ **Gateway Authorization (${errorEvent.code}):**\n${cleanDetail}`;
|
|
184
|
+
}
|
|
185
|
+
if (errorEvent?.code?.toUpperCase().includes('RATE_LIMIT')) {
|
|
186
|
+
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\`.`;
|
|
187
|
+
}
|
|
176
188
|
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
189
|
}
|
|
190
|
+
/** A turn whose provider rejected its credential may recover by re-entering a key once. */
|
|
191
|
+
export function turnFailedAuthentication(status, errorEvent) {
|
|
192
|
+
return status === 'failed' && isAuthenticationFailure(errorEvent);
|
|
193
|
+
}
|
|
178
194
|
export function resolveTurnAssistantAnswer(result, previousHistoryLength, lastErrorEvent, streamedAnswer = '', currentModel = 'unknown', otherConnections = []) {
|
|
179
195
|
if (result.status === 'cancelled') {
|
|
180
196
|
const partial = cleanConversationalFiller(streamedAnswer);
|
|
@@ -203,6 +219,8 @@ export function saveFailedTurnPrompt(history, prompt, partialResponse = '') {
|
|
|
203
219
|
return [...history, { role: 'user', content: prompt }, { role: 'assistant', content: interruptedResponse }];
|
|
204
220
|
}
|
|
205
221
|
const CONTINUE_INTERRUPTED_TASK_PROMPT = 'Continue the interrupted task from the saved conversation. Start from the unfinished work and avoid repeating completed steps.';
|
|
222
|
+
/** Braille spinner frames for the live "still running" status row. */
|
|
223
|
+
const ACTIVITY_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
206
224
|
function hasInterruptedTurn(messages) {
|
|
207
225
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
208
226
|
const message = messages[index];
|
|
@@ -466,7 +484,8 @@ export function formatDirectWebSearchAnswer(result, modelConnected = false) {
|
|
|
466
484
|
.filter((line) => line && line !== '...' && line !== '---' && !SEARCH_BLOCK_METADATA.test(line) && !/^https?:\/\//i.test(line));
|
|
467
485
|
if (!lines.length)
|
|
468
486
|
return 'Web search returned no readable content.';
|
|
469
|
-
|
|
487
|
+
const excerpt = lines.slice(0, 10).join('\n');
|
|
488
|
+
return `${excerpt}\n\n${modelConnected ? 'Live web search excerpt.' : 'Live web search excerpt. Connect a model for a summary.'}`;
|
|
470
489
|
}
|
|
471
490
|
/** Environment configuration overrides the persistent search provider preference. */
|
|
472
491
|
export function resolveWebSearchProvider(config) {
|
|
@@ -838,6 +857,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
838
857
|
else {
|
|
839
858
|
let autoContinueRequested = false;
|
|
840
859
|
const turn = await promptInteractiveTurn({
|
|
860
|
+
getProviderName: () => getProviderStatusName(activeConnection),
|
|
841
861
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace, version,
|
|
842
862
|
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
863
|
queuedCommands: commandQueue.items,
|
|
@@ -856,7 +876,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
856
876
|
}
|
|
857
877
|
if (activeConnection.kind === 'openai-compatible') {
|
|
858
878
|
const modelId = await selectCompatibleModelOverlay({
|
|
859
|
-
apiKey: activeConnection.id === 'moderado
|
|
879
|
+
apiKey: activeConnection.id === 'moderado' ? undefined : activeConnection.apiKey,
|
|
860
880
|
baseUrl: activeConnection.baseUrl,
|
|
861
881
|
providerId: activeConnection.id,
|
|
862
882
|
providerName: activeConnection.displayName,
|
|
@@ -877,7 +897,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
877
897
|
}
|
|
878
898
|
return modelId;
|
|
879
899
|
}
|
|
880
|
-
const selection = await selectModelOverlay({ apiKey: activeConnection.id === 'moderado
|
|
900
|
+
const selection = await selectModelOverlay({ apiKey: activeConnection.id === 'moderado' ? undefined : activeConnection.apiKey, currentModel, signal, saveSelectionByDefault: true, drawFrame });
|
|
881
901
|
if (selection.modelId && selection.modelId !== currentModel) {
|
|
882
902
|
currentModel = selection.modelId;
|
|
883
903
|
activeConnection = { ...activeConnection, defaultModel: selection.modelId };
|
|
@@ -895,7 +915,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
895
915
|
const connection = await connectProviderInteractive({
|
|
896
916
|
signal,
|
|
897
917
|
drawFrame,
|
|
898
|
-
gatewayUrl: config.connections?.
|
|
918
|
+
gatewayUrl: config.connections?.moderado?.baseUrl,
|
|
899
919
|
activeConnection,
|
|
900
920
|
savedConnections: config.connections,
|
|
901
921
|
connectProviders: config.connectProviders,
|
|
@@ -1160,7 +1180,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1160
1180
|
lastTokenUsage = undefined;
|
|
1161
1181
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1162
1182
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1163
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1183
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1164
1184
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1165
1185
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1166
1186
|
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
@@ -1176,7 +1196,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1176
1196
|
lastTokenUsage = undefined;
|
|
1177
1197
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1178
1198
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1179
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1199
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1180
1200
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1181
1201
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1182
1202
|
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
@@ -1211,7 +1231,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1211
1231
|
}
|
|
1212
1232
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1213
1233
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1214
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1234
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1215
1235
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1216
1236
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1217
1237
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1250,7 +1270,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1250
1270
|
lastTokenUsage = undefined;
|
|
1251
1271
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1252
1272
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1253
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1273
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1254
1274
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1255
1275
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1256
1276
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1289,12 +1309,12 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1289
1309
|
lastOutputTokenRate = undefined;
|
|
1290
1310
|
lastTokenUsage = undefined;
|
|
1291
1311
|
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));
|
|
1312
|
+
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
1313
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1294
1314
|
continue;
|
|
1295
1315
|
}
|
|
1296
1316
|
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
1297
|
-
exitCleanly(exitMessage());
|
|
1317
|
+
return await exitCleanly(exitMessage());
|
|
1298
1318
|
}
|
|
1299
1319
|
if (trimmed === '/help') {
|
|
1300
1320
|
lastQuestion = trimmed;
|
|
@@ -1304,7 +1324,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1304
1324
|
lastTokenUsage = undefined;
|
|
1305
1325
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1306
1326
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1307
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1327
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1308
1328
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1309
1329
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1310
1330
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1336,7 +1356,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1336
1356
|
sessionStore.save(activeSession);
|
|
1337
1357
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1338
1358
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1339
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1359
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1340
1360
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1341
1361
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1342
1362
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1356,7 +1376,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1356
1376
|
sessionStore.save(activeSession);
|
|
1357
1377
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1358
1378
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1359
|
-
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1379
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1360
1380
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1361
1381
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1362
1382
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1381,7 +1401,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1381
1401
|
lastTokenUsage = undefined;
|
|
1382
1402
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1383
1403
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1384
|
-
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1404
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1385
1405
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1386
1406
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1387
1407
|
}, process.stdout.rows));
|
|
@@ -1402,7 +1422,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1402
1422
|
sessionStore.save(activeSession);
|
|
1403
1423
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1404
1424
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1405
|
-
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1425
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1406
1426
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1407
1427
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1408
1428
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1418,13 +1438,13 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1418
1438
|
drawFrame: (popupLines) => {
|
|
1419
1439
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1420
1440
|
process.stdout.write(renderWelcomePopupLayer({
|
|
1421
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1441
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1422
1442
|
tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace,
|
|
1423
1443
|
mode: activeMode, autoApprove: activeAutoApprove,
|
|
1424
1444
|
chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1425
1445
|
}, popupLines, process.stdout.columns, process.stdout.rows));
|
|
1426
1446
|
},
|
|
1427
|
-
gatewayUrl: config.connections?.
|
|
1447
|
+
gatewayUrl: config.connections?.moderado?.baseUrl,
|
|
1428
1448
|
activeConnection,
|
|
1429
1449
|
savedConnections: config.connections,
|
|
1430
1450
|
connectProviders: config.connectProviders,
|
|
@@ -1472,7 +1492,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1472
1492
|
lastAnswer = '';
|
|
1473
1493
|
lastTurnDone = false;
|
|
1474
1494
|
let taskStep;
|
|
1475
|
-
let
|
|
1495
|
+
let activityLabel;
|
|
1496
|
+
let activityFrame = 0;
|
|
1497
|
+
let turnActive = true;
|
|
1476
1498
|
let outputTokenRate;
|
|
1477
1499
|
let budgetWarningIssued = false;
|
|
1478
1500
|
let budgetStopped = false;
|
|
@@ -1480,12 +1502,32 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1480
1502
|
const usedModels = new Set();
|
|
1481
1503
|
let answerPosition = { row: 15, column: getChatAnswerArea(process.stdout.columns).startColumn };
|
|
1482
1504
|
let lastErrorEvent;
|
|
1505
|
+
// Keep a live spinner on the status row for the whole turn: streaming
|
|
1506
|
+
// text and silent tool execution both leave the screen looking settled.
|
|
1507
|
+
const stopActivity = () => {
|
|
1508
|
+
if (thinkingTimer) {
|
|
1509
|
+
clearInterval(thinkingTimer);
|
|
1510
|
+
thinkingTimer = undefined;
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
const paintActivity = (glyph, label) => {
|
|
1514
|
+
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, taskStep, process.stdout.columns, { glyph, label }));
|
|
1515
|
+
};
|
|
1516
|
+
const startActivity = () => {
|
|
1517
|
+
stopActivity();
|
|
1518
|
+
if (!turnActive)
|
|
1519
|
+
return;
|
|
1520
|
+
activityLabel = undefined;
|
|
1521
|
+
thinkingTimer = setInterval(() => {
|
|
1522
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel ?? 'Working');
|
|
1523
|
+
}, 200);
|
|
1524
|
+
};
|
|
1483
1525
|
const redrawChatFrame = () => {
|
|
1484
1526
|
const thoughtTime = Math.max(0.001, (Date.now() - startedAt) / 1000);
|
|
1485
1527
|
const displayAnswer = streamedAnswer || lastAnswer;
|
|
1486
1528
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1487
1529
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1488
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1530
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1489
1531
|
tokens: Math.round(sessionTokens + (usageRecorded ? 0 : turnUsage?.usage.totalTokens ?? 0)),
|
|
1490
1532
|
cost: costLabel(),
|
|
1491
1533
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
@@ -1507,8 +1549,24 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1507
1549
|
const area = getChatAnswerArea(process.stdout.columns);
|
|
1508
1550
|
answerPosition = renderChatAnswerDelta(displayAnswer, { row: 15, column: area.startColumn }, area.wrapWidth, area.startColumn);
|
|
1509
1551
|
}
|
|
1552
|
+
// The frame paints a static status; keep the live row authoritative while the turn runs.
|
|
1553
|
+
if (thinkingTimer)
|
|
1554
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel ?? 'Working');
|
|
1510
1555
|
};
|
|
1556
|
+
let generationEscapes = 0;
|
|
1511
1557
|
const onGenerationKeypress = (str, key) => {
|
|
1558
|
+
// Raw mode turns Ctrl+C into a keypress, not SIGINT, so the session-level
|
|
1559
|
+
// handler never sees it. The first press aborts the turn; a second press
|
|
1560
|
+
// means the abort did not take (a stalled stream or a hung tool), so
|
|
1561
|
+
// leave instead of freezing with no way out.
|
|
1562
|
+
const isCtrlC = (key?.ctrl && key.name === 'c') || str === '\x03';
|
|
1563
|
+
if (isCtrlC) {
|
|
1564
|
+
generationEscapes += 1;
|
|
1565
|
+
if (generationEscapes > 1) {
|
|
1566
|
+
void forceExit(130);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1512
1570
|
handleGenerationKeypress(str, key, commandQueue, {
|
|
1513
1571
|
abort: () => {
|
|
1514
1572
|
commandQueue.clear();
|
|
@@ -1522,9 +1580,18 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1522
1580
|
},
|
|
1523
1581
|
onBtw: async (command) => {
|
|
1524
1582
|
const resume = suspendGenerationInput?.();
|
|
1583
|
+
stopActivity();
|
|
1525
1584
|
const ac = new AbortController();
|
|
1585
|
+
let btwEscapes = 0;
|
|
1526
1586
|
const onBtwKey = (s, k) => {
|
|
1527
|
-
|
|
1587
|
+
const isCtrlC = (k && k.ctrl && k.name === 'c') || s === '\x03';
|
|
1588
|
+
if ((k && k.name === 'escape') || s === '\x1b' || isCtrlC) {
|
|
1589
|
+
// Escape/Ctrl+C stops the /btw query; a second Ctrl+C means the
|
|
1590
|
+
// query ignores the abort, so leave instead of freezing.
|
|
1591
|
+
if (isCtrlC && ++btwEscapes > 1) {
|
|
1592
|
+
void forceExit(130);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1528
1595
|
ac.abort();
|
|
1529
1596
|
}
|
|
1530
1597
|
};
|
|
@@ -1533,7 +1600,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1533
1600
|
await executeBtwQuery(command, async (popupLines, frameOpts) => {
|
|
1534
1601
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1535
1602
|
process.stdout.write(renderWelcomePopupLayer({
|
|
1536
|
-
model: currentModel ?? 'No model connected',
|
|
1603
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected',
|
|
1537
1604
|
tokens: Math.round(sessionTokens),
|
|
1538
1605
|
cost: costLabel(),
|
|
1539
1606
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
@@ -1550,7 +1617,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1550
1617
|
await new Promise((dismissResolve) => {
|
|
1551
1618
|
const onDismiss = (s, k) => {
|
|
1552
1619
|
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
1553
|
-
s === 'q' || s === 'Q' || s === '\x1b'
|
|
1620
|
+
s === 'q' || s === 'Q' || s === '\x1b' ||
|
|
1621
|
+
(k && k.ctrl && k.name === 'c')) {
|
|
1554
1622
|
process.stdin.removeListener('keypress', onDismiss);
|
|
1555
1623
|
dismissResolve();
|
|
1556
1624
|
}
|
|
@@ -1562,6 +1630,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1562
1630
|
}
|
|
1563
1631
|
finally {
|
|
1564
1632
|
process.stdin.removeListener('keypress', onBtwKey);
|
|
1633
|
+
startActivity();
|
|
1565
1634
|
redrawChatFrame();
|
|
1566
1635
|
resume?.();
|
|
1567
1636
|
}
|
|
@@ -1569,6 +1638,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1569
1638
|
});
|
|
1570
1639
|
};
|
|
1571
1640
|
if (process.stdin.isTTY) {
|
|
1641
|
+
const onGenerationResize = () => redrawChatFrame();
|
|
1642
|
+
process.stdout.on('resize', onGenerationResize);
|
|
1572
1643
|
const attachGenerationListener = () => {
|
|
1573
1644
|
readline.emitKeypressEvents(process.stdin);
|
|
1574
1645
|
process.stdin.resume();
|
|
@@ -1588,16 +1659,13 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1588
1659
|
};
|
|
1589
1660
|
removeGenerationListener = () => {
|
|
1590
1661
|
process.stdin.removeListener('keypress', onGenerationKeypress);
|
|
1662
|
+
process.stdout.removeListener('resize', onGenerationResize);
|
|
1591
1663
|
suspendGenerationInput = undefined;
|
|
1592
1664
|
};
|
|
1593
1665
|
}
|
|
1594
1666
|
// Move to the chat layout before the provider can emit its first event.
|
|
1595
1667
|
redrawChatFrame();
|
|
1596
|
-
|
|
1597
|
-
if (!firstAssistantDeltaAt && !turnUsage) {
|
|
1598
|
-
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, taskStep, process.stdout.columns));
|
|
1599
|
-
}
|
|
1600
|
-
}, 400);
|
|
1668
|
+
startActivity();
|
|
1601
1669
|
const evidenceTask = liveSearch ? buildSearchAnswerTask(effectivePrompt, liveSearch) : undefined;
|
|
1602
1670
|
const runAgent = () => loop.run(isReview ? createReviewTask(reviewFiles) : evidenceTask ?? createAgentTask(effectivePrompt, activeMode), {
|
|
1603
1671
|
workspaceRoot: canonicalWorkspace, provider: provider, tools: isReview ? createReviewToolRegistry() : tools, approvalHandler, router, policy,
|
|
@@ -1609,9 +1677,21 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1609
1677
|
usedModels.add(event.newModelId);
|
|
1610
1678
|
if (event.type === 'progress') {
|
|
1611
1679
|
taskStep = { step: event.step, maxSteps: event.maxSteps ?? 0 };
|
|
1612
|
-
|
|
1613
|
-
|
|
1680
|
+
activityLabel = event.status;
|
|
1681
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel ?? 'Working');
|
|
1682
|
+
}
|
|
1683
|
+
if (event.type === 'tool_call_initiated') {
|
|
1684
|
+
activityLabel = `Running ${event.toolName}`;
|
|
1685
|
+
paintActivity(ACTIVITY_SPINNER_FRAMES[activityFrame++ % ACTIVITY_SPINNER_FRAMES.length], activityLabel);
|
|
1686
|
+
}
|
|
1687
|
+
if (event.type === 'tool_result') {
|
|
1688
|
+
activityLabel = undefined;
|
|
1689
|
+
if (!thinkingTimer)
|
|
1690
|
+
startActivity();
|
|
1614
1691
|
}
|
|
1692
|
+
// The approval prompt owns the screen until the tool result arrives.
|
|
1693
|
+
if (event.type === 'approval_request')
|
|
1694
|
+
stopActivity();
|
|
1615
1695
|
if (event.type === 'gateway_status')
|
|
1616
1696
|
process.stdout.write(renderGatewayFallbackUpdate(event, process.stdout.columns || 80));
|
|
1617
1697
|
if (event.type === 'usage') {
|
|
@@ -1632,11 +1712,6 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1632
1712
|
}
|
|
1633
1713
|
}
|
|
1634
1714
|
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
1715
|
streamedAnswer += event.delta;
|
|
1641
1716
|
if (Date.now() - lastSessionCheckpointAt >= 1_000)
|
|
1642
1717
|
checkpointInterruptedTurn();
|
|
@@ -1661,28 +1736,24 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1661
1736
|
checkpoints.recordPostWrite(canonicalWorkspace, paths);
|
|
1662
1737
|
},
|
|
1663
1738
|
});
|
|
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
|
|
1739
|
+
/** Prompt for a replacement credential once, persist it, and activate it. Falsy when the user cancels. */
|
|
1740
|
+
const recoverAuthenticationFailure = async () => {
|
|
1741
|
+
if (!activeConnection)
|
|
1742
|
+
return undefined;
|
|
1743
|
+
stopActivity();
|
|
1744
|
+
const connection = activeConnection;
|
|
1745
|
+
const moderadoOAuth = isModeradoOAuthConnection(connection);
|
|
1746
|
+
const moderadoWithoutKey = connection.id === 'moderado' && !connection.apiKey;
|
|
1747
|
+
process.stdout.write(moderadoWithoutKey
|
|
1677
1748
|
? '\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(
|
|
1749
|
+
: moderadoOAuth
|
|
1750
|
+
? '\nModerado rejected this browser login. Sign in again to retry once.\n'
|
|
1751
|
+
: `\nThe saved ${connection.displayName} API key was rejected. Enter a replacement key to retry once.\n`);
|
|
1752
|
+
const replacement = moderadoOAuth || moderadoWithoutKey
|
|
1753
|
+
? await loginModeradoInteractive({ signal, gatewayUrl: connection.baseUrl })
|
|
1754
|
+
: await replaceProviderKeyInteractive(connection, { signal });
|
|
1684
1755
|
if (!replacement)
|
|
1685
|
-
|
|
1756
|
+
return undefined;
|
|
1686
1757
|
recordTurnUsage();
|
|
1687
1758
|
turnUsage = undefined;
|
|
1688
1759
|
usageRecorded = false;
|
|
@@ -1690,8 +1761,37 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1690
1761
|
saveConnection(secured);
|
|
1691
1762
|
config = loadConfig();
|
|
1692
1763
|
activateConnection(secured);
|
|
1764
|
+
startActivity();
|
|
1765
|
+
return secured;
|
|
1766
|
+
};
|
|
1767
|
+
let result;
|
|
1768
|
+
try {
|
|
1769
|
+
checkpointInterruptedTurn();
|
|
1693
1770
|
result = await runAgent();
|
|
1771
|
+
// The agent loop reports provider failures as error events and resolves
|
|
1772
|
+
// with a failed result instead of throwing, so key re-entry must be
|
|
1773
|
+
// offered here too. Retry once; a second failure falls through to the
|
|
1774
|
+
// formatted error answer.
|
|
1775
|
+
if (turnFailedAuthentication(result.status, lastErrorEvent) && activeConnection && await recoverAuthenticationFailure()) {
|
|
1776
|
+
lastErrorEvent = undefined;
|
|
1777
|
+
result = await runAgent();
|
|
1778
|
+
}
|
|
1694
1779
|
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
if (!activeConnection || !isAuthenticationFailure(error))
|
|
1782
|
+
throw error;
|
|
1783
|
+
if (!(await recoverAuthenticationFailure()))
|
|
1784
|
+
throw error;
|
|
1785
|
+
result = await runAgent();
|
|
1786
|
+
}
|
|
1787
|
+
// The turn is over: replace the spinner with the settled state while
|
|
1788
|
+
// usage accounting and catalog refresh finish.
|
|
1789
|
+
turnActive = false;
|
|
1790
|
+
stopActivity();
|
|
1791
|
+
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, undefined, process.stdout.columns, {
|
|
1792
|
+
glyph: result.status === 'completed' ? '✔' : '✖',
|
|
1793
|
+
label: result.status === 'completed' ? 'Done' : result.status === 'cancelled' ? 'Stopped' : 'Failed',
|
|
1794
|
+
}));
|
|
1695
1795
|
if (turnUsage) {
|
|
1696
1796
|
let pricing;
|
|
1697
1797
|
const modelIds = new Set([...usedModels, result.selectedModel.id]);
|
|
@@ -1763,14 +1863,16 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1763
1863
|
lastQuestion = trimmed;
|
|
1764
1864
|
lastThoughtTime = Math.max(0.001, (Date.now() - startedAt) / 1000);
|
|
1765
1865
|
lastOutputTokenRate = outputTokenRate;
|
|
1766
|
-
|
|
1767
|
-
|
|
1866
|
+
turnActive = false;
|
|
1867
|
+
stopActivity();
|
|
1768
1868
|
removeGenerationListener?.();
|
|
1769
1869
|
redrawChatFrame();
|
|
1770
1870
|
}
|
|
1771
1871
|
catch (err) {
|
|
1772
|
-
if (thinkingTimer)
|
|
1872
|
+
if (thinkingTimer) {
|
|
1773
1873
|
clearInterval(thinkingTimer);
|
|
1874
|
+
thinkingTimer = undefined;
|
|
1875
|
+
}
|
|
1774
1876
|
removeGenerationListener?.();
|
|
1775
1877
|
recordTurnUsage();
|
|
1776
1878
|
if (turnUsage)
|
|
@@ -1796,7 +1898,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1796
1898
|
lastOutputTokenRate = undefined;
|
|
1797
1899
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1798
1900
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1799
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1901
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1800
1902
|
tokens: Math.round(sessionTokens),
|
|
1801
1903
|
cost: costLabel(),
|
|
1802
1904
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|