@theia/ai-chat-ui 1.71.0-next.8 → 1.71.1
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/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +10 -9
- package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.d.ts +1 -0
- package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.js +3 -0
- package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
- package/lib/browser/chat-input-mode-contribution.js +1 -1
- package/lib/browser/chat-input-mode-contribution.js.map +1 -1
- package/lib/browser/chat-input-widget.d.ts +57 -3
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +382 -21
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-response-part-renderer.d.ts +6 -0
- package/lib/browser/chat-response-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts +2 -0
- package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/delegation-tool-renderer.js +76 -5
- package/lib/browser/chat-response-renderer/delegation-tool-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/index.d.ts +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/index.js +1 -0
- package/lib/browser/chat-response-renderer/index.js.map +1 -1
- package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +3 -0
- package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/question-part-renderer.js +19 -6
- package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/tool-call-rendering.d.ts +21 -0
- package/lib/browser/chat-response-renderer/tool-call-rendering.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/tool-call-rendering.js +53 -0
- package/lib/browser/chat-response-renderer/tool-call-rendering.js.map +1 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +4 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/tool-confirmation.js +22 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +1 -0
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +7 -18
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-utils.d.ts +1 -0
- package/lib/browser/chat-response-renderer/toolcall-utils.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-utils.js +3 -0
- package/lib/browser/chat-response-renderer/toolcall-utils.js.map +1 -1
- package/lib/browser/chat-token-usage-indicator-util.d.ts +35 -0
- package/lib/browser/chat-token-usage-indicator-util.d.ts.map +1 -0
- package/lib/browser/chat-token-usage-indicator-util.js +138 -0
- package/lib/browser/chat-token-usage-indicator-util.js.map +1 -0
- package/lib/browser/chat-token-usage-indicator-util.spec.d.ts +2 -0
- package/lib/browser/chat-token-usage-indicator-util.spec.d.ts.map +1 -0
- package/lib/browser/chat-token-usage-indicator-util.spec.js +226 -0
- package/lib/browser/chat-token-usage-indicator-util.spec.js.map +1 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +7 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js +20 -3
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/browser/chat-view-preferences.d.ts +18 -0
- package/lib/browser/chat-view-preferences.d.ts.map +1 -0
- package/lib/browser/chat-view-preferences.js +69 -0
- package/lib/browser/chat-view-preferences.js.map +1 -0
- package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-widget-toolbar-contribution.js +3 -2
- package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget.d.ts +1 -2
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +3 -7
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/lib/browser/session-settings-dialog.d.ts +2 -5
- package/lib/browser/session-settings-dialog.d.ts.map +1 -1
- package/lib/browser/session-settings-dialog.js +15 -33
- package/lib/browser/session-settings-dialog.js.map +1 -1
- package/lib/common/toolcall-utils.d.ts +6 -0
- package/lib/common/toolcall-utils.d.ts.map +1 -0
- package/lib/common/toolcall-utils.js +40 -0
- package/lib/common/toolcall-utils.js.map +1 -0
- package/lib/common/toolcall-utils.spec.d.ts +2 -0
- package/lib/common/toolcall-utils.spec.d.ts.map +1 -0
- package/lib/common/toolcall-utils.spec.js +102 -0
- package/lib/common/toolcall-utils.spec.js.map +1 -0
- package/package.json +12 -12
- package/src/browser/ai-chat-ui-contribution.ts +10 -9
- package/src/browser/ai-chat-ui-frontend-module.ts +4 -0
- package/src/browser/chat-input-mode-contribution.ts +1 -1
- package/src/browser/chat-input-widget.tsx +481 -14
- package/src/browser/chat-response-part-renderer.ts +6 -0
- package/src/browser/chat-response-renderer/delegation-tool-renderer.tsx +105 -7
- package/src/browser/chat-response-renderer/index.ts +1 -0
- package/src/browser/chat-response-renderer/question-part-renderer.tsx +24 -8
- package/src/browser/chat-response-renderer/tool-call-rendering.tsx +89 -0
- package/src/browser/chat-response-renderer/tool-confirmation.tsx +26 -0
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +21 -18
- package/src/browser/chat-response-renderer/toolcall-utils.ts +2 -0
- package/src/browser/chat-token-usage-indicator-util.spec.ts +262 -0
- package/src/browser/chat-token-usage-indicator-util.ts +151 -0
- package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +9 -1
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +22 -3
- package/src/browser/chat-view-preferences.ts +87 -0
- package/src/browser/chat-view-widget-toolbar-contribution.tsx +3 -2
- package/src/browser/chat-view-widget.tsx +4 -7
- package/src/browser/session-settings-dialog.tsx +15 -97
- package/src/browser/style/index.css +164 -54
- package/src/browser/style/tool-call-rendering.css +327 -0
- package/src/common/toolcall-utils.spec.ts +118 -0
- package/src/common/toolcall-utils.ts +36 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { Emitter, Event } from '@theia/core';
|
|
19
|
+
import { ChatModel, ChatRequestModel, ResponseTokenUsage } from '@theia/ai-chat';
|
|
20
|
+
import {
|
|
21
|
+
computeSessionTokenUsage,
|
|
22
|
+
decideTokenUsageWarning,
|
|
23
|
+
formatTokenCount,
|
|
24
|
+
getUsageColorClass,
|
|
25
|
+
isAboveTokenUsageWarningThreshold
|
|
26
|
+
} from './chat-token-usage-indicator-util';
|
|
27
|
+
|
|
28
|
+
const THRESHOLD = 100;
|
|
29
|
+
const CONTEXT_WINDOW = 200;
|
|
30
|
+
|
|
31
|
+
function createMockRequest(tokenUsage?: ResponseTokenUsage, isComplete = true): Partial<ChatRequestModel> {
|
|
32
|
+
return {
|
|
33
|
+
response: {
|
|
34
|
+
tokenUsage,
|
|
35
|
+
isComplete
|
|
36
|
+
} as ChatRequestModel['response']
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createMockChatModel(requests: Partial<ChatRequestModel>[]): ChatModel {
|
|
41
|
+
const emitter = new Emitter<unknown>();
|
|
42
|
+
return {
|
|
43
|
+
onDidChange: emitter.event as Event<unknown>,
|
|
44
|
+
id: 'test-model',
|
|
45
|
+
getRequests: () => requests as ChatRequestModel[],
|
|
46
|
+
} as ChatModel;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('ChatTokenUsageIndicator', () => {
|
|
50
|
+
describe('formatTokenCount', () => {
|
|
51
|
+
it('should return "-" for undefined', () => {
|
|
52
|
+
expect(formatTokenCount(undefined)).to.equal('-');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return "-" for 0', () => {
|
|
56
|
+
expect(formatTokenCount(0)).to.equal('-');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should format numbers below 1000 as-is', () => {
|
|
60
|
+
expect(formatTokenCount(500)).to.equal('500');
|
|
61
|
+
expect(formatTokenCount(999)).to.equal('999');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should format numbers >= 1000 with k suffix and one decimal', () => {
|
|
65
|
+
expect(formatTokenCount(1000)).to.equal('1.0k');
|
|
66
|
+
expect(formatTokenCount(1500)).to.equal('1.5k');
|
|
67
|
+
expect(formatTokenCount(125000)).to.equal('125.0k');
|
|
68
|
+
expect(formatTokenCount(200000)).to.equal('200.0k');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('getUsageColorClass', () => {
|
|
73
|
+
it('returns none for 0 tokens regardless of threshold', () => {
|
|
74
|
+
expect(getUsageColorClass(0, THRESHOLD, CONTEXT_WINDOW)).to.equal('token-usage-none');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns green for tokens below the threshold', () => {
|
|
78
|
+
expect(getUsageColorClass(1, THRESHOLD, CONTEXT_WINDOW)).to.equal('token-usage-green');
|
|
79
|
+
expect(getUsageColorClass(THRESHOLD - 1, THRESHOLD, CONTEXT_WINDOW)).to.equal('token-usage-green');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns yellow for tokens at or above the threshold but below the context window size', () => {
|
|
83
|
+
expect(getUsageColorClass(THRESHOLD, THRESHOLD, CONTEXT_WINDOW)).to.equal('token-usage-yellow');
|
|
84
|
+
expect(getUsageColorClass(CONTEXT_WINDOW - 1, THRESHOLD, CONTEXT_WINDOW)).to.equal('token-usage-yellow');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns red for tokens at or above the context window size', () => {
|
|
88
|
+
expect(getUsageColorClass(CONTEXT_WINDOW, THRESHOLD, CONTEXT_WINDOW)).to.equal('token-usage-red');
|
|
89
|
+
expect(getUsageColorClass(CONTEXT_WINDOW + 1, THRESHOLD, CONTEXT_WINDOW)).to.equal('token-usage-red');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('computeSessionTokenUsage', () => {
|
|
94
|
+
it('should return 0 when chatModel is undefined', () => {
|
|
95
|
+
expect(computeSessionTokenUsage(undefined)).to.equal(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return 0 when there are no requests', () => {
|
|
99
|
+
const model = createMockChatModel([]);
|
|
100
|
+
expect(computeSessionTokenUsage(model)).to.equal(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return 0 when requests have no token usage', () => {
|
|
104
|
+
const model = createMockChatModel([
|
|
105
|
+
createMockRequest(undefined),
|
|
106
|
+
createMockRequest(undefined)
|
|
107
|
+
]);
|
|
108
|
+
expect(computeSessionTokenUsage(model)).to.equal(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should sum all tokens from a single request', () => {
|
|
112
|
+
const model = createMockChatModel([
|
|
113
|
+
createMockRequest({ inputTokens: 1000, outputTokens: 500 })
|
|
114
|
+
]);
|
|
115
|
+
expect(computeSessionTokenUsage(model)).to.equal(1500);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should return only the last request tokens, not the sum', () => {
|
|
119
|
+
const model = createMockChatModel([
|
|
120
|
+
createMockRequest({ inputTokens: 1000, outputTokens: 500 }),
|
|
121
|
+
createMockRequest({ inputTokens: 2000, outputTokens: 800 })
|
|
122
|
+
]);
|
|
123
|
+
// Only the last request: 2000+800 = 2800
|
|
124
|
+
expect(computeSessionTokenUsage(model)).to.equal(2800);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should return last request tokens from 3 requests (2 complete, 1 in-progress)', () => {
|
|
128
|
+
const model = createMockChatModel([
|
|
129
|
+
createMockRequest({ inputTokens: 1000, outputTokens: 500 }, true),
|
|
130
|
+
createMockRequest({ inputTokens: 2000, outputTokens: 800 }, true),
|
|
131
|
+
createMockRequest({ inputTokens: 3000, outputTokens: 200 }, false) // in-progress
|
|
132
|
+
]);
|
|
133
|
+
// Only the last request: 3000+200 = 3200
|
|
134
|
+
expect(computeSessionTokenUsage(model)).to.equal(3200);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should skip requests without token usage and return last with usage', () => {
|
|
138
|
+
const model = createMockChatModel([
|
|
139
|
+
createMockRequest({ inputTokens: 1000, outputTokens: 500 }),
|
|
140
|
+
createMockRequest(undefined),
|
|
141
|
+
createMockRequest({ inputTokens: 3000, outputTokens: 1000 })
|
|
142
|
+
]);
|
|
143
|
+
// Last request with usage: 3000+1000 = 4000
|
|
144
|
+
expect(computeSessionTokenUsage(model)).to.equal(4000);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should include cache creation and cache read tokens in the total', () => {
|
|
148
|
+
const model = createMockChatModel([
|
|
149
|
+
createMockRequest({
|
|
150
|
+
inputTokens: 1000,
|
|
151
|
+
outputTokens: 500,
|
|
152
|
+
cacheCreationInputTokens: 200,
|
|
153
|
+
cacheReadInputTokens: 300
|
|
154
|
+
})
|
|
155
|
+
]);
|
|
156
|
+
// 1000+500+200+300 = 2000
|
|
157
|
+
expect(computeSessionTokenUsage(model)).to.equal(2000);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return only last request cache tokens, not sum across requests', () => {
|
|
161
|
+
const model = createMockChatModel([
|
|
162
|
+
createMockRequest({
|
|
163
|
+
inputTokens: 1000,
|
|
164
|
+
outputTokens: 500,
|
|
165
|
+
cacheCreationInputTokens: 200,
|
|
166
|
+
cacheReadInputTokens: 300
|
|
167
|
+
}),
|
|
168
|
+
createMockRequest({
|
|
169
|
+
inputTokens: 500,
|
|
170
|
+
outputTokens: 100,
|
|
171
|
+
cacheCreationInputTokens: 50,
|
|
172
|
+
cacheReadInputTokens: 50
|
|
173
|
+
})
|
|
174
|
+
]);
|
|
175
|
+
// Only last request: 500+100+50+50 = 700
|
|
176
|
+
expect(computeSessionTokenUsage(model)).to.equal(700);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return in-progress request token usage when it is last', () => {
|
|
180
|
+
const model = createMockChatModel([
|
|
181
|
+
createMockRequest({ inputTokens: 1000, outputTokens: 500 }, true),
|
|
182
|
+
createMockRequest({ inputTokens: 5000, outputTokens: 200 }, false) // in-progress
|
|
183
|
+
]);
|
|
184
|
+
// Only last request: 5000+200 = 5200
|
|
185
|
+
expect(computeSessionTokenUsage(model)).to.equal(5200);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should return total from in-progress request when it is the only request', () => {
|
|
189
|
+
const model = createMockChatModel([
|
|
190
|
+
createMockRequest({ inputTokens: 5000, outputTokens: 200 }, false)
|
|
191
|
+
]);
|
|
192
|
+
expect(computeSessionTokenUsage(model)).to.equal(5200);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should skip trailing requests without usage and find last with usage', () => {
|
|
196
|
+
const model = createMockChatModel([
|
|
197
|
+
createMockRequest({ inputTokens: 1000, outputTokens: 500 }),
|
|
198
|
+
createMockRequest({ inputTokens: 3000, outputTokens: 1000 }),
|
|
199
|
+
createMockRequest(undefined)
|
|
200
|
+
]);
|
|
201
|
+
// Last request with usage: 3000+1000 = 4000
|
|
202
|
+
expect(computeSessionTokenUsage(model)).to.equal(4000);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('isAboveTokenUsageWarningThreshold', () => {
|
|
207
|
+
it('is false for 0 tokens', () => {
|
|
208
|
+
expect(isAboveTokenUsageWarningThreshold(0, THRESHOLD)).to.equal(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('is false for tokens below the threshold', () => {
|
|
212
|
+
expect(isAboveTokenUsageWarningThreshold(THRESHOLD - 1, THRESHOLD)).to.equal(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('is true for tokens at the threshold', () => {
|
|
216
|
+
expect(isAboveTokenUsageWarningThreshold(THRESHOLD, THRESHOLD)).to.equal(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('is true for tokens above the threshold', () => {
|
|
220
|
+
expect(isAboveTokenUsageWarningThreshold(THRESHOLD + 1, THRESHOLD)).to.equal(true);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('decideTokenUsageWarning', () => {
|
|
225
|
+
it('resets when usage is below the threshold', () => {
|
|
226
|
+
expect(decideTokenUsageWarning({
|
|
227
|
+
totalTokens: 50,
|
|
228
|
+
threshold: THRESHOLD,
|
|
229
|
+
alreadyNotified: true
|
|
230
|
+
})).to.equal('reset');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('resets when usage is zero', () => {
|
|
234
|
+
expect(decideTokenUsageWarning({
|
|
235
|
+
totalTokens: 0,
|
|
236
|
+
threshold: THRESHOLD,
|
|
237
|
+
alreadyNotified: true
|
|
238
|
+
})).to.equal('reset');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('skips when the session has already been notified', () => {
|
|
242
|
+
expect(decideTokenUsageWarning({
|
|
243
|
+
totalTokens: THRESHOLD + 50,
|
|
244
|
+
threshold: THRESHOLD,
|
|
245
|
+
alreadyNotified: true
|
|
246
|
+
})).to.equal('skip');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('notifies when usage crosses the threshold and the session has not been notified yet', () => {
|
|
250
|
+
expect(decideTokenUsageWarning({
|
|
251
|
+
totalTokens: THRESHOLD,
|
|
252
|
+
threshold: THRESHOLD,
|
|
253
|
+
alreadyNotified: false
|
|
254
|
+
})).to.equal('notify');
|
|
255
|
+
expect(decideTokenUsageWarning({
|
|
256
|
+
totalTokens: THRESHOLD * 2,
|
|
257
|
+
threshold: THRESHOLD,
|
|
258
|
+
alreadyNotified: false
|
|
259
|
+
})).to.equal('notify');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { ChatModel, ResponseTokenUsage } from '@theia/ai-chat';
|
|
18
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
19
|
+
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Provisional context window size used as the denominator for the indicator bar
|
|
23
|
+
* fill and the tooltip's "Total: X / Y" display until per-model context sizes
|
|
24
|
+
* are available. See issue #17323 comments for context.
|
|
25
|
+
*/
|
|
26
|
+
export const CHAT_CONTEXT_WINDOW_SIZE = 200000;
|
|
27
|
+
|
|
28
|
+
export type TokenUsageWarningDecision = 'notify' | 'reset' | 'skip';
|
|
29
|
+
|
|
30
|
+
/** Returns true when the given total has crossed (>=) the configured warning threshold. */
|
|
31
|
+
export function isAboveTokenUsageWarningThreshold(totalTokens: number, threshold: number): boolean {
|
|
32
|
+
return totalTokens > 0 && totalTokens >= threshold;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pure decision function for whether to show the token usage warning for a session.
|
|
37
|
+
* Callers are expected to short-circuit before invoking this when the warning feature
|
|
38
|
+
* is disabled, so this helper is not concerned with the enabled state.
|
|
39
|
+
* - `reset`: usage is below the threshold; any prior "already notified" state for this session should be cleared.
|
|
40
|
+
* - `skip`: we already notified for this session while still above the threshold.
|
|
41
|
+
* - `notify`: warning should be shown now and the session marked as notified.
|
|
42
|
+
*/
|
|
43
|
+
export function decideTokenUsageWarning(args: {
|
|
44
|
+
totalTokens: number;
|
|
45
|
+
threshold: number;
|
|
46
|
+
alreadyNotified: boolean;
|
|
47
|
+
}): TokenUsageWarningDecision {
|
|
48
|
+
if (!isAboveTokenUsageWarningThreshold(args.totalTokens, args.threshold)) {
|
|
49
|
+
return 'reset';
|
|
50
|
+
}
|
|
51
|
+
if (args.alreadyNotified) {
|
|
52
|
+
return 'skip';
|
|
53
|
+
}
|
|
54
|
+
return 'notify';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatTokenCount(count: number | undefined): string {
|
|
58
|
+
if (count === undefined || count === 0) {
|
|
59
|
+
return '-';
|
|
60
|
+
}
|
|
61
|
+
if (count >= 1000) {
|
|
62
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
63
|
+
}
|
|
64
|
+
return count.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the CSS class for the token usage indicator based on the current
|
|
69
|
+
* total, the configured warning threshold, and the assumed context window size.
|
|
70
|
+
* Yellow band: [threshold, contextWindowSize). Red band: [contextWindowSize, ∞).
|
|
71
|
+
*/
|
|
72
|
+
export function getUsageColorClass(totalTokens: number, threshold: number, contextWindowSize: number = CHAT_CONTEXT_WINDOW_SIZE): string {
|
|
73
|
+
if (totalTokens === 0) {
|
|
74
|
+
return 'token-usage-none';
|
|
75
|
+
}
|
|
76
|
+
if (totalTokens < threshold) {
|
|
77
|
+
return 'token-usage-green';
|
|
78
|
+
}
|
|
79
|
+
if (totalTokens < contextWindowSize) {
|
|
80
|
+
return 'token-usage-yellow';
|
|
81
|
+
}
|
|
82
|
+
return 'token-usage-red';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function computeSessionTokenUsage(chatModel?: ChatModel): number {
|
|
86
|
+
if (!chatModel) {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const requests = chatModel.getRequests();
|
|
90
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
91
|
+
const usage: ResponseTokenUsage | undefined = requests[i].response.tokenUsage;
|
|
92
|
+
if (usage) {
|
|
93
|
+
return usage.inputTokens
|
|
94
|
+
+ usage.outputTokens
|
|
95
|
+
+ (usage.cacheCreationInputTokens ?? 0)
|
|
96
|
+
+ (usage.cacheReadInputTokens ?? 0);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getLatestTokenUsage(chatModel?: ChatModel): ResponseTokenUsage | undefined {
|
|
103
|
+
if (!chatModel) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const requests = chatModel.getRequests();
|
|
107
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
108
|
+
const usage = requests[i].response.tokenUsage;
|
|
109
|
+
if (usage) {
|
|
110
|
+
return usage;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildBarTooltip(usage: ResponseTokenUsage | undefined, totalTokens: number, threshold: number): MarkdownString | undefined {
|
|
117
|
+
if (!usage) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const lines: string[] = [
|
|
121
|
+
`**${nls.localize('theia/ai/chat-ui/tokenUsageLabel', 'Token Usage')}**`
|
|
122
|
+
];
|
|
123
|
+
const colorClass = getUsageColorClass(totalTokens, threshold);
|
|
124
|
+
if (colorClass === 'token-usage-yellow') {
|
|
125
|
+
lines.push(`⚠ ${nls.localize('theia/ai/chat-ui/tokenUsageWarning', 'Token usage warning threshold reached.')}`, '');
|
|
126
|
+
} else if (colorClass === 'token-usage-red') {
|
|
127
|
+
lines.push(`⚠ ${nls.localize('theia/ai/chat-ui/tokenUsageOverflow', 'Token usage well past the warning threshold. Consider compacting or starting a new session.')}`, '');
|
|
128
|
+
}
|
|
129
|
+
lines.push(`${nls.localizeByDefault('Input: {0}',
|
|
130
|
+
formatTokenCount(usage.inputTokens))} | ${nls.localizeByDefault('Output: {0}',
|
|
131
|
+
formatTokenCount(usage.outputTokens))}`);
|
|
132
|
+
const cacheParts: string[] = [];
|
|
133
|
+
if (usage.cacheReadInputTokens) {
|
|
134
|
+
cacheParts.push(nls.localize('theia/ai/chat-ui/tokenUsageTooltipCacheRead', 'Cache read: {0}', formatTokenCount(usage.cacheReadInputTokens)));
|
|
135
|
+
}
|
|
136
|
+
if (usage.cacheCreationInputTokens) {
|
|
137
|
+
cacheParts.push(nls.localize('theia/ai/chat-ui/tokenUsageTooltipCacheCreate', 'Cache creation: {0}', formatTokenCount(usage.cacheCreationInputTokens)));
|
|
138
|
+
}
|
|
139
|
+
if (cacheParts.length > 0) {
|
|
140
|
+
lines.push(cacheParts.join(' | '));
|
|
141
|
+
}
|
|
142
|
+
const percentage = Math.round((totalTokens / CHAT_CONTEXT_WINDOW_SIZE) * 100);
|
|
143
|
+
lines.push(nls.localize(
|
|
144
|
+
'theia/ai/chat-ui/tokenUsageTooltipTotal',
|
|
145
|
+
'Total: {0} / {1} ({2}%)',
|
|
146
|
+
formatTokenCount(totalTokens),
|
|
147
|
+
formatTokenCount(CHAT_CONTEXT_WINDOW_SIZE),
|
|
148
|
+
percentage
|
|
149
|
+
));
|
|
150
|
+
return { value: lines.join(' \n') };
|
|
151
|
+
}
|
|
@@ -19,7 +19,7 @@ import { AIChatInputWidget, type AIChatInputConfiguration } from '../chat-input-
|
|
|
19
19
|
import type { EditableRequestNode } from './chat-view-tree-widget';
|
|
20
20
|
import { URI } from '@theia/core';
|
|
21
21
|
import { CHAT_VIEW_LANGUAGE_EXTENSION } from '../chat-view-language-contribution';
|
|
22
|
-
import type { ChatRequestModel, EditableChatRequestModel, ChatHierarchyBranch } from '@theia/ai-chat';
|
|
22
|
+
import type { ChatModel, ChatRequestModel, EditableChatRequestModel, ChatHierarchyBranch } from '@theia/ai-chat';
|
|
23
23
|
import type { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
24
24
|
import { Key } from '@theia/core/lib/browser';
|
|
25
25
|
|
|
@@ -120,4 +120,12 @@ export class AIChatTreeInputWidget extends AIChatInputWidget {
|
|
|
120
120
|
protected override deleteContextElement(index: number): void {
|
|
121
121
|
this.request.editContextManager.deleteVariables(index);
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* No-op in edit mode: the token usage warning is owned by the main chat input
|
|
126
|
+
* widget so we avoid double-notifying when an edit widget is open alongside it.
|
|
127
|
+
*/
|
|
128
|
+
protected override evaluateTokenUsageWarning(_chatModel: ChatModel): void {
|
|
129
|
+
// Intentionally empty.
|
|
130
|
+
}
|
|
123
131
|
}
|
|
@@ -57,8 +57,10 @@ import {
|
|
|
57
57
|
} from '@theia/core/shared/inversify';
|
|
58
58
|
import * as React from '@theia/core/shared/react';
|
|
59
59
|
import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
|
|
60
|
+
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
|
60
61
|
import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution';
|
|
61
62
|
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
63
|
+
import { formatTokenCount } from '../chat-token-usage-indicator-util';
|
|
62
64
|
import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';
|
|
63
65
|
import { ProgressMessage } from '../chat-progress-message';
|
|
64
66
|
import { AIChatTreeInputFactory, type AIChatTreeInputWidget } from './chat-view-tree-input-widget';
|
|
@@ -219,7 +221,7 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
219
221
|
|
|
220
222
|
this.toDispose.pushAll([
|
|
221
223
|
this.toDisposeOnChatModelChange,
|
|
222
|
-
this.activationService.
|
|
224
|
+
this.activationService.onDidChangeCanRun(change => {
|
|
223
225
|
this.chatInputs.forEach(widget => {
|
|
224
226
|
widget.setEnabled(change);
|
|
225
227
|
});
|
|
@@ -599,9 +601,26 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
599
601
|
<h3 ref={agentLabel}
|
|
600
602
|
className='theia-AgentLabel'
|
|
601
603
|
onMouseEnter={() => {
|
|
602
|
-
|
|
604
|
+
const tokenUsage = isResponseNode(node) ? node.response.tokenUsage : undefined;
|
|
605
|
+
const hasTokenInfo = tokenUsage && (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0);
|
|
606
|
+
const tokenInfo = hasTokenInfo
|
|
607
|
+
? `${nls.localize('theia/ai/chat-ui/tokenUsageLabel', 'Token Usage')}: ${nls.localizeByDefault(
|
|
608
|
+
'Input: {0}', formatTokenCount(tokenUsage.inputTokens))} | ${nls.localizeByDefault(
|
|
609
|
+
'Output: {0}', formatTokenCount(tokenUsage.outputTokens))}`
|
|
610
|
+
: undefined;
|
|
611
|
+
if (agentDescription || tokenInfo) {
|
|
612
|
+
const md = new MarkdownStringImpl();
|
|
613
|
+
if (agentDescription) {
|
|
614
|
+
md.appendMarkdown(agentDescription);
|
|
615
|
+
}
|
|
616
|
+
if (agentDescription && tokenInfo) {
|
|
617
|
+
md.appendMarkdown('\n\n---\n\n');
|
|
618
|
+
}
|
|
619
|
+
if (tokenInfo) {
|
|
620
|
+
md.appendMarkdown(tokenInfo);
|
|
621
|
+
}
|
|
603
622
|
this.hoverService.requestHover({
|
|
604
|
-
content:
|
|
623
|
+
content: md,
|
|
605
624
|
target: agentLabel.current!,
|
|
606
625
|
position: 'right'
|
|
607
626
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService } from '@theia/core/lib/common/preferences';
|
|
18
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
19
|
+
import { interfaces } from '@theia/core/shared/inversify';
|
|
20
|
+
|
|
21
|
+
export const CHAT_VIEW_TOKEN_USAGE_ENABLED = 'ai-features.chat.tokenUsageIndicator.enabled';
|
|
22
|
+
export const CHAT_VIEW_TOKEN_USAGE_WARNING_ENABLED = 'ai-features.chat.tokenUsageWarning.enabled';
|
|
23
|
+
export const CHAT_VIEW_TOKEN_USAGE_WARNING_THRESHOLD_PERCENTAGE = 'ai-features.chat.tokenUsageWarning.defaultThresholdPercentage';
|
|
24
|
+
|
|
25
|
+
export const CHAT_VIEW_TOKEN_USAGE_WARNING_THRESHOLD_PERCENTAGE_DEFAULT = 80;
|
|
26
|
+
|
|
27
|
+
export const chatViewPreferenceSchema: PreferenceSchema = {
|
|
28
|
+
properties: {
|
|
29
|
+
[CHAT_VIEW_TOKEN_USAGE_ENABLED]: {
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false,
|
|
32
|
+
description: nls.localize(
|
|
33
|
+
'theia/ai/chat-ui/tokenUsageIndicatorEnabled',
|
|
34
|
+
'Controls whether the experimental token usage indicator is shown in the chat view. ' +
|
|
35
|
+
'This feature is experimental and token counts may be inaccurate depending on the model and provider.'
|
|
36
|
+
),
|
|
37
|
+
tags: ['experimental']
|
|
38
|
+
},
|
|
39
|
+
[CHAT_VIEW_TOKEN_USAGE_WARNING_ENABLED]: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
default: false,
|
|
42
|
+
description: nls.localize(
|
|
43
|
+
'theia/ai/chat-ui/tokenUsageWarningEnabled',
|
|
44
|
+
'Controls whether a notification is shown when a chat session\'s token usage crosses the configured threshold. ' +
|
|
45
|
+
'Requires the language model provider to report token usage.'
|
|
46
|
+
),
|
|
47
|
+
tags: ['experimental']
|
|
48
|
+
},
|
|
49
|
+
[CHAT_VIEW_TOKEN_USAGE_WARNING_THRESHOLD_PERCENTAGE]: {
|
|
50
|
+
type: 'number',
|
|
51
|
+
minimum: 1,
|
|
52
|
+
maximum: 100,
|
|
53
|
+
default: CHAT_VIEW_TOKEN_USAGE_WARNING_THRESHOLD_PERCENTAGE_DEFAULT,
|
|
54
|
+
description: nls.localize(
|
|
55
|
+
'theia/ai/chat-ui/tokenUsageWarningThresholdPercentage',
|
|
56
|
+
'Percentage of the model\'s context window at which the token usage warning is triggered. ' +
|
|
57
|
+
'This value also drives the warning/error color bands of the token usage indicator. ' +
|
|
58
|
+
'Currently resolves against an assumed 200k context window; will use the real per-model context size once available.'
|
|
59
|
+
),
|
|
60
|
+
tags: ['experimental']
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export interface ChatViewConfiguration {
|
|
66
|
+
[CHAT_VIEW_TOKEN_USAGE_ENABLED]: boolean;
|
|
67
|
+
[CHAT_VIEW_TOKEN_USAGE_WARNING_ENABLED]: boolean;
|
|
68
|
+
[CHAT_VIEW_TOKEN_USAGE_WARNING_THRESHOLD_PERCENTAGE]: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const ChatViewPreferenceContribution = Symbol('ChatViewPreferenceContribution');
|
|
72
|
+
export const ChatViewPreferences = Symbol('ChatViewPreferences');
|
|
73
|
+
export type ChatViewPreferences = PreferenceProxy<ChatViewConfiguration>;
|
|
74
|
+
|
|
75
|
+
export function createChatViewPreferences(preferences: PreferenceService, schema: PreferenceSchema = chatViewPreferenceSchema): ChatViewPreferences {
|
|
76
|
+
return createPreferenceProxy(preferences, schema);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function bindChatViewPreferences(bind: interfaces.Bind): void {
|
|
80
|
+
bind(ChatViewPreferences).toDynamicValue(ctx => {
|
|
81
|
+
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
|
82
|
+
const contribution = ctx.container.get<PreferenceContribution>(ChatViewPreferenceContribution);
|
|
83
|
+
return createChatViewPreferences(preferences, contribution.schema);
|
|
84
|
+
}).inSingletonScope();
|
|
85
|
+
bind(ChatViewPreferenceContribution).toConstantValue({ schema: chatViewPreferenceSchema });
|
|
86
|
+
bind(PreferenceContribution).toService(ChatViewPreferenceContribution);
|
|
87
|
+
}
|
|
@@ -57,7 +57,7 @@ export class ChatViewWidgetToolbarContribution implements TabBarToolbarContribut
|
|
|
57
57
|
|
|
58
58
|
this.commandRegistry.registerCommand(ChatCommands.EDIT_SESSION_SETTINGS, {
|
|
59
59
|
execute: () => this.openJsonDataDialog(),
|
|
60
|
-
isEnabled: widget => this.activationService.
|
|
60
|
+
isEnabled: widget => this.activationService.canRun && widget instanceof ChatViewWidget,
|
|
61
61
|
isVisible: widget => this.activationService.isActive && widget instanceof ChatViewWidget
|
|
62
62
|
});
|
|
63
63
|
}
|
|
@@ -82,8 +82,9 @@ export class ChatViewWidgetToolbarContribution implements TabBarToolbarContribut
|
|
|
82
82
|
registry.registerItem({
|
|
83
83
|
id: ChatCommands.EDIT_SESSION_SETTINGS.id,
|
|
84
84
|
command: ChatCommands.EDIT_SESSION_SETTINGS.id,
|
|
85
|
-
tooltip: nls.localize('theia/ai/session-settings-dialog/tooltip', 'Set Session Settings'),
|
|
85
|
+
tooltip: nls.localize('theia/ai/session-settings-dialog/tooltip', 'Set Session Settings...'),
|
|
86
86
|
priority: 3,
|
|
87
|
+
group: 'chat-settings',
|
|
87
88
|
when: ENABLE_AI_CONTEXT_KEY
|
|
88
89
|
});
|
|
89
90
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
|
-
import { CommandService, ContributionProvider, deepClone, Emitter, Event, MessageService,
|
|
16
|
+
import { CommandService, ContributionProvider, deepClone, Emitter, Event, MessageService, URI } from '@theia/core';
|
|
17
17
|
import { ChatRequest, ChatRequestModel, ChatService, ChatSession, ChatSessionSettings, isActiveSessionChangedEvent, MutableChatModel } from '@theia/ai-chat';
|
|
18
18
|
import { GenericCapabilitySelections, AIVariableResolutionRequest } from '@theia/ai-core';
|
|
19
19
|
import { BaseWidget, codicon, ExtractableWidget, Message, PanelLayout, StatefulWidget } from '@theia/core/lib/browser';
|
|
@@ -46,9 +46,6 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
|
|
|
46
46
|
@inject(MessageService)
|
|
47
47
|
protected messageService: MessageService;
|
|
48
48
|
|
|
49
|
-
@inject(PreferenceService)
|
|
50
|
-
protected readonly preferenceService: PreferenceService;
|
|
51
|
-
|
|
52
49
|
@inject(CommandService)
|
|
53
50
|
protected readonly commandService: CommandService;
|
|
54
51
|
|
|
@@ -127,7 +124,7 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
|
|
|
127
124
|
|
|
128
125
|
this.updateInputEnabledState();
|
|
129
126
|
|
|
130
|
-
this.activationService.
|
|
127
|
+
this.activationService.onDidChangeCanRun(change => {
|
|
131
128
|
this.treeWidget.setEnabled(change);
|
|
132
129
|
this.updateInputEnabledState();
|
|
133
130
|
this.update();
|
|
@@ -152,9 +149,9 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
|
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
protected async updateInputEnabledState(): Promise<void> {
|
|
155
|
-
const shouldEnable = this.activationService.
|
|
152
|
+
const shouldEnable = this.activationService.canRun && await this.shouldEnableInput();
|
|
156
153
|
this.inputWidget.setEnabled(shouldEnable);
|
|
157
|
-
this.treeWidget.setEnabled(this.activationService.
|
|
154
|
+
this.treeWidget.setEnabled(this.activationService.canRun);
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/**
|