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.
Files changed (73) hide show
  1. package/README.md +82 -66
  2. package/dist/args.d.ts.map +1 -1
  3. package/dist/args.js +6 -2
  4. package/dist/args.js.map +1 -1
  5. package/dist/commands/chat.d.ts +7 -1
  6. package/dist/commands/chat.d.ts.map +1 -1
  7. package/dist/commands/chat.js +230 -82
  8. package/dist/commands/chat.js.map +1 -1
  9. package/dist/commands/models.d.ts.map +1 -1
  10. package/dist/commands/models.js +14 -12
  11. package/dist/commands/models.js.map +1 -1
  12. package/dist/commands/run.d.ts +11 -0
  13. package/dist/commands/run.d.ts.map +1 -1
  14. package/dist/commands/run.js +25 -8
  15. package/dist/commands/run.js.map +1 -1
  16. package/dist/config.d.ts +2 -1
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +50 -19
  19. package/dist/config.js.map +1 -1
  20. package/dist/sessions.d.ts.map +1 -1
  21. package/dist/sessions.js +6 -1
  22. package/dist/sessions.js.map +1 -1
  23. package/dist/ui/box.d.ts +6 -0
  24. package/dist/ui/box.d.ts.map +1 -1
  25. package/dist/ui/box.js +33 -1
  26. package/dist/ui/box.js.map +1 -1
  27. package/dist/ui/file_mentions.js +18 -9
  28. package/dist/ui/file_mentions.js.map +1 -1
  29. package/dist/ui/help_content.d.ts +17 -0
  30. package/dist/ui/help_content.d.ts.map +1 -0
  31. package/dist/ui/help_content.js +13 -0
  32. package/dist/ui/help_content.js.map +1 -0
  33. package/dist/ui/help_modal.d.ts.map +1 -1
  34. package/dist/ui/help_modal.js +5 -5
  35. package/dist/ui/help_modal.js.map +1 -1
  36. package/dist/ui/model_selector.d.ts.map +1 -1
  37. package/dist/ui/model_selector.js +6 -4
  38. package/dist/ui/model_selector.js.map +1 -1
  39. package/dist/ui/popup.d.ts.map +1 -1
  40. package/dist/ui/popup.js +4 -3
  41. package/dist/ui/popup.js.map +1 -1
  42. package/dist/ui/provider_connect.d.ts +13 -4
  43. package/dist/ui/provider_connect.d.ts.map +1 -1
  44. package/dist/ui/provider_connect.js +37 -29
  45. package/dist/ui/provider_connect.js.map +1 -1
  46. package/dist/ui/welcome.d.ts +16 -2
  47. package/dist/ui/welcome.d.ts.map +1 -1
  48. package/dist/ui/welcome.js +63 -33
  49. package/dist/ui/welcome.js.map +1 -1
  50. package/dist/vendor/contracts/provider.js +8 -8
  51. package/dist/vendor/contracts/provider.js.map +1 -1
  52. package/dist/vendor/core/agent.d.ts.map +1 -1
  53. package/dist/vendor/core/agent.js +6 -3
  54. package/dist/vendor/core/agent.js.map +1 -1
  55. package/dist/vendor/providers/model_discovery.js +1 -1
  56. package/dist/vendor/providers/model_discovery.js.map +1 -1
  57. package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts +1 -1
  58. package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts.map +1 -1
  59. package/dist/vendor/providers/nvidia/nvidia_adapter.js +4 -4
  60. package/dist/vendor/providers/nvidia/nvidia_adapter.js.map +1 -1
  61. package/dist/vendor/providers/nvidia/sse_parser.d.ts +1 -0
  62. package/dist/vendor/providers/nvidia/sse_parser.d.ts.map +1 -1
  63. package/dist/vendor/providers/nvidia/sse_parser.js +2 -1
  64. package/dist/vendor/providers/nvidia/sse_parser.js.map +1 -1
  65. package/dist/vendor/providers/presets.d.ts +12 -4
  66. package/dist/vendor/providers/presets.d.ts.map +1 -1
  67. package/dist/vendor/providers/presets.js +24 -15
  68. package/dist/vendor/providers/presets.js.map +1 -1
  69. package/package.json +3 -2
  70. package/dist/commands/interactive_menu.d.ts +0 -3
  71. package/dist/commands/interactive_menu.d.ts.map +0 -1
  72. package/dist/commands/interactive_menu.js +0 -98
  73. package/dist/commands/interactive_menu.js.map +0 -1
@@ -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, isModeradoCloudOAuthConnection, loginModeradoCloudInteractive, replaceProviderKeyInteractive } from '../ui/provider_connect.js';
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
- return [...history, { role: 'user', content: prompt }];
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
- return `${lines.slice(0, 10).join('\n')}\n\n(${modelConnected ? 'model summary unavailable — ' : 'no model connected — '}raw search excerpt)`;
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
- let lastQuestion = '';
783
- let lastAnswer = '';
784
- let lastTurnDone = false;
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-cloud' ? undefined : activeConnection.apiKey,
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-cloud' ? undefined : activeConnection.apiKey, currentModel, signal, saveSelectionByDefault: true, drawFrame });
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?.['moderado-cloud']?.baseUrl,
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
- action = (await selectListPopup('Session', [
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
- ], { drawFrame, signal })) ?? '';
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?.['moderado-cloud']?.baseUrl,
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 streamedAnswer = '';
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 firstAssistantDeltaAt;
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
- thinkingTimer = setInterval(() => {
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
- if (!firstAssistantDeltaAt)
1547
- process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000, taskStep, process.stdout.columns));
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
- let result;
1597
- try {
1598
- result = await runAgent();
1599
- }
1600
- catch (error) {
1601
- if (!activeConnection || !isAuthenticationFailure(error))
1602
- throw error;
1603
- if (thinkingTimer)
1604
- clearInterval(thinkingTimer);
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
- : cloudOAuth
1610
- ? '\nModerado Cloud rejected this browser login. Sign in again to retry once.\n'
1611
- : `\nThe saved ${activeConnection.displayName} API key was rejected. Enter a replacement key to retry once.\n`);
1612
- const replacement = cloudOAuth || cloudWithoutKey
1613
- ? await loginModeradoCloudInteractive({ signal, gatewayUrl: activeConnection.baseUrl })
1614
- : await replaceProviderKeyInteractive(activeConnection, { signal });
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
- throw error;
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
- if (thinkingTimer)
1698
- clearInterval(thinkingTimer);
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,