codeep 2.15.0 → 2.17.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 +41 -7
- package/dist/acp/serverHandlers.js +1 -1
- package/dist/acp/session.js +22 -1
- package/dist/config/index.js +20 -4
- package/dist/config/providers.d.ts +3 -2
- package/dist/config/providers.js +163 -69
- package/dist/renderer/App.d.ts +89 -0
- package/dist/renderer/App.js +637 -43
- package/dist/renderer/Screen.d.ts +1 -0
- package/dist/renderer/Screen.js +8 -3
- package/dist/renderer/commands/helpers.d.ts +189 -0
- package/dist/renderer/commands/helpers.js +345 -0
- package/dist/renderer/commands/registry.js +2 -1
- package/dist/renderer/commands.js +218 -267
- package/dist/renderer/components/AgentTimeline.d.ts +44 -0
- package/dist/renderer/components/AgentTimeline.js +157 -0
- package/dist/renderer/components/Autocomplete.d.ts +25 -0
- package/dist/renderer/components/Autocomplete.js +35 -0
- package/dist/renderer/components/Status.d.ts +2 -0
- package/dist/renderer/layout.d.ts +5 -1
- package/dist/renderer/layout.js +12 -0
- package/dist/renderer/main.js +110 -30
- 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/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/resourceImpact.d.ts +25 -0
- package/dist/utils/resourceImpact.js +54 -0
- package/dist/utils/tokenTracker.js +52 -37
- package/dist/utils/webFetch.d.ts +101 -0
- package/dist/utils/webFetch.js +375 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
|
@@ -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
|
+
}
|
|
@@ -31,3 +31,28 @@ export interface AutocompleteResult {
|
|
|
31
31
|
* @returns Match list, or `null` when the dropdown should be hidden.
|
|
32
32
|
*/
|
|
33
33
|
export declare function filterCommands(value: string, commands: string[]): AutocompleteResult | null;
|
|
34
|
+
/**
|
|
35
|
+
* The position and query of an in-progress `@mention`, or `null` when
|
|
36
|
+
* the cursor isn't inside a mention being typed.
|
|
37
|
+
*
|
|
38
|
+
* A mention is "in progress" when, scanning backwards from `cursorPos`:
|
|
39
|
+
* 1. We find an `@`.
|
|
40
|
+
* 2. Between `@` and the cursor there are only "path characters"
|
|
41
|
+
* (letters, digits, `/`, `.`, `_`, `-`, `\`) and no whitespace.
|
|
42
|
+
* 3. The `@` itself is at the start of the string OR preceded by a
|
|
43
|
+
* boundary char (space, `(`, `[`, …) — so `user@host` doesn't
|
|
44
|
+
* count. Mirrors `extractMentions` in `utils/mentions.ts`.
|
|
45
|
+
*/
|
|
46
|
+
export interface MentionQuery {
|
|
47
|
+
/** Start index of the `@` in the source string. */
|
|
48
|
+
atStart: number;
|
|
49
|
+
/** The text typed so far after the `@` (may be empty). */
|
|
50
|
+
query: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Detect whether the cursor sits inside an `@mention` being typed, and
|
|
54
|
+
* if so, return the query text (everything after `@`). Pure — no FS.
|
|
55
|
+
*
|
|
56
|
+
* Used by the autocomplete layer to know when to show the file picker.
|
|
57
|
+
*/
|
|
58
|
+
export declare function detectMentionQuery(text: string, cursorPos: number): MentionQuery | null;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* only triggered for command-shaped input) can be unit-tested without
|
|
6
6
|
* the editor / render machinery.
|
|
7
7
|
*/
|
|
8
|
+
import { MENTION_BOUNDARY } from '../../utils/mentions.js';
|
|
8
9
|
/**
|
|
9
10
|
* Filter `commands` to those that start with the typed prefix and the
|
|
10
11
|
* dropdown should appear.
|
|
@@ -38,3 +39,37 @@ export function filterCommands(value, commands) {
|
|
|
38
39
|
return { items: [], index: 0 };
|
|
39
40
|
return { items, index: 0 };
|
|
40
41
|
}
|
|
42
|
+
const PATH_CHAR = /[A-Za-z0-9._\/\\-]/;
|
|
43
|
+
/**
|
|
44
|
+
* Detect whether the cursor sits inside an `@mention` being typed, and
|
|
45
|
+
* if so, return the query text (everything after `@`). Pure — no FS.
|
|
46
|
+
*
|
|
47
|
+
* Used by the autocomplete layer to know when to show the file picker.
|
|
48
|
+
*/
|
|
49
|
+
export function detectMentionQuery(text, cursorPos) {
|
|
50
|
+
if (cursorPos < 1 || cursorPos > text.length)
|
|
51
|
+
return null;
|
|
52
|
+
// Scan backwards from the cursor, collecting path chars until we hit `@`.
|
|
53
|
+
let i = cursorPos - 1;
|
|
54
|
+
let query = '';
|
|
55
|
+
while (i >= 0) {
|
|
56
|
+
const ch = text[i];
|
|
57
|
+
if (ch === '@') {
|
|
58
|
+
// Found the `@`. Check the preceding char is a boundary (or start).
|
|
59
|
+
// Boundary set mirrors `extractMentions` in `utils/mentions.ts`.
|
|
60
|
+
const before = i > 0 ? text[i - 1] : '';
|
|
61
|
+
// Keep in lockstep with `MENTION_RE`'s lookbehind in utils/mentions.ts.
|
|
62
|
+
// If this set is looser, the picker offers a completion the expander
|
|
63
|
+
// then refuses to treat as a mention and the file is never attached.
|
|
64
|
+
if (before === '' || MENTION_BOUNDARY.test(before)) {
|
|
65
|
+
return { atStart: i, query };
|
|
66
|
+
}
|
|
67
|
+
return null; // `@` not at a boundary → email/handle, not a mention.
|
|
68
|
+
}
|
|
69
|
+
if (!PATH_CHAR.test(ch))
|
|
70
|
+
return null; // hit a non-path char before `@`.
|
|
71
|
+
query = ch + query;
|
|
72
|
+
i--;
|
|
73
|
+
}
|
|
74
|
+
return null; // no `@` found before the cursor.
|
|
75
|
+
}
|
|
@@ -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,7 @@ export interface StatusInfo {
|
|
|
19
20
|
promptTokens: number;
|
|
20
21
|
completionTokens: number;
|
|
21
22
|
requestCount: number;
|
|
23
|
+
estimatedCost?: number;
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
@@ -38,6 +38,9 @@ export interface LayoutSnapshot {
|
|
|
38
38
|
readonly settingsCount: number;
|
|
39
39
|
readonly showAutocomplete: boolean;
|
|
40
40
|
readonly autocompleteItemCount: number;
|
|
41
|
+
readonly hunkPickerOpen: boolean;
|
|
42
|
+
readonly mentionPickerOpen: boolean;
|
|
43
|
+
readonly mentionItemCount: number;
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
43
46
|
* Compute how many terminal rows the bottom panel (paste info, agent box,
|
|
@@ -144,7 +147,7 @@ export declare function statusBarRightHint(args: {
|
|
|
144
147
|
isLoading: boolean;
|
|
145
148
|
}): string;
|
|
146
149
|
/** The panel that currently owns keyboard focus, in priority order. */
|
|
147
|
-
export type ActivePanel = 'pasteInfo' | 'permission' | 'sessionPicker' | 'confirm' | 'status' | 'help' | 'settings' | 'search' | 'export' | 'logout' | 'login' | 'menu' | 'autocomplete' | 'chat';
|
|
150
|
+
export type ActivePanel = 'pasteInfo' | 'permission' | 'sessionPicker' | 'confirm' | 'status' | 'help' | 'settings' | 'search' | 'export' | 'logout' | 'login' | 'menu' | 'autocomplete' | 'hunkPicker' | 'chat';
|
|
148
151
|
export interface PanelState {
|
|
149
152
|
readonly pasteInfoOpen: boolean;
|
|
150
153
|
readonly permissionOpen: boolean;
|
|
@@ -159,6 +162,7 @@ export interface PanelState {
|
|
|
159
162
|
readonly loginOpen: boolean;
|
|
160
163
|
readonly menuOpen: boolean;
|
|
161
164
|
readonly showAutocomplete: boolean;
|
|
165
|
+
readonly hunkPickerOpen: boolean;
|
|
162
166
|
}
|
|
163
167
|
/**
|
|
164
168
|
* Return the highest-priority open panel. `chat` is the fallback when no
|
package/dist/renderer/layout.js
CHANGED
|
@@ -36,6 +36,10 @@ export function bottomPanelHeight(s) {
|
|
|
36
36
|
if (s.confirmOpen) {
|
|
37
37
|
return s.confirmMessageCount + 5; // title + messages + buttons + padding
|
|
38
38
|
}
|
|
39
|
+
if (s.hunkPickerOpen) {
|
|
40
|
+
// Title + progress + path + header + up to 12 diff lines + more marker + legend.
|
|
41
|
+
return 18;
|
|
42
|
+
}
|
|
39
43
|
if (s.statusOpen) {
|
|
40
44
|
return 16; // Status info panel
|
|
41
45
|
}
|
|
@@ -65,6 +69,12 @@ export function bottomPanelHeight(s) {
|
|
|
65
69
|
if (s.showAutocomplete && s.autocompleteItemCount > 0) {
|
|
66
70
|
return Math.min(s.autocompleteItemCount + 3, 12);
|
|
67
71
|
}
|
|
72
|
+
// `@`-mention picker: separator + title + up to 8 rows + footer. Without
|
|
73
|
+
// this branch the panel measured 0 while the picker still painted its rows,
|
|
74
|
+
// so it drew straight over the bottom of the chat transcript.
|
|
75
|
+
if (s.mentionPickerOpen && s.mentionItemCount > 0) {
|
|
76
|
+
return Math.min(s.mentionItemCount, 8) + 3;
|
|
77
|
+
}
|
|
68
78
|
return 0;
|
|
69
79
|
}
|
|
70
80
|
export function chatLayout(height, panelHeight) {
|
|
@@ -237,6 +247,8 @@ export function activePanel(s) {
|
|
|
237
247
|
return 'login';
|
|
238
248
|
if (s.menuOpen)
|
|
239
249
|
return 'menu';
|
|
250
|
+
if (s.hunkPickerOpen)
|
|
251
|
+
return 'hunkPicker';
|
|
240
252
|
if (s.showAutocomplete)
|
|
241
253
|
return 'autocomplete';
|
|
242
254
|
return 'chat';
|
package/dist/renderer/main.js
CHANGED
|
@@ -16,15 +16,20 @@ 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
|
+
import { expandFileAndFolderMentions, expandGitMentions } from '../utils/mentions.js';
|
|
24
|
+
import { expandWebMentions } from '../utils/webFetch.js';
|
|
23
25
|
import { handleCommand as dispatchCommand } from './commands.js';
|
|
24
26
|
import { logAppError } from '../utils/logger.js';
|
|
25
27
|
import { executeAgentTask, runAgentTask, } from './agentExecution.js';
|
|
26
28
|
// ─── Global state ─────────────────────────────────────────────────────────────
|
|
27
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;
|
|
28
33
|
let projectContext = null;
|
|
29
34
|
let hasWriteAccess = false;
|
|
30
35
|
let sessionId = getCurrentSessionId();
|
|
@@ -32,6 +37,14 @@ let app;
|
|
|
32
37
|
/** Human-readable session name derived from the first user message */
|
|
33
38
|
let sessionDisplayName = null;
|
|
34
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
|
+
}
|
|
35
48
|
/** Derive a short display name from a user message (first ~5 words, max 48 chars). */
|
|
36
49
|
export function deriveSessionName(message) {
|
|
37
50
|
const clean = message.replace(/\s+/g, ' ').trim();
|
|
@@ -53,7 +66,10 @@ function makeCtx() {
|
|
|
53
66
|
sessionDisplayName: sessionDisplayName ?? undefined,
|
|
54
67
|
abortController: agentAbortController,
|
|
55
68
|
isAgentRunning: () => isAgentRunningFlag,
|
|
56
|
-
|
|
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; },
|
|
57
73
|
setAbortController: (ctrl) => { agentAbortController = ctrl; },
|
|
58
74
|
formatAddedFilesContext,
|
|
59
75
|
handleCommand: (cmd, args) => dispatchCommand(cmd, args, makeCtx()),
|
|
@@ -75,7 +91,7 @@ function getStatus() {
|
|
|
75
91
|
const stats = getSessionStats();
|
|
76
92
|
// Show the thinking-effort tier beside the model. Resolve the (global) tier
|
|
77
93
|
// to what THIS model actually runs — e.g. a global 'low' shows as 'high' on
|
|
78
|
-
//
|
|
94
|
+
// Kimi K3, where the global medium tier resolves to high. 'auto'/unsupported → hidden.
|
|
79
95
|
const resolved = resolveReasoningTier(provider.id, config.get('model'), config.get('reasoningEffort'));
|
|
80
96
|
const reasoningEffort = resolved !== 'auto' ? resolved : undefined;
|
|
81
97
|
return {
|
|
@@ -85,6 +101,7 @@ function getStatus() {
|
|
|
85
101
|
agentMode: config.get('agentMode') || 'off',
|
|
86
102
|
reasoningEffort,
|
|
87
103
|
projectPath,
|
|
104
|
+
branch: getHeaderBranch(),
|
|
88
105
|
hasWriteAccess,
|
|
89
106
|
sessionId,
|
|
90
107
|
messageCount: app ? app.getMessages().length : 0,
|
|
@@ -93,6 +110,7 @@ function getStatus() {
|
|
|
93
110
|
promptTokens: stats.totalPromptTokens,
|
|
94
111
|
completionTokens: stats.totalCompletionTokens,
|
|
95
112
|
requestCount: stats.requestCount,
|
|
113
|
+
estimatedCost: stats.estimatedCost,
|
|
96
114
|
},
|
|
97
115
|
};
|
|
98
116
|
}
|
|
@@ -160,14 +178,85 @@ async function handleSubmit(message) {
|
|
|
160
178
|
runAgentTask(message, false, ctx, () => pendingInteractiveContext, (v) => { pendingInteractiveContext = v; });
|
|
161
179
|
return;
|
|
162
180
|
}
|
|
181
|
+
// Captured before the turn starts so the delta can be reported from both the
|
|
182
|
+
// success path and the catch. An aborted or failed turn has still burned
|
|
183
|
+
// tokens, and gracefulShutdown no longer sends a cumulative catch-all that
|
|
184
|
+
// would have swept them up later.
|
|
185
|
+
const tokenReportStart = getRecordCount();
|
|
186
|
+
// Cloud stats are append-only events, so report only this prompt's delta.
|
|
187
|
+
// Sending the full session accumulator after every prompt makes totals grow
|
|
188
|
+
// 1× + 2× + 3× and is the source of the inflated dashboard token count.
|
|
189
|
+
// pingWhenEmpty sends a bare session event when nothing was spent — wanted on
|
|
190
|
+
// the success path, not when the turn failed before reaching the model.
|
|
191
|
+
const reportTurnStats = (pingWhenEmpty) => {
|
|
192
|
+
const sharedFields = {
|
|
193
|
+
sessionId,
|
|
194
|
+
sessionName: sessionDisplayName || sessionId,
|
|
195
|
+
messageCount: app.getMessages().length,
|
|
196
|
+
cliVersion: getCurrentVersion(),
|
|
197
|
+
projectName: projectContext?.name,
|
|
198
|
+
projectId: projectPath ? generateProjectId(projectPath) : undefined,
|
|
199
|
+
language: projectContext?.type,
|
|
200
|
+
isGit: isGitRepository(process.cwd()),
|
|
201
|
+
};
|
|
202
|
+
const costBreakdown = getCostBreakdown(tokenReportStart);
|
|
203
|
+
if (costBreakdown.length === 0) {
|
|
204
|
+
if (pingWhenEmpty) {
|
|
205
|
+
reportStats({ ...sharedFields, model: config.get('model'), provider: config.get('provider') });
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const entry of costBreakdown) {
|
|
210
|
+
reportStats({
|
|
211
|
+
...sharedFields,
|
|
212
|
+
model: entry.model,
|
|
213
|
+
provider: entry.provider,
|
|
214
|
+
inputTokens: entry.promptTokens || undefined,
|
|
215
|
+
outputTokens: entry.completionTokens || undefined,
|
|
216
|
+
cacheCreationTokens: entry.cacheCreationTokens || undefined,
|
|
217
|
+
cacheReadTokens: entry.cacheReadTokens || undefined,
|
|
218
|
+
estimatedCost: entry.estimatedCost || undefined,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
163
222
|
try {
|
|
164
223
|
app.startStreaming();
|
|
165
224
|
const history = app.getChatHistory();
|
|
225
|
+
// Expand inline @-mentions (@src/file.ts) into the prompt's context.
|
|
226
|
+
// Done after history capture (mentions are per-message) but before
|
|
227
|
+
// deriveSessionName so the title reflects what the user typed, not the
|
|
228
|
+
// expanded path.
|
|
229
|
+
const mentionRoot = projectContext?.root || projectPath || process.cwd();
|
|
230
|
+
// Expand @folder and @file mentions in one pass, merged into a single
|
|
231
|
+
// [Attached files] block.
|
|
232
|
+
const { enrichedPrompt: fileExpanded, loaded: loadedMentions, failures: mentionFailures } = expandFileAndFolderMentions(message, { root: mentionRoot });
|
|
233
|
+
if (loadedMentions.length > 0) {
|
|
234
|
+
app.notify(`Loaded ${loadedMentions.length} file(s) from @mentions/@folder`);
|
|
235
|
+
}
|
|
236
|
+
for (const f of mentionFailures) {
|
|
237
|
+
app.notify(`${f.mention}: ${f.reason}`);
|
|
238
|
+
}
|
|
239
|
+
// Expand @git <ref> mentions — resolve diffs / file-at-ref / commit
|
|
240
|
+
// patches into a [Git ref] block. Runs between file and web mentions
|
|
241
|
+
// so the prompt flows: [files] [git] [web] <text>.
|
|
242
|
+
const { enrichedPrompt: gitExpanded, failures: gitFailures } = await expandGitMentions(fileExpanded, { root: mentionRoot });
|
|
243
|
+
for (const f of gitFailures) {
|
|
244
|
+
app.notify(`${f.mention}: ${f.reason}`);
|
|
245
|
+
}
|
|
246
|
+
// Expand @web <url> mentions — fetch each page and prepend its text.
|
|
247
|
+
// Runs after file mentions so the prompt flows: [files] [web] <text>.
|
|
248
|
+
const { enrichedPrompt: webExpanded, loaded: loadedPages, failures: webFailures } = await expandWebMentions(gitExpanded);
|
|
249
|
+
if (loadedPages.length > 0) {
|
|
250
|
+
app.notify(`Fetched ${loadedPages.length} page(s) from @web`);
|
|
251
|
+
}
|
|
252
|
+
for (const f of webFailures) {
|
|
253
|
+
app.notify(`${f.mention}: ${f.reason}`);
|
|
254
|
+
}
|
|
166
255
|
if (!sessionDisplayName && history.filter(m => m.role === 'user').length === 0) {
|
|
167
256
|
sessionDisplayName = deriveSessionName(message);
|
|
168
257
|
}
|
|
169
258
|
const fileContext = formatAddedFilesContext();
|
|
170
|
-
const enrichedMessage = fileContext ? fileContext +
|
|
259
|
+
const enrichedMessage = fileContext ? fileContext + webExpanded : webExpanded;
|
|
171
260
|
await chat(enrichedMessage, history, (chunk) => app.addStreamChunk(chunk), undefined, projectContext, undefined);
|
|
172
261
|
app.endStreaming();
|
|
173
262
|
autoSaveSession(app.getMessages(), projectPath);
|
|
@@ -194,7 +283,10 @@ async function handleSubmit(message) {
|
|
|
194
283
|
language: projectContext?.type,
|
|
195
284
|
isGit: isGitRepository(process.cwd()),
|
|
196
285
|
};
|
|
197
|
-
|
|
286
|
+
// Cloud stats are append-only events, so report only this prompt's delta.
|
|
287
|
+
// Sending the full session accumulator after every prompt makes totals grow
|
|
288
|
+
// 1× + 2× + 3× and is the source of the inflated dashboard token count.
|
|
289
|
+
const costBreakdown = getCostBreakdown(tokenReportStart);
|
|
198
290
|
if (costBreakdown.length > 0) {
|
|
199
291
|
for (const entry of costBreakdown) {
|
|
200
292
|
reportStats({
|
|
@@ -621,6 +713,7 @@ Commands (in chat):
|
|
|
621
713
|
getStatus,
|
|
622
714
|
hasWriteAccess: () => hasWriteAccess,
|
|
623
715
|
hasProjectContext: () => projectContext !== null,
|
|
716
|
+
getProjectRoot: () => projectContext?.root || projectPath || process.cwd(),
|
|
624
717
|
});
|
|
625
718
|
const provider = getCurrentProvider();
|
|
626
719
|
const providers = getProviderList();
|
|
@@ -905,31 +998,18 @@ async function gracefulShutdown() {
|
|
|
905
998
|
return;
|
|
906
999
|
const messages = app.getMessages();
|
|
907
1000
|
autoSaveSession(messages, projectPath);
|
|
908
|
-
const { syncSessionAsync,
|
|
909
|
-
const tokenStats = getSessionStats();
|
|
1001
|
+
const { syncSessionAsync, generateProjectId } = require('../utils/codeepCloud.js');
|
|
910
1002
|
const projectId = projectPath ? generateProjectId(projectPath) : undefined;
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
sessionId,
|
|
922
|
-
sessionName: sessionId,
|
|
923
|
-
messageCount: messages.length,
|
|
924
|
-
projectName: projectContext?.name,
|
|
925
|
-
projectId,
|
|
926
|
-
inputTokens: tokenStats.totalPromptTokens || undefined,
|
|
927
|
-
outputTokens: tokenStats.totalCompletionTokens || undefined,
|
|
928
|
-
cacheCreationTokens: tokenStats.totalCacheCreationTokens || undefined,
|
|
929
|
-
cacheReadTokens: tokenStats.totalCacheReadTokens || undefined,
|
|
930
|
-
estimatedCost: tokenStats.estimatedCost || undefined,
|
|
931
|
-
}),
|
|
932
|
-
]);
|
|
1003
|
+
// Successful manual and agent turns report their token deltas immediately.
|
|
1004
|
+
// Re-sending the cumulative session total here would count every token a
|
|
1005
|
+
// second time when the append-only dashboard endpoint stores this event.
|
|
1006
|
+
await syncSessionAsync({
|
|
1007
|
+
sessionId,
|
|
1008
|
+
sessionName: sessionDisplayName || sessionId,
|
|
1009
|
+
projectName: projectContext?.name,
|
|
1010
|
+
projectId,
|
|
1011
|
+
messages,
|
|
1012
|
+
});
|
|
933
1013
|
}
|
|
934
1014
|
// ─── Last-resort crash handlers ───────────────────────────────────────────────
|
|
935
1015
|
// 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
|
|
@@ -55,3 +55,34 @@ export declare function formatDiffPreview(diffs: FileDiff[]): string;
|
|
|
55
55
|
* Calculate diff statistics
|
|
56
56
|
*/
|
|
57
57
|
export declare function getDiffStats(diffs: FileDiff[]): DiffPreviewResult;
|
|
58
|
+
/**
|
|
59
|
+
* Apply a subset of a file diff's hunks to the original content.
|
|
60
|
+
*
|
|
61
|
+
* Hunk indices in `acceptedHunks` refer to positions in `diff.hunks`
|
|
62
|
+
* (0-based). Hunks not in the set are skipped — their original lines
|
|
63
|
+
* stay, their additions are dropped.
|
|
64
|
+
*
|
|
65
|
+
* Returns the resulting file content. The caller writes it to disk.
|
|
66
|
+
*
|
|
67
|
+
* For `type === 'create'`, the whole file is either accepted (any hunk
|
|
68
|
+
* accepted) or rejected (empty set) — there's no original to merge
|
|
69
|
+
* against. For `type === 'delete'`, accepting any hunk deletes the file.
|
|
70
|
+
*/
|
|
71
|
+
export declare function applyHunks(diff: FileDiff, acceptedHunks: Set<number>): string;
|
|
72
|
+
/**
|
|
73
|
+
* Apply accepted hunks across multiple file diffs and return the
|
|
74
|
+
* resulting content for each. The caller writes the files to disk.
|
|
75
|
+
*
|
|
76
|
+
* `accepted` maps file path → set of accepted hunk indices. Files not
|
|
77
|
+
* in the map are skipped entirely.
|
|
78
|
+
*/
|
|
79
|
+
export declare function applyHunksToFiles(diffs: FileDiff[], accepted: Map<string, Set<number>>): Array<{
|
|
80
|
+
path: string;
|
|
81
|
+
content: string;
|
|
82
|
+
type: FileDiff['type'];
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Count how many hunks in a diff contain actual changes (not just
|
|
86
|
+
* context). Used to label hunks in the UI ("hunk 2/5").
|
|
87
|
+
*/
|
|
88
|
+
export declare function countChangeHunks(diff: FileDiff): number;
|