codeep 2.16.0 → 2.18.0
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 +29 -4
- package/dist/acp/serverHandlers.js +1 -1
- package/dist/api/index.js +7 -1
- package/dist/config/index.js +20 -4
- package/dist/config/providers.d.ts +12 -2
- package/dist/config/providers.js +223 -83
- package/dist/renderer/App.d.ts +12 -0
- package/dist/renderer/App.js +360 -40
- package/dist/renderer/Screen.d.ts +1 -0
- package/dist/renderer/Screen.js +8 -3
- package/dist/renderer/commands/helpers.d.ts +3 -0
- package/dist/renderer/commands/helpers.js +20 -3
- package/dist/renderer/commands.js +26 -4
- package/dist/renderer/components/AgentTimeline.d.ts +44 -0
- package/dist/renderer/components/AgentTimeline.js +157 -0
- package/dist/renderer/components/Status.d.ts +8 -0
- package/dist/renderer/main.js +78 -29
- package/dist/utils/agent.js +1 -1
- package/dist/utils/agents.d.ts +1 -1
- package/dist/utils/agents.js +1 -1
- package/dist/utils/checkpoints.d.ts +1 -1
- package/dist/utils/checkpoints.js +1 -1
- package/dist/utils/resourceImpact.d.ts +25 -0
- package/dist/utils/resourceImpact.js +54 -0
- package/dist/utils/tokenTracker.d.ts +6 -0
- package/dist/utils/tokenTracker.js +83 -47
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
|
@@ -166,6 +166,26 @@ export async function handleCommand(command, args, ctx) {
|
|
|
166
166
|
});
|
|
167
167
|
break;
|
|
168
168
|
}
|
|
169
|
+
if (providerId === 'modelscope') {
|
|
170
|
+
ctx.app.notify('Fetching ModelScope catalog…');
|
|
171
|
+
const { fetchOpenAiCompatibleModels, getApiKey: _getKey } = await import('../config/index.js');
|
|
172
|
+
const base = 'https://api-inference.modelscope.cn/v1';
|
|
173
|
+
const models = await fetchOpenAiCompatibleModels(base, _getKey('modelscope') || undefined);
|
|
174
|
+
const fallback = getModelsForCurrentProvider();
|
|
175
|
+
const available = models && models.length > 0
|
|
176
|
+
? models
|
|
177
|
+
: Object.keys(fallback).map(id => ({ id, name: id, description: 'Built-in fallback' }));
|
|
178
|
+
if (!models || models.length === 0) {
|
|
179
|
+
ctx.app.notify('Could not fetch the ModelScope catalog. Using the built-in fallback model.');
|
|
180
|
+
}
|
|
181
|
+
const modelItems = available.map(m => ({ key: m.id, label: m.name, description: m.description }));
|
|
182
|
+
const currentModel = config.get('model');
|
|
183
|
+
ctx.app.showSelect(`Select ModelScope Model (${available.length})`, modelItems, currentModel, (item) => {
|
|
184
|
+
config.set('model', item.key);
|
|
185
|
+
ctx.app.notify(`Model: ${item.key}`);
|
|
186
|
+
});
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
169
189
|
if (providerId === 'custom') {
|
|
170
190
|
const base = config.get('customBaseUrl') || 'http://localhost:8000/v1';
|
|
171
191
|
ctx.app.notify(`Fetching models from ${base}…`);
|
|
@@ -304,7 +324,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
304
324
|
const providerId = config.get('provider');
|
|
305
325
|
const model = config.get('model');
|
|
306
326
|
const supported = modelSupportsReasoningEffort(providerId, model);
|
|
307
|
-
// Tiers THIS model actually distinguishes (e.g.
|
|
327
|
+
// Tiers THIS model actually distinguishes (e.g. Kimi K3 → auto/low/high/max).
|
|
308
328
|
const available = availableReasoningTiers(providerId, model);
|
|
309
329
|
const sub = args[0]?.toLowerCase();
|
|
310
330
|
if (sub && REASONING_TIERS.includes(sub)) {
|
|
@@ -313,11 +333,11 @@ export async function handleCommand(command, args, ctx) {
|
|
|
313
333
|
ctx.app.notify('Thinking effort: auto — each model uses its own default.');
|
|
314
334
|
}
|
|
315
335
|
else if (!supported) {
|
|
316
|
-
ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x, Gemini 3, DeepSeek V4,
|
|
336
|
+
ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x, Gemini 3, DeepSeek V4, Kimi K3).`);
|
|
317
337
|
}
|
|
318
338
|
else {
|
|
319
339
|
// Tell the user what THIS model will actually run (the tier may
|
|
320
|
-
// collapse onto a level the model distinguishes, e.g.
|
|
340
|
+
// collapse onto a level the model distinguishes, e.g. medium→high on Kimi K3).
|
|
321
341
|
const resolved = resolveReasoningTier(providerId, model, sub);
|
|
322
342
|
const note = resolved === sub ? '' : ` (${model} runs this as "${resolved}")`;
|
|
323
343
|
ctx.app.notify(`Thinking effort: ${sub}${note} — sending ${JSON.stringify(reasoningParamsFor(providerId, model, sub))}.`);
|
|
@@ -346,7 +366,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
346
366
|
if (supported)
|
|
347
367
|
tLines.push(`**Available** ${available.join(' · ')}`);
|
|
348
368
|
tLines.push('');
|
|
349
|
-
tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (
|
|
369
|
+
tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (DeepSeek → high · max; Kimi K3 → low · high · max; Gemini → low · high; Opus/Sonnet & GPT-5.x → the full set). The setting is global and clamps to the active model, so it never sends a value the API rejects. `/effort` is an alias.');
|
|
350
370
|
ctx.app.addMessage({ role: 'system', content: tLines.join('\n') });
|
|
351
371
|
break;
|
|
352
372
|
}
|
|
@@ -2261,6 +2281,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2261
2281
|
// case no longer also claims 'cost' (which always hit the handler above).
|
|
2262
2282
|
case 'stats': {
|
|
2263
2283
|
const { getCostBreakdown, getSessionStats, formatTokenCount, getPricingTable, getCacheStats } = await import('../utils/tokenTracker.js');
|
|
2284
|
+
const { formatResourceImpactReport } = await import('../utils/resourceImpact.js');
|
|
2264
2285
|
const stats = getSessionStats();
|
|
2265
2286
|
const content = formatStatsReport({
|
|
2266
2287
|
totals: stats,
|
|
@@ -2269,6 +2290,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2269
2290
|
pricing: getPricingTable(),
|
|
2270
2291
|
currentProvider: config.get('provider'),
|
|
2271
2292
|
fmt: formatTokenCount,
|
|
2293
|
+
impactLines: formatResourceImpactReport(stats.totalTokens),
|
|
2272
2294
|
});
|
|
2273
2295
|
ctx.app.addMessage({ role: 'system', content });
|
|
2274
2296
|
break;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type TimelineStageId = 'PLAN' | 'READ' | 'EDIT' | 'VERIFY' | 'SUMMARY';
|
|
2
|
+
export type TimelineStageStatus = 'done' | 'active' | 'pending';
|
|
3
|
+
export interface TimelineAction {
|
|
4
|
+
type: string;
|
|
5
|
+
target: string;
|
|
6
|
+
result: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TimelineStage {
|
|
9
|
+
id: TimelineStageId;
|
|
10
|
+
status: TimelineStageStatus;
|
|
11
|
+
summary: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TimelineFile {
|
|
15
|
+
type: 'write' | 'edit' | 'delete' | 'mkdir';
|
|
16
|
+
target: string;
|
|
17
|
+
result: string;
|
|
18
|
+
}
|
|
19
|
+
export interface TimelineCheck {
|
|
20
|
+
target: string;
|
|
21
|
+
result: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AgentTimelineModel {
|
|
24
|
+
currentStage: TimelineStageId;
|
|
25
|
+
currentTarget: string;
|
|
26
|
+
stages: TimelineStage[];
|
|
27
|
+
files: TimelineFile[];
|
|
28
|
+
checks: TimelineCheck[];
|
|
29
|
+
progress: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function buildAgentTimelineModel(args: {
|
|
32
|
+
actions: TimelineAction[];
|
|
33
|
+
thinking: string;
|
|
34
|
+
waitingForAI: boolean;
|
|
35
|
+
iteration: number;
|
|
36
|
+
maxIterations: number;
|
|
37
|
+
}): AgentTimelineModel;
|
|
38
|
+
export declare function formatElapsed(milliseconds: number): string;
|
|
39
|
+
/**
|
|
40
|
+
* Truncate to a column budget, not a code-unit budget: Screen.write advances
|
|
41
|
+
* by charWidth, so a CJK/emoji prompt measured with String.length would draw
|
|
42
|
+
* up to twice as wide as the pane it was sized for.
|
|
43
|
+
*/
|
|
44
|
+
export declare function truncateMiddle(value: string, maxLength: number): string;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { charWidth, stringWidth } from '../ansi.js';
|
|
2
|
+
const READ_TYPES = new Set(['read', 'search', 'list', 'fetch']);
|
|
3
|
+
const EDIT_TYPES = new Set(['write', 'edit', 'delete', 'mkdir']);
|
|
4
|
+
function stageForAction(type) {
|
|
5
|
+
if (READ_TYPES.has(type))
|
|
6
|
+
return 'READ';
|
|
7
|
+
if (EDIT_TYPES.has(type))
|
|
8
|
+
return 'EDIT';
|
|
9
|
+
if (type === 'command')
|
|
10
|
+
return 'VERIFY';
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function parseThinking(thinking) {
|
|
14
|
+
const separator = thinking.indexOf(':');
|
|
15
|
+
if (separator < 0)
|
|
16
|
+
return { type: '', target: '' };
|
|
17
|
+
const type = thinking.slice(0, separator).trim().toLowerCase();
|
|
18
|
+
const knownType = READ_TYPES.has(type) || EDIT_TYPES.has(type) || type === 'command';
|
|
19
|
+
if (!knownType)
|
|
20
|
+
return { type: '', target: '' };
|
|
21
|
+
return {
|
|
22
|
+
type,
|
|
23
|
+
target: thinking.slice(separator + 1).trim(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function uniqueByTarget(items) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const result = [];
|
|
29
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
30
|
+
const item = items[index];
|
|
31
|
+
if (!item.target || seen.has(item.target))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(item.target);
|
|
34
|
+
result.unshift(item);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
export function buildAgentTimelineModel(args) {
|
|
39
|
+
const thinking = parseThinking(args.thinking);
|
|
40
|
+
const lastAction = args.actions[args.actions.length - 1];
|
|
41
|
+
const explicitStage = !args.waitingForAI ? stageForAction(thinking.type) : null;
|
|
42
|
+
const lastStage = lastAction ? stageForAction(lastAction.type) : null;
|
|
43
|
+
const currentStage = explicitStage
|
|
44
|
+
?? lastStage
|
|
45
|
+
?? 'PLAN';
|
|
46
|
+
const currentTarget = explicitStage
|
|
47
|
+
? thinking.target
|
|
48
|
+
: (lastAction?.target || thinking.target);
|
|
49
|
+
const readActions = args.actions.filter(action => READ_TYPES.has(action.type));
|
|
50
|
+
const fileActions = args.actions.filter((action) => EDIT_TYPES.has(action.type));
|
|
51
|
+
const commandActions = args.actions.filter(action => action.type === 'command');
|
|
52
|
+
const stageStatus = (id, hasCompletedAction) => {
|
|
53
|
+
if (id === currentStage)
|
|
54
|
+
return 'active';
|
|
55
|
+
if (id === 'PLAN' && (args.iteration > 0 || args.actions.length > 0))
|
|
56
|
+
return 'done';
|
|
57
|
+
if (hasCompletedAction)
|
|
58
|
+
return 'done';
|
|
59
|
+
return 'pending';
|
|
60
|
+
};
|
|
61
|
+
const stages = [
|
|
62
|
+
{
|
|
63
|
+
id: 'PLAN',
|
|
64
|
+
status: stageStatus('PLAN', args.iteration > 0),
|
|
65
|
+
summary: 'Understand the request and choose the next safe step',
|
|
66
|
+
detail: args.iteration > 0 ? `${args.iteration} model step${args.iteration === 1 ? '' : 's'}` : 'Building execution plan',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'READ',
|
|
70
|
+
status: stageStatus('READ', readActions.length > 0),
|
|
71
|
+
summary: 'Inspect project context and relevant files',
|
|
72
|
+
detail: readActions.length > 0
|
|
73
|
+
? `${readActions.length} read/search action${readActions.length === 1 ? '' : 's'}`
|
|
74
|
+
: 'Waiting for project inspection',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'EDIT',
|
|
78
|
+
status: stageStatus('EDIT', fileActions.some(action => action.result === 'success')),
|
|
79
|
+
summary: 'Apply focused changes to the workspace',
|
|
80
|
+
detail: fileActions.length > 0
|
|
81
|
+
? `${uniqueByTarget(fileActions).length} file${uniqueByTarget(fileActions).length === 1 ? '' : 's'} touched`
|
|
82
|
+
: 'No file changes yet',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'VERIFY',
|
|
86
|
+
status: stageStatus('VERIFY', commandActions.some(action => action.result === 'success')),
|
|
87
|
+
summary: 'Run commands and confirm the result',
|
|
88
|
+
detail: commandActions.length > 0
|
|
89
|
+
? `${commandActions.length} command${commandActions.length === 1 ? '' : 's'} run`
|
|
90
|
+
: 'Checks pending',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'SUMMARY',
|
|
94
|
+
status: 'pending',
|
|
95
|
+
summary: 'Summarize changes and next steps',
|
|
96
|
+
detail: 'Appears when the run completes',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
return {
|
|
100
|
+
currentStage,
|
|
101
|
+
currentTarget,
|
|
102
|
+
stages,
|
|
103
|
+
files: uniqueByTarget(fileActions),
|
|
104
|
+
checks: commandActions.slice(-4).map(action => ({
|
|
105
|
+
target: action.target,
|
|
106
|
+
result: action.result,
|
|
107
|
+
})),
|
|
108
|
+
progress: args.maxIterations > 0
|
|
109
|
+
? Math.min(Math.max(args.iteration / args.maxIterations, 0), 1)
|
|
110
|
+
: 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function formatElapsed(milliseconds) {
|
|
114
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
115
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
116
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
117
|
+
const seconds = totalSeconds % 60;
|
|
118
|
+
if (hours > 0) {
|
|
119
|
+
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
120
|
+
}
|
|
121
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Truncate to a column budget, not a code-unit budget: Screen.write advances
|
|
125
|
+
* by charWidth, so a CJK/emoji prompt measured with String.length would draw
|
|
126
|
+
* up to twice as wide as the pane it was sized for.
|
|
127
|
+
*/
|
|
128
|
+
export function truncateMiddle(value, maxLength) {
|
|
129
|
+
if (maxLength <= 0)
|
|
130
|
+
return '';
|
|
131
|
+
if (stringWidth(value) <= maxLength)
|
|
132
|
+
return value;
|
|
133
|
+
if (maxLength <= 3)
|
|
134
|
+
return '.'.repeat(maxLength);
|
|
135
|
+
const chars = [...value];
|
|
136
|
+
const leftBudget = Math.ceil((maxLength - 1) / 2);
|
|
137
|
+
const rightBudget = Math.floor((maxLength - 1) / 2);
|
|
138
|
+
let head = '';
|
|
139
|
+
let headWidth = 0;
|
|
140
|
+
for (const char of chars) {
|
|
141
|
+
const w = charWidth(char);
|
|
142
|
+
if (headWidth + w > leftBudget)
|
|
143
|
+
break;
|
|
144
|
+
head += char;
|
|
145
|
+
headWidth += w;
|
|
146
|
+
}
|
|
147
|
+
let tail = '';
|
|
148
|
+
let tailWidth = 0;
|
|
149
|
+
for (let index = chars.length - 1; index >= 0; index--) {
|
|
150
|
+
const w = charWidth(chars[index]);
|
|
151
|
+
if (tailWidth + w > rightBudget)
|
|
152
|
+
break;
|
|
153
|
+
tail = chars[index] + tail;
|
|
154
|
+
tailWidth += w;
|
|
155
|
+
}
|
|
156
|
+
return `${head}…${tail}`;
|
|
157
|
+
}
|
|
@@ -11,6 +11,7 @@ export interface StatusInfo {
|
|
|
11
11
|
* when non-auto AND the active model supports a graded knob; undefined hides it. */
|
|
12
12
|
reasoningEffort?: string;
|
|
13
13
|
projectPath: string;
|
|
14
|
+
branch?: string;
|
|
14
15
|
hasWriteAccess: boolean;
|
|
15
16
|
sessionId: string;
|
|
16
17
|
messageCount: number;
|
|
@@ -19,6 +20,13 @@ export interface StatusInfo {
|
|
|
19
20
|
promptTokens: number;
|
|
20
21
|
completionTokens: number;
|
|
21
22
|
requestCount: number;
|
|
23
|
+
estimatedCost?: number;
|
|
24
|
+
/** `estimatedCost` minus flat-fee entries — the only spend we may show in
|
|
25
|
+
* dollars (see providers.flatFee). */
|
|
26
|
+
billableCost?: number;
|
|
27
|
+
/** At least one entry came from a flat-fee provider, so the footer says
|
|
28
|
+
* "in plan" instead of pricing those tokens. */
|
|
29
|
+
hasFlatFeeUsage?: boolean;
|
|
22
30
|
};
|
|
23
31
|
}
|
|
24
32
|
/**
|
package/dist/renderer/main.js
CHANGED
|
@@ -16,8 +16,8 @@ import { config, loadApiKey, loadAllApiKeys, getCurrentProvider, autoSaveSession
|
|
|
16
16
|
import { isProjectDirectory, getProjectContext, } from '../utils/project.js';
|
|
17
17
|
import { getCurrentVersion, checkForUpdates, getUpdateInstructions } from '../utils/update.js';
|
|
18
18
|
import { getProviderList, isNoApiKeyProvider, resolveReasoningTier } from '../config/providers.js';
|
|
19
|
-
import { getSessionStats, getCostBreakdown } from '../utils/tokenTracker.js';
|
|
20
|
-
import { isGitRepository } from '../utils/git.js';
|
|
19
|
+
import { getSessionStats, getCostBreakdown, getRecordCount } from '../utils/tokenTracker.js';
|
|
20
|
+
import { getGitStatus, isGitRepository } from '../utils/git.js';
|
|
21
21
|
import { reportStats, syncSession, generateProjectId } from '../utils/codeepCloud.js';
|
|
22
22
|
import { checkApiRateLimit } from '../utils/ratelimit.js';
|
|
23
23
|
import { expandFileAndFolderMentions, expandGitMentions } from '../utils/mentions.js';
|
|
@@ -27,6 +27,9 @@ import { logAppError } from '../utils/logger.js';
|
|
|
27
27
|
import { executeAgentTask, runAgentTask, } from './agentExecution.js';
|
|
28
28
|
// ─── Global state ─────────────────────────────────────────────────────────────
|
|
29
29
|
let projectPath = process.cwd();
|
|
30
|
+
/** Cached header branch. Resolved on first use so `--version`/`--help` never
|
|
31
|
+
* shell out to git, and cached because getStatus runs on every render frame. */
|
|
32
|
+
let gitBranchCache = null;
|
|
30
33
|
let projectContext = null;
|
|
31
34
|
let hasWriteAccess = false;
|
|
32
35
|
let sessionId = getCurrentSessionId();
|
|
@@ -34,6 +37,14 @@ let app;
|
|
|
34
37
|
/** Human-readable session name derived from the first user message */
|
|
35
38
|
let sessionDisplayName = null;
|
|
36
39
|
const addedFiles = new Map();
|
|
40
|
+
/** Branch shown in the persistent header, re-read whenever the project moved
|
|
41
|
+
* or an agent run finished (an agent can check out a different branch). */
|
|
42
|
+
function getHeaderBranch() {
|
|
43
|
+
if (!gitBranchCache || gitBranchCache.path !== projectPath) {
|
|
44
|
+
gitBranchCache = { path: projectPath, branch: getGitStatus(projectPath).branch };
|
|
45
|
+
}
|
|
46
|
+
return gitBranchCache.branch;
|
|
47
|
+
}
|
|
37
48
|
/** Derive a short display name from a user message (first ~5 words, max 48 chars). */
|
|
38
49
|
export function deriveSessionName(message) {
|
|
39
50
|
const clean = message.replace(/\s+/g, ' ').trim();
|
|
@@ -55,7 +66,10 @@ function makeCtx() {
|
|
|
55
66
|
sessionDisplayName: sessionDisplayName ?? undefined,
|
|
56
67
|
abortController: agentAbortController,
|
|
57
68
|
isAgentRunning: () => isAgentRunningFlag,
|
|
58
|
-
|
|
69
|
+
// A finished run may have switched branches — drop the cache so the next
|
|
70
|
+
// header render re-reads it.
|
|
71
|
+
setAgentRunning: (v) => { isAgentRunningFlag = v; if (!v)
|
|
72
|
+
gitBranchCache = null; },
|
|
59
73
|
setAbortController: (ctrl) => { agentAbortController = ctrl; },
|
|
60
74
|
formatAddedFilesContext,
|
|
61
75
|
handleCommand: (cmd, args) => dispatchCommand(cmd, args, makeCtx()),
|
|
@@ -77,7 +91,7 @@ function getStatus() {
|
|
|
77
91
|
const stats = getSessionStats();
|
|
78
92
|
// Show the thinking-effort tier beside the model. Resolve the (global) tier
|
|
79
93
|
// to what THIS model actually runs — e.g. a global 'low' shows as 'high' on
|
|
80
|
-
//
|
|
94
|
+
// Kimi K3, where the global medium tier resolves to high. 'auto'/unsupported → hidden.
|
|
81
95
|
const resolved = resolveReasoningTier(provider.id, config.get('model'), config.get('reasoningEffort'));
|
|
82
96
|
const reasoningEffort = resolved !== 'auto' ? resolved : undefined;
|
|
83
97
|
return {
|
|
@@ -87,6 +101,7 @@ function getStatus() {
|
|
|
87
101
|
agentMode: config.get('agentMode') || 'off',
|
|
88
102
|
reasoningEffort,
|
|
89
103
|
projectPath,
|
|
104
|
+
branch: getHeaderBranch(),
|
|
90
105
|
hasWriteAccess,
|
|
91
106
|
sessionId,
|
|
92
107
|
messageCount: app ? app.getMessages().length : 0,
|
|
@@ -95,6 +110,9 @@ function getStatus() {
|
|
|
95
110
|
promptTokens: stats.totalPromptTokens,
|
|
96
111
|
completionTokens: stats.totalCompletionTokens,
|
|
97
112
|
requestCount: stats.requestCount,
|
|
113
|
+
estimatedCost: stats.estimatedCost,
|
|
114
|
+
billableCost: stats.billableCost,
|
|
115
|
+
hasFlatFeeUsage: stats.hasFlatFeeUsage,
|
|
98
116
|
},
|
|
99
117
|
};
|
|
100
118
|
}
|
|
@@ -162,6 +180,47 @@ async function handleSubmit(message) {
|
|
|
162
180
|
runAgentTask(message, false, ctx, () => pendingInteractiveContext, (v) => { pendingInteractiveContext = v; });
|
|
163
181
|
return;
|
|
164
182
|
}
|
|
183
|
+
// Captured before the turn starts so the delta can be reported from both the
|
|
184
|
+
// success path and the catch. An aborted or failed turn has still burned
|
|
185
|
+
// tokens, and gracefulShutdown no longer sends a cumulative catch-all that
|
|
186
|
+
// would have swept them up later.
|
|
187
|
+
const tokenReportStart = getRecordCount();
|
|
188
|
+
// Cloud stats are append-only events, so report only this prompt's delta.
|
|
189
|
+
// Sending the full session accumulator after every prompt makes totals grow
|
|
190
|
+
// 1× + 2× + 3× and is the source of the inflated dashboard token count.
|
|
191
|
+
// pingWhenEmpty sends a bare session event when nothing was spent — wanted on
|
|
192
|
+
// the success path, not when the turn failed before reaching the model.
|
|
193
|
+
const reportTurnStats = (pingWhenEmpty) => {
|
|
194
|
+
const sharedFields = {
|
|
195
|
+
sessionId,
|
|
196
|
+
sessionName: sessionDisplayName || sessionId,
|
|
197
|
+
messageCount: app.getMessages().length,
|
|
198
|
+
cliVersion: getCurrentVersion(),
|
|
199
|
+
projectName: projectContext?.name,
|
|
200
|
+
projectId: projectPath ? generateProjectId(projectPath) : undefined,
|
|
201
|
+
language: projectContext?.type,
|
|
202
|
+
isGit: isGitRepository(process.cwd()),
|
|
203
|
+
};
|
|
204
|
+
const costBreakdown = getCostBreakdown(tokenReportStart);
|
|
205
|
+
if (costBreakdown.length === 0) {
|
|
206
|
+
if (pingWhenEmpty) {
|
|
207
|
+
reportStats({ ...sharedFields, model: config.get('model'), provider: config.get('provider') });
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const entry of costBreakdown) {
|
|
212
|
+
reportStats({
|
|
213
|
+
...sharedFields,
|
|
214
|
+
model: entry.model,
|
|
215
|
+
provider: entry.provider,
|
|
216
|
+
inputTokens: entry.promptTokens || undefined,
|
|
217
|
+
outputTokens: entry.completionTokens || undefined,
|
|
218
|
+
cacheCreationTokens: entry.cacheCreationTokens || undefined,
|
|
219
|
+
cacheReadTokens: entry.cacheReadTokens || undefined,
|
|
220
|
+
estimatedCost: entry.estimatedCost || undefined,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
165
224
|
try {
|
|
166
225
|
app.startStreaming();
|
|
167
226
|
const history = app.getChatHistory();
|
|
@@ -226,7 +285,10 @@ async function handleSubmit(message) {
|
|
|
226
285
|
language: projectContext?.type,
|
|
227
286
|
isGit: isGitRepository(process.cwd()),
|
|
228
287
|
};
|
|
229
|
-
|
|
288
|
+
// Cloud stats are append-only events, so report only this prompt's delta.
|
|
289
|
+
// Sending the full session accumulator after every prompt makes totals grow
|
|
290
|
+
// 1× + 2× + 3× and is the source of the inflated dashboard token count.
|
|
291
|
+
const costBreakdown = getCostBreakdown(tokenReportStart);
|
|
230
292
|
if (costBreakdown.length > 0) {
|
|
231
293
|
for (const entry of costBreakdown) {
|
|
232
294
|
reportStats({
|
|
@@ -938,31 +1000,18 @@ async function gracefulShutdown() {
|
|
|
938
1000
|
return;
|
|
939
1001
|
const messages = app.getMessages();
|
|
940
1002
|
autoSaveSession(messages, projectPath);
|
|
941
|
-
const { syncSessionAsync,
|
|
942
|
-
const tokenStats = getSessionStats();
|
|
1003
|
+
const { syncSessionAsync, generateProjectId } = require('../utils/codeepCloud.js');
|
|
943
1004
|
const projectId = projectPath ? generateProjectId(projectPath) : undefined;
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
sessionId,
|
|
955
|
-
sessionName: sessionId,
|
|
956
|
-
messageCount: messages.length,
|
|
957
|
-
projectName: projectContext?.name,
|
|
958
|
-
projectId,
|
|
959
|
-
inputTokens: tokenStats.totalPromptTokens || undefined,
|
|
960
|
-
outputTokens: tokenStats.totalCompletionTokens || undefined,
|
|
961
|
-
cacheCreationTokens: tokenStats.totalCacheCreationTokens || undefined,
|
|
962
|
-
cacheReadTokens: tokenStats.totalCacheReadTokens || undefined,
|
|
963
|
-
estimatedCost: tokenStats.estimatedCost || undefined,
|
|
964
|
-
}),
|
|
965
|
-
]);
|
|
1005
|
+
// Successful manual and agent turns report their token deltas immediately.
|
|
1006
|
+
// Re-sending the cumulative session total here would count every token a
|
|
1007
|
+
// second time when the append-only dashboard endpoint stores this event.
|
|
1008
|
+
await syncSessionAsync({
|
|
1009
|
+
sessionId,
|
|
1010
|
+
sessionName: sessionDisplayName || sessionId,
|
|
1011
|
+
projectName: projectContext?.name,
|
|
1012
|
+
projectId,
|
|
1013
|
+
messages,
|
|
1014
|
+
});
|
|
966
1015
|
}
|
|
967
1016
|
// ─── Last-resort crash handlers ───────────────────────────────────────────────
|
|
968
1017
|
// Without these, a stray throw or rejected promise (deep in the agent loop or a
|
package/dist/utils/agent.js
CHANGED
|
@@ -163,7 +163,7 @@ export function buildPausedResult(kind, ctx) {
|
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
165
|
const DEFAULT_OPTIONS = {
|
|
166
|
-
// Modern models (GLM-5.
|
|
166
|
+
// Modern models (GLM-5.2, Claude 5, GPT-5.x) complete typical coding tasks in
|
|
167
167
|
// 3–8 iterations. The old cap of 100 mostly let broken loops wander for minutes
|
|
168
168
|
// before giving up. 25 is still generous — covers multi-file refactors — without
|
|
169
169
|
// turning small fixes into marathons. Users can still raise this via /settings.
|
package/dist/utils/agents.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* name: reviewer
|
|
19
19
|
* description: Reviews a diff for correctness & security
|
|
20
20
|
* tools: [read_file, search_code, execute_command] # allowlist; omit = all
|
|
21
|
-
* model: glm-5.
|
|
21
|
+
* model: glm-5.2 # optional provider/model or model override
|
|
22
22
|
* personality: security # optional — reuse a personality preset
|
|
23
23
|
* maxIterations: 15 # optional budget
|
|
24
24
|
* ---
|
package/dist/utils/agents.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* name: reviewer
|
|
19
19
|
* description: Reviews a diff for correctness & security
|
|
20
20
|
* tools: [read_file, search_code, execute_command] # allowlist; omit = all
|
|
21
|
-
* model: glm-5.
|
|
21
|
+
* model: glm-5.2 # optional provider/model or model override
|
|
22
22
|
* personality: security # optional — reuse a personality preset
|
|
23
23
|
* maxIterations: 15 # optional budget
|
|
24
24
|
* ---
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* "createdAt": "...",
|
|
28
28
|
* "sessionId": "session-2026-05-18-...",
|
|
29
29
|
* "provider": "z.ai",
|
|
30
|
-
* "model": "glm-5.
|
|
30
|
+
* "model": "glm-5.2",
|
|
31
31
|
* "messages": [ ... ],
|
|
32
32
|
* "filesTouched": ["src/a.ts", "src/b.ts"],
|
|
33
33
|
* "gitHead": "abcdef0" // optional, recorded only if cwd is a git repo
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* "createdAt": "...",
|
|
28
28
|
* "sessionId": "session-2026-05-18-...",
|
|
29
29
|
* "provider": "z.ai",
|
|
30
|
-
* "model": "glm-5.
|
|
30
|
+
* "model": "glm-5.2",
|
|
31
31
|
* "messages": [ ... ],
|
|
32
32
|
* "filesTouched": ["src/a.ts", "src/b.ts"],
|
|
33
33
|
* "gitHead": "abcdef0" // optional, recorded only if cwd is a git repo
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broad operational-impact estimate for hosted LLM inference.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately a range, not a meter reading:
|
|
5
|
+
* - 0.3–1.5 J/token covers published H100 inference benchmarks.
|
|
6
|
+
* - 0.27–1.08 L/kWh spans efficient direct cooling and a full-stack
|
|
7
|
+
* production estimate that also captures associated infrastructure.
|
|
8
|
+
*
|
|
9
|
+
* Model size, batching, context length, hardware, data-centre location and
|
|
10
|
+
* provider efficiency can move the real result outside this band. Local
|
|
11
|
+
* models are included because their token usage still consumes electricity,
|
|
12
|
+
* but Codeep cannot measure the device directly.
|
|
13
|
+
*/
|
|
14
|
+
export interface ResourceImpactEstimate {
|
|
15
|
+
energyWhLow: number;
|
|
16
|
+
energyWhHigh: number;
|
|
17
|
+
waterMlLow: number;
|
|
18
|
+
waterMlHigh: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function estimateResourceImpact(totalTokens: number): ResourceImpactEstimate;
|
|
21
|
+
export declare function formatResourceImpact(estimate: ResourceImpactEstimate): {
|
|
22
|
+
energy: string;
|
|
23
|
+
water: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function formatResourceImpactReport(totalTokens: number): string[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broad operational-impact estimate for hosted LLM inference.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately a range, not a meter reading:
|
|
5
|
+
* - 0.3–1.5 J/token covers published H100 inference benchmarks.
|
|
6
|
+
* - 0.27–1.08 L/kWh spans efficient direct cooling and a full-stack
|
|
7
|
+
* production estimate that also captures associated infrastructure.
|
|
8
|
+
*
|
|
9
|
+
* Model size, batching, context length, hardware, data-centre location and
|
|
10
|
+
* provider efficiency can move the real result outside this band. Local
|
|
11
|
+
* models are included because their token usage still consumes electricity,
|
|
12
|
+
* but Codeep cannot measure the device directly.
|
|
13
|
+
*/
|
|
14
|
+
const ENERGY_JOULES_PER_TOKEN = { low: 0.3, high: 1.5 };
|
|
15
|
+
const WATER_LITRES_PER_KWH = { low: 0.27, high: 1.08 };
|
|
16
|
+
export function estimateResourceImpact(totalTokens) {
|
|
17
|
+
const tokens = Number.isFinite(totalTokens) ? Math.max(0, totalTokens) : 0;
|
|
18
|
+
const energyWhLow = (tokens * ENERGY_JOULES_PER_TOKEN.low) / 3600;
|
|
19
|
+
const energyWhHigh = (tokens * ENERGY_JOULES_PER_TOKEN.high) / 3600;
|
|
20
|
+
return {
|
|
21
|
+
energyWhLow,
|
|
22
|
+
energyWhHigh,
|
|
23
|
+
waterMlLow: (energyWhLow / 1000) * WATER_LITRES_PER_KWH.low * 1000,
|
|
24
|
+
waterMlHigh: (energyWhHigh / 1000) * WATER_LITRES_PER_KWH.high * 1000,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function compact(value) {
|
|
28
|
+
if (value === 0)
|
|
29
|
+
return '0';
|
|
30
|
+
if (value < 0.01)
|
|
31
|
+
return value.toFixed(3);
|
|
32
|
+
if (value < 1)
|
|
33
|
+
return value.toFixed(2);
|
|
34
|
+
if (value < 10)
|
|
35
|
+
return value.toFixed(1);
|
|
36
|
+
return Math.round(value).toLocaleString('en-US');
|
|
37
|
+
}
|
|
38
|
+
export function formatResourceImpact(estimate) {
|
|
39
|
+
const energy = estimate.energyWhHigh >= 1000
|
|
40
|
+
? `${compact(estimate.energyWhLow / 1000)}–${compact(estimate.energyWhHigh / 1000)} kWh`
|
|
41
|
+
: `${compact(estimate.energyWhLow)}–${compact(estimate.energyWhHigh)} Wh`;
|
|
42
|
+
const water = estimate.waterMlHigh >= 1000
|
|
43
|
+
? `${compact(estimate.waterMlLow / 1000)}–${compact(estimate.waterMlHigh / 1000)} L`
|
|
44
|
+
: `${compact(estimate.waterMlLow)}–${compact(estimate.waterMlHigh)} mL`;
|
|
45
|
+
return { energy, water };
|
|
46
|
+
}
|
|
47
|
+
export function formatResourceImpactReport(totalTokens) {
|
|
48
|
+
const formatted = formatResourceImpact(estimateResourceImpact(totalTokens));
|
|
49
|
+
return [
|
|
50
|
+
'### Estimated compute impact',
|
|
51
|
+
`**Electricity:** ${formatted.energy} · **Cooling water:** ${formatted.water}`,
|
|
52
|
+
'_Research-based range, not a provider measurement. Actual usage varies widely by model, hardware, batching, context length, data-centre location, and cooling system._',
|
|
53
|
+
];
|
|
54
|
+
}
|
|
@@ -19,6 +19,12 @@ export interface SessionTokenStats {
|
|
|
19
19
|
totalTokens: number;
|
|
20
20
|
requestCount: number;
|
|
21
21
|
estimatedCost: number;
|
|
22
|
+
/** `estimatedCost` minus every flat-fee entry — the only figure we may show
|
|
23
|
+
* as a dollar total, since flat-fee tokens carry no per-token charge. */
|
|
24
|
+
billableCost: number;
|
|
25
|
+
/** True when at least one entry came from a flat-fee provider, so callers can
|
|
26
|
+
* say "included in plan" instead of silently dropping that usage. */
|
|
27
|
+
hasFlatFeeUsage: boolean;
|
|
22
28
|
/** Anthropic prompt caching: total tokens written to cache this session. */
|
|
23
29
|
totalCacheCreationTokens: number;
|
|
24
30
|
/** Anthropic prompt caching: total tokens read from cache this session. */
|