@theia/ai-ide 1.74.0-next.9 → 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.
- package/lib/browser/ai-allow-all-mode-chat-banner.d.ts +68 -0
- package/lib/browser/ai-allow-all-mode-chat-banner.d.ts.map +1 -0
- package/lib/browser/ai-allow-all-mode-chat-banner.js +184 -0
- package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -0
- package/lib/browser/ai-allow-all-mode-chat-banner.spec.d.ts +2 -0
- package/lib/browser/ai-allow-all-mode-chat-banner.spec.d.ts.map +1 -0
- package/lib/browser/ai-allow-all-mode-chat-banner.spec.js +170 -0
- package/lib/browser/ai-allow-all-mode-chat-banner.spec.js.map +1 -0
- package/lib/browser/ai-configuration/tools-configuration-widget.d.ts +3 -4
- package/lib/browser/ai-configuration/tools-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/tools-configuration-widget.js +17 -10
- package/lib/browser/ai-configuration/tools-configuration-widget.js.map +1 -1
- package/lib/browser/ai-first-perspective-contribution.d.ts +6 -0
- package/lib/browser/ai-first-perspective-contribution.d.ts.map +1 -0
- package/lib/browser/ai-first-perspective-contribution.js +50 -0
- package/lib/browser/ai-first-perspective-contribution.js.map +1 -0
- package/lib/browser/ai-sessions-view-contribution.d.ts +13 -0
- package/lib/browser/ai-sessions-view-contribution.d.ts.map +1 -0
- package/lib/browser/ai-sessions-view-contribution.js +69 -0
- package/lib/browser/ai-sessions-view-contribution.js.map +1 -0
- package/lib/browser/ai-sessions-widget.d.ts +27 -0
- package/lib/browser/ai-sessions-widget.d.ts.map +1 -0
- package/lib/browser/ai-sessions-widget.js +131 -0
- package/lib/browser/ai-sessions-widget.js.map +1 -0
- package/lib/browser/chat-session-item-action-contribution.js +2 -2
- package/lib/browser/chat-session-item-action-contribution.js.map +1 -1
- package/lib/browser/chat-session-item.d.ts +15 -0
- package/lib/browser/chat-session-item.d.ts.map +1 -1
- package/lib/browser/chat-session-item.js +39 -18
- package/lib/browser/chat-session-item.js.map +1 -1
- package/lib/browser/chat-session-list-components.d.ts +38 -0
- package/lib/browser/chat-session-list-components.d.ts.map +1 -0
- package/lib/browser/chat-session-list-components.js +87 -0
- package/lib/browser/chat-session-list-components.js.map +1 -0
- package/lib/browser/chat-session-list-service.d.ts +34 -0
- package/lib/browser/chat-session-list-service.d.ts.map +1 -0
- package/lib/browser/chat-session-list-service.js +251 -0
- package/lib/browser/chat-session-list-service.js.map +1 -0
- package/lib/browser/chat-session-list-service.spec.d.ts +2 -0
- package/lib/browser/chat-session-list-service.spec.d.ts.map +1 -0
- package/lib/browser/{chat-sessions-welcome-message-provider.spec.js → chat-session-list-service.spec.js} +133 -39
- package/lib/browser/chat-session-list-service.spec.js.map +1 -0
- package/lib/browser/chat-session-tooltip.d.ts +2 -2
- package/lib/browser/chat-session-tooltip.d.ts.map +1 -1
- package/lib/browser/chat-session-tooltip.js +33 -10
- package/lib/browser/chat-session-tooltip.js.map +1 -1
- package/lib/browser/chat-sessions-welcome-message-provider.d.ts +9 -35
- package/lib/browser/chat-sessions-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/chat-sessions-welcome-message-provider.js +51 -205
- package/lib/browser/chat-sessions-welcome-message-provider.js.map +1 -1
- package/lib/browser/context-file-validation-service-impl.spec.js +2 -2
- package/lib/browser/context-file-validation-service-impl.spec.js.map +1 -1
- package/lib/browser/frontend-module.d.ts.map +1 -1
- package/lib/browser/frontend-module.js +20 -0
- package/lib/browser/frontend-module.js.map +1 -1
- package/lib/browser/workspace-functions.d.ts +5 -6
- package/lib/browser/workspace-functions.d.ts.map +1 -1
- package/lib/browser/workspace-functions.js +24 -9
- package/lib/browser/workspace-functions.js.map +1 -1
- package/lib/browser/workspace-functions.spec.js +29 -29
- package/lib/browser/workspace-functions.spec.js.map +1 -1
- package/lib/browser/workspace-launch-provider.spec.js +3 -3
- package/lib/browser/workspace-launch-provider.spec.js.map +1 -1
- package/lib/browser/workspace-search-provider.d.ts.map +1 -1
- package/lib/browser/workspace-search-provider.js +28 -20
- package/lib/browser/workspace-search-provider.js.map +1 -1
- package/lib/browser/workspace-search-provider.spec.js +68 -0
- package/lib/browser/workspace-search-provider.spec.js.map +1 -1
- package/lib/browser/workspace-task-provider.spec.js +4 -4
- package/lib/browser/workspace-task-provider.spec.js.map +1 -1
- package/package.json +25 -25
- package/src/browser/ai-allow-all-mode-chat-banner.spec.ts +205 -0
- package/src/browser/ai-allow-all-mode-chat-banner.tsx +217 -0
- package/src/browser/ai-configuration/tools-configuration-widget.tsx +20 -13
- package/src/browser/ai-first-perspective-contribution.ts +48 -0
- package/src/browser/ai-sessions-view-contribution.ts +67 -0
- package/src/browser/ai-sessions-widget.tsx +151 -0
- package/src/browser/chat-session-item-action-contribution.ts +2 -2
- package/src/browser/chat-session-item.tsx +82 -27
- package/src/browser/chat-session-list-components.tsx +136 -0
- package/src/browser/{chat-sessions-welcome-message-provider.spec.ts → chat-session-list-service.spec.ts} +136 -30
- package/src/browser/chat-session-list-service.ts +261 -0
- package/src/browser/chat-session-tooltip.ts +31 -11
- package/src/browser/chat-sessions-welcome-message-provider.tsx +78 -268
- package/src/browser/context-file-validation-service-impl.spec.ts +3 -3
- package/src/browser/frontend-module.ts +23 -0
- package/src/browser/style/index.css +150 -3
- package/src/browser/workspace-functions.spec.ts +36 -37
- package/src/browser/workspace-functions.ts +8 -9
- package/src/browser/workspace-launch-provider.spec.ts +5 -5
- package/src/browser/workspace-search-provider.spec.ts +101 -0
- package/src/browser/workspace-search-provider.ts +34 -23
- package/src/browser/workspace-task-provider.spec.ts +6 -7
- package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts +0 -2
- package/lib/browser/chat-sessions-welcome-message-provider.spec.d.ts.map +0 -1
- 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,
|
|
20
|
+
ChatAgentService, ChatService, ChatSessionMetadata
|
|
21
21
|
} from '@theia/ai-chat';
|
|
22
|
-
import { BYPASS_MODEL_REQUIREMENT_PREF,
|
|
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,
|
|
29
|
-
import { ApplicationShell,
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
this.
|
|
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 ===
|
|
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
|
-
|
|
160
|
+
rows={rows}
|
|
368
161
|
maxSessions={maxSessions}
|
|
369
|
-
|
|
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
|
|
378
|
-
|
|
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
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
});
|