moderado 0.3.4 → 0.3.8
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 +215 -215
- package/dist/args.js +29 -29
- package/dist/budget.d.ts +6 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +25 -0
- package/dist/budget.js.map +1 -0
- package/dist/chat_mode.d.ts +14 -0
- package/dist/chat_mode.d.ts.map +1 -0
- package/dist/chat_mode.js +13 -0
- package/dist/chat_mode.js.map +1 -0
- package/dist/commands/chat.d.ts +5 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +172 -43
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/host.d.ts +4 -0
- package/dist/commands/host.d.ts.map +1 -0
- package/dist/commands/host.js +30 -0
- package/dist/commands/host.js.map +1 -0
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +63 -16
- package/dist/commands/models.js.map +1 -1
- package/dist/config.d.ts +73 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +116 -0
- package/dist/config.js.map +1 -1
- package/dist/host/approval_queue.d.ts +9 -0
- package/dist/host/approval_queue.d.ts.map +1 -0
- package/dist/host/approval_queue.js +63 -0
- package/dist/host/approval_queue.js.map +1 -0
- package/dist/host/auto_approve.d.ts +22 -0
- package/dist/host/auto_approve.d.ts.map +1 -0
- package/dist/host/auto_approve.js +36 -0
- package/dist/host/auto_approve.js.map +1 -0
- package/dist/host/ndjson.d.ts +3 -0
- package/dist/host/ndjson.d.ts.map +1 -0
- package/dist/host/ndjson.js +68 -0
- package/dist/host/ndjson.js.map +1 -0
- package/dist/host/protocol_server.d.ts +9 -0
- package/dist/host/protocol_server.d.ts.map +1 -0
- package/dist/host/protocol_server.js +101 -0
- package/dist/host/protocol_server.js.map +1 -0
- package/dist/host/runtime.d.ts +30 -0
- package/dist/host/runtime.d.ts.map +1 -0
- package/dist/host/runtime.js +603 -0
- package/dist/host/runtime.js.map +1 -0
- package/dist/model_pricing.d.ts +14 -1
- package/dist/model_pricing.d.ts.map +1 -1
- package/dist/model_pricing.js +52 -0
- package/dist/model_pricing.js.map +1 -1
- package/dist/repo_map.d.ts +3 -0
- package/dist/repo_map.d.ts.map +1 -0
- package/dist/repo_map.js +29 -0
- package/dist/repo_map.js.map +1 -0
- package/dist/sessions.d.ts +22 -15
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +16 -1
- package/dist/sessions.js.map +1 -1
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +2 -2
- package/dist/ui/model_selector.d.ts +2 -2
- package/dist/ui/model_selector.d.ts.map +1 -1
- package/dist/ui/model_selector.js +25 -16
- package/dist/ui/model_selector.js.map +1 -1
- package/dist/ui/provider_connect.d.ts +6 -3
- package/dist/ui/provider_connect.d.ts.map +1 -1
- package/dist/ui/provider_connect.js +41 -14
- package/dist/ui/provider_connect.js.map +1 -1
- package/dist/ui/renderer.d.ts +4 -0
- package/dist/ui/renderer.d.ts.map +1 -1
- package/dist/ui/renderer.js +36 -0
- package/dist/ui/renderer.js.map +1 -1
- package/dist/ui/session_screen.d.ts +32 -0
- package/dist/ui/session_screen.d.ts.map +1 -0
- package/dist/ui/session_screen.js +148 -0
- package/dist/ui/session_screen.js.map +1 -0
- package/dist/ui/session_terminal.d.ts +46 -0
- package/dist/ui/session_terminal.d.ts.map +1 -0
- package/dist/ui/session_terminal.js +342 -0
- package/dist/ui/session_terminal.js.map +1 -0
- package/dist/ui/welcome.d.ts +11 -0
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +49 -32
- package/dist/ui/welcome.js.map +1 -1
- package/dist/vendor/contracts/events.d.ts +159 -0
- package/dist/vendor/contracts/events.d.ts.map +1 -1
- package/dist/vendor/contracts/events.js +12 -0
- package/dist/vendor/contracts/events.js.map +1 -1
- package/dist/vendor/contracts/host_protocol.d.ts +3545 -0
- package/dist/vendor/contracts/host_protocol.d.ts.map +1 -0
- package/dist/vendor/contracts/host_protocol.js +292 -0
- package/dist/vendor/contracts/host_protocol.js.map +1 -0
- package/dist/vendor/contracts/provider.d.ts +1 -0
- package/dist/vendor/contracts/provider.d.ts.map +1 -1
- package/dist/vendor/contracts/provider.js +6 -0
- package/dist/vendor/contracts/provider.js.map +1 -1
- package/dist/vendor/core/agent.d.ts +2 -1
- package/dist/vendor/core/agent.d.ts.map +1 -1
- package/dist/vendor/core/agent.js +145 -31
- package/dist/vendor/core/agent.js.map +1 -1
- package/dist/vendor/providers/model_discovery.d.ts +2 -2
- package/dist/vendor/providers/model_discovery.d.ts.map +1 -1
- package/dist/vendor/providers/model_discovery.js +4 -2
- package/dist/vendor/providers/model_discovery.js.map +1 -1
- package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts.map +1 -1
- package/dist/vendor/providers/nvidia/nvidia_adapter.js +1 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.js.map +1 -1
- package/dist/vendor/providers/nvidia/sse_parser.d.ts +2 -1
- package/dist/vendor/providers/nvidia/sse_parser.d.ts.map +1 -1
- package/dist/vendor/providers/nvidia/sse_parser.js +30 -16
- package/dist/vendor/providers/nvidia/sse_parser.js.map +1 -1
- package/dist/web_search_route.d.ts +16 -0
- package/dist/web_search_route.d.ts.map +1 -0
- package/dist/web_search_route.js +60 -0
- package/dist/web_search_route.js.map +1 -0
- package/package.json +28 -28
package/dist/commands/chat.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import readline from 'node:readline';
|
|
3
4
|
import { AgentLoop, PolicyManager, Router, cleanConversationalFiller, DEFAULT_MAX_OUTPUT_TOKENS } from '../vendor/core/index.js';
|
|
4
5
|
import { NvidiaAdapter } from '../vendor/providers/index.js';
|
|
5
|
-
import { createDefaultToolRegistry, canonicalizeRoot, createMcpTools, createWebSearchTool, discoverMcpServers, resolveInJail, WorkspaceCheckpointStore, WriteFileTool } from '../vendor/tools/index.js';
|
|
6
|
+
import { createDefaultToolRegistry, ToolRegistry, canonicalizeRoot, createMcpTools, createWebSearchTool, discoverMcpServers, resolveInJail, WorkspaceCheckpointStore, WriteFileTool } from '../vendor/tools/index.js';
|
|
6
7
|
import { McpServerConfigSchema } from '../vendor/contracts/index.js';
|
|
7
8
|
import { getActiveConnection, loadConfig, removeMcpServer, resolveApiKey, resolveConnectionCredential, saveConnection, saveMcpServer, setMcpServerEnabled, storeConnectionCredential } from '../config.js';
|
|
8
9
|
import { MemoryCredentialStore } from '../credentials.js';
|
|
@@ -13,13 +14,29 @@ import { connectProviderInteractive, isAuthenticationFailure, replaceProviderKey
|
|
|
13
14
|
import { initWorkspace } from './init.js';
|
|
14
15
|
import { expandMentions, createWorkspaceFileSource } from '../ui/file_mentions.js';
|
|
15
16
|
import { listFiles } from '../vendor/tools/index.js';
|
|
16
|
-
import { exitCleanly, promptInteractiveTurn, renderChatAnswerDelta, renderChatComposerCursor, renderChatThoughtTimeUpdate, renderFullWelcomeScreen, renderWelcomePopupLayer, terminalCleanExitDone, wrapText } from '../ui/welcome.js';
|
|
17
|
-
import {
|
|
17
|
+
import { exitCleanly, promptInteractiveTurn, renderChatAnswerDelta, renderChatComposerCursor, renderChatThoughtTimeUpdate, renderChatUsageUpdate, renderFullWelcomeScreen, renderWelcomePopupLayer, terminalCleanExitDone, wrapText } from '../ui/welcome.js';
|
|
18
|
+
import { accumulateSessionUsage, compactSessionMessages, createSession, exportSessionMarkdown, formatSessionCost, SessionStore } from '../sessions.js';
|
|
18
19
|
import { layerPromptBox, renderBoxLines, selectConfirmPopup, selectListPopup } from '../ui/popup.js';
|
|
19
20
|
import { askModalChoice } from '../ui/prompt.js';
|
|
20
21
|
import { findModelPricing } from '../model_pricing.js';
|
|
21
22
|
import { inspectGitWorkspace, readGitDiff } from '../vendor/tools/index.js';
|
|
23
|
+
import { buildWorkspaceMap } from '../repo_map.js';
|
|
24
|
+
import { evaluateTokenBudget, parseBudgetCommand } from '../budget.js';
|
|
22
25
|
import { discoverSkills, formatSkillContext } from '../skills.js';
|
|
26
|
+
export function formatSessionExitSummary(session, modelId) {
|
|
27
|
+
const usage = session.usage;
|
|
28
|
+
const tokens = usage.available
|
|
29
|
+
? `Input ${usage.promptTokens.toLocaleString('en-US')} | Output ${usage.completionTokens.toLocaleString('en-US')} | Total ${usage.totalTokens.toLocaleString('en-US')}${usage.estimated ? ' (estimated)' : ''}`
|
|
30
|
+
: 'Usage unavailable';
|
|
31
|
+
return [
|
|
32
|
+
'\x1b[1;38;5;25mSession Summary\x1b[0m',
|
|
33
|
+
`ID: ${session.id}`,
|
|
34
|
+
`Model: ${modelId ?? session.modelId ?? 'No model selected'}`,
|
|
35
|
+
`Tokens: ${tokens}`,
|
|
36
|
+
`Cost: ${formatSessionCost(usage)}`,
|
|
37
|
+
'Resume: /session resume',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
23
40
|
function mutationPaths(toolName, parameters) {
|
|
24
41
|
const value = parameters;
|
|
25
42
|
if (toolName === 'apply_patch' && Array.isArray(value.edits)) {
|
|
@@ -32,6 +49,19 @@ export function createAgentTask(request, mode) {
|
|
|
32
49
|
return request;
|
|
33
50
|
return `Create a concise implementation checklist for this request. Inspect files as needed, but do not modify files or run commands.\n\nUser request:\n${request}`;
|
|
34
51
|
}
|
|
52
|
+
export function createReviewTask(changedFiles) {
|
|
53
|
+
return `Review the current workspace changes. Inspect staged and unstaged Git diffs with git_diff, then inspect relevant surrounding code. Also review untracked files listed below with read_file. Do not modify files or run commands. Report only actionable bugs or security issues, ordered by severity, with file paths and line numbers. If none are found, say "No findings". Keep the report concise.\n\nChanged files:\n${changedFiles.join('\n') || '(none)'}`;
|
|
54
|
+
}
|
|
55
|
+
export function createReviewToolRegistry() {
|
|
56
|
+
const source = createDefaultToolRegistry();
|
|
57
|
+
const registry = new ToolRegistry();
|
|
58
|
+
for (const name of ['git_diff', 'read_file', 'list_files', 'search_files', 'get_definition', 'find_references']) {
|
|
59
|
+
const tool = source.get(name);
|
|
60
|
+
if (tool)
|
|
61
|
+
registry.register(tool);
|
|
62
|
+
}
|
|
63
|
+
return registry;
|
|
64
|
+
}
|
|
35
65
|
export const STANDARD_SLASH_COMMANDS = [
|
|
36
66
|
'/connect',
|
|
37
67
|
'/model',
|
|
@@ -41,6 +71,9 @@ export const STANDARD_SLASH_COMMANDS = [
|
|
|
41
71
|
'/session',
|
|
42
72
|
'/queue',
|
|
43
73
|
'/workflow',
|
|
74
|
+
'/review',
|
|
75
|
+
'/map',
|
|
76
|
+
'/budget',
|
|
44
77
|
'/skills',
|
|
45
78
|
'/clear',
|
|
46
79
|
'/help',
|
|
@@ -139,6 +172,10 @@ export function formatTurnFailureAnswer(status, errorEvent, modelId, otherConnec
|
|
|
139
172
|
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 switch providers with \`/connect\`${altSuggestion}.`;
|
|
140
173
|
}
|
|
141
174
|
export function resolveTurnAssistantAnswer(result, previousHistoryLength, lastErrorEvent, streamedAnswer = '', currentModel = 'unknown', otherConnections = []) {
|
|
175
|
+
if (result.status === 'cancelled') {
|
|
176
|
+
const partial = cleanConversationalFiller(streamedAnswer);
|
|
177
|
+
return { answer: [partial, 'Task stopped.'].filter(Boolean).join('\n\n'), isError: true };
|
|
178
|
+
}
|
|
142
179
|
if (result.status === 'failed' || lastErrorEvent) {
|
|
143
180
|
return {
|
|
144
181
|
answer: formatTurnFailureAnswer(result.status, lastErrorEvent, result.selectedModel?.id || currentModel, otherConnections),
|
|
@@ -216,15 +253,9 @@ export function handleGenerationKeypress(str, key, queue, actions) {
|
|
|
216
253
|
actions.abort();
|
|
217
254
|
return;
|
|
218
255
|
}
|
|
219
|
-
// 2. Escape
|
|
256
|
+
// 2. Escape always stops the active task; the caller clears queued input.
|
|
220
257
|
if (isGenerationCancelKey(key, str)) {
|
|
221
|
-
|
|
222
|
-
queue.setDraft('');
|
|
223
|
-
actions.onDraftChange();
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
actions.abort();
|
|
227
|
-
}
|
|
258
|
+
actions.abort();
|
|
228
259
|
return;
|
|
229
260
|
}
|
|
230
261
|
// 3. Return / Enter: commit draft or execute /btw immediately
|
|
@@ -422,9 +453,6 @@ export async function decideApproval(request, options, signal) {
|
|
|
422
453
|
if (options.autoApprove && !request.toolName.startsWith('mcp.')) {
|
|
423
454
|
return { requestId: request.requestId, status: 'approved' };
|
|
424
455
|
}
|
|
425
|
-
if (['write_file', 'edit_file', 'apply_patch', 'run_command', 'run_diagnostics'].includes(request.toolName)) {
|
|
426
|
-
return { requestId: request.requestId, status: 'approved' };
|
|
427
|
-
}
|
|
428
456
|
return options.requestInteractiveApproval(request, signal);
|
|
429
457
|
}
|
|
430
458
|
export async function createMcpToolRegistry(servers, webSearch = {}) {
|
|
@@ -697,7 +725,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
697
725
|
if (activeConnection)
|
|
698
726
|
activateConnection(activeConnection, args.model);
|
|
699
727
|
let activeMode = args.readOnly ? 'Plan' : 'Execute';
|
|
700
|
-
let activeAutoApprove =
|
|
728
|
+
let activeAutoApprove = !args.nonInteractive;
|
|
729
|
+
let tokenBudget;
|
|
701
730
|
const sessionStore = new SessionStore();
|
|
702
731
|
let activeSession = sessionStore.loadLatestSession(canonicalWorkspace) ??
|
|
703
732
|
createSession(canonicalWorkspace, {
|
|
@@ -742,8 +771,10 @@ export async function handleChatSession(args, version, signal) {
|
|
|
742
771
|
let lastAnswer = '';
|
|
743
772
|
let lastThoughtTime = 0;
|
|
744
773
|
let lastOutputTokenRate;
|
|
774
|
+
let lastTokenUsage;
|
|
745
775
|
let activePlan;
|
|
746
776
|
const costLabel = () => formatSessionCost(activeSession.usage);
|
|
777
|
+
const exitMessage = () => `\x1b[32mGoodbye! Stay Tuned with Moderado!\x1b[0m\n\n${formatSessionExitSummary(activeSession, currentModel)}`;
|
|
747
778
|
const commandQueue = new TurnCommandQueue();
|
|
748
779
|
const btwHistory = [];
|
|
749
780
|
while (!signal?.aborted) {
|
|
@@ -755,8 +786,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
755
786
|
else {
|
|
756
787
|
const turn = await promptInteractiveTurn({
|
|
757
788
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace, version,
|
|
758
|
-
usageAvailable: activeSession.usage.available, initialMode: activeMode, initialAutoApprove: activeAutoApprove, isFirstTurn: isFirst, signal, chatQuestion: lastQuestion || undefined, chatAnswer: lastAnswer || undefined, chatThoughtTime: lastThoughtTime, outputTokenRate: lastOutputTokenRate,
|
|
789
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, initialMode: activeMode, initialAutoApprove: activeAutoApprove, isFirstTurn: isFirst, signal, chatQuestion: lastQuestion || undefined, chatAnswer: lastAnswer || undefined, chatThoughtTime: lastThoughtTime, outputTokenRate: lastOutputTokenRate, tokenUsage: lastTokenUsage,
|
|
759
790
|
queuedCommands: commandQueue.items,
|
|
791
|
+
onExit: exitMessage,
|
|
760
792
|
questionHistory: conversationHistory.flatMap((message) => message.role === 'user' && message.content?.trim() ? [message.content] : []),
|
|
761
793
|
onModelSelect: async (drawFrame) => {
|
|
762
794
|
if (!activeConnection) {
|
|
@@ -770,7 +802,6 @@ export async function handleChatSession(args, version, signal) {
|
|
|
770
802
|
providerId: activeConnection.id,
|
|
771
803
|
providerName: activeConnection.displayName,
|
|
772
804
|
currentModel,
|
|
773
|
-
allModelsFree: activeConnection.id === 'agnes-ai',
|
|
774
805
|
signal,
|
|
775
806
|
drawFrame,
|
|
776
807
|
});
|
|
@@ -792,7 +823,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
792
823
|
return selection.modelId;
|
|
793
824
|
},
|
|
794
825
|
onConnect: async (drawFrame) => {
|
|
795
|
-
const connection = await connectProviderInteractive({ signal, drawFrame, savedConnections: config.connections, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
826
|
+
const connection = await connectProviderInteractive({ signal, drawFrame, savedConnections: config.connections, connectProviders: config.connectProviders, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
796
827
|
if (!connection)
|
|
797
828
|
return currentModel;
|
|
798
829
|
const runtimeConnection = process.platform === 'win32' ? await storeConnectionCredential(connection, credentialStore) : connection;
|
|
@@ -808,6 +839,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
808
839
|
lastAnswer = '';
|
|
809
840
|
lastThoughtTime = 0;
|
|
810
841
|
lastOutputTokenRate = undefined;
|
|
842
|
+
lastTokenUsage = undefined;
|
|
811
843
|
activeSession.messages = [];
|
|
812
844
|
sessionStore.save(activeSession);
|
|
813
845
|
},
|
|
@@ -926,6 +958,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
926
958
|
lastAnswer = '';
|
|
927
959
|
lastThoughtTime = 0;
|
|
928
960
|
lastOutputTokenRate = undefined;
|
|
961
|
+
lastTokenUsage = undefined;
|
|
929
962
|
sessionStore.save(activeSession);
|
|
930
963
|
return;
|
|
931
964
|
}
|
|
@@ -1026,11 +1059,46 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1026
1059
|
continue;
|
|
1027
1060
|
}
|
|
1028
1061
|
}
|
|
1062
|
+
if (trimmed === '/budget' || trimmed.startsWith('/budget ')) {
|
|
1063
|
+
const budget = parseBudgetCommand(trimmed, tokenBudget);
|
|
1064
|
+
tokenBudget = budget.limit;
|
|
1065
|
+
lastQuestion = trimmed;
|
|
1066
|
+
lastAnswer = budget.message;
|
|
1067
|
+
lastThoughtTime = 0.001;
|
|
1068
|
+
lastOutputTokenRate = undefined;
|
|
1069
|
+
lastTokenUsage = undefined;
|
|
1070
|
+
process.stdout.write('\x1b[H\x1b[J');
|
|
1071
|
+
process.stdout.write(renderFullWelcomeScreen({
|
|
1072
|
+
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1073
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1074
|
+
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1075
|
+
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
1076
|
+
}, process.stdout.rows));
|
|
1077
|
+
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (trimmed === '/map') {
|
|
1081
|
+
lastQuestion = trimmed;
|
|
1082
|
+
lastAnswer = await buildWorkspaceMap(canonicalWorkspace);
|
|
1083
|
+
lastThoughtTime = 0.001;
|
|
1084
|
+
lastOutputTokenRate = undefined;
|
|
1085
|
+
lastTokenUsage = undefined;
|
|
1086
|
+
process.stdout.write('\x1b[H\x1b[J');
|
|
1087
|
+
process.stdout.write(renderFullWelcomeScreen({
|
|
1088
|
+
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1089
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1090
|
+
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1091
|
+
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
1092
|
+
}, process.stdout.rows));
|
|
1093
|
+
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1029
1096
|
if (trimmed.startsWith('/queue')) {
|
|
1030
1097
|
const args = trimmed.slice('/queue'.length).trim();
|
|
1031
1098
|
lastQuestion = trimmed;
|
|
1032
1099
|
lastThoughtTime = 0.001;
|
|
1033
1100
|
lastOutputTokenRate = undefined;
|
|
1101
|
+
lastTokenUsage = undefined;
|
|
1034
1102
|
if (!args || args === 'list') {
|
|
1035
1103
|
lastAnswer = commandQueue.length === 0
|
|
1036
1104
|
? 'Command queue is empty. Use `/queue <command>` to add commands.'
|
|
@@ -1053,7 +1121,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1053
1121
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1054
1122
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1055
1123
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1056
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1124
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1057
1125
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1058
1126
|
chatThoughtTime: lastThoughtTime,
|
|
1059
1127
|
queuedCommands: commandQueue.items,
|
|
@@ -1068,6 +1136,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1068
1136
|
lastAnswer = '';
|
|
1069
1137
|
lastThoughtTime = 0;
|
|
1070
1138
|
lastOutputTokenRate = undefined;
|
|
1139
|
+
lastTokenUsage = undefined;
|
|
1071
1140
|
activeSession.messages = [];
|
|
1072
1141
|
sessionStore.save(activeSession);
|
|
1073
1142
|
continue;
|
|
@@ -1077,6 +1146,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1077
1146
|
lastAnswer = 'To leave Moderado, type /exit.';
|
|
1078
1147
|
lastThoughtTime = 0;
|
|
1079
1148
|
lastOutputTokenRate = undefined;
|
|
1149
|
+
lastTokenUsage = undefined;
|
|
1080
1150
|
continue;
|
|
1081
1151
|
}
|
|
1082
1152
|
// Non-standard or misspelled slash command rejection with advice
|
|
@@ -1086,10 +1156,11 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1086
1156
|
lastAnswer = slashAdvice.advice;
|
|
1087
1157
|
lastThoughtTime = 0.001;
|
|
1088
1158
|
lastOutputTokenRate = undefined;
|
|
1159
|
+
lastTokenUsage = undefined;
|
|
1089
1160
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1090
1161
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1091
1162
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1092
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1163
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1093
1164
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1094
1165
|
chatThoughtTime: lastThoughtTime,
|
|
1095
1166
|
queuedCommands: commandQueue.items,
|
|
@@ -1105,13 +1176,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1105
1176
|
: 'No valid user skills found in ~/.moderado/skills.';
|
|
1106
1177
|
lastThoughtTime = 0.001;
|
|
1107
1178
|
lastOutputTokenRate = undefined;
|
|
1179
|
+
lastTokenUsage = undefined;
|
|
1108
1180
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1109
|
-
process.stdout.write(renderFullWelcomeScreen({ model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode, autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer, chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items }, process.stdout.rows));
|
|
1181
|
+
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));
|
|
1110
1182
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1111
1183
|
continue;
|
|
1112
1184
|
}
|
|
1113
1185
|
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
1114
|
-
exitCleanly(
|
|
1186
|
+
exitCleanly(exitMessage());
|
|
1115
1187
|
}
|
|
1116
1188
|
if (trimmed === '/help') {
|
|
1117
1189
|
lastQuestion = trimmed;
|
|
@@ -1123,16 +1195,20 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1123
1195
|
' /session - Create, resume, undo, redo, share, export, or compact sessions\n' +
|
|
1124
1196
|
' /queue - Add, inspect, or clear queued follow-up commands\n' +
|
|
1125
1197
|
' /workflow - Inspect Git, build plans, or undo agent changes\n' +
|
|
1198
|
+
' /review - Review current workspace changes for bugs\n' +
|
|
1199
|
+
' /map - Show a compact workspace file map\n' +
|
|
1200
|
+
' /budget - Set a per-task token limit (/budget 5000, /budget off)\n' +
|
|
1126
1201
|
' /skills - List installed user skills and reload them\n' +
|
|
1127
1202
|
' /clear - Reset conversation memory\n' +
|
|
1128
1203
|
' /help - Display commands, shortcuts & version\n' +
|
|
1129
1204
|
' /exit - Exit Moderado';
|
|
1130
1205
|
lastThoughtTime = 0.001;
|
|
1131
1206
|
lastOutputTokenRate = undefined;
|
|
1207
|
+
lastTokenUsage = undefined;
|
|
1132
1208
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1133
1209
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1134
1210
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1135
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1211
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1136
1212
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1137
1213
|
chatThoughtTime: lastThoughtTime,
|
|
1138
1214
|
queuedCommands: commandQueue.items,
|
|
@@ -1158,13 +1234,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1158
1234
|
lastAnswer = localMetaAnswer;
|
|
1159
1235
|
lastThoughtTime = 0.001;
|
|
1160
1236
|
lastOutputTokenRate = undefined;
|
|
1237
|
+
lastTokenUsage = undefined;
|
|
1161
1238
|
conversationHistory = [...conversationHistory, { role: 'user', content: trimmed }, { role: 'assistant', content: localMetaAnswer }];
|
|
1162
1239
|
activeSession.messages = conversationHistory;
|
|
1163
1240
|
sessionStore.save(activeSession);
|
|
1164
1241
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1165
1242
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1166
1243
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1167
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1244
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1168
1245
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1169
1246
|
chatThoughtTime: lastThoughtTime,
|
|
1170
1247
|
queuedCommands: commandQueue.items,
|
|
@@ -1177,13 +1254,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1177
1254
|
lastQuestion = trimmed;
|
|
1178
1255
|
lastThoughtTime = 0.001;
|
|
1179
1256
|
lastOutputTokenRate = undefined;
|
|
1257
|
+
lastTokenUsage = undefined;
|
|
1180
1258
|
conversationHistory = [...conversationHistory, { role: 'user', content: trimmed }, { role: 'assistant', content: lastAnswer }];
|
|
1181
1259
|
activeSession.messages = conversationHistory;
|
|
1182
1260
|
sessionStore.save(activeSession);
|
|
1183
1261
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1184
1262
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1185
1263
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1186
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1264
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1187
1265
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1188
1266
|
chatThoughtTime: lastThoughtTime,
|
|
1189
1267
|
queuedCommands: commandQueue.items,
|
|
@@ -1200,10 +1278,11 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1200
1278
|
lastAnswer = 'Searching the web...';
|
|
1201
1279
|
lastThoughtTime = 0;
|
|
1202
1280
|
lastOutputTokenRate = undefined;
|
|
1281
|
+
lastTokenUsage = undefined;
|
|
1203
1282
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1204
1283
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1205
1284
|
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1206
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1285
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1207
1286
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1208
1287
|
}, process.stdout.rows));
|
|
1209
1288
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
@@ -1224,7 +1303,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1224
1303
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1225
1304
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1226
1305
|
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1227
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1306
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1228
1307
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1229
1308
|
chatThoughtTime: lastThoughtTime,
|
|
1230
1309
|
}, process.stdout.rows));
|
|
@@ -1234,7 +1313,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1234
1313
|
}
|
|
1235
1314
|
if (!provider) {
|
|
1236
1315
|
process.stdout.write('\nNo provider is connected. Let\'s connect one before sending this task.\n');
|
|
1237
|
-
const connection = await connectProviderInteractive({ signal, savedConnections: config.connections, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
1316
|
+
const connection = await connectProviderInteractive({ signal, savedConnections: config.connections, connectProviders: config.connectProviders, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
1238
1317
|
if (!connection) {
|
|
1239
1318
|
process.stdout.write('No provider connected. Use /connect whenever you are ready.\n\n');
|
|
1240
1319
|
continue;
|
|
@@ -1244,10 +1323,22 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1244
1323
|
config = loadConfig();
|
|
1245
1324
|
activateConnection(runtimeConnection);
|
|
1246
1325
|
}
|
|
1247
|
-
const
|
|
1326
|
+
const isReview = trimmed === '/review';
|
|
1327
|
+
const reviewFiles = isReview ? (await inspectGitWorkspace(canonicalWorkspace)).files.map(file => `${file.status} ${file.path}`) : [];
|
|
1328
|
+
const policy = new PolicyManager({ maxSteps: args.maxSteps, readOnly: isReview || activeMode === 'Plan' || args.readOnly, nonInteractive: args.nonInteractive, timeoutSeconds: args.timeout });
|
|
1248
1329
|
const startedAt = Date.now();
|
|
1249
1330
|
let thinkingTimer;
|
|
1250
1331
|
let removeGenerationListener;
|
|
1332
|
+
let turnUsage;
|
|
1333
|
+
let usageRecorded = false;
|
|
1334
|
+
lastTokenUsage = undefined;
|
|
1335
|
+
const recordTurnUsage = (pricing) => {
|
|
1336
|
+
if (!turnUsage || usageRecorded)
|
|
1337
|
+
return;
|
|
1338
|
+
activeSession.usage = accumulateSessionUsage(activeSession.usage, turnUsage.usage, turnUsage.estimated, pricing);
|
|
1339
|
+
sessionTokens = activeSession.usage.totalTokens;
|
|
1340
|
+
usageRecorded = true;
|
|
1341
|
+
};
|
|
1251
1342
|
try {
|
|
1252
1343
|
const requestAbort = new AbortController();
|
|
1253
1344
|
const requestSignal = signal ? AbortSignal.any([signal, requestAbort.signal]) : requestAbort.signal;
|
|
@@ -1255,6 +1346,10 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1255
1346
|
lastAnswer = '';
|
|
1256
1347
|
let firstAssistantDeltaAt;
|
|
1257
1348
|
let outputTokenRate;
|
|
1349
|
+
let budgetWarningIssued = false;
|
|
1350
|
+
let budgetStopped = false;
|
|
1351
|
+
let lastUsagePaint = 0;
|
|
1352
|
+
const usedModels = new Set();
|
|
1258
1353
|
let answerPosition = { row: 15, column: 1 };
|
|
1259
1354
|
let lastErrorEvent;
|
|
1260
1355
|
const redrawChatFrame = () => {
|
|
@@ -1265,7 +1360,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1265
1360
|
model: currentModel ?? 'No model connected — use /connect',
|
|
1266
1361
|
tokens: Math.round(sessionTokens),
|
|
1267
1362
|
cost: costLabel(),
|
|
1268
|
-
usageAvailable: activeSession.usage.available,
|
|
1363
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
1269
1364
|
workspace: canonicalWorkspace,
|
|
1270
1365
|
mode: activeMode,
|
|
1271
1366
|
autoApprove: activeAutoApprove,
|
|
@@ -1273,6 +1368,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1273
1368
|
chatAnswer: displayAnswer,
|
|
1274
1369
|
chatThoughtTime: thoughtTime,
|
|
1275
1370
|
outputTokenRate,
|
|
1371
|
+
tokenUsage: turnUsage,
|
|
1372
|
+
budgetStatus: budgetStopped ? 'Budget reached' : budgetWarningIssued ? 'Budget ≥80%' : undefined,
|
|
1276
1373
|
input: commandQueue.currentDraft,
|
|
1277
1374
|
queuedCommands: commandQueue.items,
|
|
1278
1375
|
}, process.stdout.rows));
|
|
@@ -1311,7 +1408,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1311
1408
|
model: currentModel ?? 'No model connected',
|
|
1312
1409
|
tokens: Math.round(sessionTokens),
|
|
1313
1410
|
cost: costLabel(),
|
|
1314
|
-
usageAvailable: activeSession.usage.available,
|
|
1411
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
1315
1412
|
workspace: canonicalWorkspace,
|
|
1316
1413
|
mode: activeMode,
|
|
1317
1414
|
autoApprove: activeAutoApprove,
|
|
@@ -1344,9 +1441,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1344
1441
|
});
|
|
1345
1442
|
};
|
|
1346
1443
|
if (process.stdin.isTTY) {
|
|
1347
|
-
const readlineModule = await import('node:readline');
|
|
1348
1444
|
const attachGenerationListener = () => {
|
|
1349
|
-
|
|
1445
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1350
1446
|
process.stdin.resume();
|
|
1351
1447
|
process.stdin.setRawMode(true);
|
|
1352
1448
|
process.stdin.on('keypress', onGenerationKeypress);
|
|
@@ -1370,17 +1466,36 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1370
1466
|
// Move to the chat layout before the provider can emit its first event.
|
|
1371
1467
|
redrawChatFrame();
|
|
1372
1468
|
thinkingTimer = setInterval(() => {
|
|
1373
|
-
if (!firstAssistantDeltaAt) {
|
|
1469
|
+
if (!firstAssistantDeltaAt && !turnUsage) {
|
|
1374
1470
|
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000));
|
|
1375
1471
|
}
|
|
1376
1472
|
}, 400);
|
|
1377
1473
|
const evidenceTask = liveSearch ? buildSearchAnswerTask(effectivePrompt, liveSearch) : undefined;
|
|
1378
|
-
const runAgent = () => loop.run(evidenceTask ?? createAgentTask(effectivePrompt, activeMode), {
|
|
1379
|
-
workspaceRoot: canonicalWorkspace, provider: provider, tools, approvalHandler, router, policy,
|
|
1474
|
+
const runAgent = () => loop.run(isReview ? createReviewTask(reviewFiles) : evidenceTask ?? createAgentTask(effectivePrompt, activeMode), {
|
|
1475
|
+
workspaceRoot: canonicalWorkspace, provider: provider, tools: isReview ? createReviewToolRegistry() : tools, approvalHandler, router, policy,
|
|
1380
1476
|
maxOutputTokens: args.maxTokens ?? config.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
1381
1477
|
skillContext: skillContext(),
|
|
1382
1478
|
routeOptions: { pinnedModelId: currentModel === 'auto' ? undefined : currentModel, allowPaid: config.allowPaid ?? args.allowPaid, allowUnknown: config.allowUnknown ?? args.allowUnknown, isLocalProfile: args.profile.includes('local') },
|
|
1383
1479
|
eventListener: (event) => {
|
|
1480
|
+
if (event.type === 'model_change')
|
|
1481
|
+
usedModels.add(event.newModelId);
|
|
1482
|
+
if (event.type === 'usage') {
|
|
1483
|
+
turnUsage = event;
|
|
1484
|
+
lastTokenUsage = event;
|
|
1485
|
+
outputTokenRate = event.generationMs > 0 ? event.outputTokensPerSecond : undefined;
|
|
1486
|
+
const budgetState = evaluateTokenBudget(event.usage.totalTokens, tokenBudget);
|
|
1487
|
+
const firstBudgetWarning = budgetState === 'warning' && !budgetWarningIssued;
|
|
1488
|
+
if (budgetState === 'warning')
|
|
1489
|
+
budgetWarningIssued = true;
|
|
1490
|
+
if (budgetState === 'stop' && !budgetStopped) {
|
|
1491
|
+
budgetStopped = true;
|
|
1492
|
+
requestAbort.abort();
|
|
1493
|
+
}
|
|
1494
|
+
if (budgetStopped || firstBudgetWarning || event.final || event.timestamp - lastUsagePaint >= 400) {
|
|
1495
|
+
process.stdout.write(renderChatUsageUpdate(event, process.stdout.columns || 80, budgetStopped ? 'Budget reached' : budgetWarningIssued ? 'Budget ≥80%' : undefined));
|
|
1496
|
+
lastUsagePaint = event.timestamp;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1384
1499
|
if (event.type === 'assistant_delta') {
|
|
1385
1500
|
if (!firstAssistantDeltaAt && event.delta.trim().length > 0) {
|
|
1386
1501
|
firstAssistantDeltaAt = Date.now();
|
|
@@ -1421,28 +1536,38 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1421
1536
|
const replacement = await replaceProviderKeyInteractive(activeConnection, { signal });
|
|
1422
1537
|
if (!replacement)
|
|
1423
1538
|
throw error;
|
|
1539
|
+
recordTurnUsage();
|
|
1540
|
+
turnUsage = undefined;
|
|
1541
|
+
usageRecorded = false;
|
|
1424
1542
|
const secured = process.platform === 'win32' ? await storeConnectionCredential(replacement, credentialStore) : replacement;
|
|
1425
1543
|
saveConnection(secured);
|
|
1426
1544
|
config = loadConfig();
|
|
1427
1545
|
activateConnection(secured);
|
|
1428
1546
|
result = await runAgent();
|
|
1429
1547
|
}
|
|
1430
|
-
if (
|
|
1548
|
+
if (turnUsage && !turnUsage.estimated) {
|
|
1431
1549
|
let pricing;
|
|
1432
1550
|
try {
|
|
1433
|
-
|
|
1551
|
+
if (usedModels.size <= 1)
|
|
1552
|
+
pricing = findModelPricing(await provider.discoverModels(signal), result.selectedModel.id);
|
|
1434
1553
|
}
|
|
1435
1554
|
catch {
|
|
1436
1555
|
// A valid answer remains usable when the provider catalog is unavailable.
|
|
1437
1556
|
}
|
|
1438
|
-
|
|
1439
|
-
sessionTokens = result.usage.totalTokens;
|
|
1440
|
-
if (firstAssistantDeltaAt)
|
|
1441
|
-
outputTokenRate = calculateOutputTokenRate(result.usage.completionTokens, Date.now() - firstAssistantDeltaAt);
|
|
1557
|
+
recordTurnUsage(pricing);
|
|
1442
1558
|
}
|
|
1559
|
+
recordTurnUsage();
|
|
1560
|
+
// Failed/cancelled tasks can still consume tokens, even without a saved answer.
|
|
1561
|
+
if (turnUsage)
|
|
1562
|
+
sessionStore.save(activeSession);
|
|
1443
1563
|
const otherConnections = Object.keys(config.connections ?? {}).filter((id) => id !== activeConnection?.id);
|
|
1444
1564
|
const resolution = resolveTurnAssistantAnswer(result, conversationHistory.length, lastErrorEvent, streamedAnswer, currentModel, otherConnections);
|
|
1445
1565
|
lastAnswer = resolution.answer;
|
|
1566
|
+
if (budgetStopped) {
|
|
1567
|
+
const note = `Token budget reached (${tokenBudget?.toLocaleString()} per task). Task stopped; usage may exceed the limit because providers report in chunks.`;
|
|
1568
|
+
lastAnswer = [lastAnswer, note].filter(Boolean).join('\n\n');
|
|
1569
|
+
streamedAnswer = lastAnswer;
|
|
1570
|
+
}
|
|
1446
1571
|
if (resolution.isError) {
|
|
1447
1572
|
streamedAnswer = lastAnswer;
|
|
1448
1573
|
}
|
|
@@ -1469,6 +1594,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1469
1594
|
if (thinkingTimer)
|
|
1470
1595
|
clearInterval(thinkingTimer);
|
|
1471
1596
|
removeGenerationListener?.();
|
|
1597
|
+
recordTurnUsage();
|
|
1598
|
+
if (turnUsage)
|
|
1599
|
+
sessionStore.save(activeSession);
|
|
1472
1600
|
const otherConnections = Object.keys(config.connections ?? {}).filter((id) => id !== activeConnection?.id);
|
|
1473
1601
|
const altSuggestion = otherConnections.length > 0
|
|
1474
1602
|
? ` (other configured providers: ${otherConnections.join(', ')})`
|
|
@@ -1486,13 +1614,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1486
1614
|
model: currentModel ?? 'No model connected — use /connect',
|
|
1487
1615
|
tokens: Math.round(sessionTokens),
|
|
1488
1616
|
cost: costLabel(),
|
|
1489
|
-
usageAvailable: activeSession.usage.available,
|
|
1617
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
1490
1618
|
workspace: canonicalWorkspace,
|
|
1491
1619
|
mode: activeMode,
|
|
1492
1620
|
autoApprove: activeAutoApprove,
|
|
1493
1621
|
chatQuestion: lastQuestion,
|
|
1494
1622
|
chatAnswer: lastAnswer,
|
|
1495
1623
|
chatThoughtTime: lastThoughtTime,
|
|
1624
|
+
tokenUsage: turnUsage,
|
|
1496
1625
|
queuedCommands: commandQueue.items,
|
|
1497
1626
|
}, process.stdout.rows));
|
|
1498
1627
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|