@theia/ai-ide 1.72.0-next.52 → 1.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/lib/browser/app-tester-chat-agent.js +1 -1
  2. package/lib/browser/architect-agent.js +1 -1
  3. package/lib/browser/architect-agent.js.map +1 -1
  4. package/lib/browser/chat-sessions-welcome-message-provider.d.ts +2 -1
  5. package/lib/browser/chat-sessions-welcome-message-provider.d.ts.map +1 -1
  6. package/lib/browser/chat-sessions-welcome-message-provider.js +122 -32
  7. package/lib/browser/chat-sessions-welcome-message-provider.js.map +1 -1
  8. package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts +2 -0
  9. package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts.map +1 -0
  10. package/lib/browser/chat-sessions-welcome-message-provider.spec.js +156 -0
  11. package/lib/browser/chat-sessions-welcome-message-provider.spec.js.map +1 -0
  12. package/lib/browser/explore-agent.js +1 -1
  13. package/lib/browser/frontend-module.d.ts.map +1 -1
  14. package/lib/browser/frontend-module.js +2 -0
  15. package/lib/browser/frontend-module.js.map +1 -1
  16. package/lib/browser/github-chat-agent.js +1 -1
  17. package/lib/browser/project-info-agent.js +1 -1
  18. package/lib/browser/review/pr-review-capability-contribution.d.ts +12 -0
  19. package/lib/browser/review/pr-review-capability-contribution.d.ts.map +1 -0
  20. package/lib/browser/review/pr-review-capability-contribution.js +213 -0
  21. package/lib/browser/review/pr-review-capability-contribution.js.map +1 -0
  22. package/lib/browser/review/pr-review-prompt-template.d.ts +5 -0
  23. package/lib/browser/review/pr-review-prompt-template.d.ts.map +1 -1
  24. package/lib/browser/review/pr-review-prompt-template.js +172 -139
  25. package/lib/browser/review/pr-review-prompt-template.js.map +1 -1
  26. package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
  27. package/lib/browser/user-interaction-tool-renderer.js +63 -39
  28. package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
  29. package/lib/browser/user-interaction-tool.d.ts +19 -10
  30. package/lib/browser/user-interaction-tool.d.ts.map +1 -1
  31. package/lib/browser/user-interaction-tool.js +43 -54
  32. package/lib/browser/user-interaction-tool.js.map +1 -1
  33. package/lib/browser/user-interaction-tool.spec.js +66 -41
  34. package/lib/browser/user-interaction-tool.spec.js.map +1 -1
  35. package/lib/common/command-chat-agents.js +1 -1
  36. package/lib/common/command-chat-agents.js.map +1 -1
  37. package/lib/common/orchestrator-chat-agent.js +1 -1
  38. package/lib/common/orchestrator-chat-agent.js.map +1 -1
  39. package/lib/common/user-interaction-tool.d.ts +1 -0
  40. package/lib/common/user-interaction-tool.d.ts.map +1 -1
  41. package/lib/common/user-interaction-tool.js +43 -15
  42. package/lib/common/user-interaction-tool.js.map +1 -1
  43. package/lib/common/user-interaction-tool.spec.js +27 -0
  44. package/lib/common/user-interaction-tool.spec.js.map +1 -1
  45. package/package.json +22 -22
  46. package/src/browser/app-tester-chat-agent.ts +1 -1
  47. package/src/browser/architect-agent.ts +1 -1
  48. package/src/browser/chat-sessions-welcome-message-provider.spec.ts +186 -0
  49. package/src/browser/chat-sessions-welcome-message-provider.tsx +132 -35
  50. package/src/browser/explore-agent.ts +1 -1
  51. package/src/browser/frontend-module.ts +2 -0
  52. package/src/browser/github-chat-agent.ts +1 -1
  53. package/src/browser/project-info-agent.ts +1 -1
  54. package/src/browser/review/pr-review-capability-contribution.ts +232 -0
  55. package/src/browser/review/pr-review-prompt-template.ts +181 -150
  56. package/src/browser/style/index.css +43 -3
  57. package/src/browser/user-interaction-tool-renderer.tsx +74 -49
  58. package/src/browser/user-interaction-tool.spec.ts +73 -46
  59. package/src/browser/user-interaction-tool.ts +52 -58
  60. package/src/common/command-chat-agents.ts +1 -1
  61. package/src/common/orchestrator-chat-agent.ts +1 -1
  62. package/src/common/user-interaction-tool.spec.ts +29 -0
  63. package/src/common/user-interaction-tool.ts +42 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theia/ai-ide",
3
- "version": "1.72.0-next.52+726081b2a",
3
+ "version": "1.72.0",
4
4
  "description": "AI IDE Agents Extension",
5
5
  "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
6
6
  "repository": {
@@ -15,24 +15,24 @@
15
15
  "theia-extension"
16
16
  ],
17
17
  "dependencies": {
18
- "@theia/ai-chat": "1.72.0-next.52+726081b2a",
19
- "@theia/ai-chat-ui": "1.72.0-next.52+726081b2a",
20
- "@theia/ai-core": "1.72.0-next.52+726081b2a",
21
- "@theia/ai-mcp": "1.72.0-next.52+726081b2a",
22
- "@theia/ai-terminal": "1.72.0-next.52+726081b2a",
23
- "@theia/core": "1.72.0-next.52+726081b2a",
24
- "@theia/debug": "1.72.0-next.52+726081b2a",
25
- "@theia/editor": "1.72.0-next.52+726081b2a",
26
- "@theia/filesystem": "1.72.0-next.52+726081b2a",
27
- "@theia/markers": "1.72.0-next.52+726081b2a",
28
- "@theia/monaco": "1.72.0-next.52+726081b2a",
29
- "@theia/navigator": "1.72.0-next.52+726081b2a",
30
- "@theia/preferences": "1.72.0-next.52+726081b2a",
31
- "@theia/scm": "1.72.0-next.52+726081b2a",
32
- "@theia/search-in-workspace": "1.72.0-next.52+726081b2a",
33
- "@theia/task": "1.72.0-next.52+726081b2a",
34
- "@theia/terminal": "1.72.0-next.52+726081b2a",
35
- "@theia/workspace": "1.72.0-next.52+726081b2a",
18
+ "@theia/ai-chat": "1.72.0",
19
+ "@theia/ai-chat-ui": "1.72.0",
20
+ "@theia/ai-core": "1.72.0",
21
+ "@theia/ai-mcp": "1.72.0",
22
+ "@theia/ai-terminal": "1.72.0",
23
+ "@theia/core": "1.72.0",
24
+ "@theia/debug": "1.72.0",
25
+ "@theia/editor": "1.72.0",
26
+ "@theia/filesystem": "1.72.0",
27
+ "@theia/markers": "1.72.0",
28
+ "@theia/monaco": "1.72.0",
29
+ "@theia/navigator": "1.72.0",
30
+ "@theia/preferences": "1.72.0",
31
+ "@theia/scm": "1.72.0",
32
+ "@theia/search-in-workspace": "1.72.0",
33
+ "@theia/task": "1.72.0",
34
+ "@theia/terminal": "1.72.0",
35
+ "@theia/workspace": "1.72.0",
36
36
  "date-fns": "^4.1.0",
37
37
  "ignore": "^6.0.2",
38
38
  "js-yaml": "^4.1.1",
@@ -44,8 +44,8 @@
44
44
  "access": "public"
45
45
  },
46
46
  "devDependencies": {
47
- "@theia/cli": "1.72.0-next.52+726081b2a",
48
- "@theia/test": "1.72.0-next.52+726081b2a"
47
+ "@theia/cli": "1.72.0",
48
+ "@theia/test": "1.72.0"
49
49
  },
50
50
  "theiaExtensions": [
51
51
  {
@@ -69,5 +69,5 @@
69
69
  "nyc": {
70
70
  "extends": "../../configs/nyc.json"
71
71
  },
72
- "gitHead": "726081b2aa03827611ba4a323da1a6aaf0e9a9b9"
72
+ "gitHead": "5bc20bc672aa732fb7e05234cf63c1b514868896"
73
73
  }
@@ -40,7 +40,7 @@ export class AppTesterChatAgent extends AbstractStreamParsingChatAgent {
40
40
  name = AppTesterChatAgentId;
41
41
  languageModelRequirements: LanguageModelRequirement[] = [{
42
42
  purpose: 'chat',
43
- identifier: 'default/code',
43
+ identifier: 'default/fast',
44
44
  }];
45
45
  protected defaultLanguageModelPurpose: string = 'chat';
46
46
  override description = nls.localize('theia/ai/chat/app-tester/description', 'This agent tests your application user interface to verify user-specified test scenarios through browser automation. '
@@ -52,7 +52,7 @@ export class ArchitectAgent extends AbstractModeAwareChatAgent {
52
52
  protected readonly modeDefinitions: Omit<ChatMode, 'isDefault'>[] = [
53
53
  {
54
54
  id: ARCHITECT_PLANNING_PROMPT_ID,
55
- name: nls.localize('theia/ai/ide/architectAgent/mode/plan', 'Plan Mode')
55
+ name: nls.localizeByDefault('Plan Mode')
56
56
  },
57
57
  {
58
58
  id: ARCHITECT_SIMPLE_PROMPT_ID,
@@ -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 { ChatAgentService, ChatRequestModel, ChatService, ChatSession, ChatSessionMetadata } from '@theia/ai-chat';
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 = () => setIsWorking(s.model.getRequests().some(ChatRequestModel.isInProgress));
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={`theia-chat-session-card-wrapper${isWorking ? ' theia-chat-session-card-working' : ''}`}
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` : codicon('comment-discussion')}
298
+ icon={isWorking ? `${codicon('loading')} theia-animation-spin` : agentIcon}
229
299
  title={session.title || nls.localizeByDefault('Untitled Chat')}
230
- subtitle={timeAgo}
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 (isUnread) {
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
- let messageText: string | undefined;
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 (messageText) {
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/lastMessage', 'Last message');
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
- if (!state.unread) {
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
  }
@@ -28,7 +28,7 @@ export class ExploreAgent extends AbstractStreamParsingChatAgent {
28
28
  id = ExploreAgentId;
29
29
  languageModelRequirements: LanguageModelRequirement[] = [{
30
30
  purpose: 'chat',
31
- identifier: 'default/code',
31
+ identifier: 'default/fast',
32
32
  }];
33
33
  protected defaultLanguageModelPurpose: string = 'chat';
34
34
  override description = nls.localize('theia/ai/ide/exploreAgent/description',
@@ -125,6 +125,7 @@ import { ExploreAgent } from './explore-agent';
125
125
  import { CodeReviewerAgent } from './code-reviewer-agent';
126
126
  import { CodeReviewCapabilityContribution } from './code-review-capability-contribution';
127
127
  import { PRReviewAgent } from './review/pr-review-agent';
128
+ import { PRReviewCapabilityContribution } from './review/pr-review-capability-contribution';
128
129
 
129
130
  export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
130
131
  bind(PreferenceContribution).toConstantValue({ schema: aiIdePreferenceSchema });
@@ -356,4 +357,5 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
356
357
  bind(FrontendApplicationContribution).to(ShellExecutionCapabilityContribution);
357
358
 
358
359
  bind(FrontendApplicationContribution).to(CodeReviewCapabilityContribution);
360
+ bind(FrontendApplicationContribution).to(PRReviewCapabilityContribution);
359
361
  });
@@ -51,7 +51,7 @@ export class GitHubChatAgent extends AbstractStreamParsingChatAgent {
51
51
  name = GitHubChatAgentId;
52
52
  languageModelRequirements: LanguageModelRequirement[] = [{
53
53
  purpose: 'chat',
54
- identifier: 'default/code',
54
+ identifier: 'default/fast',
55
55
  }];
56
56
  protected defaultLanguageModelPurpose: string = 'chat';
57
57
  override description = nls.localize('theia/ai/ide/github/description', 'This agent helps you interact with GitHub repositories, issues, pull requests, and other GitHub '
@@ -26,7 +26,7 @@ export class ProjectInfoAgent extends AbstractStreamParsingChatAgent {
26
26
  id = 'ProjectInfo';
27
27
  languageModelRequirements: LanguageModelRequirement[] = [{
28
28
  purpose: 'chat',
29
- identifier: 'default/code',
29
+ identifier: 'default/fast',
30
30
  }];
31
31
  protected defaultLanguageModelPurpose: string = 'chat';
32
32