@theia/ai-chat-ui 1.66.0-next.80 → 1.66.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.
@@ -16,7 +16,7 @@
16
16
  import {
17
17
  ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatHierarchyBranch,
18
18
  ChatModel, ChatRequestModel, ChatService, ChatSuggestion, EditableChatRequestModel,
19
- ChatRequestParser
19
+ ChatRequestParser, ChatMode
20
20
  } from '@theia/ai-chat';
21
21
  import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
22
22
  import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
@@ -40,7 +40,7 @@ import { IModelDeltaDecoration } from '@theia/monaco-editor-core/esm/vs/editor/c
40
40
  import { EditorOption } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
41
41
  import { ChatInputHistoryService, ChatInputNavigationState } from './chat-input-history';
42
42
 
43
- type Query = (query: string) => Promise<void>;
43
+ type Query = (query: string, mode?: string) => Promise<void>;
44
44
  type Unpin = () => void;
45
45
  type Cancel = (requestModel: ChatRequestModel) => void;
46
46
  type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
@@ -106,11 +106,11 @@ export class AIChatInputWidget extends ReactWidget {
106
106
  @inject(ChatRequestParser)
107
107
  protected readonly chatRequestParser: ChatRequestParser;
108
108
 
109
- protected navigationState: ChatInputNavigationState;
110
-
111
109
  @inject(ContextKeyService)
112
110
  protected readonly contextKeyService: ContextKeyService;
113
111
 
112
+ protected navigationState: ChatInputNavigationState;
113
+
114
114
  protected editorRef: SimpleMonacoEditor | undefined = undefined;
115
115
  protected readonly editorReady = new Deferred<void>();
116
116
 
@@ -136,15 +136,41 @@ export class AIChatInputWidget extends ReactWidget {
136
136
  return this.navigationState.getNextPrompt();
137
137
  }
138
138
 
139
+ cycleMode(): void {
140
+ if (!this.receivingAgent || !this.receivingAgent.modes || this.receivingAgent.modes.length <= 1) {
141
+ return;
142
+ }
143
+ const currentIndex = this.receivingAgent.modes.findIndex(mode => mode.id === this.receivingAgent!.currentModeId);
144
+ const nextIndex = currentIndex === -1 ? 1 : (currentIndex + 1) % this.receivingAgent.modes.length;
145
+ this.receivingAgent = {
146
+ ...this.receivingAgent,
147
+ currentModeId: this.receivingAgent.modes[nextIndex].id
148
+ };
149
+ this.update();
150
+ }
151
+
152
+ protected handleModeChange = (mode: string): void => {
153
+ if (this.receivingAgent) {
154
+ this.receivingAgent = { ...this.receivingAgent, currentModeId: mode };
155
+ this.update();
156
+ }
157
+ };
158
+
139
159
  protected chatInputFocusKey: ContextKey<boolean>;
140
160
  protected chatInputFirstLineKey: ContextKey<boolean>;
141
161
  protected chatInputLastLineKey: ContextKey<boolean>;
142
162
  protected chatInputReceivingAgentKey: ContextKey<string>;
163
+ protected chatInputHasModesKey: ContextKey<boolean>;
143
164
 
144
165
  protected isEnabled = false;
145
166
  protected heightInLines = 12;
146
167
 
147
168
  protected updateReceivingAgentTimeout: number | undefined;
169
+ protected receivingAgent: {
170
+ agentId: string;
171
+ modes: ChatMode[];
172
+ currentModeId?: string;
173
+ } | undefined;
148
174
 
149
175
  protected _branch?: ChatHierarchyBranch;
150
176
  set branch(branch: ChatHierarchyBranch | undefined) {
@@ -156,12 +182,12 @@ export class AIChatInputWidget extends ReactWidget {
156
182
 
157
183
  protected _onQuery: Query;
158
184
  set onQuery(query: Query) {
159
- this._onQuery = (prompt: string) => {
185
+ this._onQuery = (prompt: string, mode?: string) => {
160
186
  if (this.configuration?.enablePromptHistory !== false && prompt.trim()) {
161
187
  this.historyService.addToHistory(prompt);
162
188
  this.navigationState.stopNavigation();
163
189
  }
164
- return query(prompt);
190
+ return query(prompt, mode);
165
191
  };
166
192
  }
167
193
  protected _onUnpin: Unpin;
@@ -238,6 +264,7 @@ export class AIChatInputWidget extends ReactWidget {
238
264
  this.chatInputFirstLineKey = this.contextKeyService.createKey<boolean>('chatInputFirstLine', false);
239
265
  this.chatInputLastLineKey = this.contextKeyService.createKey<boolean>('chatInputLastLine', false);
240
266
  this.chatInputReceivingAgentKey = this.contextKeyService.createKey<string>('chatInputReceivingAgent', '');
267
+ this.chatInputHasModesKey = this.contextKeyService.createKey<boolean>('chatInputHasModes', false);
241
268
  }
242
269
 
243
270
  updateCursorPositionKeys(): void {
@@ -288,7 +315,12 @@ export class AIChatInputWidget extends ReactWidget {
288
315
 
289
316
  protected async updateReceivingAgent(): Promise<void> {
290
317
  if (!this.editorRef || !this._chatModel) {
291
- this.chatInputReceivingAgentKey.set('');
318
+ if (this.receivingAgent !== undefined) {
319
+ this.chatInputReceivingAgentKey.set('');
320
+ this.chatInputHasModesKey.set(false);
321
+ this.receivingAgent = undefined;
322
+ this.update();
323
+ }
292
324
  return;
293
325
  }
294
326
 
@@ -300,13 +332,39 @@ export class AIChatInputWidget extends ReactWidget {
300
332
  const session = this.chatService.getSessions().find(s => s.model.id === this._chatModel.id);
301
333
  if (session) {
302
334
  const agent = this.chatService.getAgent(parsedRequest, session);
303
- this.chatInputReceivingAgentKey.set(agent?.id ?? '');
304
- } else {
335
+ const agentId = agent?.id ?? '';
336
+ const previousAgentId = this.receivingAgent?.agentId;
337
+
338
+ this.chatInputReceivingAgentKey.set(agentId);
339
+
340
+ // Only update and re-render when the agent changes
341
+ if (agent && agentId !== previousAgentId) {
342
+ const modes = agent.modes ?? [];
343
+ this.receivingAgent = {
344
+ agentId: agentId,
345
+ modes
346
+ };
347
+ this.chatInputHasModesKey.set(modes.length > 1);
348
+ this.update();
349
+ } else if (!agent && this.receivingAgent !== undefined) {
350
+ this.receivingAgent = undefined;
351
+ this.chatInputHasModesKey.set(false);
352
+ this.update();
353
+ }
354
+ } else if (this.receivingAgent !== undefined) {
305
355
  this.chatInputReceivingAgentKey.set('');
356
+ this.chatInputHasModesKey.set(false);
357
+ this.receivingAgent = undefined;
358
+ this.update();
306
359
  }
307
360
  } catch (error) {
308
361
  console.warn('Failed to determine receiving agent:', error);
309
- this.chatInputReceivingAgentKey.set('');
362
+ if (this.receivingAgent !== undefined) {
363
+ this.chatInputReceivingAgentKey.set('');
364
+ this.chatInputHasModesKey.set(false);
365
+ this.receivingAgent = undefined;
366
+ this.update();
367
+ }
310
368
  }
311
369
  }
312
370
 
@@ -433,6 +491,11 @@ export class AIChatInputWidget extends ReactWidget {
433
491
  }
434
492
  }}
435
493
  onResize={() => this.onDidResizeEmitter.fire()}
494
+ modeSelectorProps={{
495
+ receivingAgentModes: this.receivingAgent?.modes,
496
+ currentMode: this.receivingAgent?.currentModeId,
497
+ onModeChange: this.handleModeChange,
498
+ }}
436
499
  />
437
500
  );
438
501
  }
@@ -543,7 +606,7 @@ export class AIChatInputWidget extends ReactWidget {
543
606
  interface ChatInputProperties {
544
607
  branch?: ChatHierarchyBranch;
545
608
  onCancel: (requestModel: ChatRequestModel) => void;
546
- onQuery: (query: string) => void;
609
+ onQuery: (query: string, mode?: string) => void;
547
610
  onUnpin: () => void;
548
611
  onDragOver: (event: React.DragEvent) => void;
549
612
  onDrop: (event: React.DragEvent) => void;
@@ -580,6 +643,11 @@ interface ChatInputProperties {
580
643
  heightInLines?: number;
581
644
  onResponseChanged: () => void;
582
645
  onResize: () => void;
646
+ modeSelectorProps: {
647
+ receivingAgentModes?: ChatMode[];
648
+ currentMode?: string;
649
+ onModeChange: (mode: string) => void;
650
+ }
583
651
  }
584
652
 
585
653
  // Utility to check if we have task context in the chat model
@@ -844,12 +912,12 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
844
912
  if (!effectiveValue || effectiveValue.trim().length === 0) {
845
913
  return;
846
914
  }
847
- props.onQuery(effectiveValue);
915
+ props.onQuery(effectiveValue, props.modeSelectorProps.currentMode);
848
916
  setValue('');
849
917
  if (editorRef.current && !editorRef.current.document.textEditorModel.isDisposed()) {
850
918
  editorRef.current.document.textEditorModel.setValue('');
851
919
  }
852
- }, [props.context, props.onQuery, setValue, shouldUseTaskPlaceholder, taskPlaceholder]);
920
+ }, [props.context, props.onQuery, props.modeSelectorProps.currentMode, setValue, shouldUseTaskPlaceholder, taskPlaceholder]);
853
921
 
854
922
  const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
855
923
  if (!props.isEnabled) {
@@ -978,6 +1046,9 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
978
1046
 
979
1047
  const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
980
1048
 
1049
+ // Show mode selector if agent has multiple modes
1050
+ const showModeSelector = (props.modeSelectorProps.receivingAgentModes?.length ?? 0) > 1;
1051
+
981
1052
  return (
982
1053
  <div className='theia-ChatInput' data-ai-disabled={!props.isEnabled} onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
983
1054
  {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
@@ -991,12 +1062,101 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
991
1062
  {props.context && props.context.length > 0 &&
992
1063
  <ChatContext context={contextUI.context} />
993
1064
  }
994
- <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
1065
+ <ChatInputOptions
1066
+ leftOptions={leftOptions}
1067
+ rightOptions={rightOptions}
1068
+ isEnabled={props.isEnabled}
1069
+ modeSelectorProps={{
1070
+ show: showModeSelector,
1071
+ modes: props.modeSelectorProps.receivingAgentModes,
1072
+ currentMode: props.modeSelectorProps.currentMode,
1073
+ onModeChange: props.modeSelectorProps.onModeChange,
1074
+ }}
1075
+ />
995
1076
  </div>
996
1077
  </div>
997
1078
  );
998
1079
  };
999
1080
 
1081
+ interface ChatInputOptionsProps {
1082
+ leftOptions: Option[];
1083
+ rightOptions: Option[];
1084
+ isEnabled?: boolean;
1085
+ modeSelectorProps: {
1086
+ show: boolean;
1087
+ modes?: ChatMode[];
1088
+ currentMode?: string;
1089
+ onModeChange: (mode: string) => void;
1090
+ };
1091
+ }
1092
+
1093
+ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({
1094
+ leftOptions,
1095
+ rightOptions,
1096
+ isEnabled,
1097
+ modeSelectorProps
1098
+ }) => (
1099
+ <div className="theia-ChatInputOptions">
1100
+ <div className="theia-ChatInputOptions-left">
1101
+ {leftOptions.map((option, index) => (
1102
+ <span
1103
+ key={index}
1104
+ className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
1105
+ title={option.title}
1106
+ onClick={option.handler}
1107
+ >
1108
+ <span>{option.text?.content}</span>
1109
+ <span className={`codicon ${option.className}`} />
1110
+ </span>
1111
+ ))}
1112
+ {modeSelectorProps.show && modeSelectorProps.modes && (
1113
+ <ChatModeSelector
1114
+ modes={modeSelectorProps.modes}
1115
+ currentMode={modeSelectorProps.currentMode}
1116
+ onModeChange={modeSelectorProps.onModeChange}
1117
+ disabled={!isEnabled}
1118
+ />
1119
+ )}
1120
+ </div>
1121
+ <div className="theia-ChatInputOptions-right">
1122
+ {rightOptions.map((option, index) => (
1123
+ <span
1124
+ key={index}
1125
+ className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
1126
+ title={option.title}
1127
+ onClick={option.handler}
1128
+ >
1129
+ <span>{option.text?.content}</span>
1130
+ <span className={`codicon ${option.className}`} />
1131
+ </span>
1132
+ ))}
1133
+ </div>
1134
+ </div>
1135
+ );
1136
+
1137
+ interface ChatModeSelectorProps {
1138
+ modes: ChatMode[];
1139
+ currentMode?: string;
1140
+ onModeChange: (mode: string) => void;
1141
+ disabled?: boolean;
1142
+ }
1143
+
1144
+ const ChatModeSelector: React.FunctionComponent<ChatModeSelectorProps> = React.memo(({ modes, currentMode, onModeChange, disabled }) => (
1145
+ <select
1146
+ className="theia-ChatInput-ModeSelector"
1147
+ value={currentMode ?? modes[0]?.id ?? ''}
1148
+ onChange={e => onModeChange(e.target.value)}
1149
+ disabled={disabled}
1150
+ title={modes.find(m => m.id === (currentMode ?? modes[0]?.id))?.name}
1151
+ >
1152
+ {modes.map(mode => (
1153
+ <option key={mode.id} value={mode.id} title={mode.name}>
1154
+ {mode.name}
1155
+ </option>
1156
+ ))}
1157
+ </select>
1158
+ ));
1159
+
1000
1160
  const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
1001
1161
  handler();
1002
1162
  e.stopPropagation();
@@ -1118,11 +1278,6 @@ const ChangeSetElement: React.FC<ChangeSetUIElement> = element => (
1118
1278
  </li>
1119
1279
  );
1120
1280
 
1121
- interface ChatInputOptionsProps {
1122
- leftOptions: Option[];
1123
- rightOptions: Option[];
1124
- }
1125
-
1126
1281
  interface Option {
1127
1282
  title: string;
1128
1283
  handler: () => void;
@@ -1134,37 +1289,6 @@ interface Option {
1134
1289
  };
1135
1290
  }
1136
1291
 
1137
- const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ leftOptions, rightOptions }) => (
1138
- <div className="theia-ChatInputOptions">
1139
- <div className="theia-ChatInputOptions-left">
1140
- {leftOptions.map((option, index) => (
1141
- <span
1142
- key={index}
1143
- className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
1144
- title={option.title}
1145
- onClick={option.handler}
1146
- >
1147
- <span>{option.text?.content}</span>
1148
- <span className={`codicon ${option.className}`} />
1149
- </span>
1150
- ))}
1151
- </div>
1152
- <div className="theia-ChatInputOptions-right">
1153
- {rightOptions.map((option, index) => (
1154
- <span
1155
- key={index}
1156
- className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
1157
- title={option.title}
1158
- onClick={option.handler}
1159
- >
1160
- <span>{option.text?.content}</span>
1161
- <span className={`codicon ${option.className}`} />
1162
- </span>
1163
- ))}
1164
- </div>
1165
- </div>
1166
- );
1167
-
1168
1292
  function buildContextUI(
1169
1293
  context: readonly AIVariableResolutionRequest[] | undefined,
1170
1294
  labelProvider: LabelProvider,
@@ -180,8 +180,12 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
180
180
  return this.onStateChangedEmitter.event;
181
181
  }
182
182
 
183
- protected async onQuery(query?: string | ChatRequest): Promise<void> {
184
- const chatRequest: ChatRequest = !query ? { text: '' } : typeof query === 'string' ? { text: query } : { ...query };
183
+ protected async onQuery(query?: string | ChatRequest, modeId?: string): Promise<void> {
184
+ const chatRequest: ChatRequest = !query
185
+ ? { text: '' }
186
+ : typeof query === 'string'
187
+ ? { text: query, modeId }
188
+ : { ...query };
185
189
  if (chatRequest.text.length === 0) { return; }
186
190
 
187
191
  const requestProgress = await this.chatService.sendRequest(this.chatSession.id, chatRequest);
@@ -594,6 +594,40 @@ div:last-child > .theia-ChatNode {
594
594
  flex-direction: row-reverse;
595
595
  }
596
596
 
597
+ .theia-ChatInput-ModeSelector {
598
+ margin-left: 4px;
599
+ min-width: 21px;
600
+ height: 21px;
601
+ padding: 2px;
602
+ border: none;
603
+ background: var(--theia-editor-background);
604
+ color: var(--theia-foreground);
605
+ font-size: 13px;
606
+ border-radius: 5px;
607
+ outline: none;
608
+ cursor: pointer;
609
+ max-width: 150px;
610
+ }
611
+
612
+ .theia-ChatInput-ModeSelector:hover:not(:disabled) {
613
+ background-color: var(--theia-toolbar-hoverBackground);
614
+ }
615
+
616
+ .theia-ChatInput-ModeSelector:disabled {
617
+ opacity: var(--theia-mod-disabled-opacity);
618
+ cursor: default;
619
+ }
620
+
621
+ .theia-ChatInput-ModeSelector:focus {
622
+ outline: 1px solid var(--theia-focusBorder);
623
+ outline-offset: -1px;
624
+ }
625
+
626
+ .theia-ChatInput-ModeSelector option {
627
+ background: var(--theia-dropdown-background);
628
+ color: var(--theia-dropdown-foreground);
629
+ }
630
+
597
631
  .theia-CodePartRenderer-root {
598
632
  display: flex;
599
633
  flex-direction: column;