moderado 0.4.26 → 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 -66
- 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 +7 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +230 -82
- 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/help_content.d.ts +17 -0
- package/dist/ui/help_content.d.ts.map +1 -0
- package/dist/ui/help_content.js +13 -0
- package/dist/ui/help_content.js.map +1 -0
- package/dist/ui/help_modal.d.ts.map +1 -1
- package/dist/ui/help_modal.js +5 -5
- package/dist/ui/help_modal.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 +16 -2
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +63 -33
- 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';
|
|
@@ -23,9 +23,15 @@ import { inspectGitWorkspace, readGitDiff } from '../vendor/tools/index.js';
|
|
|
23
23
|
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
|
+
import { getHelpCommandLines } from '../ui/help_content.js';
|
|
26
27
|
export function createConnectionProvider(connection) {
|
|
27
28
|
return new NvidiaAdapter({ apiKey: connection.apiKey, baseUrl: connection.baseUrl, providerId: connection.id, providerName: connection.displayName });
|
|
28
29
|
}
|
|
30
|
+
export function getProviderStatusName(connection) {
|
|
31
|
+
return connection?.id === 'moderado' || connection?.id === 'moderado-cloud'
|
|
32
|
+
? 'Moderado'
|
|
33
|
+
: connection?.displayName;
|
|
34
|
+
}
|
|
29
35
|
export function formatSessionExitSummary(session, modelId) {
|
|
30
36
|
const usage = session.usage;
|
|
31
37
|
const tokens = usage.available
|
|
@@ -172,8 +178,18 @@ export function formatTurnFailureAnswer(status, errorEvent, modelId, otherConnec
|
|
|
172
178
|
const cleanDetail = rawDetail
|
|
173
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, '')
|
|
174
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
|
+
}
|
|
175
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}.`;
|
|
176
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
|
+
}
|
|
177
193
|
export function resolveTurnAssistantAnswer(result, previousHistoryLength, lastErrorEvent, streamedAnswer = '', currentModel = 'unknown', otherConnections = []) {
|
|
178
194
|
if (result.status === 'cancelled') {
|
|
179
195
|
const partial = cleanConversationalFiller(streamedAnswer);
|
|
@@ -192,10 +208,48 @@ export function resolveTurnAssistantAnswer(result, previousHistoryLength, lastEr
|
|
|
192
208
|
isError: false,
|
|
193
209
|
};
|
|
194
210
|
}
|
|
195
|
-
export function saveFailedTurnPrompt(history, prompt) {
|
|
211
|
+
export function saveFailedTurnPrompt(history, prompt, partialResponse = '') {
|
|
196
212
|
if (!prompt.trim())
|
|
197
213
|
return history;
|
|
198
|
-
|
|
214
|
+
const interruptionNote = '[This response was interrupted before completion. Continue the task if asked.]';
|
|
215
|
+
const interruptedResponse = partialResponse.trim()
|
|
216
|
+
? `${partialResponse.trim()}\n\n${interruptionNote}`
|
|
217
|
+
: '[This task was interrupted before a response was produced. Continue the task if asked.]';
|
|
218
|
+
return [...history, { role: 'user', content: prompt }, { role: 'assistant', content: interruptedResponse }];
|
|
219
|
+
}
|
|
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 = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
223
|
+
function hasInterruptedTurn(messages) {
|
|
224
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
225
|
+
const message = messages[index];
|
|
226
|
+
if (message.role !== 'assistant' || !message.content?.trim())
|
|
227
|
+
continue;
|
|
228
|
+
return message.content.includes('[This response was interrupted before completion.')
|
|
229
|
+
|| message.content.includes('[This task was interrupted before a response was produced.');
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
function restoreLastSessionTurn(messages) {
|
|
234
|
+
let assistantIndex = -1;
|
|
235
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
236
|
+
if (messages[index].role === 'assistant' && messages[index].content?.trim()) {
|
|
237
|
+
assistantIndex = index;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (assistantIndex < 0) {
|
|
242
|
+
const question = [...messages].reverse().find((message) => message.role === 'user' && message.content?.trim())?.content ?? '';
|
|
243
|
+
return { question, answer: question ? 'This task was interrupted. Type “continue” to resume with the current model.' : '' };
|
|
244
|
+
}
|
|
245
|
+
let question = '';
|
|
246
|
+
for (let index = assistantIndex - 1; index >= 0; index -= 1) {
|
|
247
|
+
if (messages[index].role === 'user' && messages[index].content?.trim()) {
|
|
248
|
+
question = messages[index].content ?? '';
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { question, answer: messages[assistantIndex].content ?? '' };
|
|
199
253
|
}
|
|
200
254
|
export function isBareExitCommand(input) {
|
|
201
255
|
return input.trim().toLowerCase() === 'exit';
|
|
@@ -429,7 +483,8 @@ export function formatDirectWebSearchAnswer(result, modelConnected = false) {
|
|
|
429
483
|
.filter((line) => line && line !== '...' && line !== '---' && !SEARCH_BLOCK_METADATA.test(line) && !/^https?:\/\//i.test(line));
|
|
430
484
|
if (!lines.length)
|
|
431
485
|
return 'Web search returned no readable content.';
|
|
432
|
-
|
|
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.'}`;
|
|
433
488
|
}
|
|
434
489
|
/** Environment configuration overrides the persistent search provider preference. */
|
|
435
490
|
export function resolveWebSearchProvider(config) {
|
|
@@ -739,6 +794,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
739
794
|
modelId: currentModel,
|
|
740
795
|
mode: activeMode,
|
|
741
796
|
});
|
|
797
|
+
currentModel = args.model ?? (activeSession.providerId === activeConnection?.id ? activeSession.modelId : undefined) ?? currentModel;
|
|
742
798
|
if (activeSession.usage.available &&
|
|
743
799
|
!activeSession.usage.costKnown &&
|
|
744
800
|
activeSession.providerId === activeConnection?.id &&
|
|
@@ -779,9 +835,10 @@ export async function handleChatSession(args, version, signal) {
|
|
|
779
835
|
const router = new Router();
|
|
780
836
|
const loop = new AgentLoop();
|
|
781
837
|
let conversationHistory = [...activeSession.messages];
|
|
782
|
-
|
|
783
|
-
let
|
|
784
|
-
let
|
|
838
|
+
const restoredTurn = restoreLastSessionTurn(conversationHistory);
|
|
839
|
+
let lastQuestion = restoredTurn.question;
|
|
840
|
+
let lastAnswer = restoredTurn.answer;
|
|
841
|
+
let lastTurnDone = conversationHistory.length > 0 && !hasInterruptedTurn(conversationHistory);
|
|
785
842
|
let lastThoughtTime = 0;
|
|
786
843
|
let lastOutputTokenRate;
|
|
787
844
|
let lastTokenUsage;
|
|
@@ -797,10 +854,18 @@ export async function handleChatSession(args, version, signal) {
|
|
|
797
854
|
isFirst = false;
|
|
798
855
|
}
|
|
799
856
|
else {
|
|
857
|
+
let autoContinueRequested = false;
|
|
800
858
|
const turn = await promptInteractiveTurn({
|
|
859
|
+
getProviderName: () => getProviderStatusName(activeConnection),
|
|
801
860
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace, version,
|
|
802
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,
|
|
803
862
|
queuedCommands: commandQueue.items,
|
|
863
|
+
takeAutoContinuePrompt: () => {
|
|
864
|
+
if (!autoContinueRequested)
|
|
865
|
+
return undefined;
|
|
866
|
+
autoContinueRequested = false;
|
|
867
|
+
return CONTINUE_INTERRUPTED_TASK_PROMPT;
|
|
868
|
+
},
|
|
804
869
|
onExit: exitMessage,
|
|
805
870
|
questionHistory: conversationHistory.flatMap((message) => message.role === 'user' && message.content?.trim() ? [message.content] : []),
|
|
806
871
|
onModelSelect: async (drawFrame) => {
|
|
@@ -810,7 +875,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
810
875
|
}
|
|
811
876
|
if (activeConnection.kind === 'openai-compatible') {
|
|
812
877
|
const modelId = await selectCompatibleModelOverlay({
|
|
813
|
-
apiKey: activeConnection.id === 'moderado
|
|
878
|
+
apiKey: activeConnection.id === 'moderado' ? undefined : activeConnection.apiKey,
|
|
814
879
|
baseUrl: activeConnection.baseUrl,
|
|
815
880
|
providerId: activeConnection.id,
|
|
816
881
|
providerName: activeConnection.displayName,
|
|
@@ -823,15 +888,25 @@ export async function handleChatSession(args, version, signal) {
|
|
|
823
888
|
activeConnection = { ...activeConnection, defaultModel: modelId };
|
|
824
889
|
saveConnection(activeConnection);
|
|
825
890
|
config = loadConfig();
|
|
891
|
+
activeSession.providerId = activeConnection.id;
|
|
892
|
+
activeSession.providerName = activeConnection.displayName;
|
|
893
|
+
activeSession.modelId = modelId;
|
|
894
|
+
sessionStore.save(activeSession);
|
|
895
|
+
autoContinueRequested = hasInterruptedTurn(activeSession.messages);
|
|
826
896
|
}
|
|
827
897
|
return modelId;
|
|
828
898
|
}
|
|
829
|
-
const selection = await selectModelOverlay({ apiKey: activeConnection.id === 'moderado
|
|
830
|
-
if (selection.modelId) {
|
|
899
|
+
const selection = await selectModelOverlay({ apiKey: activeConnection.id === 'moderado' ? undefined : activeConnection.apiKey, currentModel, signal, saveSelectionByDefault: true, drawFrame });
|
|
900
|
+
if (selection.modelId && selection.modelId !== currentModel) {
|
|
831
901
|
currentModel = selection.modelId;
|
|
832
902
|
activeConnection = { ...activeConnection, defaultModel: selection.modelId };
|
|
833
903
|
saveConnection(activeConnection);
|
|
834
904
|
config = loadConfig();
|
|
905
|
+
activeSession.providerId = activeConnection.id;
|
|
906
|
+
activeSession.providerName = activeConnection.displayName;
|
|
907
|
+
activeSession.modelId = selection.modelId;
|
|
908
|
+
sessionStore.save(activeSession);
|
|
909
|
+
autoContinueRequested = hasInterruptedTurn(activeSession.messages);
|
|
835
910
|
}
|
|
836
911
|
return selection.modelId;
|
|
837
912
|
},
|
|
@@ -839,7 +914,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
839
914
|
const connection = await connectProviderInteractive({
|
|
840
915
|
signal,
|
|
841
916
|
drawFrame,
|
|
842
|
-
gatewayUrl: config.connections?.
|
|
917
|
+
gatewayUrl: config.connections?.moderado?.baseUrl,
|
|
843
918
|
activeConnection,
|
|
844
919
|
savedConnections: config.connections,
|
|
845
920
|
connectProviders: config.connectProviders,
|
|
@@ -961,15 +1036,17 @@ export async function handleChatSession(args, version, signal) {
|
|
|
961
1036
|
onSession: async (command, drawFrame) => {
|
|
962
1037
|
let action = command.slice('/session'.length).trim();
|
|
963
1038
|
if (!action) {
|
|
964
|
-
|
|
1039
|
+
const sessionActions = [
|
|
965
1040
|
{ label: 'New session', value: 'new', description: 'Start with an empty conversation.' },
|
|
966
1041
|
{ label: 'List or resume', value: 'resume', description: 'Load a saved session for this workspace.' },
|
|
1042
|
+
...(hasInterruptedTurn(activeSession.messages) ? [{ label: 'Continue interrupted task', value: 'continue', description: 'Continue the last stopped task with the current model.' }] : []),
|
|
967
1043
|
{ label: 'Export transcript', value: 'export', description: 'Write a redacted Markdown transcript in this workspace.' },
|
|
968
1044
|
{ label: 'Undo latest agent change', value: 'undo', description: 'Restore the last checkpoint when safe.' },
|
|
969
1045
|
{ label: 'Redo latest agent change', value: 'redo', description: 'Reapply the last safely undone checkpoint.' },
|
|
970
1046
|
{ label: 'Share transcript', value: 'share', description: 'Choose an explicit workspace output path.' },
|
|
971
1047
|
{ label: 'Compact history', value: 'compact', description: 'Reduce older messages without calling a model.' },
|
|
972
|
-
]
|
|
1048
|
+
];
|
|
1049
|
+
action = (await selectListPopup('Session', sessionActions, { drawFrame, signal })) ?? '';
|
|
973
1050
|
}
|
|
974
1051
|
if (action === 'new') {
|
|
975
1052
|
activeSession = createSession(canonicalWorkspace, { providerId: activeConnection?.id, providerName: activeConnection?.displayName, modelId: currentModel, mode: activeMode });
|
|
@@ -983,6 +1060,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
983
1060
|
sessionStore.save(activeSession);
|
|
984
1061
|
return;
|
|
985
1062
|
}
|
|
1063
|
+
if (action === 'continue') {
|
|
1064
|
+
if (!hasInterruptedTurn(activeSession.messages)) {
|
|
1065
|
+
drawFrame(renderBoxLines('Continue task', ['There is no interrupted task in this session.', '', 'Press Esc or Enter to return.'], 64));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
autoContinueRequested = true;
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
986
1071
|
if (action === 'list' || action === 'resume') {
|
|
987
1072
|
const sessions = sessionStore.listSessions(canonicalWorkspace);
|
|
988
1073
|
const id = await selectListPopup('Saved sessions', sessions.map((entry) => ({
|
|
@@ -995,8 +1080,12 @@ export async function handleChatSession(args, version, signal) {
|
|
|
995
1080
|
activeSession = loaded;
|
|
996
1081
|
conversationHistory = [...loaded.messages];
|
|
997
1082
|
sessionTokens = loaded.usage.totalTokens;
|
|
998
|
-
currentModel = loaded.modelId ?? currentModel;
|
|
1083
|
+
currentModel = loaded.providerId === activeConnection?.id ? loaded.modelId ?? currentModel : currentModel;
|
|
999
1084
|
activeMode = loaded.mode;
|
|
1085
|
+
const restored = restoreLastSessionTurn(conversationHistory);
|
|
1086
|
+
lastQuestion = restored.question;
|
|
1087
|
+
lastAnswer = restored.answer;
|
|
1088
|
+
lastTurnDone = conversationHistory.length > 0 && !hasInterruptedTurn(conversationHistory);
|
|
1000
1089
|
}
|
|
1001
1090
|
return;
|
|
1002
1091
|
}
|
|
@@ -1090,7 +1179,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1090
1179
|
lastTokenUsage = undefined;
|
|
1091
1180
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1092
1181
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1093
|
-
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(),
|
|
1094
1183
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1095
1184
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1096
1185
|
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
@@ -1106,7 +1195,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1106
1195
|
lastTokenUsage = undefined;
|
|
1107
1196
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1108
1197
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1109
|
-
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(),
|
|
1110
1199
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1111
1200
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1112
1201
|
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
@@ -1141,7 +1230,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1141
1230
|
}
|
|
1142
1231
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1143
1232
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1144
|
-
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(),
|
|
1145
1234
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1146
1235
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1147
1236
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1180,7 +1269,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1180
1269
|
lastTokenUsage = undefined;
|
|
1181
1270
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1182
1271
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1183
|
-
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(),
|
|
1184
1273
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1185
1274
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1186
1275
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1219,7 +1308,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1219
1308
|
lastOutputTokenRate = undefined;
|
|
1220
1309
|
lastTokenUsage = undefined;
|
|
1221
1310
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1222
|
-
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));
|
|
1223
1312
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1224
1313
|
continue;
|
|
1225
1314
|
}
|
|
@@ -1228,27 +1317,13 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1228
1317
|
}
|
|
1229
1318
|
if (trimmed === '/help') {
|
|
1230
1319
|
lastQuestion = trimmed;
|
|
1231
|
-
lastAnswer = 'Available slash commands:\n' +
|
|
1232
|
-
' /connect - Choose a Gateway, hosted, or local provider\n' +
|
|
1233
|
-
' /model - Switch active AI model\n' +
|
|
1234
|
-
' /init - Scaffold AGENTS.md from workspace scan\n' +
|
|
1235
|
-
' /mcp - Manage local MCP servers\n' +
|
|
1236
|
-
' /session - Create, resume, undo, redo, share, export, or compact sessions\n' +
|
|
1237
|
-
' /queue - Add, inspect, or clear queued follow-up commands\n' +
|
|
1238
|
-
' /workflow - Inspect Git, build plans, or undo agent changes\n' +
|
|
1239
|
-
' /review - Review current workspace changes for bugs\n' +
|
|
1240
|
-
' /map - Show a compact workspace file map\n' +
|
|
1241
|
-
' /budget - Set a per-task token limit (/budget 5000, /budget off)\n' +
|
|
1242
|
-
' /skills - List skills; use /skills on|off NAME|PATTERN (* for all)\n' +
|
|
1243
|
-
' /clear - Reset conversation memory\n' +
|
|
1244
|
-
' /help - Display commands, shortcuts & version\n' +
|
|
1245
|
-
' /exit - Exit Moderado';
|
|
1320
|
+
lastAnswer = 'Available slash commands:\n' + getHelpCommandLines().map((line) => line.includes(' - ') ? line : `\n${line}`).join('\n');
|
|
1246
1321
|
lastThoughtTime = 0.001;
|
|
1247
1322
|
lastOutputTokenRate = undefined;
|
|
1248
1323
|
lastTokenUsage = undefined;
|
|
1249
1324
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1250
1325
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1251
|
-
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(),
|
|
1252
1327
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1253
1328
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1254
1329
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1280,7 +1355,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1280
1355
|
sessionStore.save(activeSession);
|
|
1281
1356
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1282
1357
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1283
|
-
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(),
|
|
1284
1359
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1285
1360
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1286
1361
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1300,7 +1375,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1300
1375
|
sessionStore.save(activeSession);
|
|
1301
1376
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1302
1377
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1303
|
-
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(),
|
|
1304
1379
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1305
1380
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1306
1381
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1325,7 +1400,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1325
1400
|
lastTokenUsage = undefined;
|
|
1326
1401
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1327
1402
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1328
|
-
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1403
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1329
1404
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1330
1405
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1331
1406
|
}, process.stdout.rows));
|
|
@@ -1346,7 +1421,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1346
1421
|
sessionStore.save(activeSession);
|
|
1347
1422
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1348
1423
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1349
|
-
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1424
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1350
1425
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1351
1426
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1352
1427
|
chatThoughtTime: lastThoughtTime,
|
|
@@ -1362,13 +1437,13 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1362
1437
|
drawFrame: (popupLines) => {
|
|
1363
1438
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1364
1439
|
process.stdout.write(renderWelcomePopupLayer({
|
|
1365
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1440
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1366
1441
|
tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace,
|
|
1367
1442
|
mode: activeMode, autoApprove: activeAutoApprove,
|
|
1368
1443
|
chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1369
1444
|
}, popupLines, process.stdout.columns, process.stdout.rows));
|
|
1370
1445
|
},
|
|
1371
|
-
gatewayUrl: config.connections?.
|
|
1446
|
+
gatewayUrl: config.connections?.moderado?.baseUrl,
|
|
1372
1447
|
activeConnection,
|
|
1373
1448
|
savedConnections: config.connections,
|
|
1374
1449
|
connectProviders: config.connectProviders,
|
|
@@ -1399,14 +1474,26 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1399
1474
|
sessionTokens = activeSession.usage.totalTokens;
|
|
1400
1475
|
usageRecorded = true;
|
|
1401
1476
|
};
|
|
1477
|
+
let streamedAnswer = '';
|
|
1402
1478
|
try {
|
|
1403
1479
|
const requestAbort = new AbortController();
|
|
1404
1480
|
const requestSignal = signal ? AbortSignal.any([signal, requestAbort.signal]) : requestAbort.signal;
|
|
1405
|
-
let
|
|
1481
|
+
let lastSessionCheckpointAt = 0;
|
|
1482
|
+
const checkpointInterruptedTurn = () => {
|
|
1483
|
+
activeSession.messages = saveFailedTurnPrompt(conversationHistory, effectivePrompt, streamedAnswer);
|
|
1484
|
+
activeSession.providerId = activeConnection?.id;
|
|
1485
|
+
activeSession.providerName = activeConnection?.displayName;
|
|
1486
|
+
activeSession.modelId = currentModel;
|
|
1487
|
+
activeSession.mode = activeMode;
|
|
1488
|
+
sessionStore.save(activeSession);
|
|
1489
|
+
lastSessionCheckpointAt = Date.now();
|
|
1490
|
+
};
|
|
1406
1491
|
lastAnswer = '';
|
|
1407
1492
|
lastTurnDone = false;
|
|
1408
1493
|
let taskStep;
|
|
1409
|
-
let
|
|
1494
|
+
let activityLabel;
|
|
1495
|
+
let activityFrame = 0;
|
|
1496
|
+
let turnActive = true;
|
|
1410
1497
|
let outputTokenRate;
|
|
1411
1498
|
let budgetWarningIssued = false;
|
|
1412
1499
|
let budgetStopped = false;
|
|
@@ -1414,12 +1501,32 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1414
1501
|
const usedModels = new Set();
|
|
1415
1502
|
let answerPosition = { row: 15, column: getChatAnswerArea(process.stdout.columns).startColumn };
|
|
1416
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
|
+
};
|
|
1417
1524
|
const redrawChatFrame = () => {
|
|
1418
1525
|
const thoughtTime = Math.max(0.001, (Date.now() - startedAt) / 1000);
|
|
1419
1526
|
const displayAnswer = streamedAnswer || lastAnswer;
|
|
1420
1527
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1421
1528
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1422
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1529
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1423
1530
|
tokens: Math.round(sessionTokens + (usageRecorded ? 0 : turnUsage?.usage.totalTokens ?? 0)),
|
|
1424
1531
|
cost: costLabel(),
|
|
1425
1532
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
@@ -1441,6 +1548,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1441
1548
|
const area = getChatAnswerArea(process.stdout.columns);
|
|
1442
1549
|
answerPosition = renderChatAnswerDelta(displayAnswer, { row: 15, column: area.startColumn }, area.wrapWidth, area.startColumn);
|
|
1443
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');
|
|
1444
1554
|
};
|
|
1445
1555
|
const onGenerationKeypress = (str, key) => {
|
|
1446
1556
|
handleGenerationKeypress(str, key, commandQueue, {
|
|
@@ -1456,6 +1566,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1456
1566
|
},
|
|
1457
1567
|
onBtw: async (command) => {
|
|
1458
1568
|
const resume = suspendGenerationInput?.();
|
|
1569
|
+
stopActivity();
|
|
1459
1570
|
const ac = new AbortController();
|
|
1460
1571
|
const onBtwKey = (s, k) => {
|
|
1461
1572
|
if ((k && k.name === 'escape') || s === '\x1b') {
|
|
@@ -1467,7 +1578,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1467
1578
|
await executeBtwQuery(command, async (popupLines, frameOpts) => {
|
|
1468
1579
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1469
1580
|
process.stdout.write(renderWelcomePopupLayer({
|
|
1470
|
-
model: currentModel ?? 'No model connected',
|
|
1581
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected',
|
|
1471
1582
|
tokens: Math.round(sessionTokens),
|
|
1472
1583
|
cost: costLabel(),
|
|
1473
1584
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
@@ -1496,6 +1607,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1496
1607
|
}
|
|
1497
1608
|
finally {
|
|
1498
1609
|
process.stdin.removeListener('keypress', onBtwKey);
|
|
1610
|
+
startActivity();
|
|
1499
1611
|
redrawChatFrame();
|
|
1500
1612
|
resume?.();
|
|
1501
1613
|
}
|
|
@@ -1503,6 +1615,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1503
1615
|
});
|
|
1504
1616
|
};
|
|
1505
1617
|
if (process.stdin.isTTY) {
|
|
1618
|
+
const onGenerationResize = () => redrawChatFrame();
|
|
1619
|
+
process.stdout.on('resize', onGenerationResize);
|
|
1506
1620
|
const attachGenerationListener = () => {
|
|
1507
1621
|
readline.emitKeypressEvents(process.stdin);
|
|
1508
1622
|
process.stdin.resume();
|
|
@@ -1522,16 +1636,13 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1522
1636
|
};
|
|
1523
1637
|
removeGenerationListener = () => {
|
|
1524
1638
|
process.stdin.removeListener('keypress', onGenerationKeypress);
|
|
1639
|
+
process.stdout.removeListener('resize', onGenerationResize);
|
|
1525
1640
|
suspendGenerationInput = undefined;
|
|
1526
1641
|
};
|
|
1527
1642
|
}
|
|
1528
1643
|
// Move to the chat layout before the provider can emit its first event.
|
|
1529
1644
|
redrawChatFrame();
|
|
1530
|
-
|
|
1531
|
-
if (!firstAssistantDeltaAt && !turnUsage) {
|
|
1532
|
-
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, taskStep, process.stdout.columns));
|
|
1533
|
-
}
|
|
1534
|
-
}, 400);
|
|
1645
|
+
startActivity();
|
|
1535
1646
|
const evidenceTask = liveSearch ? buildSearchAnswerTask(effectivePrompt, liveSearch) : undefined;
|
|
1536
1647
|
const runAgent = () => loop.run(isReview ? createReviewTask(reviewFiles) : evidenceTask ?? createAgentTask(effectivePrompt, activeMode), {
|
|
1537
1648
|
workspaceRoot: canonicalWorkspace, provider: provider, tools: isReview ? createReviewToolRegistry() : tools, approvalHandler, router, policy,
|
|
@@ -1543,9 +1654,21 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1543
1654
|
usedModels.add(event.newModelId);
|
|
1544
1655
|
if (event.type === 'progress') {
|
|
1545
1656
|
taskStep = { step: event.step, maxSteps: event.maxSteps ?? 0 };
|
|
1546
|
-
|
|
1547
|
-
|
|
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);
|
|
1548
1663
|
}
|
|
1664
|
+
if (event.type === 'tool_result') {
|
|
1665
|
+
activityLabel = undefined;
|
|
1666
|
+
if (!thinkingTimer)
|
|
1667
|
+
startActivity();
|
|
1668
|
+
}
|
|
1669
|
+
// The approval prompt owns the screen until the tool result arrives.
|
|
1670
|
+
if (event.type === 'approval_request')
|
|
1671
|
+
stopActivity();
|
|
1549
1672
|
if (event.type === 'gateway_status')
|
|
1550
1673
|
process.stdout.write(renderGatewayFallbackUpdate(event, process.stdout.columns || 80));
|
|
1551
1674
|
if (event.type === 'usage') {
|
|
@@ -1566,12 +1689,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1566
1689
|
}
|
|
1567
1690
|
}
|
|
1568
1691
|
if (event.type === 'assistant_delta') {
|
|
1569
|
-
if (!firstAssistantDeltaAt && event.delta.trim().length > 0) {
|
|
1570
|
-
firstAssistantDeltaAt = Date.now();
|
|
1571
|
-
if (thinkingTimer)
|
|
1572
|
-
clearInterval(thinkingTimer);
|
|
1573
|
-
}
|
|
1574
1692
|
streamedAnswer += event.delta;
|
|
1693
|
+
if (Date.now() - lastSessionCheckpointAt >= 1_000)
|
|
1694
|
+
checkpointInterruptedTurn();
|
|
1575
1695
|
const area = getChatAnswerArea(process.stdout.columns);
|
|
1576
1696
|
const renderedDelta = renderChatAnswerDelta(event.delta, answerPosition, area.wrapWidth, area.startColumn);
|
|
1577
1697
|
answerPosition = renderedDelta;
|
|
@@ -1593,27 +1713,24 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1593
1713
|
checkpoints.recordPostWrite(canonicalWorkspace, paths);
|
|
1594
1714
|
},
|
|
1595
1715
|
});
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
const cloudOAuth = isModeradoCloudOAuthConnection(activeConnection);
|
|
1606
|
-
const cloudWithoutKey = activeConnection.id === 'moderado-cloud' && !activeConnection.apiKey;
|
|
1607
|
-
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
|
|
1608
1725
|
? '\nThis free Gateway route requires a Moderado website API key. Sign in or enter a key to retry once.\n'
|
|
1609
|
-
:
|
|
1610
|
-
? '\nModerado
|
|
1611
|
-
: `\nThe saved ${
|
|
1612
|
-
const replacement =
|
|
1613
|
-
? await
|
|
1614
|
-
: 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 });
|
|
1615
1732
|
if (!replacement)
|
|
1616
|
-
|
|
1733
|
+
return undefined;
|
|
1617
1734
|
recordTurnUsage();
|
|
1618
1735
|
turnUsage = undefined;
|
|
1619
1736
|
usageRecorded = false;
|
|
@@ -1621,8 +1738,37 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1621
1738
|
saveConnection(secured);
|
|
1622
1739
|
config = loadConfig();
|
|
1623
1740
|
activateConnection(secured);
|
|
1741
|
+
startActivity();
|
|
1742
|
+
return secured;
|
|
1743
|
+
};
|
|
1744
|
+
let result;
|
|
1745
|
+
try {
|
|
1746
|
+
checkpointInterruptedTurn();
|
|
1624
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
|
+
}
|
|
1625
1756
|
}
|
|
1757
|
+
catch (error) {
|
|
1758
|
+
if (!activeConnection || !isAuthenticationFailure(error))
|
|
1759
|
+
throw error;
|
|
1760
|
+
if (!(await recoverAuthenticationFailure()))
|
|
1761
|
+
throw error;
|
|
1762
|
+
result = await runAgent();
|
|
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
|
+
}));
|
|
1626
1772
|
if (turnUsage) {
|
|
1627
1773
|
let pricing;
|
|
1628
1774
|
const modelIds = new Set([...usedModels, result.selectedModel.id]);
|
|
@@ -1670,7 +1816,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1670
1816
|
streamedAnswer = lastAnswer;
|
|
1671
1817
|
}
|
|
1672
1818
|
else if (resolution.isError) {
|
|
1673
|
-
conversationHistory = saveFailedTurnPrompt(conversationHistory, effectivePrompt);
|
|
1819
|
+
conversationHistory = saveFailedTurnPrompt(conversationHistory, effectivePrompt, streamedAnswer);
|
|
1674
1820
|
activeSession.messages = conversationHistory;
|
|
1675
1821
|
activeSession.providerId = activeConnection?.id;
|
|
1676
1822
|
activeSession.providerName = activeConnection?.displayName;
|
|
@@ -1694,14 +1840,16 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1694
1840
|
lastQuestion = trimmed;
|
|
1695
1841
|
lastThoughtTime = Math.max(0.001, (Date.now() - startedAt) / 1000);
|
|
1696
1842
|
lastOutputTokenRate = outputTokenRate;
|
|
1697
|
-
|
|
1698
|
-
|
|
1843
|
+
turnActive = false;
|
|
1844
|
+
stopActivity();
|
|
1699
1845
|
removeGenerationListener?.();
|
|
1700
1846
|
redrawChatFrame();
|
|
1701
1847
|
}
|
|
1702
1848
|
catch (err) {
|
|
1703
|
-
if (thinkingTimer)
|
|
1849
|
+
if (thinkingTimer) {
|
|
1704
1850
|
clearInterval(thinkingTimer);
|
|
1851
|
+
thinkingTimer = undefined;
|
|
1852
|
+
}
|
|
1705
1853
|
removeGenerationListener?.();
|
|
1706
1854
|
recordTurnUsage();
|
|
1707
1855
|
if (turnUsage)
|
|
@@ -1711,7 +1859,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1711
1859
|
? ` (other configured providers: ${otherConnections.join(', ')})`
|
|
1712
1860
|
: '';
|
|
1713
1861
|
lastQuestion = trimmed;
|
|
1714
|
-
conversationHistory = saveFailedTurnPrompt(conversationHistory, effectivePrompt);
|
|
1862
|
+
conversationHistory = saveFailedTurnPrompt(conversationHistory, effectivePrompt, streamedAnswer);
|
|
1715
1863
|
activeSession.messages = conversationHistory;
|
|
1716
1864
|
activeSession.providerId = activeConnection?.id;
|
|
1717
1865
|
activeSession.providerName = activeConnection?.displayName;
|
|
@@ -1727,7 +1875,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1727
1875
|
lastOutputTokenRate = undefined;
|
|
1728
1876
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1729
1877
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1730
|
-
model: currentModel ?? 'No model connected — use /connect',
|
|
1878
|
+
provider: getProviderStatusName(activeConnection), model: currentModel ?? 'No model connected — use /connect',
|
|
1731
1879
|
tokens: Math.round(sessionTokens),
|
|
1732
1880
|
cost: costLabel(),
|
|
1733
1881
|
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|