Skip to content

Commit d58e8c2

Browse files
Agent prompt suggestions & chat summary
1 parent 436de3c commit d58e8c2

28 files changed

+840
-137
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"license:check:review": "node scripts/check_3pp_licenses.js --review",
7676
"lint": "lerna run lint",
7777
"lint:clean": "rimraf .eslintcache",
78+
"lint:fix": "lerna run lint -- --fix",
7879
"preinstall": "node-gyp install",
7980
"postinstall": "theia-patch && npm run -s compute-references && lerna run afterInstall",
8081
"publish:latest": "lerna publish --exact --yes",

packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { inject, injectable } from '@theia/core/shared/inversify';
1818
import { CommandRegistry, isOSX, nls, QuickInputButton, QuickInputService, QuickPickItem } from '@theia/core';
1919
import { Widget } from '@theia/core/lib/browser';
20-
import { AI_CHAT_NEW_CHAT_WINDOW_COMMAND, AI_CHAT_SHOW_CHATS_COMMAND, ChatCommands } from './chat-view-commands';
20+
import { AI_CHAT_NEW_CHAT_WINDOW_COMMAND, AI_CHAT_NEW_WITH_MEMORY, AI_CHAT_SHOW_CHATS_COMMAND, ChatCommands } from './chat-view-commands';
2121
import { ChatAgentLocation, ChatService } from '@theia/ai-chat';
2222
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
2323
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
@@ -28,6 +28,7 @@ import { formatDistance } from 'date-fns';
2828
import * as locales from 'date-fns/locale';
2929
import { AI_SHOW_SETTINGS_COMMAND } from '@theia/ai-core/lib/browser';
3030
import { OPEN_AI_HISTORY_VIEW } from '@theia/ai-history/lib/browser/ai-history-contribution';
31+
import { SESSION_SUMMARY_VARIABLE } from '@theia/ai-chat/lib/browser/session-summary-variable-contribution';
3132

3233
export const AI_CHAT_TOGGLE_COMMAND_ID = 'aiChat:toggle';
3334

@@ -83,10 +84,21 @@ export class AIChatContribution extends AbstractViewContribution<ChatViewWidget>
8384
})
8485
});
8586
registry.registerCommand(AI_CHAT_NEW_CHAT_WINDOW_COMMAND, {
86-
execute: () => this.chatService.createSession(ChatAgentLocation.Panel, { focus: true }),
87-
isEnabled: widget => this.withWidget(widget, () => true),
87+
execute: () => this.openView().then(() => this.chatService.createSession(ChatAgentLocation.Panel, { focus: true })),
8888
isVisible: widget => this.withWidget(widget, () => true),
8989
});
90+
registry.registerCommand(AI_CHAT_NEW_WITH_MEMORY, {
91+
execute: () => {
92+
const activeSessions = this.chatService.getSessions().filter(candidate => candidate.isActive);
93+
if (activeSessions.length !== 1) {
94+
return;
95+
}
96+
const activeSession = activeSessions[0];
97+
const newSession = this.chatService.createSession(ChatAgentLocation.Panel, { focus: true }, activeSession.pinnedAgent);
98+
newSession.model.context.addVariables({ variable: SESSION_SUMMARY_VARIABLE, arg: activeSession.id });
99+
},
100+
isVisible: () => false
101+
});
90102
registry.registerCommand(AI_CHAT_SHOW_CHATS_COMMAND, {
91103
execute: () => this.selectChat(),
92104
isEnabled: widget => this.withWidget(widget, () => true) && this.chatService.getSessions().length > 1,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2025 EclipseSource GmbH 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 * as React from '@theia/core/shared/react';
18+
import { useMarkdownRendering } from './chat-response-renderer';
19+
import { OpenerService } from '@theia/core/lib/browser';
20+
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
21+
22+
interface ChatInputAgentSuggestionsProps {
23+
suggestions: (string | MarkdownString)[];
24+
opener: OpenerService;
25+
}
26+
27+
export const ChatInputAgentSuggestions: React.FC<ChatInputAgentSuggestionsProps> = ({suggestions, opener}) => (
28+
<div className="chat-agent-suggestions">
29+
{suggestions.map(suggestion => <ChatInputAgentSuggestion key={typeof suggestion === 'string' ? suggestion : suggestion.value} suggestion={suggestion} opener={opener}/>)}
30+
</div>
31+
);
32+
33+
interface ChatInputAgestSuggestionProps {
34+
suggestion: string | MarkdownString;
35+
opener: OpenerService;
36+
}
37+
38+
const ChatInputAgentSuggestion: React.FC<ChatInputAgestSuggestionProps> = ({suggestion, opener}) => {
39+
const ref = useMarkdownRendering(suggestion, opener, true);
40+
return <div className="chat-agent-suggestion" ref={ref}/>;
41+
};

packages/ai-chat-ui/src/browser/chat-input-widget.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// *****************************************************************************
1616
import { ChangeSet, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat';
1717
import { Disposable, DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
18-
import { ContextMenuRenderer, LabelProvider, Message, ReactWidget } from '@theia/core/lib/browser';
18+
import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
1919
import { Deferred } from '@theia/core/lib/common/promise-util';
2020
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
2121
import * as React from '@theia/core/shared/react';
@@ -27,12 +27,14 @@ import { AIVariableResolutionRequest } from '@theia/ai-core';
2727
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
2828
import { ContextVariablePicker } from './context-variable-picker';
2929
import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
30+
import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
3031

3132
type Query = (query: string) => Promise<void>;
3233
type Unpin = () => void;
3334
type Cancel = (requestModel: ChatRequestModel) => void;
3435
type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
3536
type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
37+
type OpenContextElement = (request: AIVariableResolutionRequest) => unknown;
3638

3739
export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
3840
export interface AIChatInputConfiguration {
@@ -69,6 +71,9 @@ export class AIChatInputWidget extends ReactWidget {
6971
@inject(ChangeSetActionService)
7072
protected readonly changeSetActionService: ChangeSetActionService;
7173

74+
@inject(OpenerService)
75+
protected readonly openerService: OpenerService;
76+
7277
protected editorRef: MonacoEditor | undefined = undefined;
7378
private editorReady = new Deferred<void>();
7479

@@ -94,6 +99,10 @@ export class AIChatInputWidget extends ReactWidget {
9499
set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
95100
this._onDeleteChangeSetElement = deleteChangeSetElement;
96101
}
102+
private _onOpenContextELement: OpenContextElement;
103+
set onOpenContextElement(opener: OpenContextElement) {
104+
this._onOpenContextELement = opener;
105+
}
97106

98107
protected onDisposeForChatModel = new DisposableCollection();
99108
private _chatModel: ChatModel;
@@ -142,6 +151,7 @@ export class AIChatInputWidget extends ReactWidget {
142151
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
143152
onAddContextElement={this.addContextElement.bind(this)}
144153
onDeleteContextElement={this.deleteContextElement.bind(this)}
154+
onOpenContextElement={this._onOpenContextELement.bind(this)}
145155
context={this._chatModel.context.getVariables()}
146156
chatModel={this._chatModel}
147157
pinnedAgent={this._pinnedAgent}
@@ -157,6 +167,7 @@ export class AIChatInputWidget extends ReactWidget {
157167
showPinnedAgent={this.configuration?.showPinnedAgent}
158168
labelProvider={this.labelProvider}
159169
actionService={this.changeSetActionService}
170+
openerService={this.openerService}
160171
/>
161172
);
162173
}
@@ -237,6 +248,7 @@ interface ChatInputProperties {
237248
onDeleteChangeSetElement: (sessionId: string, index: number) => void;
238249
onAddContextElement: () => void;
239250
onDeleteContextElement: (index: number) => void;
251+
onOpenContextElement: OpenContextElement;
240252
context?: readonly AIVariableResolutionRequest[];
241253
isEnabled?: boolean;
242254
chatModel: ChatModel;
@@ -249,6 +261,7 @@ interface ChatInputProperties {
249261
showPinnedAgent?: boolean;
250262
labelProvider: LabelProvider;
251263
actionService: ChangeSetActionService;
264+
openerService: OpenerService;
252265
}
253266

254267
const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
@@ -515,9 +528,10 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
515528
disabled: isInputEmpty || !props.isEnabled
516529
}];
517530

518-
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement);
531+
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
519532

520-
return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
533+
return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop}>
534+
{!!props.pinnedAgent?.suggestions?.length && <ChatInputAgentSuggestions suggestions={props.pinnedAgent?.suggestions} opener={props.openerService} />}
521535
{changeSetUI?.elements &&
522536
<ChangeSetBox changeSet={changeSetUI} />
523537
}
@@ -685,7 +699,12 @@ function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
685699
return requests.length > 0 ? requests[requests.length - 1] : undefined;
686700
}
687701

688-
function buildContextUI(context: readonly AIVariableResolutionRequest[] | undefined, labelProvider: LabelProvider, onDeleteContextElement: (index: number) => void): ChatContextUI {
702+
function buildContextUI(
703+
context: readonly AIVariableResolutionRequest[] | undefined,
704+
labelProvider: LabelProvider,
705+
onDeleteContextElement: (index: number) => void,
706+
onOpen: OpenContextElement
707+
): ChatContextUI {
689708
if (!context) {
690709
return { context: [] };
691710
}
@@ -697,6 +716,7 @@ function buildContextUI(context: readonly AIVariableResolutionRequest[] | undefi
697716
additionalInfo: labelProvider.getDetails(element),
698717
details: labelProvider.getLongName(element),
699718
delete: () => onDeleteContextElement(index),
719+
open: () => onOpen(element)
700720
}))
701721
};
702722
}
@@ -725,7 +745,7 @@ const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
725745
<span className='theia-ChatInput-ChatContext-additionalInfo'>
726746
{element.additionalInfo}
727747
</span>
728-
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={() => element.delete()} />
748+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => {e.stopPropagation(); element.delete(); }} />
729749
</li>
730750
))}
731751
</ul>

packages/ai-chat-ui/src/browser/chat-view-commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export const AI_CHAT_NEW_CHAT_WINDOW_COMMAND: Command = {
4545
iconClass: codicon('add')
4646
};
4747

48+
export const AI_CHAT_NEW_WITH_MEMORY: Command = {
49+
id: 'ai-chat.new-with-memory',
50+
};
51+
4852
export const AI_CHAT_SHOW_CHATS_COMMAND: Command = {
4953
id: 'ai-chat-ui.show-chats',
5054
iconClass: codicon('history')

packages/ai-chat-ui/src/browser/chat-view-widget.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
// *****************************************************************************
1616
import { CommandService, deepClone, Emitter, Event, MessageService } from '@theia/core';
1717
import { ChatRequest, ChatRequestModel, ChatService, ChatSession, isActiveSessionChangedEvent, MutableChatModel } from '@theia/ai-chat';
18-
import { BaseWidget, codicon, ExtractableWidget, Message, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
18+
import { BaseWidget, codicon, ExtractableWidget, Message, open, OpenerService, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
1919
import { nls } from '@theia/core/lib/common/nls';
2020
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
2121
import { AIChatInputWidget } from './chat-input-widget';
2222
import { ChatViewTreeWidget } from './chat-tree-view/chat-view-tree-widget';
2323
import { AIActivationService } from '@theia/ai-core/lib/browser/ai-activation-service';
24-
import { AIVariableResolutionRequest } from '@theia/ai-core';
24+
import { AIVariableResolutionRequest, AIVariableResourceResolver } from '@theia/ai-core';
2525

2626
export namespace ChatViewWidget {
2727
export interface State {
@@ -50,6 +50,12 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
5050
@inject(AIActivationService)
5151
protected readonly activationService: AIActivationService;
5252

53+
@inject(AIVariableResourceResolver)
54+
protected readonly variableResourceResolver: AIVariableResourceResolver;
55+
56+
@inject(OpenerService)
57+
protected readonly openerService: OpenerService;
58+
5359
protected chatSession: ChatSession;
5460

5561
protected _state: ChatViewWidget.State = { locked: false };
@@ -98,6 +104,7 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
98104
this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent;
99105
this.inputWidget.onDeleteChangeSet = this.onDeleteChangeSet.bind(this);
100106
this.inputWidget.onDeleteChangeSetElement = this.onDeleteChangeSetElement.bind(this);
107+
this.inputWidget.onOpenContextElement = this.onOpenContextElement.bind(this);
101108
this.treeWidget.trackChatModel(this.chatSession.model);
102109

103110
this.initListeners();
@@ -201,6 +208,13 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
201208
this.chatService.deleteChangeSetElement(sessionId, index);
202209
}
203210

211+
protected async onOpenContextElement(request: AIVariableResolutionRequest): Promise<void> {
212+
const context = {session: this.chatSession};
213+
const resource = this.variableResourceResolver.get(request, context);
214+
await open(this.openerService, resource.uri);
215+
resource.dispose();
216+
}
217+
204218
lock(): void {
205219
this.state = { ...deepClone(this.state), locked: true };
206220
}

packages/ai-chat-ui/src/browser/style/index.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ div:last-child > .theia-ChatNode {
181181
height: 18px;
182182
line-height: 16px;
183183
min-width: 0;
184+
user-select: none;
185+
cursor: pointer;
184186
}
185187

186188
.theia-ChatInput-ChatContext-title {
@@ -698,3 +700,7 @@ details[open].collapsible-arguments .collapsible-arguments-summary {
698700
.session-settings-container .monaco-editor {
699701
outline-color: var(--theia-editor-background);
700702
}
703+
704+
.chat-agent-suggestions {
705+
padding-inline: 16px;
706+
}

packages/ai-chat/src/browser/ai-chat-frontend-module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ import { ContextSummaryVariableContribution } from '../common/context-summary-va
4646
import { ContextDetailsVariableContribution } from '../common/context-details-variable';
4747
import { ChangeSetVariableContribution } from './change-set-variable';
4848
import { ChatSessionNamingAgent, ChatSessionNamingService } from '../common/chat-session-naming-service';
49+
import { ChatSessionSummaryAgent } from '../common/chat-session-summary-agent';
50+
import { SessionSumaryVariableContribution } from './session-summary-variable-contribution';
51+
import { SessionSummaryVariableLabelProvider } from './session-summary-variable-label-provider';
4952

5053
export default new ContainerModule(bind => {
5154
bindContributionProvider(bind, Agent);
@@ -113,4 +116,11 @@ export default new ContainerModule(bind => {
113116
bind(AIVariableContribution).to(ContextSummaryVariableContribution).inSingletonScope();
114117
bind(AIVariableContribution).to(ContextDetailsVariableContribution).inSingletonScope();
115118
bind(AIVariableContribution).to(ChangeSetVariableContribution).inSingletonScope();
119+
120+
bind(ChatSessionSummaryAgent).toSelf().inSingletonScope();
121+
bind(Agent).toService(ChatSessionSummaryAgent);
122+
bind(SessionSumaryVariableContribution).toSelf().inSingletonScope();
123+
bind(AIVariableContribution).toService(SessionSumaryVariableContribution);
124+
bind(SessionSummaryVariableLabelProvider).toSelf().inSingletonScope();
125+
bind(LabelProviderContribution).toService(SessionSummaryVariableLabelProvider);
116126
});

0 commit comments

Comments
 (0)