@theia/ai-chat-ui 1.58.2 → 1.59.0-next.62

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 (51) 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.map +1 -1
  5. package/lib/browser/ai-chat-ui-frontend-module.js +7 -4
  6. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  7. package/lib/browser/chat-input-widget.d.ts +21 -4
  8. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  9. package/lib/browser/chat-input-widget.js +177 -37
  10. package/lib/browser/chat-input-widget.js.map +1 -1
  11. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
  12. package/lib/browser/chat-response-renderer/code-part-renderer.js +4 -3
  13. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  14. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts.map +1 -1
  15. package/lib/browser/chat-response-renderer/text-part-renderer.js +3 -1
  16. package/lib/browser/chat-response-renderer/text-part-renderer.js.map +1 -1
  17. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  18. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +7 -3
  19. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  20. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  21. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +25 -16
  22. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  23. package/lib/browser/chat-view-contribution.d.ts +2 -2
  24. package/lib/browser/chat-view-contribution.d.ts.map +1 -1
  25. package/lib/browser/chat-view-contribution.js +6 -6
  26. package/lib/browser/chat-view-contribution.js.map +1 -1
  27. package/lib/browser/chat-view-language-contribution.d.ts +10 -5
  28. package/lib/browser/chat-view-language-contribution.d.ts.map +1 -1
  29. package/lib/browser/chat-view-language-contribution.js +94 -14
  30. package/lib/browser/chat-view-language-contribution.js.map +1 -1
  31. package/lib/browser/chat-view-widget.d.ts +4 -1
  32. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  33. package/lib/browser/chat-view-widget.js +18 -7
  34. package/lib/browser/chat-view-widget.js.map +1 -1
  35. package/lib/browser/context-variable-picker.d.ts +9 -0
  36. package/lib/browser/context-variable-picker.d.ts.map +1 -0
  37. package/lib/browser/context-variable-picker.js +86 -0
  38. package/lib/browser/context-variable-picker.js.map +1 -0
  39. package/package.json +11 -11
  40. package/src/browser/ai-chat-ui-contribution.ts +8 -8
  41. package/src/browser/ai-chat-ui-frontend-module.ts +8 -4
  42. package/src/browser/chat-input-widget.tsx +255 -49
  43. package/src/browser/chat-response-renderer/code-part-renderer.tsx +4 -3
  44. package/src/browser/chat-response-renderer/text-part-renderer.tsx +4 -1
  45. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +9 -3
  46. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +26 -16
  47. package/src/browser/chat-view-contribution.ts +6 -6
  48. package/src/browser/chat-view-language-contribution.ts +103 -19
  49. package/src/browser/chat-view-widget.tsx +22 -8
  50. package/src/browser/context-variable-picker.ts +85 -0
  51. package/src/browser/style/index.css +95 -9
@@ -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, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat';
17
+ import { Disposable, 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,12 @@ 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';
26
29
 
27
- type Query = (query: string) => Promise<void>;
30
+ type Query = (query: string, context?: AIVariableResolutionRequest[]) => Promise<void>;
31
+ type Unpin = () => void;
28
32
  type Cancel = (requestModel: ChatRequestModel) => void;
29
33
  type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
30
34
  type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
@@ -32,6 +36,7 @@ type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) =>
32
36
  export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
33
37
  export interface AIChatInputConfiguration {
34
38
  showContext?: boolean;
39
+ showPinnedAgent?: boolean;
35
40
  }
36
41
 
37
42
  @injectable()
@@ -42,8 +47,8 @@ export class AIChatInputWidget extends ReactWidget {
42
47
  @inject(MonacoEditorProvider)
43
48
  protected readonly editorProvider: MonacoEditorProvider;
44
49
 
45
- @inject(UntitledResourceResolver)
46
- protected readonly untitledResourceResolver: UntitledResourceResolver;
50
+ @inject(InMemoryResources)
51
+ protected readonly resources: InMemoryResources;
47
52
 
48
53
  @inject(ContextMenuRenderer)
49
54
  protected readonly contextMenuRenderer: ContextMenuRenderer;
@@ -51,18 +56,30 @@ export class AIChatInputWidget extends ReactWidget {
51
56
  @inject(AIChatInputConfiguration) @optional()
52
57
  protected readonly configuration: AIChatInputConfiguration | undefined;
53
58
 
59
+ @inject(FrontendVariableService)
60
+ protected readonly variableService: FrontendVariableService;
61
+
54
62
  @inject(LabelProvider)
55
63
  protected readonly labelProvider: LabelProvider;
56
64
 
65
+ @inject(ContextVariablePicker)
66
+ protected readonly contextVariablePicker: ContextVariablePicker;
67
+
57
68
  protected editorRef: MonacoEditor | undefined = undefined;
58
69
  private editorReady = new Deferred<void>();
59
70
 
60
71
  protected isEnabled = false;
61
72
 
73
+ protected context: AIVariableResolutionRequest[] = [];
74
+
62
75
  private _onQuery: Query;
63
76
  set onQuery(query: Query) {
64
77
  this._onQuery = query;
65
78
  }
79
+ private _onUnpin: Unpin;
80
+ set onUnpin(unpin: Unpin) {
81
+ this._onUnpin = unpin;
82
+ }
66
83
  private _onCancel: Cancel;
67
84
  set onCancel(cancel: Cancel) {
68
85
  this._onCancel = cancel;
@@ -75,11 +92,17 @@ export class AIChatInputWidget extends ReactWidget {
75
92
  set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
76
93
  this._onDeleteChangeSetElement = deleteChangeSetElement;
77
94
  }
95
+
78
96
  private _chatModel: ChatModel;
79
97
  set chatModel(chatModel: ChatModel) {
80
98
  this._chatModel = chatModel;
81
99
  this.update();
82
100
  }
101
+ private _pinnedAgent: ChatAgent | undefined;
102
+ set pinnedAgent(pinnedAgent: ChatAgent | undefined) {
103
+ this._pinnedAgent = pinnedAgent;
104
+ this.update();
105
+ }
83
106
 
84
107
  @postConstruct()
85
108
  protected init(): void {
@@ -101,12 +124,19 @@ export class AIChatInputWidget extends ReactWidget {
101
124
  return (
102
125
  <ChatInput
103
126
  onQuery={this._onQuery.bind(this)}
127
+ onUnpin={this._onUnpin.bind(this)}
104
128
  onCancel={this._onCancel.bind(this)}
129
+ onDragOver={this.onDragOver.bind(this)}
130
+ onDrop={this.onDrop.bind(this)}
105
131
  onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
106
132
  onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
133
+ onAddContextElement={this.addContextElement.bind(this)}
134
+ onDeleteContextElement={this.deleteContextElement.bind(this)}
135
+ context={this.context}
107
136
  chatModel={this._chatModel}
137
+ pinnedAgent={this._pinnedAgent}
108
138
  editorProvider={this.editorProvider}
109
- untitledResourceResolver={this.untitledResourceResolver}
139
+ resources={this.resources}
110
140
  contextMenuCallback={this.handleContextMenu.bind(this)}
111
141
  isEnabled={this.isEnabled}
112
142
  setEditorRef={editor => {
@@ -114,16 +144,64 @@ export class AIChatInputWidget extends ReactWidget {
114
144
  this.editorReady.resolve();
115
145
  }}
116
146
  showContext={this.configuration?.showContext}
147
+ showPinnedAgent={this.configuration?.showPinnedAgent}
117
148
  labelProvider={this.labelProvider}
118
149
  />
119
150
  );
120
151
  }
121
152
 
153
+ protected onDragOver(event: React.DragEvent): void {
154
+ event.preventDefault();
155
+ event.stopPropagation();
156
+ this.node.classList.add('drag-over');
157
+ if (event.dataTransfer?.types.includes('text/plain')) {
158
+ event.dataTransfer!.dropEffect = 'copy';
159
+ } else {
160
+ event.dataTransfer!.dropEffect = 'link';
161
+ }
162
+ }
163
+
164
+ protected onDrop(event: React.DragEvent): void {
165
+ event.preventDefault();
166
+ event.stopPropagation();
167
+ this.node.classList.remove('drag-over');
168
+ const dataTransferText = event.dataTransfer?.getData('text/plain');
169
+ const position = this.editorRef?.getControl().getTargetAtClientPoint(event.clientX, event.clientY)?.position;
170
+ this.variableService.getDropResult(event.nativeEvent, { type: 'ai-chat-input-widget' }).then(result => {
171
+ result.variables.forEach(variable => this.addContext(variable));
172
+ const text = result.text ?? dataTransferText;
173
+ if (position && text) {
174
+ this.editorRef?.getControl().executeEdits('drag-and-drop', [{
175
+ range: {
176
+ startLineNumber: position.lineNumber,
177
+ startColumn: position.column,
178
+ endLineNumber: position.lineNumber,
179
+ endColumn: position.column
180
+ },
181
+ text
182
+ }]);
183
+ }
184
+ });
185
+ }
186
+
122
187
  public setEnabled(enabled: boolean): void {
123
188
  this.isEnabled = enabled;
124
189
  this.update();
125
190
  }
126
191
 
192
+ protected addContextElement(): void {
193
+ this.contextVariablePicker.pickContextVariable().then(contextElement => {
194
+ if (contextElement) {
195
+ this.addContext(contextElement);
196
+ }
197
+ });
198
+ }
199
+
200
+ protected deleteContextElement(index: number): void {
201
+ this.context.splice(index, 1);
202
+ this.update();
203
+ }
204
+
127
205
  protected handleContextMenu(event: IMouseEvent): void {
128
206
  this.contextMenuRenderer.render({
129
207
  menuPath: AIChatInputWidget.CONTEXT_MENU,
@@ -132,20 +210,35 @@ export class AIChatInputWidget extends ReactWidget {
132
210
  event.preventDefault();
133
211
  }
134
212
 
213
+ addContext(variableRequest: AIVariableResolutionRequest): void {
214
+ if (this.context.some(existing => existing.variable.id === variableRequest.variable.id && existing.arg === variableRequest.arg)) {
215
+ return;
216
+ }
217
+ this.context.push(variableRequest);
218
+ this.update();
219
+ }
135
220
  }
136
221
 
137
222
  interface ChatInputProperties {
138
223
  onCancel: (requestModel: ChatRequestModel) => void;
139
- onQuery: (query: string) => void;
224
+ onQuery: (query: string, context?: AIVariableResolutionRequest[]) => void;
225
+ onUnpin: () => void;
226
+ onDragOver: (event: React.DragEvent) => void;
227
+ onDrop: (event: React.DragEvent) => void;
140
228
  onDeleteChangeSet: (sessionId: string) => void;
141
229
  onDeleteChangeSetElement: (sessionId: string, index: number) => void;
230
+ onAddContextElement: () => void;
231
+ onDeleteContextElement: (index: number) => void;
232
+ context?: AIVariableResolutionRequest[];
142
233
  isEnabled?: boolean;
143
234
  chatModel: ChatModel;
235
+ pinnedAgent?: ChatAgent;
144
236
  editorProvider: MonacoEditorProvider;
145
- untitledResourceResolver: UntitledResourceResolver;
237
+ resources: InMemoryResources;
146
238
  contextMenuCallback: (event: IMouseEvent) => void;
147
239
  setEditorRef: (editor: MonacoEditor | undefined) => void;
148
240
  showContext?: boolean;
241
+ showPinnedAgent?: boolean;
149
242
  labelProvider: LabelProvider;
150
243
  }
151
244
 
@@ -166,12 +259,13 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
166
259
  const editorRef = React.useRef<MonacoEditor | undefined>(undefined);
167
260
 
168
261
  React.useEffect(() => {
262
+ const uri = new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
263
+ const resource = props.resources.add(uri, '');
169
264
  const createInputElement = async () => {
170
265
  const paddingTop = 6;
171
266
  const lineHeight = 20;
172
267
  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!, {
268
+ const editor = await props.editorProvider.createInline(uri, editorContainerRef.current!, {
175
269
  language: CHAT_VIEW_LANGUAGE_EXTENSION,
176
270
  // Disable code lens, inlay hints and hover support to avoid console errors from other contributions
177
271
  codeLens: false,
@@ -234,7 +328,9 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
234
328
  props.setEditorRef(editor);
235
329
  };
236
330
  createInputElement();
331
+
237
332
  return () => {
333
+ resource.dispose();
238
334
  props.setEditorRef(undefined);
239
335
  if (editorRef.current) {
240
336
  editorRef.current.dispose();
@@ -277,7 +373,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
277
373
  return;
278
374
  }
279
375
  setInProgress(true);
280
- props.onQuery(value);
376
+ props.onQuery(value, props.context);
281
377
  if (editorRef.current) {
282
378
  editorRef.current.document.textEditorModel.setValue('');
283
379
  }
@@ -319,15 +415,46 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
319
415
  }
320
416
  };
321
417
 
322
- const leftOptions = props.showContext ? [{
323
- title: 'Attach elements to context',
324
- handler: () => { /* TODO */ },
325
- className: 'codicon-add'
326
- }] : [];
418
+ const handlePin = () => {
419
+ if (editorRef.current) {
420
+ editorRef.current.getControl().getModel()?.applyEdits([{
421
+ range: {
422
+ startLineNumber: 1,
423
+ startColumn: 1,
424
+ endLineNumber: 1,
425
+ endColumn: 1
426
+ },
427
+ text: '@ ',
428
+ }]);
429
+ editorRef.current.getControl().setPosition({ lineNumber: 1, column: 2 });
430
+ editorRef.current.getControl().getAction('editor.action.triggerSuggest')?.run();
431
+ }
432
+ };
433
+
434
+ const leftOptions = [
435
+ ...(props.showContext
436
+ ? [{
437
+ title: nls.localize('theia/ai/chat-ui/attachToContext', 'Attach elements to context'),
438
+ handler: () => props.onAddContextElement(),
439
+ className: 'codicon-add'
440
+ }]
441
+ : []),
442
+ ...(props.showPinnedAgent
443
+ ? [{
444
+ title: props.pinnedAgent ? nls.localize('theia/ai/chat-ui/unpinAgent', 'Unpin Agent') : nls.localize('theia/ai/chat-ui/pinAgent', 'Pin Agent'),
445
+ handler: props.pinnedAgent ? props.onUnpin : handlePin,
446
+ className: 'at-icon',
447
+ text: {
448
+ align: 'right',
449
+ content: props.pinnedAgent && props.pinnedAgent.name
450
+ },
451
+ }]
452
+ : []),
453
+ ] as Option[];
327
454
 
328
455
  const rightOptions = inProgress
329
456
  ? [{
330
- title: 'Cancel (Esc)',
457
+ title: nls.localize('theia/ai/chat-ui/cancel', 'Cancel (Esc)'),
331
458
  handler: () => {
332
459
  const latestRequest = getLatestRequest(props.chatModel);
333
460
  if (latestRequest) {
@@ -338,7 +465,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
338
465
  className: 'codicon-stop-circle'
339
466
  }]
340
467
  : [{
341
- title: 'Send (Enter)',
468
+ title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
342
469
  handler: () => {
343
470
  if (props.isEnabled) {
344
471
  submit(editorRef.current?.document.textEditorModel.getValue() || '');
@@ -348,14 +475,19 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
348
475
  disabled: isInputEmpty || !props.isEnabled
349
476
  }];
350
477
 
351
- return <div className='theia-ChatInput'>
478
+ const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement);
479
+
480
+ return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
352
481
  {changeSetUI?.elements &&
353
482
  <ChangeSetBox changeSet={changeSetUI} />
354
483
  }
355
484
  <div className='theia-ChatInput-Editor-Box'>
356
485
  <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>
486
+ <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
358
487
  </div>
488
+ {props.context && props.context.length > 0 &&
489
+ <ChatContext context={contextUI.context} />
490
+ }
359
491
  <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
360
492
  </div>
361
493
  </div>;
@@ -369,7 +501,7 @@ const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
369
501
  const buildChangeSetUI = (changeSet: ChangeSet, labelProvider: LabelProvider, onDeleteChangeSet: () => void, onDeleteChangeSetElement: (index: number) => void): ChangeSetUI => ({
370
502
  title: changeSet.title,
371
503
  disabled: !hasPendingElementsToAccept(changeSet),
372
- acceptAllPendingElements: () => acceptAllPendingElements(changeSet),
504
+ applyAllPendingElements: () => applyAllPendingElements(changeSet),
373
505
  delete: () => onDeleteChangeSet(),
374
506
  elements: changeSet.getElements().map(element => ({
375
507
  open: element?.open?.bind(element),
@@ -378,8 +510,8 @@ const buildChangeSetUI = (changeSet: ChangeSet, labelProvider: LabelProvider, on
378
510
  name: element.name ?? labelProvider.getName(element.uri),
379
511
  additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
380
512
  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,
513
+ apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
514
+ revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
383
515
  delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
384
516
  }))
385
517
  });
@@ -391,15 +523,15 @@ interface ChangeSetUIElement {
391
523
  additionalInfo: string;
392
524
  open?: () => void;
393
525
  openChange?: () => void;
394
- accept?: () => void;
395
- discard?: () => void;
526
+ apply?: () => void;
527
+ revert?: () => void;
396
528
  delete: () => void;
397
529
  }
398
530
 
399
531
  interface ChangeSetUI {
400
532
  title: string;
401
533
  disabled: boolean;
402
- acceptAllPendingElements: () => void;
534
+ applyAllPendingElements: () => void;
403
535
  delete: () => void;
404
536
  elements: ChangeSetUIElement[];
405
537
  }
@@ -412,30 +544,47 @@ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = ({ cha
412
544
  <button
413
545
  className='theia-button'
414
546
  disabled={changeSet.disabled}
415
- title='Accept all pending changes'
416
- onClick={() => changeSet.acceptAllPendingElements()}
547
+ title={nls.localize('theia/ai/chat-ui/applyAllTitle', 'Apply all pending suggestions')}
548
+ onClick={() => changeSet.applyAllPendingElements()}
417
549
  >
418
- Accept
550
+ {nls.localize('theia/ai/chat-ui/acceptAll', 'Apply All')}
419
551
  </button>
420
- <span className='codicon codicon-close action' title='Delete Change Set' onClick={() => changeSet.delete()} />
552
+ <span className='codicon codicon-close action' title={nls.localize('theia/ai/chat-ui/deleteChangeSet', 'Delete Change Set')} onClick={() => changeSet.delete()} />
421
553
  </div>
422
554
  </div>
423
555
  <div className='theia-ChatInput-ChangeSet-List'>
424
556
  <ul>
425
557
  {changeSet.elements.map((element, index) => (
426
- <li key={index} title='Open Diff' onClick={() => element.openChange?.()}>
558
+ <li key={index} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
427
559
  <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}
560
+ <span className='theia-ChatInput-ChangeSet-labelParts'>
561
+ <span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
562
+ {element.name}
563
+ </span>
564
+ <span className='theia-ChatInput-ChangeSet-additionalInfo'>
565
+ {element.additionalInfo}
566
+ </span>
433
567
  </span>
434
568
  <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())} />
569
+ {element.open && (
570
+ <span
571
+ className='codicon codicon-file action'
572
+ title={nls.localize('theia/ai/chat-ui/openOriginalFile', 'Open Original File')}
573
+ onClick={noPropagation(() => element.open!())}
574
+ />)}
575
+ {element.revert && (
576
+ <span
577
+ className='codicon codicon-discard action'
578
+ title={nls.localizeByDefault('Revert')}
579
+ onClick={noPropagation(() => element.revert!())}
580
+ />)}
581
+ {element.apply && (
582
+ <span
583
+ className='codicon codicon-check action'
584
+ title={nls.localizeByDefault('Apply')}
585
+ onClick={noPropagation(() => element.apply!())}
586
+ />)}
587
+ <span className='codicon codicon-close action' title={nls.localizeByDefault('Delete')} onClick={noPropagation(() => element.delete())} />
439
588
  </div>
440
589
  </li>
441
590
  ))}
@@ -454,6 +603,10 @@ interface Option {
454
603
  handler: () => void;
455
604
  className: string;
456
605
  disabled?: boolean;
606
+ text?: {
607
+ align?: 'left' | 'right';
608
+ content: string;
609
+ };
457
610
  }
458
611
 
459
612
  const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ leftOptions, rightOptions }) => (
@@ -462,38 +615,91 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
462
615
  {leftOptions.map((option, index) => (
463
616
  <span
464
617
  key={index}
465
- className={`codicon ${option.className} option ${option.disabled ? 'disabled' : ''}`}
618
+ className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
466
619
  title={option.title}
467
620
  onClick={option.handler}
468
- />
621
+ >
622
+ <span>{option.text?.content}</span>
623
+ <span className={`codicon ${option.className}`} />
624
+ </span>
469
625
  ))}
470
626
  </div>
471
627
  <div className="theia-ChatInputOptions-right">
472
628
  {rightOptions.map((option, index) => (
473
629
  <span
474
630
  key={index}
475
- className={`codicon ${option.className} option ${option.disabled ? 'disabled' : ''}`}
631
+ className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
476
632
  title={option.title}
477
633
  onClick={option.handler}
478
- />
634
+ >
635
+ <span>{option.text?.content}</span>
636
+ <span className={`codicon ${option.className}`} />
637
+ </span>
479
638
  ))}
480
639
  </div>
481
640
  </div>
482
641
  );
483
642
 
484
- function acceptAllPendingElements(changeSet: ChangeSet): void {
485
- acceptablePendingElements(changeSet).forEach(e => e.accept!());
643
+ function applyAllPendingElements(changeSet: ChangeSet): void {
644
+ getPendingElements(changeSet).forEach(e => e.apply!());
486
645
  }
487
646
 
488
647
  function hasPendingElementsToAccept(changeSet: ChangeSet): boolean | undefined {
489
- return acceptablePendingElements(changeSet).length > 0;
648
+ return getPendingElements(changeSet).length > 0;
490
649
  }
491
650
 
492
- function acceptablePendingElements(changeSet: ChangeSet): ChangeSetElement[] {
493
- return changeSet.getElements().filter(e => e.accept && (e.state === undefined || e.state === 'pending'));
651
+ function getPendingElements(changeSet: ChangeSet): ChangeSetElement[] {
652
+ return changeSet.getElements().filter(e => e.apply && (e.state === undefined || e.state === 'pending'));
494
653
  }
495
654
 
496
655
  function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
497
656
  const requests = chatModel.getRequests();
498
657
  return requests.length > 0 ? requests[requests.length - 1] : undefined;
499
658
  }
659
+
660
+ function buildContextUI(context: AIVariableResolutionRequest[] | undefined, labelProvider: LabelProvider, onDeleteContextElement: (index: number) => void): ChatContextUI {
661
+ if (!context) {
662
+ return { context: [] };
663
+ }
664
+ return {
665
+ context: context.map((element, index) => ({
666
+ name: labelProvider.getName(element),
667
+ iconClass: labelProvider.getIcon(element),
668
+ nameClass: element.variable.name,
669
+ additionalInfo: labelProvider.getDetails(element),
670
+ details: labelProvider.getLongName(element),
671
+ delete: () => onDeleteContextElement(index),
672
+ }))
673
+ };
674
+ }
675
+
676
+ interface ChatContextUI {
677
+ context: {
678
+ name: string;
679
+ iconClass: string;
680
+ nameClass: string;
681
+ additionalInfo?: string;
682
+ details?: string;
683
+ delete: () => void;
684
+ open?: () => void;
685
+ }[];
686
+ }
687
+
688
+ const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
689
+ <div className="theia-ChatInput-ChatContext">
690
+ <ul>
691
+ {context.map((element, index) => (
692
+ <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
693
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
694
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
695
+ {element.name}
696
+ </span>
697
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
698
+ {element.additionalInfo}
699
+ </span>
700
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={() => element.delete()} />
701
+ </li>
702
+ ))}
703
+ </ul>
704
+ </div>
705
+ );
@@ -23,6 +23,7 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
23
23
  import { inject, injectable, named } from '@theia/core/shared/inversify';
24
24
  import * as React from '@theia/core/shared/react';
25
25
  import { ReactNode } from '@theia/core/shared/react';
26
+ import { nls } from '@theia/core/lib/common/nls';
26
27
  import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
27
28
  import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
28
29
  import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
@@ -113,7 +114,7 @@ export class CodePartRenderer
113
114
  private getTitle(uri: URI | undefined, language: string | undefined): string {
114
115
  // If there is a URI, use the file name as the title. Otherwise, use the language as the title.
115
116
  // If there is no language, use a generic fallback title.
116
- return uri?.path?.toString().split('/').pop() ?? language ?? 'Generated Code';
117
+ return uri?.path?.toString().split('/').pop() ?? language ?? nls.localize('theia/ai/chat-ui/code-part-renderer/generatedCode', 'Generated Code');
117
118
  }
118
119
 
119
120
  /**
@@ -157,7 +158,7 @@ const CopyToClipboardButton = (props: { code: string, clipboardService: Clipboar
157
158
  const copyCodeToClipboard = React.useCallback(() => {
158
159
  clipboardService.writeText(code);
159
160
  }, [code, clipboardService]);
160
- return <div className='button codicon codicon-copy' title='Copy' role='button' onClick={copyCodeToClipboard}></div>;
161
+ return <div className='button codicon codicon-copy' title={nls.localizeByDefault('Copy')} role='button' onClick={copyCodeToClipboard}></div>;
161
162
  };
162
163
 
163
164
  @injectable()
@@ -189,7 +190,7 @@ const InsertCodeAtCursorButton = (props: { code: string, editorManager: EditorMa
189
190
  }]);
190
191
  }
191
192
  }, [code, editorManager]);
192
- return <div className='button codicon codicon-insert' title='Insert at Cursor' role='button' onClick={insertCode}></div>;
193
+ return <div className='button codicon codicon-insert' title={nls.localizeByDefault('Insert at Cursor')} role='button' onClick={insertCode}></div>;
193
194
  };
194
195
 
195
196
  /**
@@ -18,6 +18,7 @@ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
18
18
  import { injectable } from '@theia/core/shared/inversify';
19
19
  import { ChatResponseContent } from '@theia/ai-chat/lib/common';
20
20
  import { ReactNode } from '@theia/core/shared/react';
21
+ import { nls } from '@theia/core/lib/common/nls';
21
22
  import * as React from '@theia/core/shared/react';
22
23
 
23
24
  @injectable()
@@ -30,6 +31,8 @@ export class TextPartRenderer implements ChatResponsePartRenderer<ChatResponseCo
30
31
  if (response && ChatResponseContent.hasAsString(response)) {
31
32
  return <span>{response.asString()}</span>;
32
33
  }
33
- return <span>Can't display response, please check your ChatResponsePartRenderers! {JSON.stringify(response)}</span>;
34
+ return <span>
35
+ {nls.localize('theia/ai/chat-ui/text-part-renderer/cantDisplay',
36
+ "Can't display response, please check your ChatResponsePartRenderers!")} {JSON.stringify(response)}</span>;
34
37
  }
35
38
  }
@@ -18,6 +18,7 @@ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
18
18
  import { injectable } from '@theia/core/shared/inversify';
19
19
  import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
20
20
  import { ReactNode } from '@theia/core/shared/react';
21
+ import { nls } from '@theia/core/lib/common/nls';
21
22
  import * as React from '@theia/core/shared/react';
22
23
 
23
24
  @injectable()
@@ -35,14 +36,14 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
35
36
  <h4 className='theia-toolCall'>
36
37
  {response.finished ? (
37
38
  <details>
38
- <summary>Ran {response.name}
39
+ <summary>{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
39
40
  ({this.renderCollapsibleArguments(response.arguments)})
40
41
  </summary>
41
42
  <pre>{this.tryPrettyPrintJson(response)}</pre>
42
43
  </details>
43
44
  ) : (
44
45
  <span>
45
- <Spinner /> Running {response.name}({this.renderCollapsibleArguments(response.arguments)})
46
+ <Spinner /> {nls.localizeByDefault('Running')} {response.name}({this.renderCollapsibleArguments(response.arguments)})
46
47
  </span>
47
48
  )}
48
49
  </h4>
@@ -82,7 +83,12 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
82
83
  }
83
84
  } catch (e) {
84
85
  if (typeof responseContent !== 'string') {
85
- responseContent = `The content could not be converted to string: '${e.message}'. This is the original content: '${responseContent}'.`;
86
+ responseContent = nls.localize(
87
+ 'theia/ai/chat-ui/toolcall-part-renderer/prettyPrintError',
88
+ "The content could not be converted to string: '{0}'. This is the original content: '{1}'.",
89
+ e.message,
90
+ responseContent
91
+ );
86
92
  }
87
93
  // fall through
88
94
  }