@theia/ai-chat-ui 1.63.0-next.24 → 1.63.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.
Files changed (71) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +31 -1
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +192 -8
  4. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  5. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.js +8 -0
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/chat-input-widget.d.ts +24 -16
  9. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  10. package/lib/browser/chat-input-widget.js +199 -37
  11. package/lib/browser/chat-input-widget.js.map +1 -1
  12. package/lib/browser/chat-node-toolbar-action-contribution.d.ts +1 -0
  13. package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
  14. package/lib/browser/chat-node-toolbar-action-contribution.js +13 -0
  15. package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
  16. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +14 -0
  17. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -0
  18. package/lib/browser/chat-response-renderer/delegation-response-renderer.js +144 -0
  19. package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -0
  20. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  21. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  22. package/lib/browser/chat-response-renderer/index.js +1 -0
  23. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  24. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +2 -2
  25. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  26. package/lib/browser/chat-response-renderer/tool-confirmation.js +23 -23
  27. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  28. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +4 -1
  29. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  30. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +65 -32
  31. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  32. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +3 -1
  33. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  34. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +50 -8
  35. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  36. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
  37. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
  38. package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
  39. package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
  40. package/lib/browser/chat-view-commands.d.ts +1 -0
  41. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  42. package/lib/browser/chat-view-commands.js +5 -0
  43. package/lib/browser/chat-view-commands.js.map +1 -1
  44. package/lib/browser/chat-view-contribution.d.ts +2 -0
  45. package/lib/browser/chat-view-contribution.d.ts.map +1 -1
  46. package/lib/browser/chat-view-contribution.js +31 -18
  47. package/lib/browser/chat-view-contribution.js.map +1 -1
  48. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts +2 -0
  49. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
  50. package/lib/browser/chat-view-widget-toolbar-contribution.js +13 -5
  51. package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
  52. package/lib/browser/chat-view-widget.d.ts +1 -1
  53. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  54. package/lib/browser/chat-view-widget.js +1 -4
  55. package/lib/browser/chat-view-widget.js.map +1 -1
  56. package/package.json +11 -11
  57. package/src/browser/ai-chat-ui-contribution.ts +191 -12
  58. package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
  59. package/src/browser/chat-input-widget.tsx +253 -58
  60. package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
  61. package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
  62. package/src/browser/chat-response-renderer/index.ts +1 -0
  63. package/src/browser/chat-response-renderer/tool-confirmation.tsx +30 -30
  64. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +95 -60
  65. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +58 -8
  66. package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
  67. package/src/browser/chat-view-commands.ts +6 -0
  68. package/src/browser/chat-view-contribution.ts +29 -18
  69. package/src/browser/chat-view-widget-toolbar-contribution.tsx +12 -5
  70. package/src/browser/chat-view-widget.tsx +2 -5
  71. package/src/browser/style/index.css +209 -5
@@ -14,24 +14,27 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
  import {
17
- ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel,
18
- ChatService, ChatSuggestion, EditableChatRequestModel, ChatHierarchyBranch
17
+ ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatHierarchyBranch,
18
+ ChatModel, ChatRequestModel, ChatService, ChatSuggestion, EditableChatRequestModel
19
19
  } from '@theia/ai-chat';
20
- import { DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
20
+ import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
21
+ import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
22
+ import { AIVariableResolutionRequest } from '@theia/ai-core';
23
+ import { AgentCompletionNotificationService, FrontendVariableService, AIActivationService } from '@theia/ai-core/lib/browser';
24
+ import { DisposableCollection, Emitter, InMemoryResources, URI, nls } from '@theia/core';
21
25
  import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
22
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
23
27
  import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
24
28
  import * as React from '@theia/core/shared/react';
25
- import { IMouseEvent } from '@theia/monaco-editor-core';
26
- import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
29
+ import { IMouseEvent, Range } from '@theia/monaco-editor-core';
27
30
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
28
- import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
29
- import { AIVariableResolutionRequest } from '@theia/ai-core';
30
- import { FrontendVariableService } from '@theia/ai-core/lib/browser';
31
- import { ContextVariablePicker } from './context-variable-picker';
31
+ import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
32
32
  import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
33
- import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
34
33
  import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
34
+ import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
35
+ import { ContextVariablePicker } from './context-variable-picker';
36
+ import { TASK_CONTEXT_VARIABLE } from '@theia/ai-chat/lib/browser/task-context-variable';
37
+ import { IModelDeltaDecoration } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
35
38
 
36
39
  type Query = (query: string) => Promise<void>;
37
40
  type Unpin = () => void;
@@ -77,6 +80,9 @@ export class AIChatInputWidget extends ReactWidget {
77
80
  @inject(ChangeSetActionService)
78
81
  protected readonly changeSetActionService: ChangeSetActionService;
79
82
 
83
+ @inject(AgentCompletionNotificationService)
84
+ protected readonly agentNotificationService: AgentCompletionNotificationService;
85
+
80
86
  @inject(ChangeSetDecoratorService)
81
87
  protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
82
88
 
@@ -86,12 +92,16 @@ export class AIChatInputWidget extends ReactWidget {
86
92
  @inject(ChatService)
87
93
  protected readonly chatService: ChatService;
88
94
 
95
+ @inject(AIActivationService)
96
+ protected readonly aiActivationService: AIActivationService;
97
+
89
98
  protected editorRef: SimpleMonacoEditor | undefined = undefined;
90
99
  protected readonly editorReady = new Deferred<void>();
91
100
 
92
101
  protected isEnabled = false;
102
+ protected heightInLines = 12;
93
103
 
94
- private _branch?: ChatHierarchyBranch;
104
+ protected _branch?: ChatHierarchyBranch;
95
105
  set branch(branch: ChatHierarchyBranch | undefined) {
96
106
  if (this._branch !== branch) {
97
107
  this._branch = branch;
@@ -99,34 +109,34 @@ export class AIChatInputWidget extends ReactWidget {
99
109
  }
100
110
  }
101
111
 
102
- private _onQuery: Query;
112
+ protected _onQuery: Query;
103
113
  set onQuery(query: Query) {
104
114
  this._onQuery = query;
105
115
  }
106
- private _onUnpin: Unpin;
116
+ protected _onUnpin: Unpin;
107
117
  set onUnpin(unpin: Unpin) {
108
118
  this._onUnpin = unpin;
109
119
  }
110
- private _onCancel: Cancel;
120
+ protected _onCancel: Cancel;
111
121
  set onCancel(cancel: Cancel) {
112
122
  this._onCancel = cancel;
113
123
  }
114
- private _onDeleteChangeSet: DeleteChangeSet;
124
+ protected _onDeleteChangeSet: DeleteChangeSet;
115
125
  set onDeleteChangeSet(deleteChangeSet: DeleteChangeSet) {
116
126
  this._onDeleteChangeSet = deleteChangeSet;
117
127
  }
118
- private _onDeleteChangeSetElement: DeleteChangeSetElement;
128
+ protected _onDeleteChangeSetElement: DeleteChangeSetElement;
119
129
  set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
120
130
  this._onDeleteChangeSetElement = deleteChangeSetElement;
121
131
  }
122
132
 
123
- private _initialValue?: string;
133
+ protected _initialValue?: string;
124
134
  set initialValue(value: string | undefined) {
125
135
  this._initialValue = value;
126
136
  }
127
137
 
128
138
  protected onDisposeForChatModel = new DisposableCollection();
129
- private _chatModel: ChatModel;
139
+ protected _chatModel: ChatModel;
130
140
  set chatModel(chatModel: ChatModel) {
131
141
  this.onDisposeForChatModel.dispose();
132
142
  this.onDisposeForChatModel = new DisposableCollection();
@@ -138,17 +148,25 @@ export class AIChatInputWidget extends ReactWidget {
138
148
  this._chatModel = chatModel;
139
149
  this.update();
140
150
  }
141
- private _pinnedAgent: ChatAgent | undefined;
151
+ protected _pinnedAgent: ChatAgent | undefined;
142
152
  set pinnedAgent(pinnedAgent: ChatAgent | undefined) {
143
153
  this._pinnedAgent = pinnedAgent;
144
154
  this.update();
145
155
  }
146
156
 
157
+ protected onDidResizeEmitter = new Emitter<void>();
158
+ readonly onDidResize = this.onDidResizeEmitter.event;
159
+
147
160
  @postConstruct()
148
161
  protected init(): void {
149
162
  this.id = AIChatInputWidget.ID;
150
163
  this.title.closable = false;
151
164
  this.toDispose.push(this.resources.add(this.getResourceUri(), ''));
165
+ this.toDispose.push(this.aiActivationService.onDidChangeActiveStatus(() => {
166
+ this.setEnabled(this.aiActivationService.isActive);
167
+ }));
168
+ this.toDispose.push(this.onDidResizeEmitter);
169
+ this.setEnabled(this.aiActivationService.isActive);
152
170
  this.update();
153
171
  }
154
172
 
@@ -161,6 +179,18 @@ export class AIChatInputWidget extends ReactWidget {
161
179
  });
162
180
  }
163
181
 
182
+ protected async handleAgentCompletion(request: ChatRequestModel): Promise<void> {
183
+ try {
184
+ const agentId = request.agentId;
185
+
186
+ if (agentId) {
187
+ await this.agentNotificationService.showCompletionNotification(agentId);
188
+ }
189
+ } catch (error) {
190
+ console.error('Failed to handle agent completion notification:', error);
191
+ }
192
+ }
193
+
164
194
  protected getResourceUri(): URI {
165
195
  return new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
166
196
  }
@@ -185,12 +215,15 @@ export class AIChatInputWidget extends ReactWidget {
185
215
  onCancel={this._onCancel.bind(this)}
186
216
  onDragOver={this.onDragOver.bind(this)}
187
217
  onDrop={this.onDrop.bind(this)}
218
+ onPaste={this.onPaste.bind(this)}
219
+ onEscape={this.onEscape.bind(this)}
188
220
  onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
189
221
  onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
190
222
  onAddContextElement={this.addContextElement.bind(this)}
191
223
  onDeleteContextElement={this.deleteContextElement.bind(this)}
192
224
  onOpenContextElement={this.openContextElement.bind(this)}
193
225
  context={this.getContext()}
226
+ onAgentCompletion={this.handleAgentCompletion.bind(this)}
194
227
  chatModel={this._chatModel}
195
228
  pinnedAgent={this._pinnedAgent}
196
229
  editorProvider={this.editorProvider}
@@ -214,11 +247,13 @@ export class AIChatInputWidget extends ReactWidget {
214
247
  currentRequest={currentRequest}
215
248
  isEditing={isEditing}
216
249
  pending={pending}
250
+ heightInLines={this.heightInLines}
217
251
  onResponseChanged={() => {
218
252
  if (isPending() !== pending) {
219
253
  this.update();
220
254
  }
221
255
  }}
256
+ onResize={() => this.onDidResizeEmitter.fire()}
222
257
  />
223
258
  );
224
259
  }
@@ -257,6 +292,30 @@ export class AIChatInputWidget extends ReactWidget {
257
292
  });
258
293
  }
259
294
 
295
+ protected onPaste(event: ClipboardEvent): void {
296
+ this.variableService.getPasteResult(event, { type: 'ai-chat-input-widget' }).then(result => {
297
+ result.variables.forEach(variable => this.addContext(variable));
298
+ if (result.text) {
299
+ const position = this.editorRef?.getControl().getPosition();
300
+ if (position && result.text) {
301
+ this.editorRef?.getControl().executeEdits('paste', [{
302
+ range: {
303
+ startLineNumber: position.lineNumber,
304
+ startColumn: position.column,
305
+ endLineNumber: position.lineNumber,
306
+ endColumn: position.column
307
+ },
308
+ text: result.text
309
+ }]);
310
+ }
311
+ }
312
+ });
313
+ }
314
+
315
+ protected onEscape(): void {
316
+ // No op
317
+ }
318
+
260
319
  protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
261
320
  const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
262
321
  const context = { session };
@@ -306,11 +365,14 @@ interface ChatInputProperties {
306
365
  onUnpin: () => void;
307
366
  onDragOver: (event: React.DragEvent) => void;
308
367
  onDrop: (event: React.DragEvent) => void;
368
+ onPaste: (event: ClipboardEvent) => void;
309
369
  onDeleteChangeSet: (sessionId: string) => void;
310
370
  onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
311
371
  onAddContextElement: () => void;
312
372
  onDeleteContextElement: (index: number) => void;
373
+ onEscape: () => void;
313
374
  onOpenContextElement: OpenContextElement;
375
+ onAgentCompletion: (request: ChatRequestModel) => void;
314
376
  context?: readonly AIVariableResolutionRequest[];
315
377
  isEnabled?: boolean;
316
378
  chatModel: ChatModel;
@@ -332,9 +394,16 @@ interface ChatInputProperties {
332
394
  currentRequest?: ChatRequestModel;
333
395
  isEditing: boolean;
334
396
  pending: boolean;
397
+ heightInLines?: number;
335
398
  onResponseChanged: () => void;
399
+ onResize: () => void;
336
400
  }
337
401
 
402
+ // Utility to check if we have task context in the chat model
403
+ const hasTaskContext = (chatModel: ChatModel): boolean => chatModel.context.getVariables().some(variable =>
404
+ variable.variable?.id === TASK_CONTEXT_VARIABLE.id
405
+ );
406
+
338
407
  const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
339
408
  const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
340
409
  const onDeleteChangeSetElement = (uri: URI) => props.onDeleteChangeSetElement(props.chatModel.id, uri);
@@ -355,13 +424,44 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
355
424
  // eslint-disable-next-line no-null/no-null
356
425
  const placeholderRef = React.useRef<HTMLDivElement | null>(null);
357
426
  const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
427
+ // eslint-disable-next-line no-null/no-null
428
+ const containerRef = React.useRef<HTMLDivElement>(null);
429
+
430
+ // On the first request of the chat, if the chat has a task context and a pinned
431
+ // agent, show a "Perform this task." placeholder which is the message to send by default
432
+ const isFirstRequest = props.chatModel.getRequests().length === 0;
433
+ const shouldUseTaskPlaceholder = isFirstRequest && props.pinnedAgent && hasTaskContext(props.chatModel);
434
+ const taskPlaceholder = nls.localize('theia/ai/chat-ui/performThisTask', 'Perform this task.');
435
+ const placeholderText = !props.isEnabled
436
+ ? nls.localize('theia/ai/chat-ui/aiDisabled', 'AI features are disabled')
437
+ : shouldUseTaskPlaceholder
438
+ ? taskPlaceholder
439
+ : nls.localizeByDefault('Ask a question');
440
+
441
+ // Handle paste events on the container
442
+ const handlePaste = React.useCallback((event: ClipboardEvent) => {
443
+ props.onPaste(event);
444
+ }, [props.onPaste]);
445
+
446
+ // Set up paste handler on the container div
447
+ React.useEffect(() => {
448
+ const container = containerRef.current;
449
+ if (container) {
450
+ container.addEventListener('paste', handlePaste, true);
451
+ return () => {
452
+ container.removeEventListener('paste', handlePaste, true);
453
+ };
454
+ }
455
+ return undefined;
456
+ }, [handlePaste]);
358
457
 
359
458
  React.useEffect(() => {
360
459
  const uri = props.uri;
361
460
  const createInputElement = async () => {
362
461
  const paddingTop = 6;
363
462
  const lineHeight = 20;
364
- const maxHeight = 240;
463
+ const maxHeightPx = (props.heightInLines ?? 12) * lineHeight;
464
+
365
465
  const editor = await props.editorProvider.createSimpleInline(uri, editorContainerRef.current!, {
366
466
  language: CHAT_VIEW_LANGUAGE_EXTENSION,
367
467
  // Disable code lens, inlay hints and hover support to avoid console errors from other contributions
@@ -396,12 +496,17 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
396
496
  if (editorContainerRef.current) {
397
497
  editorContainerRef.current.style.overflowY = 'auto'; // ensure vertical scrollbar
398
498
  editorContainerRef.current.style.height = (lineHeight + (2 * paddingTop)) + 'px';
499
+
500
+ editorContainerRef.current.addEventListener('wheel', e => {
501
+ // Prevent parent from scrolling
502
+ e.stopPropagation();
503
+ }, { passive: false });
399
504
  }
400
505
 
401
506
  const updateEditorHeight = () => {
402
507
  if (editorContainerRef.current) {
403
508
  const contentHeight = editor.getControl().getContentHeight() + paddingTop;
404
- editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeight)}px`;
509
+ editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeightPx)}px`;
405
510
  }
406
511
  };
407
512
 
@@ -411,7 +516,10 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
411
516
  updateEditorHeight();
412
517
  handleOnChange();
413
518
  });
414
- const resizeObserver = new ResizeObserver(updateEditorHeight);
519
+ const resizeObserver = new ResizeObserver(() => {
520
+ updateEditorHeight();
521
+ props.onResize();
522
+ });
415
523
  if (editorContainerRef.current) {
416
524
  resizeObserver.observe(editorContainerRef.current);
417
525
  }
@@ -423,12 +531,46 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
423
531
  props.contextMenuCallback(e.event)
424
532
  );
425
533
 
534
+ const updateLineCounts = () => {
535
+ // We need the line numbers to allow scrolling by using the keyboard
536
+ const model = editor.getControl().getModel()!;
537
+ const lineCount = model.getLineCount();
538
+ const decorations: IModelDeltaDecoration[] = [];
539
+
540
+ for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
541
+ decorations.push({
542
+ range: new Range(lineNumber, 1, lineNumber, 1),
543
+ options: {
544
+ description: `line-number-${lineNumber}`,
545
+ isWholeLine: false,
546
+ className: `line-number-${lineNumber}`,
547
+ }
548
+ });
549
+ }
550
+
551
+ const lineNumbers = model.getAllDecorations().filter(predicate => predicate.options.description?.startsWith('line-number-'));
552
+ editor.getControl().removeDecorations(lineNumbers.map(d => d.id));
553
+ editor.getControl().createDecorationsCollection(decorations);
554
+ };
555
+
556
+ editor.getControl().getModel()?.onDidChangeContent(() => {
557
+ updateLineCounts();
558
+ });
559
+
560
+ editor.getControl().onDidChangeCursorPosition(e => {
561
+ const lineNumber = e.position.lineNumber;
562
+ const line = editor.getControl().getDomNode()?.querySelector(`.line-number-${lineNumber}`);
563
+ line?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
564
+ });
565
+
426
566
  editorRef.current = editor;
427
567
  props.setEditorRef(editor);
428
568
 
429
569
  if (props.initialValue) {
430
570
  setValue(props.initialValue);
431
571
  }
572
+
573
+ updateLineCounts();
432
574
  };
433
575
  createInputElement();
434
576
 
@@ -460,6 +602,15 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
460
602
  onDeleteChangeSetElement
461
603
  ));
462
604
  }
605
+ if (event.kind === 'addRequest') {
606
+ // Listen for when this request's response becomes complete
607
+ const responseListener = event.request.response.onDidChange(() => {
608
+ if (event.request.response.isComplete) {
609
+ props.onAgentCompletion(event.request);
610
+ responseListener.dispose(); // Clean up the listener once notification is sent
611
+ }
612
+ });
613
+ }
463
614
  });
464
615
  return () => {
465
616
  listener.dispose();
@@ -472,7 +623,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
472
623
  setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
473
624
  });
474
625
  return () => disposable.dispose();
475
- });
626
+ }, [props.actionService, props.chatModel.changeSet]);
476
627
 
477
628
  React.useEffect(() => {
478
629
  const disposable = props.decoratorService.onDidChangeDecorations(() => {
@@ -494,13 +645,21 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
494
645
  }
495
646
  }, [editorRef]);
496
647
 
648
+ // Without user input, if we can default to "Perform this task.", do so
497
649
  const submit = React.useCallback(function submit(value: string): void {
498
- if (!value || value.trim().length === 0) {
650
+ let effectiveValue = value;
651
+ if ((!value || value.trim().length === 0) && shouldUseTaskPlaceholder) {
652
+ effectiveValue = taskPlaceholder;
653
+ }
654
+ if (!effectiveValue || effectiveValue.trim().length === 0) {
499
655
  return;
500
656
  }
501
- props.onQuery(value);
657
+ props.onQuery(effectiveValue);
502
658
  setValue('');
503
- }, [props.context, props.onQuery, setValue]);
659
+ if (editorRef.current && !editorRef.current.document.textEditorModel.isDisposed()) {
660
+ editorRef.current.document.textEditorModel.setValue('');
661
+ }
662
+ }, [props.context, props.onQuery, setValue, shouldUseTaskPlaceholder, taskPlaceholder]);
504
663
 
505
664
  const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
506
665
  if (!props.isEnabled) {
@@ -508,7 +667,12 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
508
667
  }
509
668
  if (event.key === 'Enter' && !event.shiftKey) {
510
669
  event.preventDefault();
511
- submit(editorRef.current?.document.textEditorModel.getValue() || '');
670
+ // On Enter, read input and submit (handles task context)
671
+ const currentValue = editorRef.current?.document.textEditorModel.getValue() || '';
672
+ submit(currentValue);
673
+ } else if (event.key === 'Escape') {
674
+ event.preventDefault();
675
+ props.onEscape();
512
676
  }
513
677
  }, [props.isEnabled, submit]);
514
678
 
@@ -559,7 +723,8 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
559
723
  ? [{
560
724
  title: nls.localize('theia/ai/chat-ui/attachToContext', 'Attach elements to context'),
561
725
  handler: () => props.onAddContextElement(),
562
- className: 'codicon-add'
726
+ className: 'codicon-add',
727
+ disabled: !props.isEnabled
563
728
  }]
564
729
  : []),
565
730
  ...(props.showPinnedAgent
@@ -567,6 +732,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
567
732
  title: props.pinnedAgent ? nls.localize('theia/ai/chat-ui/unpinAgent', 'Unpin Agent') : nls.localize('theia/ai/chat-ui/pinAgent', 'Pin Agent'),
568
733
  handler: props.pinnedAgent ? props.onUnpin : handlePin,
569
734
  className: 'at-icon',
735
+ disabled: !props.isEnabled,
570
736
  text: {
571
737
  align: 'right',
572
738
  content: props.pinnedAgent && props.pinnedAgent.name
@@ -593,7 +759,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
593
759
  }
594
760
  },
595
761
  className: 'codicon-send',
596
- disabled: isInputEmpty || !props.isEnabled
762
+ disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
597
763
  }];
598
764
  } else if (pending) {
599
765
  rightOptions = [{
@@ -614,27 +780,29 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
614
780
  }
615
781
  },
616
782
  className: 'codicon-send',
617
- disabled: isInputEmpty || !props.isEnabled
783
+ disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
618
784
  }];
619
785
  }
620
786
 
621
787
  const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
622
788
 
623
- return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
624
- {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
625
- {props.showChangeSet && changeSetUI?.elements &&
626
- <ChangeSetBox changeSet={changeSetUI} />
627
- }
628
- <div className='theia-ChatInput-Editor-Box'>
629
- <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
630
- <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
631
- </div>
632
- {props.context && props.context.length > 0 &&
633
- <ChatContext context={contextUI.context} />
789
+ return (
790
+ <div className='theia-ChatInput' data-ai-disabled={!props.isEnabled} onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
791
+ {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
792
+ {props.showChangeSet && changeSetUI?.elements &&
793
+ <ChangeSetBox changeSet={changeSetUI} />
634
794
  }
635
- <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
795
+ <div className='theia-ChatInput-Editor-Box'>
796
+ <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
797
+ <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{placeholderText}</div>
798
+ </div>
799
+ {props.context && props.context.length > 0 &&
800
+ <ChatContext context={contextUI.context} />
801
+ }
802
+ <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
803
+ </div>
636
804
  </div>
637
- </div>;
805
+ );
638
806
  };
639
807
 
640
808
  const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
@@ -780,7 +948,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
780
948
  {leftOptions.map((option, index) => (
781
949
  <span
782
950
  key={index}
783
- className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
951
+ className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
784
952
  title={option.title}
785
953
  onClick={option.handler}
786
954
  >
@@ -793,7 +961,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
793
961
  {rightOptions.map((option, index) => (
794
962
  <span
795
963
  key={index}
796
- className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
964
+ className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
797
965
  title={option.title}
798
966
  onClick={option.handler}
799
967
  >
@@ -816,6 +984,7 @@ function buildContextUI(
816
984
  }
817
985
  return {
818
986
  context: context.map((element, index) => ({
987
+ variable: element,
819
988
  name: labelProvider.getName(element),
820
989
  iconClass: labelProvider.getIcon(element),
821
990
  nameClass: element.variable.name,
@@ -829,6 +998,7 @@ function buildContextUI(
829
998
 
830
999
  interface ChatContextUI {
831
1000
  context: {
1001
+ variable: AIVariableResolutionRequest,
832
1002
  name: string;
833
1003
  iconClass: string;
834
1004
  nameClass: string;
@@ -842,20 +1012,45 @@ interface ChatContextUI {
842
1012
  const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
843
1013
  <div className="theia-ChatInput-ChatContext">
844
1014
  <ul>
845
- {context.map((element, index) => (
846
- <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
847
- <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
848
- <div className="theia-ChatInput-ChatContext-labelParts">
849
- <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
850
- {element.name}
851
- </span>
852
- <span className='theia-ChatInput-ChatContext-additionalInfo'>
853
- {element.additionalInfo}
854
- </span>
1015
+ {context.map((element, index) => {
1016
+ if (ImageContextVariable.isImageContextRequest(element.variable)) {
1017
+ const variable = ImageContextVariable.parseRequest(element.variable)!;
1018
+ return <li key={index} className="theia-ChatInput-ChatContext-Element theia-ChatInput-ImageContext-Element"
1019
+ title={variable.name ?? variable.wsRelativePath} onClick={() => element.open?.()}>
1020
+ <div className="theia-ChatInput-ChatContext-Row">
1021
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
1022
+ <div className="theia-ChatInput-ChatContext-labelParts">
1023
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
1024
+ {variable.name ?? variable.wsRelativePath?.split('/').pop()}
1025
+ </span>
1026
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
1027
+ {element.additionalInfo}
1028
+ </span>
1029
+ </div>
1030
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
1031
+ </div>
1032
+ <div className="theia-ChatInput-ChatContext-ImageRow">
1033
+ <div className='theia-ChatInput-ImagePreview-Item'>
1034
+ <img src={`data:${variable.mimeType};base64,${variable.data}`} alt={variable.name} />
1035
+ </div>
1036
+ </div>
1037
+ </li>;
1038
+ }
1039
+ return <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
1040
+ <div className="theia-ChatInput-ChatContext-Row">
1041
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
1042
+ <div className="theia-ChatInput-ChatContext-labelParts">
1043
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
1044
+ {element.name}
1045
+ </span>
1046
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
1047
+ {element.additionalInfo}
1048
+ </span>
1049
+ </div>
1050
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
855
1051
  </div>
856
- <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
857
- </li>
858
- ))}
1052
+ </li>;
1053
+ })}
859
1054
  </ul>
860
1055
  </div>
861
1056
  );
@@ -78,6 +78,11 @@ export namespace ChatNodeToolbarCommands {
78
78
  id: 'chat:node:toolbar:cancel-request',
79
79
  category: CHAT_NODE_TOOLBAR_CATEGORY,
80
80
  }, '', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
81
+
82
+ export const RETRY = Command.toLocalizedCommand({
83
+ id: 'chat:node:toolbar:retry-message',
84
+ category: CHAT_NODE_TOOLBAR_CATEGORY,
85
+ }, 'Retry', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
81
86
  }
82
87
 
83
88
  export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbarActionContribution {
@@ -96,6 +101,15 @@ export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbar
96
101
  tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/edit', 'Edit'),
97
102
  }];
98
103
  } else {
104
+ const shouldShowRetry = node.response.isError || node.response.isCanceled;
105
+ if (shouldShowRetry) {
106
+ return [{
107
+ commandId: ChatNodeToolbarCommands.RETRY.id,
108
+ icon: codicon('refresh'),
109
+ tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/retry', 'Retry'),
110
+ priority: -1 // Higher priority to show it first
111
+ }];
112
+ }
99
113
  return [];
100
114
  }
101
115
  }