moderado 0.3.0 → 0.3.7
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 +182 -175
- package/dist/args.d.ts +1 -1
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +32 -28
- package/dist/args.js.map +1 -1
- 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/commands/chat.d.ts +5 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +188 -40
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +61 -14
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +2 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/skills.d.ts +2 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +13 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +117 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- 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 +23 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +78 -0
- package/dist/skills.js.map +1 -0
- package/dist/ui/help_modal.d.ts.map +1 -1
- package/dist/ui/help_modal.js +1 -0
- package/dist/ui/help_modal.js.map +1 -1
- 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 +7 -3
- package/dist/ui/provider_connect.d.ts.map +1 -1
- package/dist/ui/provider_connect.js +64 -15
- 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/welcome.d.ts +11 -0
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +40 -20
- 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/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 +5 -2
- package/dist/vendor/core/agent.d.ts.map +1 -1
- package/dist/vendor/core/agent.js +148 -34
- package/dist/vendor/core/agent.js.map +1 -1
- package/dist/vendor/providers/model_discovery.d.ts +6 -2
- package/dist/vendor/providers/model_discovery.d.ts.map +1 -1
- package/dist/vendor/providers/model_discovery.js +8 -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/package.json +2 -2
- package/dist/ui/session_screen.d.ts +0 -32
- package/dist/ui/session_screen.d.ts.map +0 -1
- package/dist/ui/session_screen.js +0 -148
- package/dist/ui/session_screen.js.map +0 -1
- package/dist/ui/session_terminal.d.ts +0 -46
- package/dist/ui/session_terminal.d.ts.map +0 -1
- package/dist/ui/session_terminal.js +0 -342
- package/dist/ui/session_terminal.js.map +0 -1
package/dist/commands/chat.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { AgentLoop, PolicyManager, Router, cleanConversationalFiller, DEFAULT_MAX_OUTPUT_TOKENS } from '../vendor/core/index.js';
|
|
4
4
|
import { NvidiaAdapter } from '../vendor/providers/index.js';
|
|
5
|
-
import { createDefaultToolRegistry, canonicalizeRoot, createMcpTools, createWebSearchTool, discoverMcpServers, resolveInJail, WorkspaceCheckpointStore, WriteFileTool } from '../vendor/tools/index.js';
|
|
5
|
+
import { createDefaultToolRegistry, ToolRegistry, canonicalizeRoot, createMcpTools, createWebSearchTool, discoverMcpServers, resolveInJail, WorkspaceCheckpointStore, WriteFileTool } from '../vendor/tools/index.js';
|
|
6
6
|
import { McpServerConfigSchema } from '../vendor/contracts/index.js';
|
|
7
7
|
import { getActiveConnection, loadConfig, removeMcpServer, resolveApiKey, resolveConnectionCredential, saveConnection, saveMcpServer, setMcpServerEnabled, storeConnectionCredential } from '../config.js';
|
|
8
8
|
import { MemoryCredentialStore } from '../credentials.js';
|
|
@@ -13,12 +13,29 @@ import { connectProviderInteractive, isAuthenticationFailure, replaceProviderKey
|
|
|
13
13
|
import { initWorkspace } from './init.js';
|
|
14
14
|
import { expandMentions, createWorkspaceFileSource } from '../ui/file_mentions.js';
|
|
15
15
|
import { listFiles } from '../vendor/tools/index.js';
|
|
16
|
-
import { exitCleanly, promptInteractiveTurn, renderChatAnswerDelta, renderChatComposerCursor, renderChatThoughtTimeUpdate, renderFullWelcomeScreen, renderWelcomePopupLayer, terminalCleanExitDone, wrapText } from '../ui/welcome.js';
|
|
17
|
-
import {
|
|
16
|
+
import { exitCleanly, promptInteractiveTurn, renderChatAnswerDelta, renderChatComposerCursor, renderChatThoughtTimeUpdate, renderChatUsageUpdate, renderFullWelcomeScreen, renderWelcomePopupLayer, terminalCleanExitDone, wrapText } from '../ui/welcome.js';
|
|
17
|
+
import { accumulateSessionUsage, compactSessionMessages, createSession, exportSessionMarkdown, formatSessionCost, SessionStore } from '../sessions.js';
|
|
18
18
|
import { layerPromptBox, renderBoxLines, selectConfirmPopup, selectListPopup } from '../ui/popup.js';
|
|
19
19
|
import { askModalChoice } from '../ui/prompt.js';
|
|
20
20
|
import { findModelPricing } from '../model_pricing.js';
|
|
21
21
|
import { inspectGitWorkspace, readGitDiff } from '../vendor/tools/index.js';
|
|
22
|
+
import { buildWorkspaceMap } from '../repo_map.js';
|
|
23
|
+
import { evaluateTokenBudget, parseBudgetCommand } from '../budget.js';
|
|
24
|
+
import { discoverSkills, formatSkillContext } from '../skills.js';
|
|
25
|
+
export function formatSessionExitSummary(session, modelId) {
|
|
26
|
+
const usage = session.usage;
|
|
27
|
+
const tokens = usage.available
|
|
28
|
+
? `Input ${usage.promptTokens.toLocaleString('en-US')} | Output ${usage.completionTokens.toLocaleString('en-US')} | Total ${usage.totalTokens.toLocaleString('en-US')}${usage.estimated ? ' (estimated)' : ''}`
|
|
29
|
+
: 'Usage unavailable';
|
|
30
|
+
return [
|
|
31
|
+
'\x1b[1;38;5;25mSession Summary\x1b[0m',
|
|
32
|
+
`ID: ${session.id}`,
|
|
33
|
+
`Model: ${modelId ?? session.modelId ?? 'No model selected'}`,
|
|
34
|
+
`Tokens: ${tokens}`,
|
|
35
|
+
`Cost: ${formatSessionCost(usage)}`,
|
|
36
|
+
'Resume: /session resume',
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
22
39
|
function mutationPaths(toolName, parameters) {
|
|
23
40
|
const value = parameters;
|
|
24
41
|
if (toolName === 'apply_patch' && Array.isArray(value.edits)) {
|
|
@@ -31,6 +48,19 @@ export function createAgentTask(request, mode) {
|
|
|
31
48
|
return request;
|
|
32
49
|
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}`;
|
|
33
50
|
}
|
|
51
|
+
export function createReviewTask(changedFiles) {
|
|
52
|
+
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)'}`;
|
|
53
|
+
}
|
|
54
|
+
export function createReviewToolRegistry() {
|
|
55
|
+
const source = createDefaultToolRegistry();
|
|
56
|
+
const registry = new ToolRegistry();
|
|
57
|
+
for (const name of ['git_diff', 'read_file', 'list_files', 'search_files', 'get_definition', 'find_references']) {
|
|
58
|
+
const tool = source.get(name);
|
|
59
|
+
if (tool)
|
|
60
|
+
registry.register(tool);
|
|
61
|
+
}
|
|
62
|
+
return registry;
|
|
63
|
+
}
|
|
34
64
|
export const STANDARD_SLASH_COMMANDS = [
|
|
35
65
|
'/connect',
|
|
36
66
|
'/model',
|
|
@@ -40,6 +70,10 @@ export const STANDARD_SLASH_COMMANDS = [
|
|
|
40
70
|
'/session',
|
|
41
71
|
'/queue',
|
|
42
72
|
'/workflow',
|
|
73
|
+
'/review',
|
|
74
|
+
'/map',
|
|
75
|
+
'/budget',
|
|
76
|
+
'/skills',
|
|
43
77
|
'/clear',
|
|
44
78
|
'/help',
|
|
45
79
|
'/exit',
|
|
@@ -137,6 +171,10 @@ export function formatTurnFailureAnswer(status, errorEvent, modelId, otherConnec
|
|
|
137
171
|
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}.`;
|
|
138
172
|
}
|
|
139
173
|
export function resolveTurnAssistantAnswer(result, previousHistoryLength, lastErrorEvent, streamedAnswer = '', currentModel = 'unknown', otherConnections = []) {
|
|
174
|
+
if (result.status === 'cancelled') {
|
|
175
|
+
const partial = cleanConversationalFiller(streamedAnswer);
|
|
176
|
+
return { answer: [partial, 'Task stopped.'].filter(Boolean).join('\n\n'), isError: true };
|
|
177
|
+
}
|
|
140
178
|
if (result.status === 'failed' || lastErrorEvent) {
|
|
141
179
|
return {
|
|
142
180
|
answer: formatTurnFailureAnswer(result.status, lastErrorEvent, result.selectedModel?.id || currentModel, otherConnections),
|
|
@@ -214,15 +252,9 @@ export function handleGenerationKeypress(str, key, queue, actions) {
|
|
|
214
252
|
actions.abort();
|
|
215
253
|
return;
|
|
216
254
|
}
|
|
217
|
-
// 2. Escape
|
|
255
|
+
// 2. Escape always stops the active task; the caller clears queued input.
|
|
218
256
|
if (isGenerationCancelKey(key, str)) {
|
|
219
|
-
|
|
220
|
-
queue.setDraft('');
|
|
221
|
-
actions.onDraftChange();
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
actions.abort();
|
|
225
|
-
}
|
|
257
|
+
actions.abort();
|
|
226
258
|
return;
|
|
227
259
|
}
|
|
228
260
|
// 3. Return / Enter: commit draft or execute /btw immediately
|
|
@@ -420,9 +452,6 @@ export async function decideApproval(request, options, signal) {
|
|
|
420
452
|
if (options.autoApprove && !request.toolName.startsWith('mcp.')) {
|
|
421
453
|
return { requestId: request.requestId, status: 'approved' };
|
|
422
454
|
}
|
|
423
|
-
if (['write_file', 'edit_file', 'apply_patch', 'run_command', 'run_diagnostics'].includes(request.toolName)) {
|
|
424
|
-
return { requestId: request.requestId, status: 'approved' };
|
|
425
|
-
}
|
|
426
455
|
return options.requestInteractiveApproval(request, signal);
|
|
427
456
|
}
|
|
428
457
|
export async function createMcpToolRegistry(servers, webSearch = {}) {
|
|
@@ -695,7 +724,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
695
724
|
if (activeConnection)
|
|
696
725
|
activateConnection(activeConnection, args.model);
|
|
697
726
|
let activeMode = args.readOnly ? 'Plan' : 'Execute';
|
|
698
|
-
let activeAutoApprove =
|
|
727
|
+
let activeAutoApprove = !args.nonInteractive;
|
|
728
|
+
let tokenBudget;
|
|
699
729
|
const sessionStore = new SessionStore();
|
|
700
730
|
let activeSession = sessionStore.loadLatestSession(canonicalWorkspace) ??
|
|
701
731
|
createSession(canonicalWorkspace, {
|
|
@@ -709,6 +739,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
709
739
|
if (config.typescriptLanguageServer)
|
|
710
740
|
process.env.MODERADO_TYPESCRIPT_LANGUAGE_SERVER = config.typescriptLanguageServer;
|
|
711
741
|
let tools = createDefaultToolRegistry();
|
|
742
|
+
let skills = discoverSkills();
|
|
743
|
+
const skillContext = () => formatSkillContext(skills);
|
|
712
744
|
const reloadMcpTools = async () => {
|
|
713
745
|
config = loadConfig();
|
|
714
746
|
tools = await createMcpToolRegistry(config.mcpServers, resolveWebSearchOptions(config));
|
|
@@ -738,8 +770,10 @@ export async function handleChatSession(args, version, signal) {
|
|
|
738
770
|
let lastAnswer = '';
|
|
739
771
|
let lastThoughtTime = 0;
|
|
740
772
|
let lastOutputTokenRate;
|
|
773
|
+
let lastTokenUsage;
|
|
741
774
|
let activePlan;
|
|
742
775
|
const costLabel = () => formatSessionCost(activeSession.usage);
|
|
776
|
+
const exitMessage = () => `\x1b[32mGoodbye! Stay Tuned with Moderado!\x1b[0m\n\n${formatSessionExitSummary(activeSession, currentModel)}`;
|
|
743
777
|
const commandQueue = new TurnCommandQueue();
|
|
744
778
|
const btwHistory = [];
|
|
745
779
|
while (!signal?.aborted) {
|
|
@@ -751,8 +785,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
751
785
|
else {
|
|
752
786
|
const turn = await promptInteractiveTurn({
|
|
753
787
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(), workspace: canonicalWorkspace, version,
|
|
754
|
-
usageAvailable: activeSession.usage.available, initialMode: activeMode, initialAutoApprove: activeAutoApprove, isFirstTurn: isFirst, signal, chatQuestion: lastQuestion || undefined, chatAnswer: lastAnswer || undefined, chatThoughtTime: lastThoughtTime, outputTokenRate: lastOutputTokenRate,
|
|
788
|
+
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,
|
|
755
789
|
queuedCommands: commandQueue.items,
|
|
790
|
+
onExit: exitMessage,
|
|
756
791
|
questionHistory: conversationHistory.flatMap((message) => message.role === 'user' && message.content?.trim() ? [message.content] : []),
|
|
757
792
|
onModelSelect: async (drawFrame) => {
|
|
758
793
|
if (!activeConnection) {
|
|
@@ -766,7 +801,6 @@ export async function handleChatSession(args, version, signal) {
|
|
|
766
801
|
providerId: activeConnection.id,
|
|
767
802
|
providerName: activeConnection.displayName,
|
|
768
803
|
currentModel,
|
|
769
|
-
allModelsFree: activeConnection.id === 'agnes-ai',
|
|
770
804
|
signal,
|
|
771
805
|
drawFrame,
|
|
772
806
|
});
|
|
@@ -788,7 +822,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
788
822
|
return selection.modelId;
|
|
789
823
|
},
|
|
790
824
|
onConnect: async (drawFrame) => {
|
|
791
|
-
const connection = await connectProviderInteractive({ signal, drawFrame, savedConnections: config.connections, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
825
|
+
const connection = await connectProviderInteractive({ signal, drawFrame, savedConnections: config.connections, connectProviders: config.connectProviders, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
792
826
|
if (!connection)
|
|
793
827
|
return currentModel;
|
|
794
828
|
const runtimeConnection = process.platform === 'win32' ? await storeConnectionCredential(connection, credentialStore) : connection;
|
|
@@ -804,6 +838,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
804
838
|
lastAnswer = '';
|
|
805
839
|
lastThoughtTime = 0;
|
|
806
840
|
lastOutputTokenRate = undefined;
|
|
841
|
+
lastTokenUsage = undefined;
|
|
807
842
|
activeSession.messages = [];
|
|
808
843
|
sessionStore.save(activeSession);
|
|
809
844
|
},
|
|
@@ -922,6 +957,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
922
957
|
lastAnswer = '';
|
|
923
958
|
lastThoughtTime = 0;
|
|
924
959
|
lastOutputTokenRate = undefined;
|
|
960
|
+
lastTokenUsage = undefined;
|
|
925
961
|
sessionStore.save(activeSession);
|
|
926
962
|
return;
|
|
927
963
|
}
|
|
@@ -1022,11 +1058,46 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1022
1058
|
continue;
|
|
1023
1059
|
}
|
|
1024
1060
|
}
|
|
1061
|
+
if (trimmed === '/budget' || trimmed.startsWith('/budget ')) {
|
|
1062
|
+
const budget = parseBudgetCommand(trimmed, tokenBudget);
|
|
1063
|
+
tokenBudget = budget.limit;
|
|
1064
|
+
lastQuestion = trimmed;
|
|
1065
|
+
lastAnswer = budget.message;
|
|
1066
|
+
lastThoughtTime = 0.001;
|
|
1067
|
+
lastOutputTokenRate = undefined;
|
|
1068
|
+
lastTokenUsage = undefined;
|
|
1069
|
+
process.stdout.write('\x1b[H\x1b[J');
|
|
1070
|
+
process.stdout.write(renderFullWelcomeScreen({
|
|
1071
|
+
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1072
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1073
|
+
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1074
|
+
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
1075
|
+
}, process.stdout.rows));
|
|
1076
|
+
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (trimmed === '/map') {
|
|
1080
|
+
lastQuestion = trimmed;
|
|
1081
|
+
lastAnswer = await buildWorkspaceMap(canonicalWorkspace);
|
|
1082
|
+
lastThoughtTime = 0.001;
|
|
1083
|
+
lastOutputTokenRate = undefined;
|
|
1084
|
+
lastTokenUsage = undefined;
|
|
1085
|
+
process.stdout.write('\x1b[H\x1b[J');
|
|
1086
|
+
process.stdout.write(renderFullWelcomeScreen({
|
|
1087
|
+
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1088
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1089
|
+
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1090
|
+
chatThoughtTime: lastThoughtTime, queuedCommands: commandQueue.items,
|
|
1091
|
+
}, process.stdout.rows));
|
|
1092
|
+
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1025
1095
|
if (trimmed.startsWith('/queue')) {
|
|
1026
1096
|
const args = trimmed.slice('/queue'.length).trim();
|
|
1027
1097
|
lastQuestion = trimmed;
|
|
1028
1098
|
lastThoughtTime = 0.001;
|
|
1029
1099
|
lastOutputTokenRate = undefined;
|
|
1100
|
+
lastTokenUsage = undefined;
|
|
1030
1101
|
if (!args || args === 'list') {
|
|
1031
1102
|
lastAnswer = commandQueue.length === 0
|
|
1032
1103
|
? 'Command queue is empty. Use `/queue <command>` to add commands.'
|
|
@@ -1049,7 +1120,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1049
1120
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1050
1121
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1051
1122
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1052
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1123
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1053
1124
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1054
1125
|
chatThoughtTime: lastThoughtTime,
|
|
1055
1126
|
queuedCommands: commandQueue.items,
|
|
@@ -1064,6 +1135,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1064
1135
|
lastAnswer = '';
|
|
1065
1136
|
lastThoughtTime = 0;
|
|
1066
1137
|
lastOutputTokenRate = undefined;
|
|
1138
|
+
lastTokenUsage = undefined;
|
|
1067
1139
|
activeSession.messages = [];
|
|
1068
1140
|
sessionStore.save(activeSession);
|
|
1069
1141
|
continue;
|
|
@@ -1073,6 +1145,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1073
1145
|
lastAnswer = 'To leave Moderado, type /exit.';
|
|
1074
1146
|
lastThoughtTime = 0;
|
|
1075
1147
|
lastOutputTokenRate = undefined;
|
|
1148
|
+
lastTokenUsage = undefined;
|
|
1076
1149
|
continue;
|
|
1077
1150
|
}
|
|
1078
1151
|
// Non-standard or misspelled slash command rejection with advice
|
|
@@ -1082,10 +1155,11 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1082
1155
|
lastAnswer = slashAdvice.advice;
|
|
1083
1156
|
lastThoughtTime = 0.001;
|
|
1084
1157
|
lastOutputTokenRate = undefined;
|
|
1158
|
+
lastTokenUsage = undefined;
|
|
1085
1159
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1086
1160
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1087
1161
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1088
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1162
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1089
1163
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1090
1164
|
chatThoughtTime: lastThoughtTime,
|
|
1091
1165
|
queuedCommands: commandQueue.items,
|
|
@@ -1093,8 +1167,22 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1093
1167
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1094
1168
|
continue;
|
|
1095
1169
|
}
|
|
1170
|
+
if (trimmed === '/skills' || trimmed.startsWith('/skills ')) {
|
|
1171
|
+
skills = discoverSkills();
|
|
1172
|
+
lastQuestion = trimmed;
|
|
1173
|
+
lastAnswer = skills.length
|
|
1174
|
+
? 'User skills loaded:\n' + skills.map((skill) => ` ${skill.name} — ${skill.description}`).join('\n')
|
|
1175
|
+
: 'No valid user skills found in ~/.moderado/skills.';
|
|
1176
|
+
lastThoughtTime = 0.001;
|
|
1177
|
+
lastOutputTokenRate = undefined;
|
|
1178
|
+
lastTokenUsage = undefined;
|
|
1179
|
+
process.stdout.write('\x1b[H\x1b[J');
|
|
1180
|
+
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));
|
|
1181
|
+
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1096
1184
|
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
1097
|
-
exitCleanly(
|
|
1185
|
+
exitCleanly(exitMessage());
|
|
1098
1186
|
}
|
|
1099
1187
|
if (trimmed === '/help') {
|
|
1100
1188
|
lastQuestion = trimmed;
|
|
@@ -1106,15 +1194,20 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1106
1194
|
' /session - Create, resume, undo, redo, share, export, or compact sessions\n' +
|
|
1107
1195
|
' /queue - Add, inspect, or clear queued follow-up commands\n' +
|
|
1108
1196
|
' /workflow - Inspect Git, build plans, or undo agent changes\n' +
|
|
1197
|
+
' /review - Review current workspace changes for bugs\n' +
|
|
1198
|
+
' /map - Show a compact workspace file map\n' +
|
|
1199
|
+
' /budget - Set a per-task token limit (/budget 5000, /budget off)\n' +
|
|
1200
|
+
' /skills - List installed user skills and reload them\n' +
|
|
1109
1201
|
' /clear - Reset conversation memory\n' +
|
|
1110
1202
|
' /help - Display commands, shortcuts & version\n' +
|
|
1111
1203
|
' /exit - Exit Moderado';
|
|
1112
1204
|
lastThoughtTime = 0.001;
|
|
1113
1205
|
lastOutputTokenRate = undefined;
|
|
1206
|
+
lastTokenUsage = undefined;
|
|
1114
1207
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1115
1208
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1116
1209
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1117
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1210
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1118
1211
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1119
1212
|
chatThoughtTime: lastThoughtTime,
|
|
1120
1213
|
queuedCommands: commandQueue.items,
|
|
@@ -1140,13 +1233,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1140
1233
|
lastAnswer = localMetaAnswer;
|
|
1141
1234
|
lastThoughtTime = 0.001;
|
|
1142
1235
|
lastOutputTokenRate = undefined;
|
|
1236
|
+
lastTokenUsage = undefined;
|
|
1143
1237
|
conversationHistory = [...conversationHistory, { role: 'user', content: trimmed }, { role: 'assistant', content: localMetaAnswer }];
|
|
1144
1238
|
activeSession.messages = conversationHistory;
|
|
1145
1239
|
sessionStore.save(activeSession);
|
|
1146
1240
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1147
1241
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1148
1242
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1149
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1243
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1150
1244
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1151
1245
|
chatThoughtTime: lastThoughtTime,
|
|
1152
1246
|
queuedCommands: commandQueue.items,
|
|
@@ -1159,13 +1253,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1159
1253
|
lastQuestion = trimmed;
|
|
1160
1254
|
lastThoughtTime = 0.001;
|
|
1161
1255
|
lastOutputTokenRate = undefined;
|
|
1256
|
+
lastTokenUsage = undefined;
|
|
1162
1257
|
conversationHistory = [...conversationHistory, { role: 'user', content: trimmed }, { role: 'assistant', content: lastAnswer }];
|
|
1163
1258
|
activeSession.messages = conversationHistory;
|
|
1164
1259
|
sessionStore.save(activeSession);
|
|
1165
1260
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1166
1261
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1167
1262
|
model: currentModel ?? 'No model connected — use /connect', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1168
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1263
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1169
1264
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1170
1265
|
chatThoughtTime: lastThoughtTime,
|
|
1171
1266
|
queuedCommands: commandQueue.items,
|
|
@@ -1182,10 +1277,11 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1182
1277
|
lastAnswer = 'Searching the web...';
|
|
1183
1278
|
lastThoughtTime = 0;
|
|
1184
1279
|
lastOutputTokenRate = undefined;
|
|
1280
|
+
lastTokenUsage = undefined;
|
|
1185
1281
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1186
1282
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1187
1283
|
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1188
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1284
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1189
1285
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1190
1286
|
}, process.stdout.rows));
|
|
1191
1287
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|
|
@@ -1206,7 +1302,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1206
1302
|
process.stdout.write('\x1b[H\x1b[J');
|
|
1207
1303
|
process.stdout.write(renderFullWelcomeScreen({
|
|
1208
1304
|
model: currentModel ?? 'Web search', tokens: Math.round(sessionTokens), cost: costLabel(),
|
|
1209
|
-
usageAvailable: activeSession.usage.available, workspace: canonicalWorkspace, mode: activeMode,
|
|
1305
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated, workspace: canonicalWorkspace, mode: activeMode,
|
|
1210
1306
|
autoApprove: activeAutoApprove, chatQuestion: lastQuestion, chatAnswer: lastAnswer,
|
|
1211
1307
|
chatThoughtTime: lastThoughtTime,
|
|
1212
1308
|
}, process.stdout.rows));
|
|
@@ -1216,7 +1312,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1216
1312
|
}
|
|
1217
1313
|
if (!provider) {
|
|
1218
1314
|
process.stdout.write('\nNo provider is connected. Let\'s connect one before sending this task.\n');
|
|
1219
|
-
const connection = await connectProviderInteractive({ signal, savedConnections: config.connections, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
1315
|
+
const connection = await connectProviderInteractive({ signal, savedConnections: config.connections, connectProviders: config.connectProviders, resolveSavedConnection: (saved) => resolveConnectionCredential(saved, credentialStore) });
|
|
1220
1316
|
if (!connection) {
|
|
1221
1317
|
process.stdout.write('No provider connected. Use /connect whenever you are ready.\n\n');
|
|
1222
1318
|
continue;
|
|
@@ -1226,10 +1322,22 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1226
1322
|
config = loadConfig();
|
|
1227
1323
|
activateConnection(runtimeConnection);
|
|
1228
1324
|
}
|
|
1229
|
-
const
|
|
1325
|
+
const isReview = trimmed === '/review';
|
|
1326
|
+
const reviewFiles = isReview ? (await inspectGitWorkspace(canonicalWorkspace)).files.map(file => `${file.status} ${file.path}`) : [];
|
|
1327
|
+
const policy = new PolicyManager({ maxSteps: args.maxSteps, readOnly: isReview || activeMode === 'Plan' || args.readOnly, nonInteractive: args.nonInteractive, timeoutSeconds: args.timeout });
|
|
1230
1328
|
const startedAt = Date.now();
|
|
1231
1329
|
let thinkingTimer;
|
|
1232
1330
|
let removeGenerationListener;
|
|
1331
|
+
let turnUsage;
|
|
1332
|
+
let usageRecorded = false;
|
|
1333
|
+
lastTokenUsage = undefined;
|
|
1334
|
+
const recordTurnUsage = (pricing) => {
|
|
1335
|
+
if (!turnUsage || usageRecorded)
|
|
1336
|
+
return;
|
|
1337
|
+
activeSession.usage = accumulateSessionUsage(activeSession.usage, turnUsage.usage, turnUsage.estimated, pricing);
|
|
1338
|
+
sessionTokens = activeSession.usage.totalTokens;
|
|
1339
|
+
usageRecorded = true;
|
|
1340
|
+
};
|
|
1233
1341
|
try {
|
|
1234
1342
|
const requestAbort = new AbortController();
|
|
1235
1343
|
const requestSignal = signal ? AbortSignal.any([signal, requestAbort.signal]) : requestAbort.signal;
|
|
@@ -1237,6 +1345,10 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1237
1345
|
lastAnswer = '';
|
|
1238
1346
|
let firstAssistantDeltaAt;
|
|
1239
1347
|
let outputTokenRate;
|
|
1348
|
+
let budgetWarningIssued = false;
|
|
1349
|
+
let budgetStopped = false;
|
|
1350
|
+
let lastUsagePaint = 0;
|
|
1351
|
+
const usedModels = new Set();
|
|
1240
1352
|
let answerPosition = { row: 15, column: 1 };
|
|
1241
1353
|
let lastErrorEvent;
|
|
1242
1354
|
const redrawChatFrame = () => {
|
|
@@ -1247,7 +1359,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1247
1359
|
model: currentModel ?? 'No model connected — use /connect',
|
|
1248
1360
|
tokens: Math.round(sessionTokens),
|
|
1249
1361
|
cost: costLabel(),
|
|
1250
|
-
usageAvailable: activeSession.usage.available,
|
|
1362
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
1251
1363
|
workspace: canonicalWorkspace,
|
|
1252
1364
|
mode: activeMode,
|
|
1253
1365
|
autoApprove: activeAutoApprove,
|
|
@@ -1255,6 +1367,8 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1255
1367
|
chatAnswer: displayAnswer,
|
|
1256
1368
|
chatThoughtTime: thoughtTime,
|
|
1257
1369
|
outputTokenRate,
|
|
1370
|
+
tokenUsage: turnUsage,
|
|
1371
|
+
budgetStatus: budgetStopped ? 'Budget reached' : budgetWarningIssued ? 'Budget ≥80%' : undefined,
|
|
1258
1372
|
input: commandQueue.currentDraft,
|
|
1259
1373
|
queuedCommands: commandQueue.items,
|
|
1260
1374
|
}, process.stdout.rows));
|
|
@@ -1293,7 +1407,7 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1293
1407
|
model: currentModel ?? 'No model connected',
|
|
1294
1408
|
tokens: Math.round(sessionTokens),
|
|
1295
1409
|
cost: costLabel(),
|
|
1296
|
-
usageAvailable: activeSession.usage.available,
|
|
1410
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
1297
1411
|
workspace: canonicalWorkspace,
|
|
1298
1412
|
mode: activeMode,
|
|
1299
1413
|
autoApprove: activeAutoApprove,
|
|
@@ -1352,16 +1466,36 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1352
1466
|
// Move to the chat layout before the provider can emit its first event.
|
|
1353
1467
|
redrawChatFrame();
|
|
1354
1468
|
thinkingTimer = setInterval(() => {
|
|
1355
|
-
if (!firstAssistantDeltaAt) {
|
|
1469
|
+
if (!firstAssistantDeltaAt && !turnUsage) {
|
|
1356
1470
|
process.stdout.write(renderChatThoughtTimeUpdate((Date.now() - startedAt) / 1000));
|
|
1357
1471
|
}
|
|
1358
1472
|
}, 400);
|
|
1359
1473
|
const evidenceTask = liveSearch ? buildSearchAnswerTask(effectivePrompt, liveSearch) : undefined;
|
|
1360
|
-
const runAgent = () => loop.run(evidenceTask ?? createAgentTask(effectivePrompt, activeMode), {
|
|
1361
|
-
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,
|
|
1362
1476
|
maxOutputTokens: args.maxTokens ?? config.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
1477
|
+
skillContext: skillContext(),
|
|
1363
1478
|
routeOptions: { pinnedModelId: currentModel === 'auto' ? undefined : currentModel, allowPaid: config.allowPaid ?? args.allowPaid, allowUnknown: config.allowUnknown ?? args.allowUnknown, isLocalProfile: args.profile.includes('local') },
|
|
1364
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
|
+
}
|
|
1365
1499
|
if (event.type === 'assistant_delta') {
|
|
1366
1500
|
if (!firstAssistantDeltaAt && event.delta.trim().length > 0) {
|
|
1367
1501
|
firstAssistantDeltaAt = Date.now();
|
|
@@ -1402,28 +1536,38 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1402
1536
|
const replacement = await replaceProviderKeyInteractive(activeConnection, { signal });
|
|
1403
1537
|
if (!replacement)
|
|
1404
1538
|
throw error;
|
|
1539
|
+
recordTurnUsage();
|
|
1540
|
+
turnUsage = undefined;
|
|
1541
|
+
usageRecorded = false;
|
|
1405
1542
|
const secured = process.platform === 'win32' ? await storeConnectionCredential(replacement, credentialStore) : replacement;
|
|
1406
1543
|
saveConnection(secured);
|
|
1407
1544
|
config = loadConfig();
|
|
1408
1545
|
activateConnection(secured);
|
|
1409
1546
|
result = await runAgent();
|
|
1410
1547
|
}
|
|
1411
|
-
if (
|
|
1548
|
+
if (turnUsage && !turnUsage.estimated) {
|
|
1412
1549
|
let pricing;
|
|
1413
1550
|
try {
|
|
1414
|
-
|
|
1551
|
+
if (usedModels.size <= 1)
|
|
1552
|
+
pricing = findModelPricing(await provider.discoverModels(signal), result.selectedModel.id);
|
|
1415
1553
|
}
|
|
1416
1554
|
catch {
|
|
1417
1555
|
// A valid answer remains usable when the provider catalog is unavailable.
|
|
1418
1556
|
}
|
|
1419
|
-
|
|
1420
|
-
sessionTokens = result.usage.totalTokens;
|
|
1421
|
-
if (firstAssistantDeltaAt)
|
|
1422
|
-
outputTokenRate = calculateOutputTokenRate(result.usage.completionTokens, Date.now() - firstAssistantDeltaAt);
|
|
1557
|
+
recordTurnUsage(pricing);
|
|
1423
1558
|
}
|
|
1559
|
+
recordTurnUsage();
|
|
1560
|
+
// Failed/cancelled tasks can still consume tokens, even without a saved answer.
|
|
1561
|
+
if (turnUsage)
|
|
1562
|
+
sessionStore.save(activeSession);
|
|
1424
1563
|
const otherConnections = Object.keys(config.connections ?? {}).filter((id) => id !== activeConnection?.id);
|
|
1425
1564
|
const resolution = resolveTurnAssistantAnswer(result, conversationHistory.length, lastErrorEvent, streamedAnswer, currentModel, otherConnections);
|
|
1426
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
|
+
}
|
|
1427
1571
|
if (resolution.isError) {
|
|
1428
1572
|
streamedAnswer = lastAnswer;
|
|
1429
1573
|
}
|
|
@@ -1450,6 +1594,9 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1450
1594
|
if (thinkingTimer)
|
|
1451
1595
|
clearInterval(thinkingTimer);
|
|
1452
1596
|
removeGenerationListener?.();
|
|
1597
|
+
recordTurnUsage();
|
|
1598
|
+
if (turnUsage)
|
|
1599
|
+
sessionStore.save(activeSession);
|
|
1453
1600
|
const otherConnections = Object.keys(config.connections ?? {}).filter((id) => id !== activeConnection?.id);
|
|
1454
1601
|
const altSuggestion = otherConnections.length > 0
|
|
1455
1602
|
? ` (other configured providers: ${otherConnections.join(', ')})`
|
|
@@ -1467,13 +1614,14 @@ export async function handleChatSession(args, version, signal) {
|
|
|
1467
1614
|
model: currentModel ?? 'No model connected — use /connect',
|
|
1468
1615
|
tokens: Math.round(sessionTokens),
|
|
1469
1616
|
cost: costLabel(),
|
|
1470
|
-
usageAvailable: activeSession.usage.available,
|
|
1617
|
+
usageAvailable: activeSession.usage.available, usageEstimated: activeSession.usage.estimated,
|
|
1471
1618
|
workspace: canonicalWorkspace,
|
|
1472
1619
|
mode: activeMode,
|
|
1473
1620
|
autoApprove: activeAutoApprove,
|
|
1474
1621
|
chatQuestion: lastQuestion,
|
|
1475
1622
|
chatAnswer: lastAnswer,
|
|
1476
1623
|
chatThoughtTime: lastThoughtTime,
|
|
1624
|
+
tokenUsage: turnUsage,
|
|
1477
1625
|
queuedCommands: commandQueue.items,
|
|
1478
1626
|
}, process.stdout.rows));
|
|
1479
1627
|
process.stdout.write(renderChatComposerCursor({ width: process.stdout.columns }, 0));
|