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 053c5f0a4dc29..9e440b112c8f2 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 12f2cd102eb0a..f549758ec4d83 100644 --- a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx @@ -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; +type Unpin = () => void; type Cancel = (requestModel: ChatRequestModel) => void; @injectable() @@ -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; @@ -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 { @@ -87,8 +97,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; @@ -267,9 +281,19 @@ const ChatInput: React.FunctionComponent = (props: ChatInpu placeholderRef.current?.classList.add('hidden'); } }; + + const handleUnpin = () => { + props.onUnpin(); + }; return
+ {props.pinnedAgent !== undefined && +
+ @{props.pinnedAgent.name} + +
+ }
Ask a question
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 3793d0f6c7f4a..367722b2af650 100644 --- a/packages/ai-chat-ui/src/browser/chat-view-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-view-widget.tsx @@ -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(); @@ -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(); } @@ -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}`); @@ -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 diff --git a/packages/ai-chat-ui/src/browser/style/index.css b/packages/ai-chat-ui/src/browser/style/index.css index faf9ada7849e9..b4c07a1c0e72d 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; } @@ -169,11 +173,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; @@ -194,8 +216,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; @@ -205,6 +227,7 @@ div:last-child > .theia-ChatNode { z-index: 10; text-align: left; } + .theia-ChatInput-Editor-Placeholder.hidden { display: none; } @@ -269,6 +292,7 @@ div:last-child > .theia-ChatNode { border-radius: 5px; cursor: pointer; } + .theia-CodePartRenderer-right .button:hover { background-color: var(--theia-toolbar-hoverBackground); } @@ -355,9 +379,11 @@ 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); } diff --git a/packages/ai-chat/src/common/chat-service.ts b/packages/ai-chat/src/common/chat-service.ts index 3aeb8dcd612a0..075149bddbafa 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 { /** @@ -56,6 +57,7 @@ export interface ChatSession { title?: string; model: ChatModel; isActive: boolean; + pinnedAgent?: ChatAgent; } export interface ActiveSessionChangedEvent { @@ -78,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; @@ -122,12 +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 + isActive: true, + pinnedAgent: pinnedAgent }; this._sessions.push(session); this.setActiveSession(session.id, options); @@ -160,8 +163,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 !== OrchestratorChatAgentId) { + 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);