@theia/ai-ide 1.72.0-next.59 → 1.72.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/architect-agent.js +1 -1
- package/lib/browser/architect-agent.js.map +1 -1
- package/lib/browser/chat-sessions-welcome-message-provider.d.ts +2 -1
- package/lib/browser/chat-sessions-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/chat-sessions-welcome-message-provider.js +122 -32
- package/lib/browser/chat-sessions-welcome-message-provider.js.map +1 -1
- package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts +2 -0
- package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts.map +1 -0
- package/lib/browser/chat-sessions-welcome-message-provider.spec.js +156 -0
- package/lib/browser/chat-sessions-welcome-message-provider.spec.js.map +1 -0
- package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.js +63 -39
- package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
- package/lib/browser/user-interaction-tool.d.ts +19 -10
- package/lib/browser/user-interaction-tool.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool.js +43 -54
- package/lib/browser/user-interaction-tool.js.map +1 -1
- package/lib/browser/user-interaction-tool.spec.js +66 -41
- package/lib/browser/user-interaction-tool.spec.js.map +1 -1
- package/lib/common/user-interaction-tool.d.ts +1 -0
- package/lib/common/user-interaction-tool.d.ts.map +1 -1
- package/lib/common/user-interaction-tool.js +43 -15
- package/lib/common/user-interaction-tool.js.map +1 -1
- package/lib/common/user-interaction-tool.spec.js +27 -0
- package/lib/common/user-interaction-tool.spec.js.map +1 -1
- package/package.json +23 -23
- package/src/browser/architect-agent.ts +1 -1
- package/src/browser/chat-sessions-welcome-message-provider.spec.ts +186 -0
- package/src/browser/chat-sessions-welcome-message-provider.tsx +132 -35
- package/src/browser/style/index.css +43 -3
- package/src/browser/user-interaction-tool-renderer.tsx +74 -49
- package/src/browser/user-interaction-tool.spec.ts +73 -46
- package/src/browser/user-interaction-tool.ts +52 -58
- package/src/common/user-interaction-tool.spec.ts +29 -0
- package/src/common/user-interaction-tool.ts +42 -15
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
import { expect } from 'chai';
|
|
22
|
+
import { Emitter } from '@theia/core';
|
|
23
|
+
import { ChatService, ChatSession } from '@theia/ai-chat';
|
|
24
|
+
import { ChatViewWidget } from '@theia/ai-chat-ui/lib/browser/chat-view-widget';
|
|
25
|
+
import { ApplicationShell, Widget } from '@theia/core/lib/browser';
|
|
26
|
+
import { ChatSessionsWelcomeMessageProvider } from './chat-sessions-welcome-message-provider';
|
|
27
|
+
disableJSDOM();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Subclass that exposes the protected watch hook and lets tests inject fake services.
|
|
31
|
+
* We don't go through inversify here because the only behaviour under test is the
|
|
32
|
+
* unread bookkeeping; spinning up the full container would pull in unrelated
|
|
33
|
+
* services and obscure the assertion.
|
|
34
|
+
*/
|
|
35
|
+
class TestableProvider extends ChatSessionsWelcomeMessageProvider {
|
|
36
|
+
constructor(chatService: ChatService, shell: ApplicationShell) {
|
|
37
|
+
super();
|
|
38
|
+
(this as unknown as { chatService: ChatService }).chatService = chatService;
|
|
39
|
+
(this as unknown as { shell: ApplicationShell }).shell = shell;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
watch(session: ChatSession): void {
|
|
43
|
+
this.watchSession(session);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface RequestStub {
|
|
48
|
+
response: { isComplete: boolean };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createFakeSession(id: string): {
|
|
52
|
+
session: ChatSession;
|
|
53
|
+
fire: () => void;
|
|
54
|
+
pushRequest: (complete: boolean) => void;
|
|
55
|
+
} {
|
|
56
|
+
const requests: RequestStub[] = [];
|
|
57
|
+
const onDidChangeEmitter = new Emitter<unknown>();
|
|
58
|
+
const session = {
|
|
59
|
+
id,
|
|
60
|
+
isActive: false,
|
|
61
|
+
model: {
|
|
62
|
+
getRequests: () => requests,
|
|
63
|
+
onDidChange: onDidChangeEmitter.event,
|
|
64
|
+
}
|
|
65
|
+
} as unknown as ChatSession;
|
|
66
|
+
return {
|
|
67
|
+
session,
|
|
68
|
+
fire: () => onDidChangeEmitter.fire(undefined),
|
|
69
|
+
pushRequest: complete => requests.push({ response: { isComplete: complete } })
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('ChatSessionsWelcomeMessageProvider unread state', () => {
|
|
74
|
+
let activeSessionId: string | undefined;
|
|
75
|
+
let activeWidget: unknown;
|
|
76
|
+
let widgetForActiveElement: Widget | undefined;
|
|
77
|
+
let chatService: ChatService;
|
|
78
|
+
let shell: ApplicationShell;
|
|
79
|
+
let chatViewWidget: ChatViewWidget;
|
|
80
|
+
let otherWidget: object;
|
|
81
|
+
|
|
82
|
+
before(() => {
|
|
83
|
+
disableJSDOM = enableJSDOM();
|
|
84
|
+
});
|
|
85
|
+
after(() => {
|
|
86
|
+
disableJSDOM();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
activeSessionId = undefined;
|
|
91
|
+
chatViewWidget = Object.create(ChatViewWidget.prototype) as ChatViewWidget;
|
|
92
|
+
otherWidget = {};
|
|
93
|
+
activeWidget = undefined;
|
|
94
|
+
widgetForActiveElement = undefined;
|
|
95
|
+
// ChatViewWidget.findActive consults document.activeElement. Provide an HTMLElement
|
|
96
|
+
// so the lookup path through shell.findWidgetForElement is exercised.
|
|
97
|
+
const focusTarget = document.createElement('div');
|
|
98
|
+
document.body.appendChild(focusTarget);
|
|
99
|
+
focusTarget.tabIndex = -1;
|
|
100
|
+
focusTarget.focus();
|
|
101
|
+
chatService = {
|
|
102
|
+
getActiveSession: () => activeSessionId ? { id: activeSessionId } as ChatSession : undefined,
|
|
103
|
+
} as unknown as ChatService;
|
|
104
|
+
shell = {
|
|
105
|
+
get activeWidget(): unknown {
|
|
106
|
+
return activeWidget;
|
|
107
|
+
},
|
|
108
|
+
findWidgetForElement: () => widgetForActiveElement
|
|
109
|
+
} as unknown as ApplicationShell;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('marks the session unread when a new request arrives and the chat view is not focused', () => {
|
|
113
|
+
const provider = new TestableProvider(chatService, shell);
|
|
114
|
+
const { session, fire, pushRequest } = createFakeSession('s1');
|
|
115
|
+
activeSessionId = 's1';
|
|
116
|
+
activeWidget = otherWidget;
|
|
117
|
+
|
|
118
|
+
provider.watch(session);
|
|
119
|
+
pushRequest(true);
|
|
120
|
+
fire();
|
|
121
|
+
|
|
122
|
+
expect(provider.isUnread('s1')).to.equal(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not mark unread when the session is active AND the chat view is focused', () => {
|
|
126
|
+
const provider = new TestableProvider(chatService, shell);
|
|
127
|
+
const { session, fire, pushRequest } = createFakeSession('s1');
|
|
128
|
+
activeSessionId = 's1';
|
|
129
|
+
activeWidget = chatViewWidget;
|
|
130
|
+
|
|
131
|
+
provider.watch(session);
|
|
132
|
+
pushRequest(true);
|
|
133
|
+
fire();
|
|
134
|
+
|
|
135
|
+
expect(provider.isUnread('s1')).to.equal(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not mark unread when focus is inside a child widget of the chat view', () => {
|
|
139
|
+
const provider = new TestableProvider(chatService, shell);
|
|
140
|
+
const { session, fire, pushRequest } = createFakeSession('s1');
|
|
141
|
+
activeSessionId = 's1';
|
|
142
|
+
// Simulate focus inside a descendant (e.g. AIChatInputWidget): the shell's
|
|
143
|
+
// activeWidget is the inner widget, but its parent chain leads to ChatViewWidget.
|
|
144
|
+
const childWidget = { parent: chatViewWidget } as unknown as Widget;
|
|
145
|
+
activeWidget = childWidget;
|
|
146
|
+
widgetForActiveElement = childWidget;
|
|
147
|
+
|
|
148
|
+
provider.watch(session);
|
|
149
|
+
pushRequest(true);
|
|
150
|
+
fire();
|
|
151
|
+
|
|
152
|
+
expect(provider.isUnread('s1')).to.equal(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('marks unread when the session is not the active one, even if the chat view is focused', () => {
|
|
156
|
+
const provider = new TestableProvider(chatService, shell);
|
|
157
|
+
const { session, fire, pushRequest } = createFakeSession('s1');
|
|
158
|
+
activeSessionId = 'other';
|
|
159
|
+
activeWidget = chatViewWidget;
|
|
160
|
+
|
|
161
|
+
provider.watch(session);
|
|
162
|
+
pushRequest(true);
|
|
163
|
+
fire();
|
|
164
|
+
|
|
165
|
+
expect(provider.isUnread('s1')).to.equal(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('fires onUnreadChanged once when a session transitions to unread', () => {
|
|
169
|
+
const provider = new TestableProvider(chatService, shell);
|
|
170
|
+
const { session, fire, pushRequest } = createFakeSession('s1');
|
|
171
|
+
activeSessionId = undefined;
|
|
172
|
+
activeWidget = otherWidget;
|
|
173
|
+
|
|
174
|
+
let fired = 0;
|
|
175
|
+
provider.onUnreadChanged(() => { fired++; });
|
|
176
|
+
|
|
177
|
+
provider.watch(session);
|
|
178
|
+
pushRequest(true);
|
|
179
|
+
fire();
|
|
180
|
+
pushRequest(true);
|
|
181
|
+
fire();
|
|
182
|
+
|
|
183
|
+
expect(provider.isUnread('s1')).to.equal(true);
|
|
184
|
+
expect(fired).to.equal(1);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
import { ChatWelcomeMessageProvider } from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
|
|
18
18
|
import { formatTimeAgo } from '@theia/ai-chat-ui/lib/browser/chat-date-utils';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ChatAgentService, ChatRequestModel, ChatResponseContent, ChatService, ChatSession, ChatSessionMetadata,
|
|
21
|
+
ErrorChatResponseContent, FormattedProviderError, formatProviderError, ThinkingChatResponseContent
|
|
22
|
+
} from '@theia/ai-chat';
|
|
20
23
|
import { BYPASS_MODEL_REQUIREMENT_PREF, PERSISTED_SESSION_LIMIT_PREF, SESSION_STORAGE_PREF, WELCOME_SCREEN_SESSIONS_PREF } from '@theia/ai-chat/lib/common/ai-chat-preferences';
|
|
21
24
|
import { AI_CHAT_SHOW_CHATS_COMMAND } from '@theia/ai-chat-ui/lib/browser/chat-view-commands';
|
|
25
|
+
import { ChatViewWidget } from '@theia/ai-chat-ui/lib/browser/chat-view-widget';
|
|
22
26
|
import { ChatSessionCardActionContribution } from './chat-session-card-action-contribution';
|
|
23
27
|
import { FrontendLanguageModelRegistry } from '@theia/ai-core/lib/common';
|
|
24
28
|
import { CommandRegistry, ContributionProvider, DisposableCollection, Emitter, Event, PreferenceService } from '@theia/core';
|
|
25
|
-
import { Card, CardActionButton, codicon, HoverService, buttonKeyboardProps, isActivationKey } from '@theia/core/lib/browser';
|
|
29
|
+
import { ApplicationShell, Card, CardActionButton, codicon, HoverService, buttonKeyboardProps, isActivationKey } from '@theia/core/lib/browser';
|
|
26
30
|
import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
|
|
27
31
|
import { nls } from '@theia/core/lib/common/nls';
|
|
28
32
|
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
|
@@ -137,6 +141,50 @@ function useTimeAgo(date: number): string {
|
|
|
137
141
|
return formatTimeAgo(date);
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
/** Read an error message from a completed-with-error response, if any. */
|
|
145
|
+
function getResponseErrorMessage(response: ChatRequestModel['response']): string | undefined {
|
|
146
|
+
if (response.errorObject?.message) {
|
|
147
|
+
return response.errorObject.message;
|
|
148
|
+
}
|
|
149
|
+
const errorPart = response.response.content.find(ErrorChatResponseContent.is);
|
|
150
|
+
return errorPart?.asDisplayString?.();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build a DOM fragment that renders a {@link FormattedProviderError} for the tooltip.
|
|
155
|
+
* Details are intentionally omitted — the hover popup is not interactive, so a
|
|
156
|
+
* <details> expander wouldn't work. The full payload is available in the chat output.
|
|
157
|
+
*/
|
|
158
|
+
function renderFormattedProviderError(error: FormattedProviderError): HTMLElement {
|
|
159
|
+
const wrapper = document.createElement('div');
|
|
160
|
+
wrapper.className = 'theia-chat-session-tooltip-error';
|
|
161
|
+
const prefix = document.createElement('span');
|
|
162
|
+
prefix.className = 'theia-chat-session-tooltip-error-prefix';
|
|
163
|
+
prefix.textContent = error.status
|
|
164
|
+
? `${nls.localizeByDefault('Error')} ${error.status}:`
|
|
165
|
+
: `${nls.localizeByDefault('Error')}:`;
|
|
166
|
+
wrapper.appendChild(prefix);
|
|
167
|
+
const headline = error.message.length > TOOLTIP_SNIPPET_MAX_LENGTH
|
|
168
|
+
? error.message.substring(0, TOOLTIP_SNIPPET_MAX_LENGTH) + '\u2026'
|
|
169
|
+
: error.message;
|
|
170
|
+
wrapper.appendChild(document.createTextNode(' ' + headline));
|
|
171
|
+
return wrapper;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Collect display text from response content, excluding thinking parts. */
|
|
175
|
+
function responseToTooltipString(content: ChatResponseContent[]): string {
|
|
176
|
+
return content
|
|
177
|
+
.filter(c => !ThinkingChatResponseContent.is(c))
|
|
178
|
+
.map(c => {
|
|
179
|
+
if (ChatResponseContent.hasAsString(c)) {
|
|
180
|
+
return c.asString();
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
})
|
|
184
|
+
.filter((text): text is string => text !== undefined && text !== '')
|
|
185
|
+
.join('\n\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
140
188
|
interface ChatSessionCardProps {
|
|
141
189
|
session: ChatSessionMetadata;
|
|
142
190
|
chatService: ChatService;
|
|
@@ -157,13 +205,29 @@ function ChatSessionCard(
|
|
|
157
205
|
|
|
158
206
|
const timeAgo = useTimeAgo(session.saveDate);
|
|
159
207
|
const [isWorking, setIsWorking] = React.useState(false);
|
|
208
|
+
const [hasError, setHasError] = React.useState(session.hasError === true);
|
|
160
209
|
const hasUnread = useUnreadMessages(session.sessionId, unreadState);
|
|
161
210
|
|
|
211
|
+
// Sync error state from metadata when it changes after initial render
|
|
212
|
+
React.useEffect(() => {
|
|
213
|
+
setHasError(session.hasError === true);
|
|
214
|
+
}, [session.hasError]);
|
|
215
|
+
|
|
216
|
+
// Resolve the agent for icon and display name
|
|
217
|
+
const agent = session.pinnedAgentId ? chatAgentService.getAgent(session.pinnedAgentId) : undefined;
|
|
218
|
+
const agentIcon = agent?.iconClass ?? codicon('comment-discussion');
|
|
219
|
+
const subtitle = agent ? `@${agent.name} \u00b7 ${timeAgo}` : timeAgo;
|
|
220
|
+
|
|
162
221
|
React.useEffect(() => {
|
|
163
222
|
const trash = new DisposableCollection();
|
|
164
223
|
|
|
165
224
|
const attach = (s: ChatSession) => {
|
|
166
|
-
const recompute = () =>
|
|
225
|
+
const recompute = () => {
|
|
226
|
+
const requests = s.model.getRequests();
|
|
227
|
+
setIsWorking(requests.some(ChatRequestModel.isInProgress));
|
|
228
|
+
const lastReq = requests.at(-1);
|
|
229
|
+
setHasError(lastReq?.response.isComplete === true && lastReq?.response.isError === true);
|
|
230
|
+
};
|
|
167
231
|
recompute();
|
|
168
232
|
s.model.onDidChange(recompute, undefined, trash);
|
|
169
233
|
};
|
|
@@ -199,9 +263,9 @@ function ChatSessionCard(
|
|
|
199
263
|
}
|
|
200
264
|
if (!hoverActiveRef.current || !chatSession) { return; }
|
|
201
265
|
|
|
202
|
-
const content = buildSessionTooltip(chatSession, session, chatAgentService, markdownRenderer, hasUnread);
|
|
266
|
+
const content = buildSessionTooltip(chatSession, session, chatAgentService, markdownRenderer, hasUnread, isWorking, hasError);
|
|
203
267
|
hoverService.requestHover({ content, target, position: 'left' });
|
|
204
|
-
}, [session, chatService, chatAgentService, hoverService, markdownRenderer, hasUnread]);
|
|
268
|
+
}, [session, chatService, chatAgentService, hoverService, markdownRenderer, hasUnread, isWorking, hasError]);
|
|
205
269
|
React.useEffect(() => () => { hoverActiveRef.current = false; }, []); // Block mouseEnter proceeding on unmount
|
|
206
270
|
|
|
207
271
|
const handleMouseLeave = React.useCallback(() => {
|
|
@@ -218,20 +282,26 @@ function ChatSessionCard(
|
|
|
218
282
|
}
|
|
219
283
|
}, [hoverService]);
|
|
220
284
|
|
|
285
|
+
const wrapperClass = [
|
|
286
|
+
'theia-chat-session-card-wrapper',
|
|
287
|
+
isWorking && 'theia-chat-session-card-working',
|
|
288
|
+
hasError && !isWorking && 'theia-chat-session-card-error'
|
|
289
|
+
].filter(Boolean).join(' ');
|
|
290
|
+
|
|
221
291
|
return (
|
|
222
292
|
<div ref={wrapperRef}
|
|
223
|
-
className={
|
|
293
|
+
className={wrapperClass}
|
|
224
294
|
onMouseEnter={handleMouseEnter}
|
|
225
295
|
onMouseLeave={handleMouseLeave}
|
|
226
296
|
onMouseOver={handleMouseOver}>
|
|
227
297
|
<Card
|
|
228
|
-
icon={isWorking ? `${codicon('loading')} theia-animation-spin` :
|
|
298
|
+
icon={isWorking ? `${codicon('loading')} theia-animation-spin` : agentIcon}
|
|
229
299
|
title={session.title || nls.localizeByDefault('Untitled Chat')}
|
|
230
|
-
subtitle={
|
|
300
|
+
subtitle={subtitle}
|
|
231
301
|
actionButtons={actionButtons}
|
|
232
302
|
onClick={onClick}
|
|
233
303
|
/>
|
|
234
|
-
{hasUnread && !isWorking && <div className="theia-chat-session-badge-unread" />}
|
|
304
|
+
{hasUnread && !isWorking && !hasError && <div className="theia-chat-session-badge-unread" />}
|
|
235
305
|
</div>
|
|
236
306
|
);
|
|
237
307
|
}
|
|
@@ -239,7 +309,7 @@ function ChatSessionCard(
|
|
|
239
309
|
function buildSessionTooltip(
|
|
240
310
|
session: ChatSession, metadata: ChatSessionMetadata,
|
|
241
311
|
agentService: ChatAgentService, markdownRenderer: MarkdownRenderer,
|
|
242
|
-
isUnread: boolean
|
|
312
|
+
isUnread: boolean, isRunning: boolean, hasError: boolean
|
|
243
313
|
): HTMLElement {
|
|
244
314
|
const requests = session.model.getRequests();
|
|
245
315
|
const lastRequest = requests.at(-1);
|
|
@@ -247,7 +317,17 @@ function buildSessionTooltip(
|
|
|
247
317
|
const container = document.createElement('div');
|
|
248
318
|
container.className = 'theia-chat-session-tooltip';
|
|
249
319
|
|
|
250
|
-
if (
|
|
320
|
+
if (isRunning) {
|
|
321
|
+
const badge = document.createElement('div');
|
|
322
|
+
badge.className = 'theia-chat-session-badge-running-tooltip';
|
|
323
|
+
badge.textContent = nls.localizeByDefault('Running');
|
|
324
|
+
container.appendChild(badge);
|
|
325
|
+
} else if (hasError) {
|
|
326
|
+
const badge = document.createElement('div');
|
|
327
|
+
badge.className = 'theia-chat-session-badge-error-tooltip';
|
|
328
|
+
badge.textContent = nls.localizeByDefault('Error');
|
|
329
|
+
container.appendChild(badge);
|
|
330
|
+
} else if (isUnread) {
|
|
251
331
|
const badge = document.createElement('div');
|
|
252
332
|
badge.className = 'theia-chat-session-badge-unread-tooltip';
|
|
253
333
|
badge.textContent = nls.localize('theia/ai/ide/tooltip/unread', 'Unread');
|
|
@@ -256,36 +336,39 @@ function buildSessionTooltip(
|
|
|
256
336
|
|
|
257
337
|
if (lastRequest) {
|
|
258
338
|
const lastResponse = lastRequest.response;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (lastResponse.isComplete && !lastResponse.isError) {
|
|
262
|
-
// Show the agent's response text (already markdown)
|
|
263
|
-
messageText = lastResponse.response.asString() || undefined;
|
|
264
|
-
} else if (!lastResponse.isComplete) {
|
|
265
|
-
// Request is still pending / no response yet — show the user's request text
|
|
266
|
-
messageText = lastRequest.request.text || undefined;
|
|
267
|
-
} else {
|
|
268
|
-
// Failure response — find the most recent successful exchange
|
|
269
|
-
const lastSuccessfulRequest = requests.findLast(r => r.response.isComplete && !r.response.isError);
|
|
270
|
-
messageText = lastSuccessfulRequest?.response.response.asString() || undefined;
|
|
271
|
-
}
|
|
339
|
+
const errorText = hasError ? getResponseErrorMessage(lastResponse) : undefined;
|
|
272
340
|
|
|
273
|
-
if (
|
|
274
|
-
const snippet = messageText.length > TOOLTIP_SNIPPET_MAX_LENGTH
|
|
275
|
-
? messageText.substring(0, TOOLTIP_SNIPPET_MAX_LENGTH) + '\u2026'
|
|
276
|
-
: messageText;
|
|
341
|
+
if (errorText) {
|
|
277
342
|
const label = document.createElement('div');
|
|
278
343
|
label.className = 'theia-chat-session-tooltip-label';
|
|
279
|
-
label.textContent = nls.localize('theia/ai/ide/tooltip/
|
|
344
|
+
label.textContent = nls.localize('theia/ai/ide/tooltip/errorMessage', 'Error message');
|
|
280
345
|
container.appendChild(label);
|
|
281
|
-
|
|
282
|
-
const snippetEl = document.createElement('div');
|
|
283
|
-
snippetEl.className = 'theia-chat-session-tooltip-snippet';
|
|
284
|
-
snippetEl.appendChild(markdownRenderer.render({ value: snippet }).element);
|
|
285
|
-
container.appendChild(snippetEl);
|
|
346
|
+
container.appendChild(renderFormattedProviderError(formatProviderError(errorText)));
|
|
286
347
|
|
|
287
348
|
const hr = document.createElement('hr');
|
|
288
349
|
container.appendChild(hr);
|
|
350
|
+
} else {
|
|
351
|
+
const messageText = lastResponse.isComplete
|
|
352
|
+
? (responseToTooltipString(lastResponse.response.content) || undefined)
|
|
353
|
+
: (lastRequest.request.text || undefined);
|
|
354
|
+
|
|
355
|
+
if (messageText) {
|
|
356
|
+
const snippet = messageText.length > TOOLTIP_SNIPPET_MAX_LENGTH
|
|
357
|
+
? messageText.substring(0, TOOLTIP_SNIPPET_MAX_LENGTH) + '\u2026'
|
|
358
|
+
: messageText;
|
|
359
|
+
const label = document.createElement('div');
|
|
360
|
+
label.className = 'theia-chat-session-tooltip-label';
|
|
361
|
+
label.textContent = nls.localize('theia/ai/ide/tooltip/lastMessage', 'Last message');
|
|
362
|
+
container.appendChild(label);
|
|
363
|
+
|
|
364
|
+
const snippetEl = document.createElement('div');
|
|
365
|
+
snippetEl.className = 'theia-chat-session-tooltip-snippet';
|
|
366
|
+
snippetEl.appendChild(markdownRenderer.render({ value: snippet }).element);
|
|
367
|
+
container.appendChild(snippetEl);
|
|
368
|
+
|
|
369
|
+
const hr = document.createElement('hr');
|
|
370
|
+
container.appendChild(hr);
|
|
371
|
+
}
|
|
289
372
|
}
|
|
290
373
|
}
|
|
291
374
|
|
|
@@ -350,6 +433,9 @@ export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessagePro
|
|
|
350
433
|
@inject(FrontendLanguageModelRegistry)
|
|
351
434
|
protected readonly languageModelRegistry: FrontendLanguageModelRegistry;
|
|
352
435
|
|
|
436
|
+
@inject(ApplicationShell)
|
|
437
|
+
protected readonly shell: ApplicationShell;
|
|
438
|
+
|
|
353
439
|
protected _inputEnabled = false;
|
|
354
440
|
|
|
355
441
|
private readonly unreadSessions = new Map<string, { unread: boolean; seenRequests: number; seenCompleted: number; listener: DisposableCollection }>();
|
|
@@ -474,7 +560,18 @@ export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessagePro
|
|
|
474
560
|
session.model.onDidChange(() => {
|
|
475
561
|
const current = session.model.getRequests();
|
|
476
562
|
if (current.length > state.seenRequests || this.countCompleted(current) > state.seenCompleted) {
|
|
477
|
-
|
|
563
|
+
// Only silently update the seen counts (instead of flashing the unread badge)
|
|
564
|
+
// when the user is actually looking at this session: it must be the active
|
|
565
|
+
// session AND the chat view must currently be the focused widget. Otherwise,
|
|
566
|
+
// the user may have switched away (e.g. to the editor) while the chat agent
|
|
567
|
+
// is still running, and we want the badge to appear so they notice the new
|
|
568
|
+
// response when they return.
|
|
569
|
+
const activeSession = this.chatService.getActiveSession();
|
|
570
|
+
const chatViewFocused = ChatViewWidget.findActive(this.shell) !== undefined;
|
|
571
|
+
if (chatViewFocused && activeSession && activeSession.id === session.id) {
|
|
572
|
+
state.seenRequests = current.length;
|
|
573
|
+
state.seenCompleted = this.countCompleted(current);
|
|
574
|
+
} else if (!state.unread) {
|
|
478
575
|
state.unread = true;
|
|
479
576
|
this.onUnreadChangedEmitter.fire(session.id);
|
|
480
577
|
}
|
|
@@ -1122,22 +1122,46 @@
|
|
|
1122
1122
|
text-decoration: underline;
|
|
1123
1123
|
}
|
|
1124
1124
|
|
|
1125
|
-
.theia-chat-session-badge-unread-tooltip
|
|
1125
|
+
.theia-chat-session-badge-unread-tooltip,
|
|
1126
|
+
.theia-chat-session-badge-running-tooltip,
|
|
1127
|
+
.theia-chat-session-badge-error-tooltip {
|
|
1126
1128
|
font-size: var(--theia-ui-font-size0);
|
|
1127
1129
|
font-weight: 600;
|
|
1128
|
-
color: var(--theia-activityBarBadge-foreground);
|
|
1129
|
-
background-color: var(--theia-activityBarBadge-background);
|
|
1130
1130
|
border-radius: calc(var(--theia-ui-padding) * 0.75);
|
|
1131
1131
|
padding: 1px calc(var(--theia-ui-padding) * 0.75);
|
|
1132
1132
|
display: inline-block;
|
|
1133
1133
|
margin-bottom: var(--theia-ui-padding);
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
|
+
.theia-chat-session-badge-unread-tooltip {
|
|
1137
|
+
color: var(--theia-activityBarBadge-foreground);
|
|
1138
|
+
background-color: var(--theia-activityBarBadge-background);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.theia-chat-session-badge-running-tooltip {
|
|
1142
|
+
color: var(--theia-activityBarBadge-foreground);
|
|
1143
|
+
background-color: var(--theia-progressBar-background);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
.theia-chat-session-badge-error-tooltip {
|
|
1147
|
+
color: var(--theia-activityBarBadge-foreground);
|
|
1148
|
+
background-color: var(--theia-errorForeground);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1136
1151
|
.theia-chat-session-tooltip-label {
|
|
1137
1152
|
font-weight: 600;
|
|
1138
1153
|
margin-bottom: calc(var(--theia-ui-padding) * 0.5);
|
|
1139
1154
|
}
|
|
1140
1155
|
|
|
1156
|
+
.theia-chat-session-tooltip-error {
|
|
1157
|
+
color: var(--theia-errorForeground);
|
|
1158
|
+
word-break: break-word;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.theia-chat-session-tooltip-error-prefix {
|
|
1162
|
+
font-weight: 600;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1141
1165
|
.theia-chat-session-tooltip-snippet h1,
|
|
1142
1166
|
.theia-chat-session-tooltip-snippet h2,
|
|
1143
1167
|
.theia-chat-session-tooltip-snippet h3,
|
|
@@ -1184,6 +1208,18 @@
|
|
|
1184
1208
|
color: var(--theia-focusBorder);
|
|
1185
1209
|
}
|
|
1186
1210
|
|
|
1211
|
+
.theia-chat-session-card-error .theia-Card-icon {
|
|
1212
|
+
color: var(--theia-errorForeground);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.theia-chat-session-card-error .theia-Card-interactive:hover {
|
|
1216
|
+
border-color: var(--theia-errorForeground);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.theia-chat-session-card-error .theia-Card-interactive:focus {
|
|
1220
|
+
outline-color: var(--theia-errorForeground);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1187
1223
|
.theia-chat-session-badge-unread {
|
|
1188
1224
|
position: absolute;
|
|
1189
1225
|
top: calc(var(--theia-ui-padding) * 1.5);
|
|
@@ -1196,6 +1232,10 @@
|
|
|
1196
1232
|
z-index: 1;
|
|
1197
1233
|
}
|
|
1198
1234
|
|
|
1235
|
+
.theia-chat-session-card-error .theia-chat-session-badge-unread {
|
|
1236
|
+
background-color: var(--theia-errorForeground);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1199
1239
|
/*
|
|
1200
1240
|
* AI Tools Configuration Widget Styles
|
|
1201
1241
|
* Only touch styles in this section for the tools configuration widget
|