@theia/ai-ide 1.74.0-next.8 → 1.74.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 (96) hide show
  1. package/lib/browser/ai-allow-all-mode-chat-banner.d.ts +68 -0
  2. package/lib/browser/ai-allow-all-mode-chat-banner.d.ts.map +1 -0
  3. package/lib/browser/ai-allow-all-mode-chat-banner.js +184 -0
  4. package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -0
  5. package/lib/browser/ai-allow-all-mode-chat-banner.spec.d.ts +2 -0
  6. package/lib/browser/ai-allow-all-mode-chat-banner.spec.d.ts.map +1 -0
  7. package/lib/browser/ai-allow-all-mode-chat-banner.spec.js +170 -0
  8. package/lib/browser/ai-allow-all-mode-chat-banner.spec.js.map +1 -0
  9. package/lib/browser/ai-configuration/tools-configuration-widget.d.ts +3 -4
  10. package/lib/browser/ai-configuration/tools-configuration-widget.d.ts.map +1 -1
  11. package/lib/browser/ai-configuration/tools-configuration-widget.js +17 -10
  12. package/lib/browser/ai-configuration/tools-configuration-widget.js.map +1 -1
  13. package/lib/browser/ai-first-perspective-contribution.d.ts +6 -0
  14. package/lib/browser/ai-first-perspective-contribution.d.ts.map +1 -0
  15. package/lib/browser/ai-first-perspective-contribution.js +50 -0
  16. package/lib/browser/ai-first-perspective-contribution.js.map +1 -0
  17. package/lib/browser/ai-sessions-view-contribution.d.ts +13 -0
  18. package/lib/browser/ai-sessions-view-contribution.d.ts.map +1 -0
  19. package/lib/browser/ai-sessions-view-contribution.js +69 -0
  20. package/lib/browser/ai-sessions-view-contribution.js.map +1 -0
  21. package/lib/browser/ai-sessions-widget.d.ts +27 -0
  22. package/lib/browser/ai-sessions-widget.d.ts.map +1 -0
  23. package/lib/browser/ai-sessions-widget.js +131 -0
  24. package/lib/browser/ai-sessions-widget.js.map +1 -0
  25. package/lib/browser/chat-session-item-action-contribution.js +2 -2
  26. package/lib/browser/chat-session-item-action-contribution.js.map +1 -1
  27. package/lib/browser/chat-session-item.d.ts +15 -0
  28. package/lib/browser/chat-session-item.d.ts.map +1 -1
  29. package/lib/browser/chat-session-item.js +39 -18
  30. package/lib/browser/chat-session-item.js.map +1 -1
  31. package/lib/browser/chat-session-list-components.d.ts +38 -0
  32. package/lib/browser/chat-session-list-components.d.ts.map +1 -0
  33. package/lib/browser/chat-session-list-components.js +87 -0
  34. package/lib/browser/chat-session-list-components.js.map +1 -0
  35. package/lib/browser/chat-session-list-service.d.ts +34 -0
  36. package/lib/browser/chat-session-list-service.d.ts.map +1 -0
  37. package/lib/browser/chat-session-list-service.js +251 -0
  38. package/lib/browser/chat-session-list-service.js.map +1 -0
  39. package/lib/browser/chat-session-list-service.spec.d.ts +2 -0
  40. package/lib/browser/chat-session-list-service.spec.d.ts.map +1 -0
  41. package/lib/browser/{chat-sessions-welcome-message-provider.spec.js → chat-session-list-service.spec.js} +133 -39
  42. package/lib/browser/chat-session-list-service.spec.js.map +1 -0
  43. package/lib/browser/chat-session-tooltip.d.ts +2 -2
  44. package/lib/browser/chat-session-tooltip.d.ts.map +1 -1
  45. package/lib/browser/chat-session-tooltip.js +33 -10
  46. package/lib/browser/chat-session-tooltip.js.map +1 -1
  47. package/lib/browser/chat-sessions-welcome-message-provider.d.ts +9 -35
  48. package/lib/browser/chat-sessions-welcome-message-provider.d.ts.map +1 -1
  49. package/lib/browser/chat-sessions-welcome-message-provider.js +51 -205
  50. package/lib/browser/chat-sessions-welcome-message-provider.js.map +1 -1
  51. package/lib/browser/context-file-validation-service-impl.spec.js +2 -2
  52. package/lib/browser/context-file-validation-service-impl.spec.js.map +1 -1
  53. package/lib/browser/frontend-module.d.ts.map +1 -1
  54. package/lib/browser/frontend-module.js +20 -0
  55. package/lib/browser/frontend-module.js.map +1 -1
  56. package/lib/browser/workspace-functions.d.ts +5 -6
  57. package/lib/browser/workspace-functions.d.ts.map +1 -1
  58. package/lib/browser/workspace-functions.js +24 -9
  59. package/lib/browser/workspace-functions.js.map +1 -1
  60. package/lib/browser/workspace-functions.spec.js +29 -29
  61. package/lib/browser/workspace-functions.spec.js.map +1 -1
  62. package/lib/browser/workspace-launch-provider.spec.js +3 -3
  63. package/lib/browser/workspace-launch-provider.spec.js.map +1 -1
  64. package/lib/browser/workspace-search-provider.d.ts.map +1 -1
  65. package/lib/browser/workspace-search-provider.js +28 -20
  66. package/lib/browser/workspace-search-provider.js.map +1 -1
  67. package/lib/browser/workspace-search-provider.spec.js +68 -0
  68. package/lib/browser/workspace-search-provider.spec.js.map +1 -1
  69. package/lib/browser/workspace-task-provider.spec.js +4 -4
  70. package/lib/browser/workspace-task-provider.spec.js.map +1 -1
  71. package/package.json +25 -25
  72. package/src/browser/ai-allow-all-mode-chat-banner.spec.ts +205 -0
  73. package/src/browser/ai-allow-all-mode-chat-banner.tsx +217 -0
  74. package/src/browser/ai-configuration/tools-configuration-widget.tsx +20 -13
  75. package/src/browser/ai-first-perspective-contribution.ts +48 -0
  76. package/src/browser/ai-sessions-view-contribution.ts +67 -0
  77. package/src/browser/ai-sessions-widget.tsx +151 -0
  78. package/src/browser/chat-session-item-action-contribution.ts +2 -2
  79. package/src/browser/chat-session-item.tsx +82 -27
  80. package/src/browser/chat-session-list-components.tsx +136 -0
  81. package/src/browser/{chat-sessions-welcome-message-provider.spec.ts → chat-session-list-service.spec.ts} +136 -30
  82. package/src/browser/chat-session-list-service.ts +261 -0
  83. package/src/browser/chat-session-tooltip.ts +31 -11
  84. package/src/browser/chat-sessions-welcome-message-provider.tsx +78 -268
  85. package/src/browser/context-file-validation-service-impl.spec.ts +3 -3
  86. package/src/browser/frontend-module.ts +23 -0
  87. package/src/browser/style/index.css +150 -3
  88. package/src/browser/workspace-functions.spec.ts +36 -37
  89. package/src/browser/workspace-functions.ts +8 -9
  90. package/src/browser/workspace-launch-provider.spec.ts +5 -5
  91. package/src/browser/workspace-search-provider.spec.ts +101 -0
  92. package/src/browser/workspace-search-provider.ts +34 -23
  93. package/src/browser/workspace-task-provider.spec.ts +6 -7
  94. package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts +0 -2
  95. package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts.map +0 -1
  96. package/lib/browser/chat-sessions-welcome-message-provider.spec.js.map +0 -1
@@ -17,109 +17,22 @@
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
19
  import {
20
- ChatAgentService, ChatService, ChatSession, ChatSessionMetadata
20
+ ChatAgentService, ChatService, ChatSessionMetadata
21
21
  } from '@theia/ai-chat';
22
- 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';
22
+ import { BYPASS_MODEL_REQUIREMENT_PREF, WELCOME_SCREEN_SESSIONS_PREF } from '@theia/ai-chat/lib/common/ai-chat-preferences';
23
23
  import { AI_CHAT_SHOW_CHATS_COMMAND } from '@theia/ai-chat-ui/lib/browser/chat-view-commands';
24
- import { ChatViewWidget } from '@theia/ai-chat-ui/lib/browser/chat-view-widget';
25
24
  import { ChatSessionItemAction, ChatSessionItemActionContribution } from './chat-session-item-action-contribution';
25
+ import { ChatSessionListService } from './chat-session-list-service';
26
+ import { SectionedSessions, SessionRow, SessionsList } from './chat-session-list-components';
26
27
  import { ChatSessionItem } from './chat-session-item';
27
28
  import { FrontendLanguageModelRegistry } from '@theia/ai-core/lib/common';
28
- import { CommandRegistry, ContributionProvider, DisposableCollection, Emitter, Event, PreferenceService } from '@theia/core';
29
- import { ApplicationShell, buttonKeyboardProps, HoverService, isActivationKey } from '@theia/core/lib/browser';
29
+ import { CommandRegistry, ContributionProvider, Emitter, Event, PreferenceService } from '@theia/core';
30
+ import { ApplicationShell, HoverService } from '@theia/core/lib/browser';
31
+ import { AISessionsWidget } from './ai-sessions-widget';
30
32
  import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
31
- import { nls } from '@theia/core/lib/common/nls';
32
33
  import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
33
34
  import * as React from '@theia/core/shared/react';
34
35
 
35
- /** When both Active and Restored sections are non-empty, keep at least this many Restored slots. */
36
- const RESTORED_MIN_RESERVATION = 5;
37
-
38
- export interface SectionedSessions {
39
- active: ChatSessionMetadata[];
40
- restored: ChatSessionMetadata[];
41
- }
42
-
43
- export interface VisibleSessionSlots {
44
- activeCount: number;
45
- restoredCount: number;
46
- }
47
-
48
- /**
49
- * Allocates the capped number of visible items between the Active and Restored sections of the
50
- * overview. When both sections are non-empty, up to {@link RESTORED_MIN_RESERVATION} slots are
51
- * reserved for Restored so active sessions cannot crowd it out entirely. A cap of 0 hides the
52
- * inline list (every session stays reachable via the "Browse all chats..." link).
53
- */
54
- export function computeVisibleSessionSlots(activeTotal: number, restoredTotal: number, maxSessions: number): VisibleSessionSlots {
55
- const cap = Math.max(0, maxSessions);
56
- if (cap === 0) {
57
- return { activeCount: 0, restoredCount: 0 };
58
- }
59
- if (restoredTotal === 0) {
60
- return { activeCount: Math.min(activeTotal, cap), restoredCount: 0 };
61
- }
62
- if (activeTotal === 0) {
63
- return { activeCount: 0, restoredCount: Math.min(restoredTotal, cap) };
64
- }
65
- const reserved = Math.min(restoredTotal, Math.min(RESTORED_MIN_RESERVATION, Math.max(1, cap - 1)));
66
- const activeCount = Math.min(activeTotal, cap - reserved);
67
- const restoredCount = Math.min(restoredTotal, cap - activeCount);
68
- return { activeCount, restoredCount };
69
- }
70
-
71
- interface SessionsListProps {
72
- sections: SectionedSessions;
73
- /** Total cap on items shown on the home view; overflow surfaces via the Browse all link. */
74
- maxSessions: number;
75
- renderItem: (session: ChatSessionMetadata, isRestored: boolean) => React.ReactNode;
76
- onBrowseAll: () => void;
77
- }
78
-
79
- function SessionsList({ sections, maxSessions, renderItem, onBrowseAll }: SessionsListProps): React.ReactElement {
80
- const total = sections.active.length + sections.restored.length;
81
- const { activeCount, restoredCount } = computeVisibleSessionSlots(sections.active.length, sections.restored.length, maxSessions);
82
- const activeVisible = sections.active.slice(0, activeCount);
83
- const restoredVisible = sections.restored.slice(0, restoredCount);
84
- const hiddenCount = total - activeVisible.length - restoredVisible.length;
85
-
86
- return (
87
- <div className="theia-WelcomeMessage-SessionsList">
88
- {activeVisible.length > 0 && (
89
- <div className="theia-WelcomeMessage-SessionsGroup">
90
- <div className="theia-WelcomeMessage-SessionsHeader">
91
- {nls.localizeByDefault('Active')}
92
- </div>
93
- {activeVisible.map(s => renderItem(s, false))}
94
- </div>
95
- )}
96
- {restoredVisible.length > 0 && (
97
- <div className="theia-WelcomeMessage-SessionsGroup">
98
- <div className="theia-WelcomeMessage-SessionsHeader">
99
- {nls.localize('theia/ai/ide/sectionRestored', 'Restored')}
100
- </div>
101
- {restoredVisible.map(s => renderItem(s, true))}
102
- </div>
103
- )}
104
- {hiddenCount > 0 && (
105
- <div className="theia-WelcomeMessage-SessionsFooter">
106
- <a className="theia-WelcomeMessage-FooterLink"
107
- {...buttonKeyboardProps(nls.localize('theia/ai/ide/browseAllChats', 'Browse all chats...'))}
108
- onClick={onBrowseAll}
109
- onKeyDown={e => {
110
- if (isActivationKey(e)) {
111
- e.preventDefault();
112
- onBrowseAll();
113
- }
114
- }}>
115
- {nls.localize('theia/ai/ide/browseAllChats', 'Browse all chats...')}
116
- </a>
117
- </div>
118
- )}
119
- </div>
120
- );
121
- }
122
-
123
36
  @injectable()
124
37
  export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessageProvider {
125
38
 
@@ -152,11 +65,12 @@ export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessagePro
152
65
  @inject(ApplicationShell)
153
66
  protected readonly shell: ApplicationShell;
154
67
 
68
+ @inject(ChatSessionListService)
69
+ protected readonly sessionListService: ChatSessionListService;
70
+
155
71
  protected _inputEnabled = false;
156
72
 
157
- private readonly unreadSessions = new Map<string, { unread: boolean; seenRequests: number; seenCompleted: number; listener: DisposableCollection }>();
158
- private readonly onUnreadChangedEmitter = new Emitter<string>();
159
- readonly onUnreadChanged: Event<string> = this.onUnreadChangedEmitter.event;
73
+ protected _sessionsWidgetAttached = false;
160
74
 
161
75
  protected _markdownRenderer: MarkdownRenderer | undefined;
162
76
  protected get markdownRenderer(): MarkdownRenderer {
@@ -166,48 +80,47 @@ export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessagePro
166
80
  return this._markdownRenderer;
167
81
  }
168
82
 
169
- /** Persisted sessions index sorted newest first. May include duplicates of active sessions. */
170
- protected _persistedSessions: ChatSessionMetadata[] = [];
171
-
172
83
  protected readonly onStateChangedEmitter = new Emitter<void>();
173
84
  readonly onStateChanged: Event<void> = this.onStateChangedEmitter.event;
174
85
 
175
86
  @postConstruct()
176
87
  protected init(): void {
177
- for (const session of this.chatService.getSessions()) {
178
- this.watchSession(session);
179
- }
180
-
181
- this.chatService.onSessionEvent(event => {
182
- if (event.type === 'created') {
183
- const s = this.chatService.getSession(event.sessionId);
184
- if (s) {
185
- this.watchSession(s);
186
- }
187
- } else if (event.type === 'activeChange' && event.sessionId) {
188
- this.markSessionRead(event.sessionId);
189
- } else if (event.type === 'deleted') {
190
- this.unwatchSession(event.sessionId);
191
- }
192
- // `loadSessions` fires `onStateChangedEmitter` once the (re)load settles, so we don't
193
- // fire it again here: a single session event should trigger a single re-render.
194
- this.loadSessions();
88
+ this.sessionListService.onStateChanged(() => {
89
+ this.onStateChangedEmitter.fire();
195
90
  });
196
91
 
197
- this.loadSessions();
198
92
  this.updateInputEnabled();
199
93
  this.languageModelRegistry.onChange(() => {
200
94
  this.updateInputEnabled();
201
95
  });
202
96
  this.preferenceService.onPreferenceChanged(e => {
203
- if (e.preferenceName === PERSISTED_SESSION_LIMIT_PREF || e.preferenceName === SESSION_STORAGE_PREF) {
204
- this.loadSessions();
205
- } else if (e.preferenceName === BYPASS_MODEL_REQUIREMENT_PREF) {
97
+ if (e.preferenceName === BYPASS_MODEL_REQUIREMENT_PREF) {
206
98
  this.updateInputEnabled();
207
99
  } else if (e.preferenceName === WELCOME_SCREEN_SESSIONS_PREF) {
208
100
  this.onStateChangedEmitter.fire();
209
101
  }
210
102
  });
103
+
104
+ this._sessionsWidgetAttached = this.isSessionsWidgetAttached();
105
+ this.shell.onDidAddWidget(widget => {
106
+ if (widget.id === AISessionsWidget.ID) {
107
+ this._sessionsWidgetAttached = true;
108
+ this.onStateChangedEmitter.fire();
109
+ }
110
+ });
111
+ this.shell.onDidRemoveWidget(widget => {
112
+ if (widget.id === AISessionsWidget.ID) {
113
+ this._sessionsWidgetAttached = false;
114
+ this.onStateChangedEmitter.fire();
115
+ }
116
+ });
117
+ }
118
+
119
+ protected isSessionsWidgetAttached(): boolean {
120
+ const areas: ApplicationShell.Area[] = ['left', 'right', 'main', 'bottom'];
121
+ return areas.some(area =>
122
+ this.shell.getWidgets(area).some(w => w.id === AISessionsWidget.ID && !w.isDisposed)
123
+ );
211
124
  }
212
125
 
213
126
  protected async updateInputEnabled(): Promise<void> {
@@ -221,138 +134,16 @@ export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessagePro
221
134
  }
222
135
  }
223
136
 
224
- protected async loadSessions(): Promise<void> {
225
- if (!this.isPersistenceEnabled()) {
226
- this._persistedSessions = [];
227
- this.onStateChangedEmitter.fire();
228
- return;
229
- }
230
-
231
- const hasSessions = await this.chatService.hasPersistedSessions();
232
- if (!hasSessions) {
233
- this._persistedSessions = [];
234
- this.onStateChangedEmitter.fire();
235
- return;
236
- }
237
-
238
- try {
239
- const index = await this.chatService.getPersistedSessions();
240
- this._persistedSessions = Object.values(index)
241
- .toSorted((a, b) => b.saveDate - a.saveDate);
242
- } catch (error) {
243
- console.error('Failed to load persisted sessions:', error);
244
- this._persistedSessions = [];
245
- } finally {
246
- // Fire once after the load settles; the data was stale until now anyway.
247
- this.onStateChangedEmitter.fire();
248
- }
249
- }
250
-
251
- protected isPersistenceEnabled(): boolean {
252
- const limit = this.preferenceService.get<number>(PERSISTED_SESSION_LIMIT_PREF, 25);
253
- return limit !== 0;
254
- }
255
-
256
- isUnread(sessionId: string): boolean {
257
- return this.unreadSessions.get(sessionId)?.unread === true;
258
- }
259
-
260
- protected watchSession(session: ChatSession): void {
261
- if (this.unreadSessions.has(session.id)) {
262
- return;
263
- }
264
- const reqs = session.model.getRequests();
265
- const state = {
266
- unread: false,
267
- seenRequests: reqs.length,
268
- seenCompleted: this.countCompleted(reqs),
269
- listener: new DisposableCollection()
270
- };
271
- this.unreadSessions.set(session.id, state);
272
-
273
- session.model.onDidChange(() => {
274
- const current = session.model.getRequests();
275
- if (current.length > state.seenRequests || this.countCompleted(current) > state.seenCompleted) {
276
- // Only silently update the seen counts (instead of flashing the unread badge)
277
- // when the user is actually looking at this session: it must be the active
278
- // session AND the chat view must currently be the focused widget. Otherwise,
279
- // the user may have switched away (e.g. to the editor) while the chat agent
280
- // is still running, and we want the badge to appear so they notice the new
281
- // response when they return.
282
- const activeSession = this.chatService.getActiveSession();
283
- const chatViewFocused = ChatViewWidget.findActive(this.shell) !== undefined;
284
- if (chatViewFocused && activeSession && activeSession.id === session.id) {
285
- state.seenRequests = current.length;
286
- state.seenCompleted = this.countCompleted(current);
287
- } else if (!state.unread) {
288
- state.unread = true;
289
- this.onUnreadChangedEmitter.fire(session.id);
290
- }
291
- }
292
- }, undefined, state.listener);
293
- }
294
-
295
- protected markSessionRead(sessionId: string): void {
296
- const state = this.unreadSessions.get(sessionId);
297
- if (!state) {
298
- return;
299
- }
300
- const session = this.chatService.getSession(sessionId);
301
- const reqs = session?.model.getRequests() ?? [];
302
- state.seenRequests = reqs.length;
303
- state.seenCompleted = this.countCompleted(reqs);
304
- if (state.unread) {
305
- state.unread = false;
306
- this.onUnreadChangedEmitter.fire(sessionId);
307
- }
308
- }
309
-
310
- protected unwatchSession(sessionId: string): void {
311
- const state = this.unreadSessions.get(sessionId);
312
- if (state) {
313
- state.listener.dispose();
314
- this.unreadSessions.delete(sessionId);
315
- }
316
- }
317
-
318
- private countCompleted(reqs: ReturnType<ChatSession['model']['getRequests']>): number {
319
- return reqs.filter(r => r.response.isComplete).length;
320
- }
321
-
322
- /**
323
- * Splits sessions into active vs. restored. Active sessions are sourced from
324
- * `chatService.getSessions()` (in-memory). Restored sessions are the persisted index entries
325
- * that aren't already loaded as an active session.
326
- */
327
- protected getSections(): SectionedSessions {
328
- const activeRaw = this.chatService.getSessions().filter(s => !!s.title);
329
- const activeIds = new Set(activeRaw.map(s => s.id));
330
- const active: ChatSessionMetadata[] = activeRaw
331
- .toSorted((a, b) => (b.lastInteraction?.getTime() ?? 0) - (a.lastInteraction?.getTime() ?? 0))
332
- .map(session => {
333
- const lastReq = session.model.getRequests().at(-1);
334
- const hasError = lastReq?.response.isComplete === true && lastReq?.response.isError === true;
335
- return {
336
- sessionId: session.id,
337
- title: session.title!,
338
- saveDate: session.lastInteraction?.getTime() ?? Date.now(),
339
- location: session.model.location,
340
- pinnedAgentId: session.pinnedAgent?.id,
341
- hasError
342
- };
343
- });
344
- const restored = this._persistedSessions.filter(metadata => !activeIds.has(metadata.sessionId));
345
- return { active, restored };
346
- }
347
-
348
137
  renderWelcomeMessage(): React.ReactNode {
138
+ if (this._sessionsWidgetAttached) {
139
+ return undefined;
140
+ }
349
141
  if (!this._inputEnabled) {
350
142
  return undefined;
351
143
  }
352
- const sections = this.getSections();
144
+ const sections = this.sessionListService.getSections();
353
145
  const sessionCount = sections.active.length + sections.restored.length;
354
- if (!this.isPersistenceEnabled() || sessionCount === 0) {
355
- // Empty state: let other providers (IDE welcome) show their onboarding content.
146
+ if (!this.sessionListService.isPersistenceEnabled() || sessionCount === 0) {
356
147
  return undefined;
357
148
  }
358
149
  return this.renderSessionsSection(sections);
@@ -360,13 +151,15 @@ export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessagePro
360
151
 
361
152
  protected renderSessionsSection(sections: SectionedSessions): React.ReactNode {
362
153
  const maxSessions = this.preferenceService.get<number>(WELCOME_SCREEN_SESSIONS_PREF, 20);
154
+ const rows = this.sessionListService.buildRows(sections);
155
+
363
156
  return (
364
157
  <div className="theia-WelcomeMessage" key="sessions-section">
365
158
  <div className="theia-WelcomeMessage-SessionsSection">
366
159
  <SessionsList
367
- sections={sections}
160
+ rows={rows}
368
161
  maxSessions={maxSessions}
369
- renderItem={this.renderSessionItem}
162
+ renderRow={this.renderSessionRow}
370
163
  onBrowseAll={this.handleBrowseAllChats}
371
164
  />
372
165
  </div>
@@ -374,29 +167,46 @@ export class ChatSessionsWelcomeMessageProvider implements ChatWelcomeMessagePro
374
167
  );
375
168
  }
376
169
 
377
- protected renderSessionItem = (session: ChatSessionMetadata, isRestored: boolean): React.ReactNode => {
378
- const actions = this.chatSessionItemActionContributions
170
+ protected getSessionActions(session: ChatSessionMetadata): ChatSessionItemAction[] {
171
+ return this.chatSessionItemActionContributions
379
172
  .getContributions()
380
173
  .flatMap(c => c.getActions(session))
381
174
  .filter(action => this.commandRegistry.isEnabled(action.commandId, session))
382
175
  .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
176
+ }
177
+
178
+ protected renderSessionRow = (row: SessionRow): React.ReactNode => this.renderSessionRowAtDepth(row, 0);
179
+
180
+ protected renderSessionRowAtDepth(row: SessionRow, depth: number): React.ReactNode {
181
+ const hasChildSessions = row.childSessions.length > 0;
182
+ const isExpanded = hasChildSessions && this.sessionListService.isExpanded(row.session.sessionId);
183
+ const descendantNeedsAttention = this.sessionListService.descendantRequiresAction(row);
184
+
383
185
  return (
384
- <ChatSessionItem
385
- key={session.sessionId}
386
- session={session}
387
- isRestored={isRestored}
388
- chatService={this.chatService}
389
- chatAgentService={this.chatAgentService}
390
- hoverService={this.hoverService}
391
- markdownRenderer={this.markdownRenderer}
392
- unreadState={this}
393
- onClick={() => this.handleSessionItemClick(session.sessionId)}
394
- actions={actions}
395
- onAction={this.handleSessionItemAction}
396
- formatTimeAgo={date => formatTimeAgo(date)}
397
- />
186
+ <React.Fragment key={row.session.sessionId}>
187
+ <ChatSessionItem
188
+ session={row.session}
189
+ isRestored={row.isRestored}
190
+ chatService={this.chatService}
191
+ chatAgentService={this.chatAgentService}
192
+ hoverService={this.hoverService}
193
+ markdownRenderer={this.markdownRenderer}
194
+ unreadState={this.sessionListService}
195
+ onClick={() => this.handleSessionItemClick(row.session.sessionId)}
196
+ actions={this.getSessionActions(row.session)}
197
+ onAction={this.handleSessionItemAction}
198
+ formatTimeAgo={date => formatTimeAgo(date)}
199
+ hasChildSessions={hasChildSessions}
200
+ isChildSession={depth > 0}
201
+ depth={depth}
202
+ isExpanded={isExpanded}
203
+ descendantNeedsAttention={descendantNeedsAttention}
204
+ onToggleExpand={hasChildSessions ? () => this.sessionListService.toggleExpand(row.session.sessionId) : undefined}
205
+ />
206
+ {isExpanded && row.childSessions.map(child => this.renderSessionRowAtDepth(child, depth + 1))}
207
+ </React.Fragment>
398
208
  );
399
- };
209
+ }
400
210
 
401
211
  protected handleSessionItemAction = (action: ChatSessionItemAction, session: ChatSessionMetadata): void => {
402
212
  this.commandRegistry.executeCommand(action.commandId, session);
@@ -28,7 +28,7 @@ import { FileStat } from '@theia/filesystem/lib/common/files';
28
28
  import { ContextFileValidationService, FileValidationState } from '@theia/ai-chat/lib/browser/context-file-validation-service';
29
29
  import { ContextFileValidationServiceImpl } from './context-file-validation-service-impl';
30
30
  import { WorkspaceFunctionScope } from './workspace-functions';
31
- import { TrustAwarePreferenceReader } from '@theia/ai-core/lib/browser/trust-aware-preference-reader';
31
+ import { AiConfigurationService } from '@theia/ai-core';
32
32
  import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
33
33
 
34
34
  disableJSDOM();
@@ -113,11 +113,11 @@ describe('ContextFileValidationService', () => {
113
113
  container.bind(FileService).toConstantValue(mockFileService);
114
114
  container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
115
115
  container.bind(PreferenceService).toConstantValue(mockPreferenceService);
116
- container.bind(TrustAwarePreferenceReader).toConstantValue({
116
+ container.bind(AiConfigurationService).toConstantValue({
117
117
  get: <T>(_name: string, fallback?: T) => fallback,
118
118
  ready: Promise.resolve(),
119
119
  onDidChangeTrust: () => ({ dispose: () => { /* noop */ } })
120
- } as unknown as TrustAwarePreferenceReader);
120
+ } as unknown as AiConfigurationService);
121
121
  container.bind(EnvVariablesServer).toConstantValue({
122
122
  getHomeDirUri: async () => 'file:///home/user',
123
123
  getExecPath: async () => '',
@@ -80,6 +80,8 @@ import { ChatWelcomeMessageProvider } from '@theia/ai-chat-ui/lib/browser/chat-t
80
80
  import { IdeChatWelcomeMessageProvider } from './ide-chat-welcome-message-provider';
81
81
  import { ChatSessionsWelcomeMessageProvider } from './chat-sessions-welcome-message-provider';
82
82
  import { ChatSessionItemActionContribution, DefaultChatSessionItemActionContribution } from './chat-session-item-action-contribution';
83
+ import { AiAllowAllModeChatBanner } from './ai-allow-all-mode-chat-banner';
84
+ import { ChatBannerProvider } from '@theia/ai-chat-ui/lib/browser/chat-banner-provider';
83
85
  import { DefaultChatAgentRecommendationService } from './default-chat-agent-recommendation-service';
84
86
  import { AITokenUsageConfigurationWidget } from './ai-configuration/token-usage-configuration-widget';
85
87
  import { AISkillsConfigurationWidget } from './ai-configuration/skills-configuration-widget';
@@ -125,6 +127,11 @@ import { CodeReviewerAgent } from './code-reviewer-agent';
125
127
  import { CodeReviewCapabilityContribution } from './code-review-capability-contribution';
126
128
  import { PRReviewAgent } from './review/pr-review-agent';
127
129
  import { PRReviewCapabilityContribution } from './review/pr-review-capability-contribution';
130
+ import { PerspectiveContribution } from '@theia/core/lib/browser/perspective-service';
131
+ import { AIFirstPerspectiveContribution } from './ai-first-perspective-contribution';
132
+ import { ChatSessionListService } from './chat-session-list-service';
133
+ import { AISessionsWidget } from './ai-sessions-widget';
134
+ import { AISessionsViewContribution } from './ai-sessions-view-contribution';
128
135
 
129
136
  export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
130
137
  bind(PreferenceContribution).toConstantValue({ schema: aiIdePreferenceSchema });
@@ -192,8 +199,11 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
192
199
  bind(Agent).toService(PRReviewAgent);
193
200
  bind(ChatAgent).toService(PRReviewAgent);
194
201
 
202
+ bind(ChatSessionListService).toSelf().inSingletonScope();
203
+
195
204
  bind(ChatWelcomeMessageProvider).to(IdeChatWelcomeMessageProvider).inSingletonScope();
196
205
  bind(ChatWelcomeMessageProvider).to(ChatSessionsWelcomeMessageProvider).inSingletonScope();
206
+ bind(ChatBannerProvider).to(AiAllowAllModeChatBanner).inSingletonScope();
197
207
  bindRootContributionProvider(bind, ChatSessionItemActionContribution);
198
208
  bind(DefaultChatSessionItemActionContribution).toSelf().inSingletonScope();
199
209
  bind(ChatSessionItemActionContribution).toService(DefaultChatSessionItemActionContribution);
@@ -350,4 +360,17 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
350
360
 
351
361
  bind(FrontendApplicationContribution).to(CodeReviewCapabilityContribution);
352
362
  bind(FrontendApplicationContribution).to(PRReviewCapabilityContribution);
363
+
364
+ bind(AIFirstPerspectiveContribution).toSelf().inSingletonScope();
365
+ bind(PerspectiveContribution).toService(AIFirstPerspectiveContribution);
366
+
367
+ bind(AISessionsWidget).toSelf();
368
+ bind(WidgetFactory)
369
+ .toDynamicValue(ctx => ({
370
+ id: AISessionsWidget.ID,
371
+ createWidget: () => ctx.container.get(AISessionsWidget)
372
+ }))
373
+ .inSingletonScope();
374
+ bindViewContribution(bind, AISessionsViewContribution);
375
+ bind(TabBarToolbarContribution).toService(AISessionsViewContribution);
353
376
  });