@theia/ai-chat-ui 1.63.0-next.0 → 1.63.0-next.52

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 (69) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +29 -1
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +158 -2
  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 +9 -6
  9. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  10. package/lib/browser/chat-input-widget.js +181 -111
  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 +2 -0
  21. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  22. package/lib/browser/chat-response-renderer/index.js +2 -0
  23. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  24. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +17 -0
  25. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -0
  26. package/lib/browser/chat-response-renderer/tool-confirmation.js +120 -0
  27. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -0
  28. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -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 +83 -19
  31. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  32. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
  33. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
  34. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +9 -0
  35. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  36. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +8 -0
  37. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  38. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +72 -3
  39. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  40. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
  41. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
  42. package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
  43. package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
  44. package/lib/browser/chat-view-commands.d.ts +1 -0
  45. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  46. package/lib/browser/chat-view-commands.js +5 -0
  47. package/lib/browser/chat-view-commands.js.map +1 -1
  48. package/lib/browser/chat-view-contribution.js +2 -1
  49. package/lib/browser/chat-view-contribution.js.map +1 -1
  50. package/lib/browser/chat-view-widget.d.ts +6 -3
  51. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  52. package/lib/browser/chat-view-widget.js +20 -10
  53. package/lib/browser/chat-view-widget.js.map +1 -1
  54. package/package.json +10 -10
  55. package/src/browser/ai-chat-ui-contribution.ts +166 -5
  56. package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
  57. package/src/browser/chat-input-widget.tsx +280 -170
  58. package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
  59. package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
  60. package/src/browser/chat-response-renderer/index.ts +2 -0
  61. package/src/browser/chat-response-renderer/tool-confirmation.tsx +173 -0
  62. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +115 -19
  63. package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +16 -1
  64. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +89 -5
  65. package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
  66. package/src/browser/chat-view-commands.ts +6 -0
  67. package/src/browser/chat-view-contribution.ts +1 -1
  68. package/src/browser/chat-view-widget.tsx +25 -12
  69. package/src/browser/style/index.css +350 -2
@@ -13,22 +13,26 @@
13
13
  //
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
- import { ChangeSet, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel, ChatService, ChatSuggestion } from '@theia/ai-chat';
17
- import { Disposable, DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
16
+ import {
17
+ ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatHierarchyBranch,
18
+ ChatModel, ChatRequestModel, ChatService, ChatSuggestion, EditableChatRequestModel
19
+ } from '@theia/ai-chat';
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 { FrontendVariableService } from '@theia/ai-core/lib/browser';
24
+ import { DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
18
25
  import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
19
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
20
27
  import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
21
28
  import * as React from '@theia/core/shared/react';
22
29
  import { IMouseEvent } from '@theia/monaco-editor-core';
23
- import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
24
30
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
25
- import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
26
- import { AIVariableResolutionRequest } from '@theia/ai-core';
27
- import { FrontendVariableService } from '@theia/ai-core/lib/browser';
28
- import { ContextVariablePicker } from './context-variable-picker';
31
+ import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
29
32
  import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
30
- import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
31
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';
32
36
 
33
37
  type Query = (query: string) => Promise<void>;
34
38
  type Unpin = () => void;
@@ -88,6 +92,14 @@ export class AIChatInputWidget extends ReactWidget {
88
92
 
89
93
  protected isEnabled = false;
90
94
 
95
+ private _branch?: ChatHierarchyBranch;
96
+ set branch(branch: ChatHierarchyBranch | undefined) {
97
+ if (this._branch !== branch) {
98
+ this._branch = branch;
99
+ this.update();
100
+ }
101
+ }
102
+
91
103
  private _onQuery: Query;
92
104
  set onQuery(query: Query) {
93
105
  this._onQuery = query;
@@ -120,7 +132,7 @@ export class AIChatInputWidget extends ReactWidget {
120
132
  this.onDisposeForChatModel.dispose();
121
133
  this.onDisposeForChatModel = new DisposableCollection();
122
134
  this.onDisposeForChatModel.push(chatModel.onDidChange(event => {
123
- if (event.kind === 'addVariable' || event.kind === 'removeVariable') {
135
+ if (event.kind === 'addVariable' || event.kind === 'removeVariable' || event.kind === 'addRequest' || event.kind === 'changeHierarchyBranch') {
124
136
  this.update();
125
137
  }
126
138
  }));
@@ -137,6 +149,7 @@ export class AIChatInputWidget extends ReactWidget {
137
149
  protected init(): void {
138
150
  this.id = AIChatInputWidget.ID;
139
151
  this.title.closable = false;
152
+ this.toDispose.push(this.resources.add(this.getResourceUri(), ''));
140
153
  this.update();
141
154
  }
142
155
 
@@ -154,24 +167,36 @@ export class AIChatInputWidget extends ReactWidget {
154
167
  }
155
168
 
156
169
  protected render(): React.ReactNode {
170
+ const branch = this._branch;
171
+ const chatModel = this._chatModel;
172
+
173
+ // State of the input widget's action buttons depends on the state of the currently active or last processed
174
+ // request, if there is one. If the chat model has branched, then the current request is the last on the
175
+ // branch. Otherwise, it's the last request in the chat model.
176
+ const currentRequest: ChatRequestModel | undefined = branch?.items?.at(-1)?.element ?? chatModel.getRequests().at(-1);
177
+ const isEditing = !!(currentRequest && (EditableChatRequestModel.isEditing(currentRequest)));
178
+ const isPending = () => !!(currentRequest && !isEditing && ChatRequestModel.isInProgress(currentRequest));
179
+ const pending = isPending();
180
+
157
181
  return (
158
182
  <ChatInput
183
+ branch={this._branch}
159
184
  onQuery={this._onQuery.bind(this)}
160
185
  onUnpin={this._onUnpin.bind(this)}
161
186
  onCancel={this._onCancel.bind(this)}
162
187
  onDragOver={this.onDragOver.bind(this)}
163
188
  onDrop={this.onDrop.bind(this)}
189
+ onPaste={this.onPaste.bind(this)}
164
190
  onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
165
191
  onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
166
192
  onAddContextElement={this.addContextElement.bind(this)}
167
193
  onDeleteContextElement={this.deleteContextElement.bind(this)}
168
- context={this.getContext()}
169
194
  onOpenContextElement={this.openContextElement.bind(this)}
195
+ context={this.getContext()}
170
196
  chatModel={this._chatModel}
171
197
  pinnedAgent={this._pinnedAgent}
172
198
  editorProvider={this.editorProvider}
173
- resources={this.resources}
174
- resourceUriProvider={this.getResourceUri.bind(this)}
199
+ uri={this.getResourceUri()}
175
200
  contextMenuCallback={this.handleContextMenu.bind(this)}
176
201
  isEnabled={this.isEnabled}
177
202
  setEditorRef={editor => {
@@ -188,6 +213,14 @@ export class AIChatInputWidget extends ReactWidget {
188
213
  initialValue={this._initialValue}
189
214
  openerService={this.openerService}
190
215
  suggestions={this._chatModel.suggestions}
216
+ currentRequest={currentRequest}
217
+ isEditing={isEditing}
218
+ pending={pending}
219
+ onResponseChanged={() => {
220
+ if (isPending() !== pending) {
221
+ this.update();
222
+ }
223
+ }}
191
224
  />
192
225
  );
193
226
  }
@@ -226,6 +259,26 @@ export class AIChatInputWidget extends ReactWidget {
226
259
  });
227
260
  }
228
261
 
262
+ protected onPaste(event: ClipboardEvent): void {
263
+ this.variableService.getPasteResult(event, { type: 'ai-chat-input-widget' }).then(result => {
264
+ result.variables.forEach(variable => this.addContext(variable));
265
+ if (result.text) {
266
+ const position = this.editorRef?.getControl().getPosition();
267
+ if (position && result.text) {
268
+ this.editorRef?.getControl().executeEdits('paste', [{
269
+ range: {
270
+ startLineNumber: position.lineNumber,
271
+ startColumn: position.column,
272
+ endLineNumber: position.lineNumber,
273
+ endColumn: position.column
274
+ },
275
+ text: result.text
276
+ }]);
277
+ }
278
+ }
279
+ });
280
+ }
281
+
229
282
  protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
230
283
  const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
231
284
  const context = { session };
@@ -269,13 +322,15 @@ export class AIChatInputWidget extends ReactWidget {
269
322
  }
270
323
 
271
324
  interface ChatInputProperties {
325
+ branch?: ChatHierarchyBranch;
272
326
  onCancel: (requestModel: ChatRequestModel) => void;
273
327
  onQuery: (query: string) => void;
274
328
  onUnpin: () => void;
275
329
  onDragOver: (event: React.DragEvent) => void;
276
330
  onDrop: (event: React.DragEvent) => void;
331
+ onPaste: (event: ClipboardEvent) => void;
277
332
  onDeleteChangeSet: (sessionId: string) => void;
278
- onDeleteChangeSetElement: (sessionId: string, index: number) => void;
333
+ onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
279
334
  onAddContextElement: () => void;
280
335
  onDeleteContextElement: (index: number) => void;
281
336
  onOpenContextElement: OpenContextElement;
@@ -284,8 +339,7 @@ interface ChatInputProperties {
284
339
  chatModel: ChatModel;
285
340
  pinnedAgent?: ChatAgent;
286
341
  editorProvider: MonacoEditorProvider;
287
- resources: InMemoryResources;
288
- resourceUriProvider: () => URI;
342
+ uri: URI;
289
343
  contextMenuCallback: (event: IMouseEvent) => void;
290
344
  setEditorRef: (editor: SimpleMonacoEditor | undefined) => void;
291
345
  showContext?: boolean;
@@ -297,37 +351,55 @@ interface ChatInputProperties {
297
351
  decoratorService: ChangeSetDecoratorService;
298
352
  initialValue?: string;
299
353
  openerService: OpenerService;
300
- suggestions: readonly ChatSuggestion[]
354
+ suggestions: readonly ChatSuggestion[];
355
+ currentRequest?: ChatRequestModel;
356
+ isEditing: boolean;
357
+ pending: boolean;
358
+ onResponseChanged: () => void;
301
359
  }
302
360
 
303
361
  const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
304
362
  const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
305
- const onDeleteChangeSetElement = (index: number) => props.onDeleteChangeSetElement(props.chatModel.id, index);
363
+ const onDeleteChangeSetElement = (uri: URI) => props.onDeleteChangeSetElement(props.chatModel.id, uri);
306
364
 
307
- const [inProgress, setInProgress] = React.useState(false);
308
365
  const [isInputEmpty, setIsInputEmpty] = React.useState(true);
309
366
  const [changeSetUI, setChangeSetUI] = React.useState(
310
- () => props.chatModel.changeSet
311
- ? buildChangeSetUI(
312
- props.chatModel.changeSet,
313
- props.labelProvider,
314
- props.decoratorService,
315
- props.actionService.getActionsForChangeset(props.chatModel.changeSet),
316
- onDeleteChangeSet,
317
- onDeleteChangeSetElement
318
- )
319
- : undefined
320
- );
367
+ () => buildChangeSetUI(
368
+ props.chatModel.changeSet,
369
+ props.labelProvider,
370
+ props.decoratorService,
371
+ props.actionService.getActionsForChangeset(props.chatModel.changeSet),
372
+ onDeleteChangeSet,
373
+ onDeleteChangeSetElement
374
+ ));
321
375
 
322
376
  // eslint-disable-next-line no-null/no-null
323
377
  const editorContainerRef = React.useRef<HTMLDivElement | null>(null);
324
378
  // eslint-disable-next-line no-null/no-null
325
379
  const placeholderRef = React.useRef<HTMLDivElement | null>(null);
326
380
  const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
381
+ // eslint-disable-next-line no-null/no-null
382
+ const containerRef = React.useRef<HTMLDivElement>(null);
383
+
384
+ // Handle paste events on the container
385
+ const handlePaste = React.useCallback((event: ClipboardEvent) => {
386
+ props.onPaste(event);
387
+ }, [props.onPaste]);
388
+
389
+ // Set up paste handler on the container div
390
+ React.useEffect(() => {
391
+ const container = containerRef.current;
392
+ if (container) {
393
+ container.addEventListener('paste', handlePaste, true);
394
+ return () => {
395
+ container.removeEventListener('paste', handlePaste, true);
396
+ };
397
+ }
398
+ return undefined;
399
+ }, [handlePaste]);
327
400
 
328
401
  React.useEffect(() => {
329
- const uri = props.resourceUriProvider();
330
- const resource = props.resources.add(uri, '');
402
+ const uri = props.uri;
331
403
  const createInputElement = async () => {
332
404
  const paddingTop = 6;
333
405
  const lineHeight = 20;
@@ -403,7 +475,6 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
403
475
  createInputElement();
404
476
 
405
477
  return () => {
406
- resource.dispose();
407
478
  props.setEditorRef(undefined);
408
479
  if (editorRef.current) {
409
480
  editorRef.current.dispose();
@@ -411,66 +482,42 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
411
482
  };
412
483
  }, []);
413
484
 
414
- const responseListenerRef = React.useRef<Disposable>();
415
- // track chat model updates to keep our UI in sync
416
- // - keep "inProgress" in sync with the request state
417
- // - keep "changeSetUI" in sync with the change set
418
485
  React.useEffect(() => {
486
+ setChangeSetUI(buildChangeSetUI(
487
+ props.chatModel.changeSet,
488
+ props.labelProvider,
489
+ props.decoratorService,
490
+ props.actionService.getActionsForChangeset(props.chatModel.changeSet),
491
+ onDeleteChangeSet,
492
+ onDeleteChangeSetElement
493
+ ));
419
494
  const listener = props.chatModel.onDidChange(event => {
420
- if (event.kind === 'addRequest') {
421
- if (event.request) {
422
- setInProgress(ChatRequestModel.isInProgress(event.request));
423
- }
424
- responseListenerRef.current?.dispose();
425
- responseListenerRef.current = event.request.response.onDidChange(() =>
426
- setInProgress(ChatRequestModel.isInProgress(event.request))
427
- );
428
- } else if (ChatChangeEvent.isChangeSetEvent(event)) {
429
- if (event.kind === 'removeChangeSet') {
430
- setChangeSetUI(undefined);
431
- } else if (event.kind === 'setChangeSet' || 'updateChangeSet') {
432
- setChangeSetUI(buildChangeSetUI(
433
- event.changeSet,
434
- props.labelProvider,
435
- props.decoratorService,
436
- props.actionService.getActionsForChangeset(event.changeSet),
437
- onDeleteChangeSet,
438
- onDeleteChangeSetElement
439
- ));
440
- }
495
+ if (ChatChangeEvent.isChangeSetEvent(event)) {
496
+ setChangeSetUI(buildChangeSetUI(
497
+ props.chatModel.changeSet,
498
+ props.labelProvider,
499
+ props.decoratorService,
500
+ props.actionService.getActionsForChangeset(props.chatModel.changeSet),
501
+ onDeleteChangeSet,
502
+ onDeleteChangeSetElement
503
+ ));
441
504
  }
442
505
  });
443
- setChangeSetUI(props.chatModel.changeSet
444
- ? buildChangeSetUI(
445
- props.chatModel.changeSet,
446
- props.labelProvider,
447
- props.decoratorService,
448
- props.actionService.getActionsForChangeset(props.chatModel.changeSet),
449
- onDeleteChangeSet,
450
- onDeleteChangeSetElement
451
- )
452
- : undefined);
453
506
  return () => {
454
- listener?.dispose();
455
- responseListenerRef.current?.dispose();
456
- responseListenerRef.current = undefined;
507
+ listener.dispose();
457
508
  };
458
- }, [props.chatModel]);
509
+ }, [props.chatModel, props.labelProvider, props.decoratorService, props.actionService]);
459
510
 
460
511
  React.useEffect(() => {
461
512
  const disposable = props.actionService.onDidChange(() => {
462
- if (!props.chatModel.changeSet) { return; }
463
513
  const newActions = props.actionService.getActionsForChangeset(props.chatModel.changeSet);
464
514
  setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
465
515
  });
466
516
  return () => disposable.dispose();
467
- });
517
+ }, [props.actionService, props.chatModel.changeSet]);
468
518
 
469
519
  React.useEffect(() => {
470
520
  const disposable = props.decoratorService.onDidChangeDecorations(() => {
471
- if (!props.chatModel.changeSet) {
472
- return;
473
- }
474
521
  setChangeSetUI(buildChangeSetUI(
475
522
  props.chatModel.changeSet,
476
523
  props.labelProvider,
@@ -493,9 +540,13 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
493
540
  if (!value || value.trim().length === 0) {
494
541
  return;
495
542
  }
496
- setInProgress(true);
543
+
497
544
  props.onQuery(value);
498
545
  setValue('');
546
+
547
+ if (editorRef.current) {
548
+ editorRef.current.document.textEditorModel.setValue('');
549
+ }
499
550
  }, [props.context, props.onQuery, setValue]);
500
551
 
501
552
  const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
@@ -571,19 +622,38 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
571
622
  : []),
572
623
  ] as Option[];
573
624
 
574
- const rightOptions = inProgress
575
- ? [{
625
+ let rightOptions: Option[] = [];
626
+ const { currentRequest: latestRequest, isEditing, pending, onResponseChanged } = props;
627
+ React.useEffect(() => {
628
+ if (!latestRequest) {
629
+ return;
630
+ }
631
+ const disposable = latestRequest.response.onDidChange(onResponseChanged);
632
+ return () => disposable.dispose();
633
+ }, [latestRequest, onResponseChanged]);
634
+ if (isEditing) {
635
+ rightOptions = [{
636
+ title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
637
+ handler: () => {
638
+ if (props.isEnabled) {
639
+ submit(editorRef.current?.document.textEditorModel.getValue() || '');
640
+ }
641
+ },
642
+ className: 'codicon-send',
643
+ disabled: isInputEmpty || !props.isEnabled
644
+ }];
645
+ } else if (pending) {
646
+ rightOptions = [{
576
647
  title: nls.localize('theia/ai/chat-ui/cancel', 'Cancel (Esc)'),
577
648
  handler: () => {
578
- const latestRequest = getLatestRequest(props.chatModel);
579
649
  if (latestRequest) {
580
650
  props.onCancel(latestRequest);
581
651
  }
582
- setInProgress(false);
583
652
  },
584
653
  className: 'codicon-stop-circle'
585
- }]
586
- : [{
654
+ }];
655
+ } else {
656
+ rightOptions = [{
587
657
  title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
588
658
  handler: () => {
589
659
  if (props.isEnabled) {
@@ -593,24 +663,27 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
593
663
  className: 'codicon-send',
594
664
  disabled: isInputEmpty || !props.isEnabled
595
665
  }];
666
+ }
596
667
 
597
668
  const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
598
669
 
599
- return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
600
- {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
601
- {props.showChangeSet && changeSetUI?.elements &&
602
- <ChangeSetBox changeSet={changeSetUI} />
603
- }
604
- <div className='theia-ChatInput-Editor-Box'>
605
- <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
606
- <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
607
- </div>
608
- {props.context && props.context.length > 0 &&
609
- <ChatContext context={contextUI.context} />
670
+ return (
671
+ <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
672
+ {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
673
+ {props.showChangeSet && changeSetUI?.elements &&
674
+ <ChangeSetBox changeSet={changeSetUI} />
610
675
  }
611
- <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
676
+ <div className='theia-ChatInput-Editor-Box'>
677
+ <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
678
+ <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
679
+ </div>
680
+ {props.context && props.context.length > 0 &&
681
+ <ChatContext context={contextUI.context} />
682
+ }
683
+ <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
684
+ </div>
612
685
  </div>
613
- </div>;
686
+ );
614
687
  };
615
688
 
616
689
  const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
@@ -624,28 +697,21 @@ const buildChangeSetUI = (
624
697
  decoratorService: ChangeSetDecoratorService,
625
698
  actions: ChangeSetActionRenderer[],
626
699
  onDeleteChangeSet: () => void,
627
- onDeleteChangeSetElement: (index: number) => void
628
- ): ChangeSetUI => ({
629
- title: changeSet.title,
630
- changeSet,
631
- deleteChangeSet: onDeleteChangeSet,
632
- elements: changeSet.getElements().map(element => ({
633
- open: element.open?.bind(element),
634
- iconClass: element.icon ?? labelProvider.getIcon(element.uri) ?? labelProvider.fileIcon,
635
- nameClass: `${element.type} ${element.state}`,
636
- name: element.name ?? labelProvider.getName(element.uri),
637
- additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
638
- additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
639
- openChange: element?.openChange?.bind(element),
640
- apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
641
- revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
642
- delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
643
- } satisfies ChangeSetUIElement)),
644
- actions
645
- });
700
+ onDeleteChangeSetElement: (uri: URI) => void
701
+ ): ChangeSetUI | undefined => {
702
+ const elements = changeSet.getElements();
703
+ return elements.length ? ({
704
+ title: changeSet.title,
705
+ changeSet,
706
+ deleteChangeSet: onDeleteChangeSet,
707
+ elements: changeSet.getElements().map(element => toUiElement(element, onDeleteChangeSetElement, labelProvider, decoratorService)),
708
+ actions
709
+ }) : undefined;
710
+ };
646
711
 
647
712
  interface ChangeSetUIElement {
648
713
  name: string;
714
+ uri: string;
649
715
  iconClass: string;
650
716
  nameClass: string;
651
717
  additionalInfo: string;
@@ -677,48 +743,70 @@ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.
677
743
  </div>
678
744
  <div className='theia-ChatInput-ChangeSet-List'>
679
745
  <ul>
680
- {elements.map((element, index) => (
681
- <li key={index} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
682
- <div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
683
- </div>
684
- <div className='theia-ChatInput-ChangeSet-labelParts'>
685
- <span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
686
- {element.name}
687
- </span>
688
- <div className='theia-ChatInput-ChangeSet-additionalInfo'>
689
- {element.additionalInfo && <span>{element.additionalInfo}</span>}
690
- {element.additionalInfoSuffixIcon
691
- && <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
692
- </div>
693
- </div>
694
- <div className='theia-ChatInput-ChangeSet-Actions'>
695
- {element.open && (
696
- <span
697
- className='codicon codicon-file action'
698
- title={nls.localize('theia/ai/chat-ui/openOriginalFile', 'Open Original File')}
699
- onClick={noPropagation(() => element.open!())}
700
- />)}
701
- {element.revert && (
702
- <span
703
- className='codicon codicon-discard action'
704
- title={nls.localizeByDefault('Revert')}
705
- onClick={noPropagation(() => element.revert!())}
706
- />)}
707
- {element.apply && (
708
- <span
709
- className='codicon codicon-check action'
710
- title={nls.localizeByDefault('Apply')}
711
- onClick={noPropagation(() => element.apply!())}
712
- />)}
713
- <span className='codicon codicon-close action' title={nls.localizeByDefault('Delete')} onClick={noPropagation(() => element.delete())} />
714
- </div>
715
- </li>
716
- ))}
746
+ {elements.map(element => ChangeSetElement(element))}
717
747
  </ul>
718
748
  </div>
719
749
  </div>
720
750
  ));
721
751
 
752
+ function toUiElement(element: ChangeSetElement,
753
+ onDeleteChangeSetElement: (uri: URI) => void,
754
+ labelProvider: LabelProvider,
755
+ decoratorService: ChangeSetDecoratorService
756
+ ): ChangeSetUIElement {
757
+ return ({
758
+ open: element.open?.bind(element),
759
+ uri: element.uri.toString(),
760
+ iconClass: element.icon ?? labelProvider.getIcon(element.uri) ?? labelProvider.fileIcon,
761
+ nameClass: `${element.type} ${element.state}`,
762
+ name: element.name ?? labelProvider.getName(element.uri),
763
+ additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
764
+ additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
765
+ openChange: element?.openChange?.bind(element),
766
+ apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
767
+ revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
768
+ delete: () => onDeleteChangeSetElement(element.uri)
769
+ } satisfies ChangeSetUIElement);
770
+ }
771
+
772
+ const ChangeSetElement: React.FC<ChangeSetUIElement> = element => (
773
+ <li key={element.uri} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
774
+ <div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
775
+ </div>
776
+ <div className='theia-ChatInput-ChangeSet-labelParts'>
777
+ <span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
778
+ {element.name}
779
+ </span>
780
+ <div className='theia-ChatInput-ChangeSet-additionalInfo'>
781
+ {element.additionalInfo && <span>{element.additionalInfo}</span>}
782
+ {element.additionalInfoSuffixIcon
783
+ && <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
784
+ </div>
785
+ </div>
786
+ <div className='theia-ChatInput-ChangeSet-Actions'>
787
+ {element.open && (
788
+ <span
789
+ className='codicon codicon-file action'
790
+ title={nls.localize('theia/ai/chat-ui/openOriginalFile', 'Open Original File')}
791
+ onClick={noPropagation(() => element.open!())}
792
+ />)}
793
+ {element.revert && (
794
+ <span
795
+ className='codicon codicon-discard action'
796
+ title={nls.localizeByDefault('Revert')}
797
+ onClick={noPropagation(() => element.revert!())}
798
+ />)}
799
+ {element.apply && (
800
+ <span
801
+ className='codicon codicon-check action'
802
+ title={nls.localizeByDefault('Apply')}
803
+ onClick={noPropagation(() => element.apply!())}
804
+ />)}
805
+ <span className='codicon codicon-close action' title={nls.localizeByDefault('Delete')} onClick={noPropagation(() => element.delete())} />
806
+ </div>
807
+ </li>
808
+ );
809
+
722
810
  interface ChatInputOptionsProps {
723
811
  leftOptions: Option[];
724
812
  rightOptions: Option[];
@@ -766,11 +854,6 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
766
854
  </div>
767
855
  );
768
856
 
769
- function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
770
- const requests = chatModel.getRequests();
771
- return requests.length > 0 ? requests[requests.length - 1] : undefined;
772
- }
773
-
774
857
  function buildContextUI(
775
858
  context: readonly AIVariableResolutionRequest[] | undefined,
776
859
  labelProvider: LabelProvider,
@@ -782,6 +865,7 @@ function buildContextUI(
782
865
  }
783
866
  return {
784
867
  context: context.map((element, index) => ({
868
+ variable: element,
785
869
  name: labelProvider.getName(element),
786
870
  iconClass: labelProvider.getIcon(element),
787
871
  nameClass: element.variable.name,
@@ -795,6 +879,7 @@ function buildContextUI(
795
879
 
796
880
  interface ChatContextUI {
797
881
  context: {
882
+ variable: AIVariableResolutionRequest,
798
883
  name: string;
799
884
  iconClass: string;
800
885
  nameClass: string;
@@ -808,20 +893,45 @@ interface ChatContextUI {
808
893
  const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
809
894
  <div className="theia-ChatInput-ChatContext">
810
895
  <ul>
811
- {context.map((element, index) => (
812
- <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
813
- <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
814
- <div className="theia-ChatInput-ChatContext-labelParts">
815
- <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
816
- {element.name}
817
- </span>
818
- <span className='theia-ChatInput-ChatContext-additionalInfo'>
819
- {element.additionalInfo}
820
- </span>
896
+ {context.map((element, index) => {
897
+ if (ImageContextVariable.isImageContextRequest(element.variable)) {
898
+ const variable = ImageContextVariable.parseRequest(element.variable)!;
899
+ return <li key={index} className="theia-ChatInput-ChatContext-Element theia-ChatInput-ImageContext-Element"
900
+ title={variable.name ?? variable.wsRelativePath} onClick={() => element.open?.()}>
901
+ <div className="theia-ChatInput-ChatContext-Row">
902
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
903
+ <div className="theia-ChatInput-ChatContext-labelParts">
904
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
905
+ {variable.name ?? variable.wsRelativePath?.split('/').pop()}
906
+ </span>
907
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
908
+ {element.additionalInfo}
909
+ </span>
910
+ </div>
911
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
912
+ </div>
913
+ <div className="theia-ChatInput-ChatContext-ImageRow">
914
+ <div className='theia-ChatInput-ImagePreview-Item'>
915
+ <img src={`data:${variable.mimeType};base64,${variable.data}`} alt={variable.name} />
916
+ </div>
917
+ </div>
918
+ </li>;
919
+ }
920
+ return <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
921
+ <div className="theia-ChatInput-ChatContext-Row">
922
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
923
+ <div className="theia-ChatInput-ChatContext-labelParts">
924
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
925
+ {element.name}
926
+ </span>
927
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
928
+ {element.additionalInfo}
929
+ </span>
930
+ </div>
931
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
821
932
  </div>
822
- <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
823
- </li>
824
- ))}
933
+ </li>;
934
+ })}
825
935
  </ul>
826
936
  </div>
827
937
  );