Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - Chat Agent Pinning #14716

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry, QuickInputButton, QuickInputService, QuickPickItem } from '@theia/core';
import { Widget } from '@theia/core/lib/browser';
import { AI_CHAT_NEW_CHAT_WINDOW_COMMAND, AI_CHAT_SHOW_CHATS_COMMAND, ChatCommands } from './chat-view-commands';
import { ChatAgentLocation, ChatService } from '@theia/ai-chat';
import { AI_CHAT_NEW_CHAT_WINDOW_COMMAND, AI_CHAT_NEW_CHAT_WINDOW_WITH_PINNED_AGENT_COMMAND, AI_CHAT_SHOW_CHATS_COMMAND, ChatCommands } from './chat-view-commands';
import { ChatAgent, ChatAgentLocation, ChatService } from '@theia/ai-chat';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ChatViewWidget } from './chat-view-widget';
Expand Down Expand Up @@ -79,6 +79,12 @@ export class AIChatContribution extends AbstractViewContribution<ChatViewWidget>
isEnabled: widget => this.withWidget(widget, () => true),
isVisible: widget => this.withWidget(widget, () => true),
});
registry.registerCommand(AI_CHAT_NEW_CHAT_WINDOW_WITH_PINNED_AGENT_COMMAND, {
// TODO - not working if function arg is set to type ChatAgent | undefined ?
execute: (...args: unknown[]) => this.chatService.createSession(ChatAgentLocation.Panel, {focus: true}, args[1] as ChatAgent | undefined),
isEnabled: widget => this.withWidget(widget, () => true),
isVisible: widget => this.withWidget(widget, () => true),
});
registry.registerCommand(AI_CHAT_SHOW_CHATS_COMMAND, {
execute: () => this.selectChat(),
isEnabled: widget => this.withWidget(widget, () => true) && this.chatService.getSessions().length > 1,
Expand Down
24 changes: 24 additions & 0 deletions packages/ai-chat-ui/src/browser/chat-input-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { IMouseEvent } from '@theia/monaco-editor-core';
import { Deferred } from '@theia/core/lib/common/promise-util';

type Query = (query: string) => Promise<void>;
type Unpin = () => void;
type Cancel = (requestModel: ChatRequestModel) => void;

@injectable()
Expand Down Expand Up @@ -53,6 +54,10 @@ export class AIChatInputWidget extends ReactWidget {
set onQuery(query: Query) {
this._onQuery = query;
}
private _onUnpin: Unpin;
set onUnpin(unpin: Unpin) {
this._onUnpin = unpin;
}
private _onCancel: Cancel;
set onCancel(cancel: Cancel) {
this._onCancel = cancel;
Expand All @@ -62,6 +67,11 @@ export class AIChatInputWidget extends ReactWidget {
this._chatModel = chatModel;
this.update();
}
private _pinnedAgent: ChatAgent | undefined;
set pinnedAgent(pinnedAgent: ChatAgent | undefined) {
this._pinnedAgent = pinnedAgent;
this.update();
}

@postConstruct()
protected init(): void {
Expand All @@ -87,8 +97,10 @@ export class AIChatInputWidget extends ReactWidget {
return (
<ChatInput
onQuery={this._onQuery.bind(this)}
onUnpin={this._onUnpin.bind(this)}
onCancel={this._onCancel.bind(this)}
chatModel={this._chatModel}
pinnedAgent={this._pinnedAgent}
getChatAgents={this.getChatAgents.bind(this)}
editorProvider={this.editorProvider}
untitledResourceResolver={this.untitledResourceResolver}
Expand Down Expand Up @@ -120,8 +132,10 @@ export class AIChatInputWidget extends ReactWidget {
interface ChatInputProperties {
onCancel: (requestModel: ChatRequestModel) => void;
onQuery: (query: string) => void;
onUnpin: () => void;
isEnabled?: boolean;
chatModel: ChatModel;
pinnedAgent?: ChatAgent;
getChatAgents: () => ChatAgent[];
editorProvider: MonacoEditorProvider;
untitledResourceResolver: UntitledResourceResolver;
Expand Down Expand Up @@ -268,8 +282,18 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
}
};

const handleUnpin = () => {
props.onUnpin();
};

return <div className='theia-ChatInput'>
<div className='theia-ChatInput-Editor-Box'>
{props.pinnedAgent !== undefined &&
<div className='theia-ChatInput-Popup'>
<span>@{props.pinnedAgent.name}</span>
<span className="codicon codicon-remove-close option" title="unpin" onClick={handleUnpin} />
</div>
}
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>Ask a question</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions packages/ai-chat-ui/src/browser/chat-view-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export const AI_CHAT_NEW_CHAT_WINDOW_COMMAND: Command = {
iconClass: codicon('add')
};

export const AI_CHAT_NEW_CHAT_WINDOW_WITH_PINNED_AGENT_COMMAND: Command = {
id: 'ai-chat-ui.new-chat-with-pinned-agent',
iconClass: codicon('add')
};

export const AI_CHAT_SHOW_CHATS_COMMAND: Command = {
id: 'ai-chat-ui.show-chats',
iconClass: codicon('history')
Expand Down
11 changes: 11 additions & 0 deletions packages/ai-chat-ui/src/browser/chat-view-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
this.chatSession = this.chatService.createSession();

this.inputWidget.onQuery = this.onQuery.bind(this);
this.inputWidget.onUnpin = this.onUnpin.bind(this);
this.inputWidget.onCancel = this.onCancel.bind(this);
this.inputWidget.chatModel = this.chatSession.model;
this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent;
this.treeWidget.trackChatModel(this.chatSession.model);

this.initListeners();
Expand All @@ -111,10 +113,12 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
this.toDispose.push(
this.chatService.onActiveSessionChanged(event => {
const session = event.sessionId ? this.chatService.getSession(event.sessionId) : this.chatService.createSession();

if (session) {
this.chatSession = session;
this.treeWidget.trackChatModel(this.chatSession.model);
this.inputWidget.chatModel = this.chatSession.model;
this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent;
if (event.focus) {
this.show();
}
Expand Down Expand Up @@ -167,6 +171,8 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
if (responseModel.isError) {
this.messageService.error(responseModel.errorObject?.message ?? 'An error occurred druring chat service invocation.');
}
}).finally(() => {
this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent;
});
if (!requestProgress) {
this.messageService.error(`Was not able to send request "${chatRequest.text}" to session ${this.chatSession.id}`);
Expand All @@ -175,6 +181,11 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
// Tree Widget currently tracks the ChatModel itself. Therefore no notification necessary.
}

protected onUnpin(): void {
this.chatSession.pinnedAgent = undefined;
this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent;
}

protected onCancel(requestModel: ChatRequestModel): void {
// TODO we should pass a cancellation token with the request (or retrieve one from the request invocation) so we can cleanly cancel here
// For now we cancel manually via casting
Expand Down
22 changes: 20 additions & 2 deletions packages/ai-chat-ui/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,29 @@ div:last-child > .theia-ChatNode {
overflow: hidden;
}

.theia-ChatInput-Popup {
position: relative;
bottom: -5px;
right: -2px;
padding-top: 9px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 11px;
display: flex;
flex-direction: row;
align-items: start;
align-self: flex-end;
gap: 10px;
border: var(--theia-border-width) solid var(--theia-dropdown-border);
border-radius: 4px;
}

.theia-ChatInput-Editor {
width: 100%;
height: auto;
border: var(--theia-border-width) solid var(--theia-dropdown-border);
border-radius: 4px;
position: relative;
display: flex;
flex-direction: column-reverse;
overflow: hidden;
Expand All @@ -194,8 +212,8 @@ div:last-child > .theia-ChatNode {

.theia-ChatInput-Editor-Placeholder {
position: absolute;
top: -3px;
left: 19px;
top: 0;
left: 8px;
right: 0;
bottom: 0;
display: flex;
Expand Down
16 changes: 12 additions & 4 deletions packages/ai-chat/src/common/chat-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface ChatSession {
title?: string;
model: ChatModel;
isActive: boolean;
pinnedAgent?: ChatAgent;
}

export interface ActiveSessionChangedEvent {
Expand All @@ -78,7 +79,7 @@ export interface ChatService {

getSession(id: string): ChatSession | undefined;
getSessions(): ChatSession[];
createSession(location?: ChatAgentLocation, options?: SessionOptions): ChatSession;
createSession(location?: ChatAgentLocation, options?: SessionOptions, pinnedAgent?: ChatAgent): ChatSession;
deleteSession(sessionId: string): void;
setActiveSession(sessionId: string, options?: SessionOptions): void;

Expand Down Expand Up @@ -122,12 +123,13 @@ export class ChatServiceImpl implements ChatService {
return this._sessions.find(session => session.id === id);
}

createSession(location = ChatAgentLocation.Panel, options?: SessionOptions): ChatSession {
createSession(location = ChatAgentLocation.Panel, options?: SessionOptions, pinnedAgent?: ChatAgent): ChatSession {
const model = new ChatModelImpl(location);
const session: ChatSessionInternal = {
id: model.id,
model,
isActive: true
isActive: true,
pinnedAgent: pinnedAgent
};
this._sessions.push(session);
this.setActiveSession(session.id, options);
Expand Down Expand Up @@ -160,8 +162,14 @@ export class ChatServiceImpl implements ChatService {
session.title = request.text;

const parsedRequest = this.chatRequestParser.parseChatRequest(request, session.model.location);
let agent = this.getAgent(parsedRequest);

if (!session.pinnedAgent && agent && agent.id !== this.defaultChatAgentId?.id) {
session.pinnedAgent = agent;
} else if (session.pinnedAgent && this.getMentionedAgent(parsedRequest) === undefined) {
agent = session.pinnedAgent;
}

const agent = this.getAgent(parsedRequest);
if (agent === undefined) {
const error = 'No ChatAgents available to handle request!';
this.logger.error(error);
Expand Down
Loading