@theia/ai-chat-ui 1.58.3 → 1.59.0-next.72

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 (75) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-ui-contribution.js +7 -7
  3. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  4. package/lib/browser/ai-chat-ui-frontend-module.d.ts +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 +17 -13
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/change-set-actions/change-set-accept-action.d.ts +10 -0
  9. package/lib/browser/change-set-actions/change-set-accept-action.d.ts.map +1 -0
  10. package/lib/browser/change-set-actions/change-set-accept-action.js +47 -0
  11. package/lib/browser/change-set-actions/change-set-accept-action.js.map +1 -0
  12. package/lib/browser/change-set-actions/change-set-action-service.d.ts +31 -0
  13. package/lib/browser/change-set-actions/change-set-action-service.d.ts.map +1 -0
  14. package/lib/browser/change-set-actions/change-set-action-service.js +57 -0
  15. package/lib/browser/change-set-actions/change-set-action-service.js.map +1 -0
  16. package/lib/browser/chat-input-widget.d.ts +22 -3
  17. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  18. package/lib/browser/chat-input-widget.js +209 -56
  19. package/lib/browser/chat-input-widget.js.map +1 -1
  20. package/lib/browser/chat-response-renderer/ai-selection-resolver.d.ts +23 -0
  21. package/lib/browser/chat-response-renderer/ai-selection-resolver.d.ts.map +1 -0
  22. package/lib/browser/chat-response-renderer/{ai-editor-manager.js → ai-selection-resolver.js} +2 -38
  23. package/lib/browser/chat-response-renderer/ai-selection-resolver.js.map +1 -0
  24. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
  25. package/lib/browser/chat-response-renderer/code-part-renderer.js +4 -3
  26. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  27. package/lib/browser/chat-response-renderer/index.d.ts +1 -1
  28. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  29. package/lib/browser/chat-response-renderer/index.js +1 -1
  30. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  31. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts.map +1 -1
  32. package/lib/browser/chat-response-renderer/text-part-renderer.js +3 -1
  33. package/lib/browser/chat-response-renderer/text-part-renderer.js.map +1 -1
  34. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  35. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +7 -3
  36. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  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 +25 -16
  39. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  40. package/lib/browser/chat-view-contribution.d.ts +2 -2
  41. package/lib/browser/chat-view-contribution.d.ts.map +1 -1
  42. package/lib/browser/chat-view-contribution.js +6 -6
  43. package/lib/browser/chat-view-contribution.js.map +1 -1
  44. package/lib/browser/chat-view-language-contribution.d.ts +10 -5
  45. package/lib/browser/chat-view-language-contribution.d.ts.map +1 -1
  46. package/lib/browser/chat-view-language-contribution.js +94 -14
  47. package/lib/browser/chat-view-language-contribution.js.map +1 -1
  48. package/lib/browser/chat-view-widget.d.ts +3 -0
  49. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  50. package/lib/browser/chat-view-widget.js +15 -5
  51. package/lib/browser/chat-view-widget.js.map +1 -1
  52. package/lib/browser/context-variable-picker.d.ts +9 -0
  53. package/lib/browser/context-variable-picker.d.ts.map +1 -0
  54. package/lib/browser/context-variable-picker.js +86 -0
  55. package/lib/browser/context-variable-picker.js.map +1 -0
  56. package/package.json +11 -11
  57. package/src/browser/ai-chat-ui-contribution.ts +8 -8
  58. package/src/browser/ai-chat-ui-frontend-module.ts +18 -16
  59. package/src/browser/change-set-actions/change-set-accept-action.tsx +52 -0
  60. package/src/browser/change-set-actions/change-set-action-service.ts +65 -0
  61. package/src/browser/chat-input-widget.tsx +307 -75
  62. package/src/browser/chat-response-renderer/{ai-editor-manager.ts → ai-selection-resolver.ts} +6 -45
  63. package/src/browser/chat-response-renderer/code-part-renderer.tsx +4 -3
  64. package/src/browser/chat-response-renderer/index.ts +1 -1
  65. package/src/browser/chat-response-renderer/text-part-renderer.tsx +4 -1
  66. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +9 -3
  67. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +26 -16
  68. package/src/browser/chat-view-contribution.ts +6 -6
  69. package/src/browser/chat-view-language-contribution.ts +103 -19
  70. package/src/browser/chat-view-widget.tsx +19 -6
  71. package/src/browser/context-variable-picker.ts +85 -0
  72. package/src/browser/style/index.css +110 -12
  73. package/lib/browser/chat-response-renderer/ai-editor-manager.d.ts +0 -36
  74. package/lib/browser/chat-response-renderer/ai-editor-manager.d.ts.map +0 -1
  75. package/lib/browser/chat-response-renderer/ai-editor-manager.js.map +0 -1
@@ -13,8 +13,8 @@
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, ChangeSetElement, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat';
17
- import { Disposable, UntitledResourceResolver } from '@theia/core';
16
+ import { ChangeSet, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat';
17
+ import { Disposable, DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
18
18
  import { ContextMenuRenderer, LabelProvider, Message, ReactWidget } from '@theia/core/lib/browser';
19
19
  import { Deferred } from '@theia/core/lib/common/promise-util';
20
20
  import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
@@ -23,8 +23,13 @@ import { IMouseEvent } from '@theia/monaco-editor-core';
23
23
  import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
24
24
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
25
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';
29
+ import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
26
30
 
27
31
  type Query = (query: string) => Promise<void>;
32
+ type Unpin = () => void;
28
33
  type Cancel = (requestModel: ChatRequestModel) => void;
29
34
  type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
30
35
  type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
@@ -32,6 +37,7 @@ type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) =>
32
37
  export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
33
38
  export interface AIChatInputConfiguration {
34
39
  showContext?: boolean;
40
+ showPinnedAgent?: boolean;
35
41
  }
36
42
 
37
43
  @injectable()
@@ -42,8 +48,8 @@ export class AIChatInputWidget extends ReactWidget {
42
48
  @inject(MonacoEditorProvider)
43
49
  protected readonly editorProvider: MonacoEditorProvider;
44
50
 
45
- @inject(UntitledResourceResolver)
46
- protected readonly untitledResourceResolver: UntitledResourceResolver;
51
+ @inject(InMemoryResources)
52
+ protected readonly resources: InMemoryResources;
47
53
 
48
54
  @inject(ContextMenuRenderer)
49
55
  protected readonly contextMenuRenderer: ContextMenuRenderer;
@@ -51,9 +57,18 @@ export class AIChatInputWidget extends ReactWidget {
51
57
  @inject(AIChatInputConfiguration) @optional()
52
58
  protected readonly configuration: AIChatInputConfiguration | undefined;
53
59
 
60
+ @inject(FrontendVariableService)
61
+ protected readonly variableService: FrontendVariableService;
62
+
54
63
  @inject(LabelProvider)
55
64
  protected readonly labelProvider: LabelProvider;
56
65
 
66
+ @inject(ContextVariablePicker)
67
+ protected readonly contextVariablePicker: ContextVariablePicker;
68
+
69
+ @inject(ChangeSetActionService)
70
+ protected readonly changeSetActionService: ChangeSetActionService;
71
+
57
72
  protected editorRef: MonacoEditor | undefined = undefined;
58
73
  private editorReady = new Deferred<void>();
59
74
 
@@ -63,6 +78,10 @@ export class AIChatInputWidget extends ReactWidget {
63
78
  set onQuery(query: Query) {
64
79
  this._onQuery = query;
65
80
  }
81
+ private _onUnpin: Unpin;
82
+ set onUnpin(unpin: Unpin) {
83
+ this._onUnpin = unpin;
84
+ }
66
85
  private _onCancel: Cancel;
67
86
  set onCancel(cancel: Cancel) {
68
87
  this._onCancel = cancel;
@@ -75,11 +94,25 @@ export class AIChatInputWidget extends ReactWidget {
75
94
  set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
76
95
  this._onDeleteChangeSetElement = deleteChangeSetElement;
77
96
  }
97
+
98
+ protected onDisposeForChatModel = new DisposableCollection();
78
99
  private _chatModel: ChatModel;
79
100
  set chatModel(chatModel: ChatModel) {
101
+ this.onDisposeForChatModel.dispose();
102
+ this.onDisposeForChatModel = new DisposableCollection();
103
+ this.onDisposeForChatModel.push(chatModel.onDidChange(event => {
104
+ if (event.kind === 'addVariable' || event.kind === 'removeVariable') {
105
+ this.update();
106
+ }
107
+ }));
80
108
  this._chatModel = chatModel;
81
109
  this.update();
82
110
  }
111
+ private _pinnedAgent: ChatAgent | undefined;
112
+ set pinnedAgent(pinnedAgent: ChatAgent | undefined) {
113
+ this._pinnedAgent = pinnedAgent;
114
+ this.update();
115
+ }
83
116
 
84
117
  @postConstruct()
85
118
  protected init(): void {
@@ -101,12 +134,19 @@ export class AIChatInputWidget extends ReactWidget {
101
134
  return (
102
135
  <ChatInput
103
136
  onQuery={this._onQuery.bind(this)}
137
+ onUnpin={this._onUnpin.bind(this)}
104
138
  onCancel={this._onCancel.bind(this)}
139
+ onDragOver={this.onDragOver.bind(this)}
140
+ onDrop={this.onDrop.bind(this)}
105
141
  onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
106
142
  onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
143
+ onAddContextElement={this.addContextElement.bind(this)}
144
+ onDeleteContextElement={this.deleteContextElement.bind(this)}
145
+ context={this._chatModel.context.getVariables()}
107
146
  chatModel={this._chatModel}
147
+ pinnedAgent={this._pinnedAgent}
108
148
  editorProvider={this.editorProvider}
109
- untitledResourceResolver={this.untitledResourceResolver}
149
+ resources={this.resources}
110
150
  contextMenuCallback={this.handleContextMenu.bind(this)}
111
151
  isEnabled={this.isEnabled}
112
152
  setEditorRef={editor => {
@@ -114,16 +154,64 @@ export class AIChatInputWidget extends ReactWidget {
114
154
  this.editorReady.resolve();
115
155
  }}
116
156
  showContext={this.configuration?.showContext}
157
+ showPinnedAgent={this.configuration?.showPinnedAgent}
117
158
  labelProvider={this.labelProvider}
159
+ actionService={this.changeSetActionService}
118
160
  />
119
161
  );
120
162
  }
121
163
 
164
+ protected onDragOver(event: React.DragEvent): void {
165
+ event.preventDefault();
166
+ event.stopPropagation();
167
+ this.node.classList.add('drag-over');
168
+ if (event.dataTransfer?.types.includes('text/plain')) {
169
+ event.dataTransfer!.dropEffect = 'copy';
170
+ } else {
171
+ event.dataTransfer!.dropEffect = 'link';
172
+ }
173
+ }
174
+
175
+ protected onDrop(event: React.DragEvent): void {
176
+ event.preventDefault();
177
+ event.stopPropagation();
178
+ this.node.classList.remove('drag-over');
179
+ const dataTransferText = event.dataTransfer?.getData('text/plain');
180
+ const position = this.editorRef?.getControl().getTargetAtClientPoint(event.clientX, event.clientY)?.position;
181
+ this.variableService.getDropResult(event.nativeEvent, { type: 'ai-chat-input-widget' }).then(result => {
182
+ result.variables.forEach(variable => this.addContext(variable));
183
+ const text = result.text ?? dataTransferText;
184
+ if (position && text) {
185
+ this.editorRef?.getControl().executeEdits('drag-and-drop', [{
186
+ range: {
187
+ startLineNumber: position.lineNumber,
188
+ startColumn: position.column,
189
+ endLineNumber: position.lineNumber,
190
+ endColumn: position.column
191
+ },
192
+ text
193
+ }]);
194
+ }
195
+ });
196
+ }
197
+
122
198
  public setEnabled(enabled: boolean): void {
123
199
  this.isEnabled = enabled;
124
200
  this.update();
125
201
  }
126
202
 
203
+ protected addContextElement(): void {
204
+ this.contextVariablePicker.pickContextVariable().then(contextElement => {
205
+ if (contextElement) {
206
+ this._chatModel.context.addVariables(contextElement);
207
+ }
208
+ });
209
+ }
210
+
211
+ protected deleteContextElement(index: number): void {
212
+ this._chatModel.context.deleteVariables(index);
213
+ }
214
+
127
215
  protected handleContextMenu(event: IMouseEvent): void {
128
216
  this.contextMenuRenderer.render({
129
217
  menuPath: AIChatInputWidget.CONTEXT_MENU,
@@ -132,21 +220,33 @@ export class AIChatInputWidget extends ReactWidget {
132
220
  event.preventDefault();
133
221
  }
134
222
 
223
+ addContext(variable: AIVariableResolutionRequest): void {
224
+ this._chatModel.context.addVariables(variable);
225
+ }
135
226
  }
136
227
 
137
228
  interface ChatInputProperties {
138
229
  onCancel: (requestModel: ChatRequestModel) => void;
139
230
  onQuery: (query: string) => void;
231
+ onUnpin: () => void;
232
+ onDragOver: (event: React.DragEvent) => void;
233
+ onDrop: (event: React.DragEvent) => void;
140
234
  onDeleteChangeSet: (sessionId: string) => void;
141
235
  onDeleteChangeSetElement: (sessionId: string, index: number) => void;
236
+ onAddContextElement: () => void;
237
+ onDeleteContextElement: (index: number) => void;
238
+ context?: readonly AIVariableResolutionRequest[];
142
239
  isEnabled?: boolean;
143
240
  chatModel: ChatModel;
241
+ pinnedAgent?: ChatAgent;
144
242
  editorProvider: MonacoEditorProvider;
145
- untitledResourceResolver: UntitledResourceResolver;
243
+ resources: InMemoryResources;
146
244
  contextMenuCallback: (event: IMouseEvent) => void;
147
245
  setEditorRef: (editor: MonacoEditor | undefined) => void;
148
246
  showContext?: boolean;
247
+ showPinnedAgent?: boolean;
149
248
  labelProvider: LabelProvider;
249
+ actionService: ChangeSetActionService;
150
250
  }
151
251
 
152
252
  const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
@@ -156,7 +256,15 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
156
256
  const [inProgress, setInProgress] = React.useState(false);
157
257
  const [isInputEmpty, setIsInputEmpty] = React.useState(true);
158
258
  const [changeSetUI, setChangeSetUI] = React.useState(
159
- () => props.chatModel.changeSet ? buildChangeSetUI(props.chatModel.changeSet, props.labelProvider, onDeleteChangeSet, onDeleteChangeSetElement) : undefined
259
+ () => props.chatModel.changeSet
260
+ ? buildChangeSetUI(
261
+ props.chatModel.changeSet,
262
+ props.labelProvider,
263
+ props.actionService.getActionsForChangeset(props.chatModel.changeSet),
264
+ onDeleteChangeSet,
265
+ onDeleteChangeSetElement
266
+ )
267
+ : undefined
160
268
  );
161
269
 
162
270
  // eslint-disable-next-line no-null/no-null
@@ -166,12 +274,13 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
166
274
  const editorRef = React.useRef<MonacoEditor | undefined>(undefined);
167
275
 
168
276
  React.useEffect(() => {
277
+ const uri = new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
278
+ const resource = props.resources.add(uri, '');
169
279
  const createInputElement = async () => {
170
280
  const paddingTop = 6;
171
281
  const lineHeight = 20;
172
282
  const maxHeight = 240;
173
- const resource = await props.untitledResourceResolver.createUntitledResource('', CHAT_VIEW_LANGUAGE_EXTENSION);
174
- const editor = await props.editorProvider.createInline(resource.uri, editorContainerRef.current!, {
283
+ const editor = await props.editorProvider.createInline(uri, editorContainerRef.current!, {
175
284
  language: CHAT_VIEW_LANGUAGE_EXTENSION,
176
285
  // Disable code lens, inlay hints and hover support to avoid console errors from other contributions
177
286
  codeLens: false,
@@ -234,7 +343,9 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
234
343
  props.setEditorRef(editor);
235
344
  };
236
345
  createInputElement();
346
+
237
347
  return () => {
348
+ resource.dispose();
238
349
  props.setEditorRef(undefined);
239
350
  if (editorRef.current) {
240
351
  editorRef.current.dispose();
@@ -257,14 +368,28 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
257
368
  setInProgress(ChatRequestModel.isInProgress(event.request))
258
369
  );
259
370
  } else if (ChatChangeEvent.isChangeSetEvent(event)) {
260
- if (event.changeSet) {
261
- setChangeSetUI(buildChangeSetUI(event.changeSet, props.labelProvider, onDeleteChangeSet, onDeleteChangeSetElement));
262
- } else {
371
+ if (event.kind === 'removeChangeSet') {
263
372
  setChangeSetUI(undefined);
373
+ } else if (event.kind === 'setChangeSet' || 'updateChangeSet') {
374
+ setChangeSetUI(buildChangeSetUI(
375
+ event.changeSet,
376
+ props.labelProvider,
377
+ props.actionService.getActionsForChangeset(event.changeSet),
378
+ onDeleteChangeSet,
379
+ onDeleteChangeSetElement
380
+ ));
264
381
  }
265
382
  }
266
383
  });
267
- setChangeSetUI(props.chatModel.changeSet ? buildChangeSetUI(props.chatModel.changeSet, props.labelProvider, onDeleteChangeSet, onDeleteChangeSetElement) : undefined);
384
+ setChangeSetUI(props.chatModel.changeSet
385
+ ? buildChangeSetUI(
386
+ props.chatModel.changeSet,
387
+ props.labelProvider,
388
+ props.actionService.getActionsForChangeset(props.chatModel.changeSet),
389
+ onDeleteChangeSet,
390
+ onDeleteChangeSetElement
391
+ )
392
+ : undefined);
268
393
  return () => {
269
394
  listener?.dispose();
270
395
  responseListenerRef.current?.dispose();
@@ -272,7 +397,16 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
272
397
  };
273
398
  }, [props.chatModel]);
274
399
 
275
- function submit(value: string): void {
400
+ React.useEffect(() => {
401
+ const disposable = props.actionService.onDidChange(() => {
402
+ if (!props.chatModel.changeSet) { return; }
403
+ const newActions = props.actionService.getActionsForChangeset(props.chatModel.changeSet);
404
+ setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
405
+ });
406
+ return () => disposable.dispose();
407
+ });
408
+
409
+ const submit = React.useCallback(function submit(value: string): void {
276
410
  if (!value || value.trim().length === 0) {
277
411
  return;
278
412
  }
@@ -281,7 +415,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
281
415
  if (editorRef.current) {
282
416
  editorRef.current.document.textEditorModel.setValue('');
283
417
  }
284
- }
418
+ }, [props.context, props.onQuery, editorRef]);
285
419
 
286
420
  const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
287
421
  if (!props.isEnabled) {
@@ -291,7 +425,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
291
425
  event.preventDefault();
292
426
  submit(editorRef.current?.document.textEditorModel.getValue() || '');
293
427
  }
294
- }, [props.isEnabled]);
428
+ }, [props.isEnabled, submit]);
295
429
 
296
430
  const handleInputFocus = () => {
297
431
  hidePlaceholderIfEditorFilled();
@@ -319,15 +453,46 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
319
453
  }
320
454
  };
321
455
 
322
- const leftOptions = props.showContext ? [{
323
- title: 'Attach elements to context',
324
- handler: () => { /* TODO */ },
325
- className: 'codicon-add'
326
- }] : [];
456
+ const handlePin = () => {
457
+ if (editorRef.current) {
458
+ editorRef.current.getControl().getModel()?.applyEdits([{
459
+ range: {
460
+ startLineNumber: 1,
461
+ startColumn: 1,
462
+ endLineNumber: 1,
463
+ endColumn: 1
464
+ },
465
+ text: '@ ',
466
+ }]);
467
+ editorRef.current.getControl().setPosition({ lineNumber: 1, column: 2 });
468
+ editorRef.current.getControl().getAction('editor.action.triggerSuggest')?.run();
469
+ }
470
+ };
471
+
472
+ const leftOptions = [
473
+ ...(props.showContext
474
+ ? [{
475
+ title: nls.localize('theia/ai/chat-ui/attachToContext', 'Attach elements to context'),
476
+ handler: () => props.onAddContextElement(),
477
+ className: 'codicon-add'
478
+ }]
479
+ : []),
480
+ ...(props.showPinnedAgent
481
+ ? [{
482
+ title: props.pinnedAgent ? nls.localize('theia/ai/chat-ui/unpinAgent', 'Unpin Agent') : nls.localize('theia/ai/chat-ui/pinAgent', 'Pin Agent'),
483
+ handler: props.pinnedAgent ? props.onUnpin : handlePin,
484
+ className: 'at-icon',
485
+ text: {
486
+ align: 'right',
487
+ content: props.pinnedAgent && props.pinnedAgent.name
488
+ },
489
+ }]
490
+ : []),
491
+ ] as Option[];
327
492
 
328
493
  const rightOptions = inProgress
329
494
  ? [{
330
- title: 'Cancel (Esc)',
495
+ title: nls.localize('theia/ai/chat-ui/cancel', 'Cancel (Esc)'),
331
496
  handler: () => {
332
497
  const latestRequest = getLatestRequest(props.chatModel);
333
498
  if (latestRequest) {
@@ -338,7 +503,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
338
503
  className: 'codicon-stop-circle'
339
504
  }]
340
505
  : [{
341
- title: 'Send (Enter)',
506
+ title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
342
507
  handler: () => {
343
508
  if (props.isEnabled) {
344
509
  submit(editorRef.current?.document.textEditorModel.getValue() || '');
@@ -348,14 +513,19 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
348
513
  disabled: isInputEmpty || !props.isEnabled
349
514
  }];
350
515
 
351
- return <div className='theia-ChatInput'>
516
+ const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement);
517
+
518
+ return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
352
519
  {changeSetUI?.elements &&
353
520
  <ChangeSetBox changeSet={changeSetUI} />
354
521
  }
355
522
  <div className='theia-ChatInput-Editor-Box'>
356
523
  <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
357
- <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>Ask a question</div>
524
+ <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
358
525
  </div>
526
+ {props.context && props.context.length > 0 &&
527
+ <ChatContext context={contextUI.context} />
528
+ }
359
529
  <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
360
530
  </div>
361
531
  </div>;
@@ -366,22 +536,28 @@ const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
366
536
  e.stopPropagation();
367
537
  };
368
538
 
369
- const buildChangeSetUI = (changeSet: ChangeSet, labelProvider: LabelProvider, onDeleteChangeSet: () => void, onDeleteChangeSetElement: (index: number) => void): ChangeSetUI => ({
539
+ const buildChangeSetUI = (
540
+ changeSet: ChangeSet,
541
+ labelProvider: LabelProvider,
542
+ actions: ChangeSetActionRenderer[],
543
+ onDeleteChangeSet: () => void,
544
+ onDeleteChangeSetElement: (index: number) => void
545
+ ): ChangeSetUI => ({
370
546
  title: changeSet.title,
371
- disabled: !hasPendingElementsToAccept(changeSet),
372
- acceptAllPendingElements: () => acceptAllPendingElements(changeSet),
373
- delete: () => onDeleteChangeSet(),
547
+ changeSet,
548
+ deleteChangeSet: onDeleteChangeSet,
374
549
  elements: changeSet.getElements().map(element => ({
375
- open: element?.open?.bind(element),
550
+ open: element.open?.bind(element),
376
551
  iconClass: element.icon ?? labelProvider.getIcon(element.uri) ?? labelProvider.fileIcon,
377
552
  nameClass: `${element.type} ${element.state}`,
378
553
  name: element.name ?? labelProvider.getName(element.uri),
379
554
  additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
380
555
  openChange: element?.openChange?.bind(element),
381
- accept: element.state !== 'applied' ? element?.accept?.bind(element) : undefined,
382
- discard: element.state === 'applied' ? element?.discard?.bind(element) : undefined,
556
+ apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
557
+ revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
383
558
  delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
384
- }))
559
+ })),
560
+ actions
385
561
  });
386
562
 
387
563
  interface ChangeSetUIElement {
@@ -391,58 +567,69 @@ interface ChangeSetUIElement {
391
567
  additionalInfo: string;
392
568
  open?: () => void;
393
569
  openChange?: () => void;
394
- accept?: () => void;
395
- discard?: () => void;
570
+ apply?: () => void;
571
+ revert?: () => void;
396
572
  delete: () => void;
397
573
  }
398
574
 
399
575
  interface ChangeSetUI {
576
+ changeSet: ChangeSet;
400
577
  title: string;
401
- disabled: boolean;
402
- acceptAllPendingElements: () => void;
403
- delete: () => void;
578
+ deleteChangeSet: () => void;
404
579
  elements: ChangeSetUIElement[];
580
+ actions: ChangeSetActionRenderer[];
405
581
  }
406
582
 
407
- const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = ({ changeSet }) => (
583
+ /** Memo because the parent element rerenders on every key press in the chat widget. */
584
+ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.memo(({ changeSet: { changeSet, title, deleteChangeSet, elements, actions } }) => (
408
585
  <div className='theia-ChatInput-ChangeSet-Box'>
409
586
  <div className='theia-ChatInput-ChangeSet-Header'>
410
- <h3>{changeSet.title}</h3>
587
+ <h3>{title}</h3>
411
588
  <div className='theia-ChatInput-ChangeSet-Header-Actions'>
412
- <button
413
- className='theia-button'
414
- disabled={changeSet.disabled}
415
- title='Accept all pending changes'
416
- onClick={() => changeSet.acceptAllPendingElements()}
417
- >
418
- Accept
419
- </button>
420
- <span className='codicon codicon-close action' title='Delete Change Set' onClick={() => changeSet.delete()} />
589
+ {actions.map(action => <div key={action.id} className='theia-changeSet-Action'>{action.render(changeSet)}</div>)}
590
+ <span className='codicon codicon-close action' title={nls.localize('theia/ai/chat-ui/deleteChangeSet', 'Delete Change Set')} onClick={() => deleteChangeSet()} />
421
591
  </div>
422
592
  </div>
423
593
  <div className='theia-ChatInput-ChangeSet-List'>
424
594
  <ul>
425
- {changeSet.elements.map((element, index) => (
426
- <li key={index} title='Open Diff' onClick={() => element.openChange?.()}>
595
+ {elements.map((element, index) => (
596
+ <li key={index} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
427
597
  <div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`} />
428
- <span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
429
- {element.name}
430
- </span>
431
- <span className='theia-ChatInput-ChangeSet-additionalInfo'>
432
- {element.additionalInfo}
598
+ <span className='theia-ChatInput-ChangeSet-labelParts'>
599
+ <span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
600
+ {element.name}
601
+ </span>
602
+ <span className='theia-ChatInput-ChangeSet-additionalInfo'>
603
+ {element.additionalInfo}
604
+ </span>
433
605
  </span>
434
606
  <div className='theia-ChatInput-ChangeSet-Actions'>
435
- {element.open && (<span className='codicon codicon-file action' title='Open Original File' onClick={noPropagation(() => element.open!())} />)}
436
- {element.discard && (<span className='codicon codicon-discard action' title='Undo' onClick={noPropagation(() => element.discard!())} />)}
437
- {element.accept && (<span className='codicon codicon-check action' title='Accept' onClick={noPropagation(() => element.accept!())} />)}
438
- <span className='codicon codicon-close action' title='Delete' onClick={noPropagation(() => element.delete())} />
607
+ {element.open && (
608
+ <span
609
+ className='codicon codicon-file action'
610
+ title={nls.localize('theia/ai/chat-ui/openOriginalFile', 'Open Original File')}
611
+ onClick={noPropagation(() => element.open!())}
612
+ />)}
613
+ {element.revert && (
614
+ <span
615
+ className='codicon codicon-discard action'
616
+ title={nls.localizeByDefault('Revert')}
617
+ onClick={noPropagation(() => element.revert!())}
618
+ />)}
619
+ {element.apply && (
620
+ <span
621
+ className='codicon codicon-check action'
622
+ title={nls.localizeByDefault('Apply')}
623
+ onClick={noPropagation(() => element.apply!())}
624
+ />)}
625
+ <span className='codicon codicon-close action' title={nls.localizeByDefault('Delete')} onClick={noPropagation(() => element.delete())} />
439
626
  </div>
440
627
  </li>
441
628
  ))}
442
629
  </ul>
443
630
  </div>
444
631
  </div>
445
- );
632
+ ));
446
633
 
447
634
  interface ChatInputOptionsProps {
448
635
  leftOptions: Option[];
@@ -454,6 +641,10 @@ interface Option {
454
641
  handler: () => void;
455
642
  className: string;
456
643
  disabled?: boolean;
644
+ text?: {
645
+ align?: 'left' | 'right';
646
+ content: string;
647
+ };
457
648
  }
458
649
 
459
650
  const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ leftOptions, rightOptions }) => (
@@ -462,38 +653,79 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
462
653
  {leftOptions.map((option, index) => (
463
654
  <span
464
655
  key={index}
465
- className={`codicon ${option.className} option ${option.disabled ? 'disabled' : ''}`}
656
+ className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
466
657
  title={option.title}
467
658
  onClick={option.handler}
468
- />
659
+ >
660
+ <span>{option.text?.content}</span>
661
+ <span className={`codicon ${option.className}`} />
662
+ </span>
469
663
  ))}
470
664
  </div>
471
665
  <div className="theia-ChatInputOptions-right">
472
666
  {rightOptions.map((option, index) => (
473
667
  <span
474
668
  key={index}
475
- className={`codicon ${option.className} option ${option.disabled ? 'disabled' : ''}`}
669
+ className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
476
670
  title={option.title}
477
671
  onClick={option.handler}
478
- />
672
+ >
673
+ <span>{option.text?.content}</span>
674
+ <span className={`codicon ${option.className}`} />
675
+ </span>
479
676
  ))}
480
677
  </div>
481
678
  </div>
482
679
  );
483
680
 
484
- function acceptAllPendingElements(changeSet: ChangeSet): void {
485
- acceptablePendingElements(changeSet).forEach(e => e.accept!());
681
+ function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
682
+ const requests = chatModel.getRequests();
683
+ return requests.length > 0 ? requests[requests.length - 1] : undefined;
486
684
  }
487
685
 
488
- function hasPendingElementsToAccept(changeSet: ChangeSet): boolean | undefined {
489
- return acceptablePendingElements(changeSet).length > 0;
686
+ function buildContextUI(context: readonly AIVariableResolutionRequest[] | undefined, labelProvider: LabelProvider, onDeleteContextElement: (index: number) => void): ChatContextUI {
687
+ if (!context) {
688
+ return { context: [] };
689
+ }
690
+ return {
691
+ context: context.map((element, index) => ({
692
+ name: labelProvider.getName(element),
693
+ iconClass: labelProvider.getIcon(element),
694
+ nameClass: element.variable.name,
695
+ additionalInfo: labelProvider.getDetails(element),
696
+ details: labelProvider.getLongName(element),
697
+ delete: () => onDeleteContextElement(index),
698
+ }))
699
+ };
490
700
  }
491
701
 
492
- function acceptablePendingElements(changeSet: ChangeSet): ChangeSetElement[] {
493
- return changeSet.getElements().filter(e => e.accept && (e.state === undefined || e.state === 'pending'));
702
+ interface ChatContextUI {
703
+ context: {
704
+ name: string;
705
+ iconClass: string;
706
+ nameClass: string;
707
+ additionalInfo?: string;
708
+ details?: string;
709
+ delete: () => void;
710
+ open?: () => void;
711
+ }[];
494
712
  }
495
713
 
496
- function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
497
- const requests = chatModel.getRequests();
498
- return requests.length > 0 ? requests[requests.length - 1] : undefined;
499
- }
714
+ const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
715
+ <div className="theia-ChatInput-ChatContext">
716
+ <ul>
717
+ {context.map((element, index) => (
718
+ <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
719
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
720
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
721
+ {element.name}
722
+ </span>
723
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
724
+ {element.additionalInfo}
725
+ </span>
726
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={() => element.delete()} />
727
+ </li>
728
+ ))}
729
+ </ul>
730
+ </div>
731
+ );