@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.
Files changed (106) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-ui-contribution.js +10 -9
  3. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  4. package/lib/browser/ai-chat-ui-frontend-module.d.ts +1 -0
  5. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.js +3 -0
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/chat-input-mode-contribution.js +1 -1
  9. package/lib/browser/chat-input-mode-contribution.js.map +1 -1
  10. package/lib/browser/chat-input-widget.d.ts +57 -3
  11. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  12. package/lib/browser/chat-input-widget.js +382 -21
  13. package/lib/browser/chat-input-widget.js.map +1 -1
  14. package/lib/browser/chat-response-part-renderer.d.ts +6 -0
  15. package/lib/browser/chat-response-part-renderer.d.ts.map +1 -1
  16. package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts +2 -0
  17. package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts.map +1 -1
  18. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js +76 -5
  19. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js.map +1 -1
  20. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  21. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  22. package/lib/browser/chat-response-renderer/index.js +1 -0
  23. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  24. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +3 -0
  25. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -1
  26. package/lib/browser/chat-response-renderer/question-part-renderer.js +19 -6
  27. package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -1
  28. package/lib/browser/chat-response-renderer/tool-call-rendering.d.ts +21 -0
  29. package/lib/browser/chat-response-renderer/tool-call-rendering.d.ts.map +1 -0
  30. package/lib/browser/chat-response-renderer/tool-call-rendering.js +53 -0
  31. package/lib/browser/chat-response-renderer/tool-call-rendering.js.map +1 -0
  32. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +4 -0
  33. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  34. package/lib/browser/chat-response-renderer/tool-confirmation.js +22 -0
  35. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  36. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +1 -0
  37. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  38. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +7 -18
  39. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  40. package/lib/browser/chat-response-renderer/toolcall-utils.d.ts +1 -0
  41. package/lib/browser/chat-response-renderer/toolcall-utils.d.ts.map +1 -1
  42. package/lib/browser/chat-response-renderer/toolcall-utils.js +3 -0
  43. package/lib/browser/chat-response-renderer/toolcall-utils.js.map +1 -1
  44. package/lib/browser/chat-token-usage-indicator-util.d.ts +35 -0
  45. package/lib/browser/chat-token-usage-indicator-util.d.ts.map +1 -0
  46. package/lib/browser/chat-token-usage-indicator-util.js +138 -0
  47. package/lib/browser/chat-token-usage-indicator-util.js.map +1 -0
  48. package/lib/browser/chat-token-usage-indicator-util.spec.d.ts +2 -0
  49. package/lib/browser/chat-token-usage-indicator-util.spec.d.ts.map +1 -0
  50. package/lib/browser/chat-token-usage-indicator-util.spec.js +226 -0
  51. package/lib/browser/chat-token-usage-indicator-util.spec.js.map +1 -0
  52. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
  53. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
  54. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +7 -0
  55. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  56. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  57. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +20 -3
  58. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  59. package/lib/browser/chat-view-preferences.d.ts +18 -0
  60. package/lib/browser/chat-view-preferences.d.ts.map +1 -0
  61. package/lib/browser/chat-view-preferences.js +69 -0
  62. package/lib/browser/chat-view-preferences.js.map +1 -0
  63. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
  64. package/lib/browser/chat-view-widget-toolbar-contribution.js +3 -2
  65. package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
  66. package/lib/browser/chat-view-widget.d.ts +1 -2
  67. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  68. package/lib/browser/chat-view-widget.js +3 -7
  69. package/lib/browser/chat-view-widget.js.map +1 -1
  70. package/lib/browser/session-settings-dialog.d.ts +2 -5
  71. package/lib/browser/session-settings-dialog.d.ts.map +1 -1
  72. package/lib/browser/session-settings-dialog.js +15 -33
  73. package/lib/browser/session-settings-dialog.js.map +1 -1
  74. package/lib/common/toolcall-utils.d.ts +6 -0
  75. package/lib/common/toolcall-utils.d.ts.map +1 -0
  76. package/lib/common/toolcall-utils.js +40 -0
  77. package/lib/common/toolcall-utils.js.map +1 -0
  78. package/lib/common/toolcall-utils.spec.d.ts +2 -0
  79. package/lib/common/toolcall-utils.spec.d.ts.map +1 -0
  80. package/lib/common/toolcall-utils.spec.js +102 -0
  81. package/lib/common/toolcall-utils.spec.js.map +1 -0
  82. package/package.json +12 -12
  83. package/src/browser/ai-chat-ui-contribution.ts +10 -9
  84. package/src/browser/ai-chat-ui-frontend-module.ts +4 -0
  85. package/src/browser/chat-input-mode-contribution.ts +1 -1
  86. package/src/browser/chat-input-widget.tsx +481 -14
  87. package/src/browser/chat-response-part-renderer.ts +6 -0
  88. package/src/browser/chat-response-renderer/delegation-tool-renderer.tsx +105 -7
  89. package/src/browser/chat-response-renderer/index.ts +1 -0
  90. package/src/browser/chat-response-renderer/question-part-renderer.tsx +24 -8
  91. package/src/browser/chat-response-renderer/tool-call-rendering.tsx +89 -0
  92. package/src/browser/chat-response-renderer/tool-confirmation.tsx +26 -0
  93. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +21 -18
  94. package/src/browser/chat-response-renderer/toolcall-utils.ts +2 -0
  95. package/src/browser/chat-token-usage-indicator-util.spec.ts +262 -0
  96. package/src/browser/chat-token-usage-indicator-util.ts +151 -0
  97. package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +9 -1
  98. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +22 -3
  99. package/src/browser/chat-view-preferences.ts +87 -0
  100. package/src/browser/chat-view-widget-toolbar-contribution.tsx +3 -2
  101. package/src/browser/chat-view-widget.tsx +4 -7
  102. package/src/browser/session-settings-dialog.tsx +15 -97
  103. package/src/browser/style/index.css +164 -54
  104. package/src/browser/style/tool-call-rendering.css +327 -0
  105. package/src/common/toolcall-utils.spec.ts +118 -0
  106. 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.onDidChangeActiveStatus(change => {
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
- if (agentDescription) {
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: agentDescription,
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.isActive && widget instanceof ChatViewWidget,
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, PreferenceService, URI } from '@theia/core';
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.onDidChangeActiveStatus(change => {
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.isActive && await this.shouldEnableInput();
152
+ const shouldEnable = this.activationService.canRun && await this.shouldEnableInput();
156
153
  this.inputWidget.setEnabled(shouldEnable);
157
- this.treeWidget.setEnabled(this.activationService.isActive);
154
+ this.treeWidget.setEnabled(this.activationService.canRun);
158
155
  }
159
156
 
160
157
  /**