From f77eea7f66abe2fbeb231b94ca1b3cb920951372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 27 Nov 2024 12:23:10 +0100 Subject: [PATCH 01/13] Initial implementation of pinning feature Co-authored-by: mihaela21k Co-authored-by: Fuyao Tang Co-authored-by: Zidong Wang --- .../src/browser/chat-input-widget.tsx | 24 +++++++++++++++++ .../src/browser/chat-view-widget.tsx | 27 +++++++++++++++++++ .../ai-chat-ui/src/browser/style/index.css | 16 ++++++++--- packages/ai-chat/src/common/chat-service.ts | 17 +++++++++--- 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index 2f46a91252b6b..9ed8c2e4d4bad 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -24,6 +24,7 @@ import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution' import { IMouseEvent } from '@theia/monaco-editor-core'; type Query = (query: string) => Promise; +type Unpin = () => void; type Cancel = (requestModel: ChatRequestModel) => void; @injectable() @@ -49,6 +50,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; @@ -58,6 +63,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 { @@ -78,8 +88,10 @@ export class AIChatInputWidget extends ReactWidget { return ( void; onQuery: (query: string) => void; + onUnpin: () => void; isEnabled?: boolean; chatModel: ChatModel; + pinnedAgent?: ChatAgent; getChatAgents: () => ChatAgent[]; editorProvider: MonacoEditorProvider; untitledResourceResolver: UntitledResourceResolver; @@ -233,7 +247,17 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu } }; + const handleUnpin = () => { + props.onUnpin(); + } + return
+ {props.pinnedAgent !== undefined && +
+

dsakdjaldkjasda {props.pinnedAgent.name}

+ < span className="codicon codicon-remove-close option" title="unpin" onClick={handleUnpin} /> +
+ }
Ask a question
diff --git a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx index 7763b2a8ec8c3..06f73f0c72f63 100644 --- a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx @@ -22,6 +22,8 @@ import { AIChatInputWidget } from './chat-input-widget'; import { ChatViewTreeWidget } from './chat-tree-view/chat-view-tree-widget'; import { AIActivationService } from '@theia/ai-core/lib/browser/ai-activation-service'; +import { ILogger } from '@theia/core'; + export namespace ChatViewWidget { export interface State { locked?: boolean; @@ -49,6 +51,9 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta @inject(AIActivationService) protected readonly activationService: AIActivationService; + @inject(ILogger) + protected readonly logger: ILogger; + protected chatSession: ChatSession; protected _state: ChatViewWidget.State = { locked: false }; @@ -91,8 +96,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(); @@ -111,10 +118,15 @@ 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.logger.info("session:", 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(); } @@ -157,6 +169,15 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta text: query }; + // TODO + // if (!this.chatSession.pinnedAgent && query.startsWith('@')) { + // const agent = query.split(' ')[0].substring(1); + // this.chatSession.pinnedAgent = agent; + // this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent; + // } + + // this.logger.info("pinned agent", this.chatSession.pinnedAgent); + const requestProgress = await this.chatService.sendRequest(this.chatSession.id, chatRequest); requestProgress?.responseCompleted.then(responseModel => { if (responseModel.isError) { @@ -170,6 +191,12 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta // Tree Widget currently tracks the ChatModel itself. Therefore no notification necessary. } + protected onUnpin(): void { + this.logger.info("unpin") + 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 diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index 4c86cfe272689..624a64b9b911b 100644 --- a/packages/ai-chat-ui/src/browser/style/index.css +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -7,8 +7,8 @@ flex: 1; } -.chat-input-widget > .ps__rail-x, -.chat-input-widget > .ps__rail-y { +.chat-input-widget>.ps__rail-x, +.chat-input-widget>.ps__rail-y { display: none !important; } @@ -23,7 +23,7 @@ overflow-wrap: break-word; } -div:last-child > .theia-ChatNode { +div:last-child>.theia-ChatNode { border: none; } @@ -59,6 +59,7 @@ div:last-child > .theia-ChatNode { } @keyframes dots { + 0%, 20% { content: ""; @@ -95,15 +96,18 @@ div:last-child > .theia-ChatNode { margin-left: auto; line-height: 18px; } + .theia-ChatNodeToolbar .theia-ChatNodeToolbarAction { display: none; align-items: center; padding: 4px; border-radius: 5px; } + .theia-ChatNode:hover .theia-ChatNodeToolbar .theia-ChatNodeToolbarAction { display: inline-block; } + .theia-ChatNodeToolbar .theia-ChatNodeToolbarAction:hover { cursor: pointer; background-color: var(--theia-toolbar-hoverBackground); @@ -118,7 +122,7 @@ div:last-child > .theia-ChatNode { padding-inline-start: 1rem; } -.theia-ChatNode li > p { +.theia-ChatNode li>p { margin-top: 0; margin-bottom: 0; } @@ -191,6 +195,7 @@ div:last-child > .theia-ChatNode { z-index: 10; text-align: left; } + .theia-ChatInput-Editor-Placeholder.hidden { display: none; } @@ -255,6 +260,7 @@ div:last-child > .theia-ChatNode { border-radius: 5px; cursor: pointer; } + .theia-CodePartRenderer-right .button:hover { background-color: var(--theia-toolbar-hoverBackground); } @@ -299,9 +305,11 @@ div:last-child > .theia-ChatNode { .theia-ResponseNode-ProgressMessage .inProgress { color: var(--theia-progressBar-background); } + .theia-ResponseNode-ProgressMessage .completed { color: var(--theia-successBackground); } + .theia-ResponseNode-ProgressMessage .failed { color: var(--theia-errorForeground); } diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts index 3aeb8dcd612a0..f83336aa88fc5 100644 --- a/packages/ai-chat/src/common/chat-service.ts +++ b/packages/ai-chat/src/common/chat-service.ts @@ -56,6 +56,7 @@ export interface ChatSession { title?: string; model: ChatModel; isActive: boolean; + pinnedAgent?: ChatAgent; } export interface ActiveSessionChangedEvent { @@ -127,7 +128,8 @@ export class ChatServiceImpl implements ChatService { const session: ChatSessionInternal = { id: model.id, model, - isActive: true + isActive: true, + pinnedAgent: undefined }; this._sessions.push(session); this.setActiveSession(session.id, options); @@ -160,8 +162,10 @@ export class ChatServiceImpl implements ChatService { session.title = request.text; const parsedRequest = this.chatRequestParser.parseChatRequest(request, session.model.location); + if (session.pinnedAgent) { - const agent = this.getAgent(parsedRequest); + } + const agent = this.getAgent(parsedRequest, session); if (agent === undefined) { const error = 'No ChatAgents available to handle request!'; this.logger.error(error); @@ -219,10 +223,15 @@ export class ChatServiceImpl implements ChatService { return invocation; } - protected getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined { + protected getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined { const agentPart = this.getMentionedAgent(parsedRequest); + + if (session.pinnedAgent) { + return session.pinnedAgent + } if (agentPart) { - return this.chatAgentService.getAgent(agentPart.agentId); + session.pinnedAgent = this.chatAgentService.getAgent(agentPart.agentId); + return session.pinnedAgent; } if (this.defaultChatAgentId) { return this.chatAgentService.getAgent(this.defaultChatAgentId.id); From 3db5078aaa75da7fc2831a6cb8eca35f9a3bfd7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Thu, 28 Nov 2024 12:00:32 +0100 Subject: [PATCH 02/13] finilize pinning functionality Co-authored-by: Fuyao Tang Co-authored-by: mihaela21k Co-authored-by: Zidong Wang --- .../src/browser/chat-input-widget.tsx | 6 +++--- .../src/browser/chat-view-widget.tsx | 11 ++--------- packages/ai-chat/src/common/chat-service.ts | 18 +++++++++--------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index 9ed8c2e4d4bad..5fb44439f54eb 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -253,9 +253,9 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu return
{props.pinnedAgent !== undefined && -
-

dsakdjaldkjasda {props.pinnedAgent.name}

- < span className="codicon codicon-remove-close option" title="unpin" onClick={handleUnpin} /> +
+

{props.pinnedAgent.name}

+
}
diff --git a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx index 06f73f0c72f63..616733b9d7f05 100644 --- a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx @@ -169,20 +169,13 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta text: query }; - // TODO - // if (!this.chatSession.pinnedAgent && query.startsWith('@')) { - // const agent = query.split(' ')[0].substring(1); - // this.chatSession.pinnedAgent = agent; - // this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent; - // } - - // this.logger.info("pinned agent", this.chatSession.pinnedAgent); - const requestProgress = await this.chatService.sendRequest(this.chatSession.id, chatRequest); requestProgress?.responseCompleted.then(responseModel => { 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}`); diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts index f83336aa88fc5..85b0fe1bff0e8 100644 --- a/packages/ai-chat/src/common/chat-service.ts +++ b/packages/ai-chat/src/common/chat-service.ts @@ -35,6 +35,7 @@ import { ChatAgent, ChatAgentLocation } from './chat-agents'; import { ParsedChatRequestAgentPart, ParsedChatRequestVariablePart, ParsedChatRequest } from './parsed-chat-request'; import { AIVariableService } from '@theia/ai-core'; import { Event } from '@theia/core/shared/vscode-languageserver-protocol'; +import { OrchestratorChatAgentId } from "./orchestrator-chat-agent"; export interface ChatRequestInvocation { /** @@ -162,10 +163,14 @@ export class ChatServiceImpl implements ChatService { session.title = request.text; const parsedRequest = this.chatRequestParser.parseChatRequest(request, session.model.location); - if (session.pinnedAgent) { + let agent = this.getAgent(parsedRequest); + if (!session.pinnedAgent && agent && agent.id !== OrchestratorChatAgentId) { + session.pinnedAgent = agent; + } else if (session.pinnedAgent && this.getMentionedAgent(parsedRequest) === undefined) { + agent = session.pinnedAgent; } - const agent = this.getAgent(parsedRequest, session); + if (agent === undefined) { const error = 'No ChatAgents available to handle request!'; this.logger.error(error); @@ -223,15 +228,10 @@ export class ChatServiceImpl implements ChatService { return invocation; } - protected getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined { + protected getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined { const agentPart = this.getMentionedAgent(parsedRequest); - - if (session.pinnedAgent) { - return session.pinnedAgent - } if (agentPart) { - session.pinnedAgent = this.chatAgentService.getAgent(agentPart.agentId); - return session.pinnedAgent; + return this.chatAgentService.getAgent(agentPart.agentId); } if (this.defaultChatAgentId) { return this.chatAgentService.getAgent(this.defaultChatAgentId.id); From 44221b74a49cafff3e71e08f3469fcd006b0bf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 29 Nov 2024 13:25:23 +0100 Subject: [PATCH 03/13] remove logger & improve ui --- .../src/browser/chat-input-widget.tsx | 12 +++++----- .../src/browser/chat-view-widget.tsx | 9 -------- .../ai-chat-ui/src/browser/style/index.css | 22 +++++++++++++++++-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index 5fb44439f54eb..1ea74f5d31856 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -252,13 +252,13 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu } return
- {props.pinnedAgent !== undefined && -
-

{props.pinnedAgent.name}

- -
- }
+ {props.pinnedAgent !== undefined && +
+ @{props.pinnedAgent.name} + +
+ }
Ask a question
diff --git a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx index 616733b9d7f05..015506d705be3 100644 --- a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx @@ -22,8 +22,6 @@ import { AIChatInputWidget } from './chat-input-widget'; import { ChatViewTreeWidget } from './chat-tree-view/chat-view-tree-widget'; import { AIActivationService } from '@theia/ai-core/lib/browser/ai-activation-service'; -import { ILogger } from '@theia/core'; - export namespace ChatViewWidget { export interface State { locked?: boolean; @@ -51,9 +49,6 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta @inject(AIActivationService) protected readonly activationService: AIActivationService; - @inject(ILogger) - protected readonly logger: ILogger; - protected chatSession: ChatSession; protected _state: ChatViewWidget.State = { locked: false }; @@ -120,9 +115,6 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta const session = event.sessionId ? this.chatService.getSession(event.sessionId) : this.chatService.createSession(); if (session) { - - this.logger.info("session:", session) - this.chatSession = session; this.treeWidget.trackChatModel(this.chatSession.model); this.inputWidget.chatModel = this.chatSession.model; @@ -185,7 +177,6 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta } protected onUnpin(): void { - this.logger.info("unpin") this.chatSession.pinnedAgent = undefined; this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent; } diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index 624a64b9b911b..5799e1cc9b8cc 100644 --- a/packages/ai-chat-ui/src/browser/style/index.css +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -159,11 +159,29 @@ div:last-child>.theia-ChatNode { overflow: hidden; } +.theia-ChatInput-Popup { + position: relative; + bottom: -5px; + right: -2px; + padding-top: 7px; + padding-left: 10px; + padding-right: 10px; + padding-bottom: 9px; + 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; @@ -184,8 +202,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; From 905c243eec4eea48690c823600c52c01368dbce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 20 Dec 2024 04:07:49 +0100 Subject: [PATCH 04/13] register pinned agent command to command service --- .../ai-chat-ui/src/browser/ai-chat-ui-contribution.ts | 10 ++++++++-- packages/ai-chat-ui/src/browser/chat-input-widget.tsx | 2 +- packages/ai-chat-ui/src/browser/chat-view-commands.ts | 5 +++++ packages/ai-chat-ui/src/browser/chat-view-widget.tsx | 2 +- packages/ai-chat-ui/src/browser/style/index.css | 4 ++-- packages/ai-chat/src/common/chat-service.ts | 8 ++++---- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts b/packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts index 3c7c25eac7472..7afe2c39aa1d4 100644 --- a/packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts +++ b/packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts @@ -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'; @@ -79,6 +79,12 @@ export class AIChatContribution extends AbstractViewContribution 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, diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index 1ea74f5d31856..435975cfc474b 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -249,7 +249,7 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu const handleUnpin = () => { props.onUnpin(); - } + }; return
diff --git a/packages/ai-chat-ui/src/browser/chat-view-commands.ts b/packages/ai-chat-ui/src/browser/chat-view-commands.ts index f513e32690444..082c819dff22e 100644 --- a/packages/ai-chat-ui/src/browser/chat-view-commands.ts +++ b/packages/ai-chat-ui/src/browser/chat-view-commands.ts @@ -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') diff --git a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx index 015506d705be3..b032e832ad5fe 100644 --- a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx @@ -91,7 +91,7 @@ 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.onUnpin = this.onUnpin.bind(this); this.inputWidget.onCancel = this.onCancel.bind(this); this.inputWidget.chatModel = this.chatSession.model; this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent; diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index 5799e1cc9b8cc..2ef0b364b4980 100644 --- a/packages/ai-chat-ui/src/browser/style/index.css +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -163,10 +163,10 @@ div:last-child>.theia-ChatNode { position: relative; bottom: -5px; right: -2px; - padding-top: 7px; + padding-top: 9px; padding-left: 10px; padding-right: 10px; - padding-bottom: 9px; + padding-bottom: 11px; display: flex; flex-direction: row; align-items: start; diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts index 85b0fe1bff0e8..075149bddbafa 100644 --- a/packages/ai-chat/src/common/chat-service.ts +++ b/packages/ai-chat/src/common/chat-service.ts @@ -35,7 +35,7 @@ import { ChatAgent, ChatAgentLocation } from './chat-agents'; import { ParsedChatRequestAgentPart, ParsedChatRequestVariablePart, ParsedChatRequest } from './parsed-chat-request'; import { AIVariableService } from '@theia/ai-core'; import { Event } from '@theia/core/shared/vscode-languageserver-protocol'; -import { OrchestratorChatAgentId } from "./orchestrator-chat-agent"; +import { OrchestratorChatAgentId } from './orchestrator-chat-agent'; export interface ChatRequestInvocation { /** @@ -80,7 +80,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; @@ -124,13 +124,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, - pinnedAgent: undefined + pinnedAgent: pinnedAgent }; this._sessions.push(session); this.setActiveSession(session.id, options); From d5c0c9ac6957331f070a9fcc3e568b440a8b7d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Tue, 14 Jan 2025 23:39:23 +0100 Subject: [PATCH 05/13] linting --- packages/ai-chat-ui/src/browser/chat-input-widget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index f549758ec4d83..fd3864f1ee588 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -281,7 +281,7 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu placeholderRef.current?.classList.add('hidden'); } }; - + const handleUnpin = () => { props.onUnpin(); }; From 1cfeb9f0fcd9d15c07fc47239d5787ad6550be2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 22 Jan 2025 18:05:41 +0100 Subject: [PATCH 06/13] block pinning default chat agent instead of orchestrator --- packages/ai-chat/src/common/chat-service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts index 075149bddbafa..62743d6f7ec7f 100644 --- a/packages/ai-chat/src/common/chat-service.ts +++ b/packages/ai-chat/src/common/chat-service.ts @@ -35,7 +35,6 @@ import { ChatAgent, ChatAgentLocation } from './chat-agents'; import { ParsedChatRequestAgentPart, ParsedChatRequestVariablePart, ParsedChatRequest } from './parsed-chat-request'; import { AIVariableService } from '@theia/ai-core'; import { Event } from '@theia/core/shared/vscode-languageserver-protocol'; -import { OrchestratorChatAgentId } from './orchestrator-chat-agent'; export interface ChatRequestInvocation { /** @@ -165,7 +164,7 @@ export class ChatServiceImpl implements ChatService { const parsedRequest = this.chatRequestParser.parseChatRequest(request, session.model.location); let agent = this.getAgent(parsedRequest); - if (!session.pinnedAgent && agent && agent.id !== OrchestratorChatAgentId) { + if (!session.pinnedAgent && agent && agent.id !== this.defaultChatAgentId?.id) { session.pinnedAgent = agent; } else if (session.pinnedAgent && this.getMentionedAgent(parsedRequest) === undefined) { agent = session.pinnedAgent; From 253a27d0fa0badaf58ceec46b22b8702d377fe39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 22 Jan 2025 18:09:44 +0100 Subject: [PATCH 07/13] revert reformatting --- packages/ai-chat-ui/src/browser/style/index.css | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index b4c07a1c0e72d..dbfe55cc38405 100644 --- a/packages/ai-chat-ui/src/browser/style/index.css +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -7,8 +7,8 @@ flex: 1; } -.chat-input-widget>.ps__rail-x, -.chat-input-widget>.ps__rail-y { +.chat-input-widget > .ps__rail-x, +.chat-input-widget > .ps__rail-y { display: none !important; } @@ -23,7 +23,7 @@ overflow-wrap: break-word; } -div:last-child>.theia-ChatNode { +div:last-child >. theia-ChatNode { border: none; } @@ -59,7 +59,6 @@ div:last-child>.theia-ChatNode { } @keyframes dots { - 0%, 20% { content: ""; @@ -96,18 +95,15 @@ div:last-child>.theia-ChatNode { margin-left: auto; line-height: 18px; } - .theia-ChatNodeToolbar .theia-ChatNodeToolbarAction { display: none; align-items: center; padding: 4px; border-radius: 5px; } - .theia-ChatNode:hover .theia-ChatNodeToolbar .theia-ChatNodeToolbarAction { display: inline-block; } - .theia-ChatNodeToolbar .theia-ChatNodeToolbarAction:hover { cursor: pointer; background-color: var(--theia-toolbar-hoverBackground); @@ -122,7 +118,7 @@ div:last-child>.theia-ChatNode { padding-inline-start: 1rem; } -.theia-ChatNode li>p { +.theia-ChatNode li > p { margin-top: 0; margin-bottom: 0; } @@ -227,7 +223,6 @@ div:last-child>.theia-ChatNode { z-index: 10; text-align: left; } - .theia-ChatInput-Editor-Placeholder.hidden { display: none; } @@ -292,7 +287,6 @@ div:last-child>.theia-ChatNode { border-radius: 5px; cursor: pointer; } - .theia-CodePartRenderer-right .button:hover { background-color: var(--theia-toolbar-hoverBackground); } @@ -379,11 +373,9 @@ details[open].collapsible-arguments .collapsible-arguments-summary { .theia-ResponseNode-ProgressMessage .inProgress { color: var(--theia-progressBar-background); } - .theia-ResponseNode-ProgressMessage .completed { color: var(--theia-successBackground); } - .theia-ResponseNode-ProgressMessage .failed { color: var(--theia-errorForeground); } From 8557b7570f385fd29a7bcf3b26866a49a3373500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 22 Jan 2025 18:10:32 +0100 Subject: [PATCH 08/13] revert reformatting --- packages/ai-chat-ui/src/browser/style/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index dbfe55cc38405..e0e2035f77ddc 100644 --- a/packages/ai-chat-ui/src/browser/style/index.css +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -23,7 +23,7 @@ overflow-wrap: break-word; } -div:last-child >. theia-ChatNode { +div:last-child > .theia-ChatNode { border: none; } From 79b80d8d0c59efdc74ec81fde01f4d89ea2b8d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Tue, 28 Jan 2025 12:19:57 +0100 Subject: [PATCH 09/13] remove spaces --- packages/ai-chat-ui/src/browser/chat-input-widget.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index 06bba859a6337..e31e90cbebb46 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -13,7 +13,7 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { ChangeSet, ChangeSetElement, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat'; +import { ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat'; import { Disposable, UntitledResourceResolver } from '@theia/core'; import { ContextMenuRenderer, LabelProvider, Message, ReactWidget } from '@theia/core/lib/browser'; import { Deferred } from '@theia/core/lib/common/promise-util'; @@ -149,11 +149,11 @@ export class AIChatInputWidget extends ReactWidget { interface ChatInputProperties { onCancel: (requestModel: ChatRequestModel) => void; onQuery: (query: string) => void; - onUnpin: () => void; + onUnpin: () => void; onDeleteChangeSet: (sessionId: string) => void; onDeleteChangeSetElement: (sessionId: string, index: number) => void; isEnabled?: boolean; - chatModel: ChatModel; + chatModel: ChatModel; pinnedAgent?: ChatAgent; editorProvider: MonacoEditorProvider; untitledResourceResolver: UntitledResourceResolver; @@ -336,7 +336,7 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu const handleUnpin = () => { props.onUnpin(); }; - + const leftOptions = props.showContext ? [{ title: 'Attach elements to context', handler: () => { /* TODO */ }, From 933a30851ed6d65bd22ffe0450d4a069635c3b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Mon, 3 Feb 2025 02:04:59 +0100 Subject: [PATCH 10/13] update pinned agent ui --- .../src/browser/chat-input-widget.tsx | 53 ++++++++++++------- .../ai-chat-ui/src/browser/style/index.css | 11 +++- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index e31e90cbebb46..9937ebbe78225 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -333,15 +333,26 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu } }; - const handleUnpin = () => { - props.onUnpin(); - }; - - const leftOptions = props.showContext ? [{ - title: 'Attach elements to context', - handler: () => { /* TODO */ }, - className: 'codicon-add' - }] : []; + const leftOptions = [ + ...(props.showContext + ? [{ + title: 'Attach elements to context', + handler: () => { /* TODO */ }, + className: 'codicon-add' + }] + : []), + ...(props.pinnedAgent + ? [{ + title: 'Unpin Agent', + handler: props.onUnpin, + className: 'at-icon', + text: { + align: 'right', + content: props.pinnedAgent.name + }, + }] + : []), + ] as Option[]; const rightOptions = inProgress ? [{ @@ -371,12 +382,6 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu }
- {props.pinnedAgent !== undefined && -
- @{props.pinnedAgent.name} - -
- }
Ask a question
@@ -478,6 +483,10 @@ interface Option { handler: () => void; className: string; disabled?: boolean; + text?: { + align?: 'left' | 'right'; + content: string; + }; } const ChatInputOptions: React.FunctionComponent = ({ leftOptions, rightOptions }) => ( @@ -486,20 +495,26 @@ const ChatInputOptions: React.FunctionComponent = ({ left {leftOptions.map((option, index) => ( + > + {option.text?.content} + + ))}
{rightOptions.map((option, index) => ( + > + {option.text?.content} + + ))}
diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index 5450f465e89fa..c8c20b2523668 100644 --- a/packages/ai-chat-ui/src/browser/style/index.css +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -398,10 +398,13 @@ div:last-child > .theia-ChatNode { } .theia-ChatInputOptions .option { - width: 21px; + min-width: 21px; height: 21px; padding: 2px; - display: inline-block; + display: flex; + justify-content: space-between; + align-items: center; + gap: 2px; box-sizing: border-box; user-select: none; background-repeat: no-repeat; @@ -422,6 +425,10 @@ div:last-child > .theia-ChatNode { background-color: var(--theia-toolbar-hoverBackground); } +.theia-ChatInputOptions .reverse { + flex-direction: row-reverse; +} + .theia-CodePartRenderer-root { display: flex; flex-direction: column; From 6f483f05e783ca535ef77aafa9c5945894e7bc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 7 Feb 2025 02:57:25 +0100 Subject: [PATCH 11/13] remove unused style --- packages/ai-chat-ui/src/browser/style/index.css | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index c8c20b2523668..f04d77e94437c 100644 --- a/packages/ai-chat-ui/src/browser/style/index.css +++ b/packages/ai-chat-ui/src/browser/style/index.css @@ -280,23 +280,6 @@ div:last-child > .theia-ChatNode { text-overflow: ellipsis; } -.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-ChangeSet-Header-Actions .codicon.action { font-size: 18px; height: 20px; From 7f8547da6ec706a9498693f51b23d71d6b24897a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 7 Feb 2025 03:01:52 +0100 Subject: [PATCH 12/13] Cleanup --- packages/ai-chat-ui/src/browser/chat-view-widget.tsx | 1 - packages/ai-chat/src/common/chat-service.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx index 145ff7c9c1f91..47ce0cb86694e 100644 --- a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx @@ -115,7 +115,6 @@ 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); diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts index c1fbc62f00464..99be1e23c2acc 100644 --- a/packages/ai-chat/src/common/chat-service.ts +++ b/packages/ai-chat/src/common/chat-service.ts @@ -134,7 +134,7 @@ export class ChatServiceImpl implements ChatService { id: model.id, model, isActive: true, - pinnedAgent: pinnedAgent + pinnedAgent }; this._sessions.push(session); this.setActiveSession(session.id, options); From cc017fb94549e3cbb34c307582772eaa1c45c5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atahan=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 7 Feb 2025 04:49:08 +0100 Subject: [PATCH 13/13] add user setting option for dis/enable agent pinning feature --- .../src/browser/ai-chat-ui-frontend-module.ts | 3 +- .../src/browser/chat-input-widget.tsx | 27 ++++++++++++++--- .../src/browser/ai-chat-frontend-module.ts | 4 ++- .../src/browser/ai-chat-preferences.ts | 9 ++++++ .../src/browser/frontend-chat-service.ts | 19 ++++++++++-- packages/ai-chat/src/common/chat-service.ts | 29 ++++++++++++++----- 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts b/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts index 2ed0c779bacef..acac40855b1fe 100644 --- a/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts +++ b/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts @@ -61,7 +61,8 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(AIChatInputWidget).toSelf(); bind(AIChatInputConfiguration).toConstantValue({ - showContext: false + showContext: false, + showPinnedAgent: true }); bind(WidgetFactory).toDynamicValue(({ container }) => ({ id: AIChatInputWidget.ID, diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx index 9937ebbe78225..22c17b18a1cae 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -33,6 +33,7 @@ type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration'); export interface AIChatInputConfiguration { showContext?: boolean; + showPinnedAgent?: boolean; } @injectable() @@ -126,6 +127,7 @@ export class AIChatInputWidget extends ReactWidget { this.editorReady.resolve(); }} showContext={this.configuration?.showContext} + showPinnedAgent={this.configuration?.showPinnedAgent} labelProvider={this.labelProvider} /> ); @@ -160,6 +162,7 @@ interface ChatInputProperties { contextMenuCallback: (event: IMouseEvent) => void; setEditorRef: (editor: MonacoEditor | undefined) => void; showContext?: boolean; + showPinnedAgent?: boolean; labelProvider: LabelProvider; } @@ -333,6 +336,22 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu } }; + const handlePin = () => { + if (editorRef.current) { + editorRef.current.getControl().getModel()?.applyEdits([{ + range: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1 + }, + text: '@ ', + }]); + editorRef.current.getControl().setPosition({ lineNumber: 1, column: 2 }); + editorRef.current.getControl().getAction('editor.action.triggerSuggest')?.run(); + } + }; + const leftOptions = [ ...(props.showContext ? [{ @@ -341,14 +360,14 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu className: 'codicon-add' }] : []), - ...(props.pinnedAgent + ...(props.showPinnedAgent ? [{ - title: 'Unpin Agent', - handler: props.onUnpin, + title: props.pinnedAgent ? 'Unpin Agent' : 'Pin Agent', + handler: props.pinnedAgent ? props.onUnpin : handlePin, className: 'at-icon', text: { align: 'right', - content: props.pinnedAgent.name + content: props.pinnedAgent && props.pinnedAgent.name }, }] : []), diff --git a/packages/ai-chat/src/browser/ai-chat-frontend-module.ts b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts index 91de95e74c79e..eeb87670f3d89 100644 --- a/packages/ai-chat/src/browser/ai-chat-frontend-module.ts +++ b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts @@ -25,7 +25,8 @@ import { ChatRequestParser, ChatRequestParserImpl, ChatService, - DefaultChatAgentId + DefaultChatAgentId, + PinChatAgent } from '../common'; import { ChatAgentsVariableContribution } from '../common/chat-agents-variable-contribution'; import { CommandChatAgent } from '../common/command-chat-agents'; @@ -51,6 +52,7 @@ export default new ContainerModule(bind => { bind(ChatAgentServiceImpl).toSelf().inSingletonScope(); bind(ChatAgentService).toService(ChatAgentServiceImpl); bind(DefaultChatAgentId).toConstantValue({ id: OrchestratorChatAgentId }); + bind(PinChatAgent).toConstantValue(true); bindContributionProvider(bind, ResponseContentMatcherProvider); bind(DefaultResponseContentMatcherProvider).toSelf().inSingletonScope(); diff --git a/packages/ai-chat/src/browser/ai-chat-preferences.ts b/packages/ai-chat/src/browser/ai-chat-preferences.ts index 11dee619e4ff0..d71b30865f725 100644 --- a/packages/ai-chat/src/browser/ai-chat-preferences.ts +++ b/packages/ai-chat/src/browser/ai-chat-preferences.ts @@ -18,6 +18,7 @@ import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-pr import { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; export const DEFAULT_CHAT_AGENT_PREF = 'ai-features.chat.defaultChatAgent'; +export const PIN_CHAT_AGENT_PREF = 'ai-features.chat.pinChatAgent'; export const aiChatPreferences: PreferenceSchema = { type: 'object', @@ -27,6 +28,14 @@ export const aiChatPreferences: PreferenceSchema = { description: 'Optional: of the Chat Agent that shall be invoked, if no agent is explicitly mentioned with @ in the user query.\ If no Default Agent is configured, Theia´s defaults will be applied.', title: AI_CORE_PREFERENCES_TITLE, + }, + [PIN_CHAT_AGENT_PREF]: { + type: 'boolean', + description: 'Enable agent pinning to automatically keep a mentioned chat agent active across prompts, reducing the need for repeated mentions.\ + \n\ + You can manually unpin or switch agents anytime.', + default: true, + title: AI_CORE_PREFERENCES_TITLE, } } }; diff --git a/packages/ai-chat/src/browser/frontend-chat-service.ts b/packages/ai-chat/src/browser/frontend-chat-service.ts index 10e7dde4a1d46..fdf6057bb0df3 100644 --- a/packages/ai-chat/src/browser/frontend-chat-service.ts +++ b/packages/ai-chat/src/browser/frontend-chat-service.ts @@ -15,9 +15,9 @@ // ***************************************************************************** import { inject, injectable } from '@theia/core/shared/inversify'; -import { ChatAgent, ChatServiceImpl, ParsedChatRequest } from '../common'; +import { ChatAgent, ChatServiceImpl, ChatSession, ParsedChatRequest } from '../common'; import { PreferenceService } from '@theia/core/lib/browser'; -import { DEFAULT_CHAT_AGENT_PREF } from './ai-chat-preferences'; +import { DEFAULT_CHAT_AGENT_PREF, PIN_CHAT_AGENT_PREF } from './ai-chat-preferences'; @injectable() export class FrontendChatServiceImpl extends ChatServiceImpl { @@ -25,7 +25,20 @@ export class FrontendChatServiceImpl extends ChatServiceImpl { @inject(PreferenceService) protected preferenceService: PreferenceService; - protected override getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined { + protected override getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined { + let agent = this.initialAgentSelection(parsedRequest); + if (!this.preferenceService.get(PIN_CHAT_AGENT_PREF)) { + return agent; + } + if (!session.pinnedAgent && agent && agent.id !== this.defaultChatAgentId?.id) { + session.pinnedAgent = agent; + } else if (session.pinnedAgent && this.getMentionedAgent(parsedRequest) === undefined) { + agent = session.pinnedAgent; + } + return agent; + } + + protected override initialAgentSelection(parsedRequest: ParsedChatRequest): ChatAgent | undefined { const agentPart = this.getMentionedAgent(parsedRequest); if (agentPart) { return this.chatAgentService.getAgent(agentPart.agentId); diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts index 99be1e23c2acc..08a45cecbfca2 100644 --- a/packages/ai-chat/src/common/chat-service.ts +++ b/packages/ai-chat/src/common/chat-service.ts @@ -73,6 +73,9 @@ export interface DefaultChatAgentId { id: string; } +export const PinChatAgent = Symbol('PinChatAgent'); +export type PinChatAgent = boolean; + export const ChatService = Symbol('ChatService'); export interface ChatService { onActiveSessionChanged: Event @@ -109,6 +112,9 @@ export class ChatServiceImpl implements ChatService { @inject(DefaultChatAgentId) @optional() protected defaultChatAgentId: DefaultChatAgentId | undefined; + @inject(PinChatAgent) @optional() + protected pinChatAgent: boolean | undefined; + @inject(ChatRequestParser) protected chatRequestParser: ChatRequestParser; @@ -167,13 +173,7 @@ 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, session); if (agent === undefined) { const error = 'No ChatAgents available to handle request!'; @@ -236,7 +236,20 @@ export class ChatServiceImpl implements ChatService { return this.getSession(sessionId)?.model.getRequest(requestId)?.response.cancel(); } - protected getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined { + protected getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined { + let agent = this.initialAgentSelection(parsedRequest); + if (this.pinChatAgent === false) { + return agent; + } + if (!session.pinnedAgent && agent && agent.id !== this.defaultChatAgentId?.id) { + session.pinnedAgent = agent; + } else if (session.pinnedAgent && this.getMentionedAgent(parsedRequest) === undefined) { + agent = session.pinnedAgent; + } + return agent; + } + + protected initialAgentSelection(parsedRequest: ParsedChatRequest): ChatAgent | undefined { const agentPart = this.getMentionedAgent(parsedRequest); if (agentPart) { return this.chatAgentService.getAgent(agentPart.agentId);