@theia/ai-chat-ui 1.55.1 → 1.57.0-next.112

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 (49) hide show
  1. package/README.md +2 -1
  2. package/lib/browser/ai-chat-ui-contribution.js +1 -1
  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 +11 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  7. package/lib/browser/chat-input-widget.d.ts +18 -5
  8. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  9. package/lib/browser/chat-input-widget.js +239 -79
  10. package/lib/browser/chat-input-widget.js.map +1 -1
  11. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts +30 -2
  12. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
  13. package/lib/browser/chat-response-renderer/code-part-renderer.js +45 -10
  14. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  15. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +10 -4
  16. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
  17. package/lib/browser/chat-response-renderer/markdown-part-renderer.js +41 -11
  18. package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
  19. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +10 -0
  20. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -0
  21. package/lib/browser/chat-response-renderer/question-part-renderer.js +43 -0
  22. package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -0
  23. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +2 -0
  24. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  25. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +38 -12
  26. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  27. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +6 -1
  28. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  29. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +85 -14
  30. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  31. package/lib/browser/chat-view-language-contribution.d.ts.map +1 -1
  32. package/lib/browser/chat-view-language-contribution.js +0 -1
  33. package/lib/browser/chat-view-language-contribution.js.map +1 -1
  34. package/lib/browser/chat-view-widget.d.ts +4 -1
  35. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  36. package/lib/browser/chat-view-widget.js +14 -4
  37. package/lib/browser/chat-view-widget.js.map +1 -1
  38. package/package.json +12 -12
  39. package/src/browser/ai-chat-ui-contribution.ts +1 -1
  40. package/src/browser/ai-chat-ui-frontend-module.ts +29 -5
  41. package/src/browser/chat-input-widget.tsx +351 -99
  42. package/src/browser/chat-response-renderer/code-part-renderer.tsx +48 -9
  43. package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +42 -13
  44. package/src/browser/chat-response-renderer/question-part-renderer.tsx +59 -0
  45. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +46 -11
  46. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +141 -12
  47. package/src/browser/chat-view-language-contribution.ts +0 -1
  48. package/src/browser/chat-view-widget.tsx +19 -6
  49. package/src/browser/style/index.css +243 -22
@@ -15,7 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
18
- import { injectable } from '@theia/core/shared/inversify';
18
+ import { inject, injectable } from '@theia/core/shared/inversify';
19
19
  import {
20
20
  ChatResponseContent,
21
21
  InformationalChatResponseContent,
@@ -26,9 +26,12 @@ import * as React from '@theia/core/shared/react';
26
26
  import * as markdownit from '@theia/core/shared/markdown-it';
27
27
  import * as DOMPurify from '@theia/core/shared/dompurify';
28
28
  import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
29
+ import { OpenerService, open } from '@theia/core/lib/browser';
30
+ import { URI } from '@theia/core';
29
31
 
30
32
  @injectable()
31
33
  export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownChatResponseContent | InformationalChatResponseContent> {
34
+ @inject(OpenerService) protected readonly openerService: OpenerService;
32
35
  protected readonly markdownIt = markdownit();
33
36
  canHandle(response: ChatResponseContent): number {
34
37
  if (MarkdownChatResponseContent.is(response)) {
@@ -47,13 +50,12 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownCh
47
50
  return null;
48
51
  }
49
52
 
50
- return <MarkdownRender response={response} />;
53
+ return <MarkdownRender response={response} openerService={this.openerService} />;
51
54
  }
52
-
53
55
  }
54
56
 
55
- const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | InformationalChatResponseContent }) => {
56
- const ref = useMarkdownRendering(response.content);
57
+ const MarkdownRender = ({ response, openerService }: { response: MarkdownChatResponseContent | InformationalChatResponseContent; openerService: OpenerService }) => {
58
+ const ref = useMarkdownRendering(response.content, openerService);
57
59
 
58
60
  return <div ref={ref}></div>;
59
61
  };
@@ -61,30 +63,57 @@ const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent |
61
63
  /**
62
64
  * This hook uses markdown-it directly to render markdown.
63
65
  * The reason to use markdown-it directly is that the MarkdownRenderer is
64
- * overriden by theia with a monaco version. This monaco version strips all html
65
- * tags from the markdown with empty content.
66
- * This leads to unexpected behavior when rendering markdown with html tags.
66
+ * overridden by theia with a monaco version. This monaco version strips all html
67
+ * tags from the markdown with empty content. This leads to unexpected behavior when
68
+ * rendering markdown with html tags.
69
+ *
70
+ * Moreover, we want to intercept link clicks to use the Theia OpenerService instead of the default browser behavior.
67
71
  *
68
72
  * @param markdown the string to render as markdown
73
+ * @param skipSurroundingParagraph whether to remove a surrounding paragraph element (default: false)
74
+ * @param openerService the service to handle link opening
69
75
  * @returns the ref to use in an element to render the markdown
70
76
  */
71
- export const useMarkdownRendering = (markdown: string | MarkdownString) => {
77
+ export const useMarkdownRendering = (markdown: string | MarkdownString, openerService: OpenerService, skipSurroundingParagraph: boolean = false) => {
78
+ // null is valid in React
72
79
  // eslint-disable-next-line no-null/no-null
73
80
  const ref = useRef<HTMLDivElement | null>(null);
74
81
  const markdownString = typeof markdown === 'string' ? markdown : markdown.value;
75
82
  useEffect(() => {
76
83
  const markdownIt = markdownit();
77
84
  const host = document.createElement('div');
78
- const html = markdownIt.render(markdownString);
85
+
86
+ // markdownIt always puts the content in a paragraph element, so we remove it if we don't want that
87
+ const html = skipSurroundingParagraph ? markdownIt.render(markdownString).replace(/^<p>|<\/p>|<p><\/p>$/g, '') : markdownIt.render(markdownString);
88
+
79
89
  host.innerHTML = DOMPurify.sanitize(html, {
80
- ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs
90
+ // DOMPurify usually strips non http(s) links from hrefs
91
+ // but we want to allow them (see handleClick via OpenerService below)
92
+ ALLOW_UNKNOWN_PROTOCOLS: true
81
93
  });
82
94
  while (ref?.current?.firstChild) {
83
95
  ref.current.removeChild(ref.current.firstChild);
84
96
  }
85
-
86
97
  ref?.current?.appendChild(host);
87
- }, [markdownString]);
98
+
99
+ // intercept link clicks to use the Theia OpenerService instead of the default browser behavior
100
+ const handleClick = (event: MouseEvent) => {
101
+ let target = event.target as HTMLElement;
102
+ while (target && target.tagName !== 'A') {
103
+ target = target.parentElement as HTMLElement;
104
+ }
105
+ if (target && target.tagName === 'A') {
106
+ const href = target.getAttribute('href');
107
+ if (href) {
108
+ open(openerService, new URI(href));
109
+ event.preventDefault();
110
+ }
111
+ }
112
+ };
113
+
114
+ ref?.current?.addEventListener('click', handleClick);
115
+ return () => ref.current?.removeEventListener('click', handleClick);
116
+ }, [markdownString, skipSurroundingParagraph, openerService]);
88
117
 
89
118
  return ref;
90
119
  };
@@ -0,0 +1,59 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+ import { ChatResponseContent, QuestionResponseContent } from '@theia/ai-chat';
17
+ import { injectable } from '@theia/core/shared/inversify';
18
+ import * as React from '@theia/core/shared/react';
19
+ import { ReactNode } from '@theia/core/shared/react';
20
+ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
21
+ import { ResponseNode } from '../chat-tree-view';
22
+
23
+ @injectable()
24
+ export class QuestionPartRenderer
25
+ implements ChatResponsePartRenderer<QuestionResponseContent> {
26
+
27
+ canHandle(response: ChatResponseContent): number {
28
+ if (QuestionResponseContent.is(response)) {
29
+ return 10;
30
+ }
31
+ return -1;
32
+ }
33
+
34
+ render(question: QuestionResponseContent, node: ResponseNode): ReactNode {
35
+ return (
36
+ <div className="theia-QuestionPartRenderer-root">
37
+ <div className="theia-QuestionPartRenderer-question">{question.question}</div>
38
+ <div className="theia-QuestionPartRenderer-options">
39
+ {
40
+ question.options.map((option, index) => (
41
+ <button
42
+ className={`theia-button theia-QuestionPartRenderer-option ${question.selectedOption === option ? 'selected' : ''}`}
43
+ onClick={() => {
44
+ question.selectedOption = option;
45
+ question.handler(option);
46
+ }}
47
+ disabled={question.selectedOption !== undefined || !node.response.isWaitingForInput}
48
+ key={index}
49
+ >
50
+ {option.text}
51
+ </button>
52
+ ))
53
+ }
54
+ </div>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ }
@@ -29,26 +29,61 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
29
29
  }
30
30
  return -1;
31
31
  }
32
+
32
33
  render(response: ToolCallChatResponseContent): ReactNode {
33
- return <h4 className='theia-toolCall'>
34
- {response.finished ?
35
- <details>
36
- <summary>Ran {response.name}</summary>
37
- <pre>{this.tryPrettyPrintJson(response)}</pre>
38
- </details>
39
- : <span><Spinner /> Running [{response.name}]</span>
40
- }
41
- </h4>;
34
+ return (
35
+ <h4 className='theia-toolCall'>
36
+ {response.finished ? (
37
+ <details>
38
+ <summary>Ran {response.name}
39
+ ({this.renderCollapsibleArguments(response.arguments)})
40
+ </summary>
41
+ <pre>{this.tryPrettyPrintJson(response)}</pre>
42
+ </details>
43
+ ) : (
44
+ <span>
45
+ <Spinner /> Running {response.name}({this.renderCollapsibleArguments(response.arguments)})
46
+ </span>
47
+ )}
48
+ </h4>
49
+ );
50
+ }
51
+
52
+ protected renderCollapsibleArguments(args: string | undefined): ReactNode {
53
+ if (!args || !args.trim() || args.trim() === '{}') {
54
+ return undefined;
55
+ }
56
+
57
+ return (
58
+ <details className="collapsible-arguments">
59
+ <summary className="collapsible-arguments-summary">...</summary>
60
+ <span>{this.prettyPrintArgs(args)}</span>
61
+ </details>
62
+ );
63
+ }
42
64
 
65
+ private prettyPrintArgs(args: string): string {
66
+ try {
67
+ return JSON.stringify(JSON.parse(args), undefined, 2);
68
+ } catch (e) {
69
+ // fall through
70
+ return args;
71
+ }
43
72
  }
44
73
 
45
74
  private tryPrettyPrintJson(response: ToolCallChatResponseContent): string | undefined {
46
75
  let responseContent = response.result;
47
76
  try {
48
- if (response.result) {
49
- responseContent = JSON.stringify(JSON.parse(response.result), undefined, 2);
77
+ if (responseContent) {
78
+ if (typeof responseContent === 'string') {
79
+ responseContent = JSON.parse(responseContent);
80
+ }
81
+ responseContent = JSON.stringify(responseContent, undefined, 2);
50
82
  }
51
83
  } catch (e) {
84
+ if (typeof responseContent !== 'string') {
85
+ responseContent = `The content could not be converted to string: '${e.message}'. This is the original content: '${responseContent}'.`;
86
+ }
52
87
  // fall through
53
88
  }
54
89
  return responseContent;
@@ -14,12 +14,15 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
  import {
17
+ ChatAgent,
17
18
  ChatAgentService,
18
19
  ChatModel,
19
20
  ChatProgressMessage,
20
21
  ChatRequestModel,
21
22
  ChatResponseContent,
22
23
  ChatResponseModel,
24
+ ParsedChatRequestAgentPart,
25
+ ParsedChatRequestVariablePart,
23
26
  } from '@theia/ai-chat';
24
27
  import { CommandRegistry, ContributionProvider } from '@theia/core';
25
28
  import {
@@ -27,9 +30,11 @@ import {
27
30
  CommonCommands,
28
31
  CompositeTreeNode,
29
32
  ContextMenuRenderer,
33
+ HoverService,
30
34
  Key,
31
35
  KeyCode,
32
36
  NodeProps,
37
+ OpenerService,
33
38
  TreeModel,
34
39
  TreeNode,
35
40
  TreeProps,
@@ -46,6 +51,7 @@ import * as React from '@theia/core/shared/react';
46
51
  import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution';
47
52
  import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
48
53
  import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';
54
+ import { AIVariableService } from '@theia/ai-core';
49
55
 
50
56
  // TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model
51
57
  export interface RequestNode extends TreeNode {
@@ -77,9 +83,18 @@ export class ChatViewTreeWidget extends TreeWidget {
77
83
  @inject(ChatAgentService)
78
84
  protected chatAgentService: ChatAgentService;
79
85
 
86
+ @inject(AIVariableService)
87
+ protected readonly variableService: AIVariableService;
88
+
80
89
  @inject(CommandRegistry)
81
90
  private commandRegistry: CommandRegistry;
82
91
 
92
+ @inject(OpenerService)
93
+ protected readonly openerService: OpenerService;
94
+
95
+ @inject(HoverService)
96
+ private hoverService: HoverService;
97
+
83
98
  protected _shouldScrollToEnd = true;
84
99
 
85
100
  protected isEnabled = false;
@@ -267,22 +282,39 @@ export class ChatViewTreeWidget extends TreeWidget {
267
282
 
268
283
  private renderAgent(node: RequestNode | ResponseNode): React.ReactNode {
269
284
  const inProgress = isResponseNode(node) && !node.response.isComplete && !node.response.isCanceled && !node.response.isError;
285
+ const waitingForInput = isResponseNode(node) && node.response.isWaitingForInput;
270
286
  const toolbarContributions = !inProgress
271
287
  ? this.chatNodeToolbarActionContributions.getContributions()
272
288
  .flatMap(c => c.getToolbarActions(node))
273
289
  .filter(action => this.commandRegistry.isEnabled(action.commandId, node))
274
290
  .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
275
291
  : [];
292
+ const agentLabel = React.createRef<HTMLHeadingElement>();
293
+ const agentDescription = this.getAgent(node)?.description;
276
294
  return <React.Fragment>
277
295
  <div className='theia-ChatNodeHeader'>
278
296
  <div className={`theia-AgentAvatar ${this.getAgentIconClassName(node)}`}></div>
279
- <h3 className='theia-AgentLabel'>{this.getAgentLabel(node)}</h3>
280
- {inProgress && <span className='theia-ChatContentInProgress'>Generating</span>}
297
+ <h3 ref={agentLabel}
298
+ className='theia-AgentLabel'
299
+ onMouseEnter={() => {
300
+ if (agentDescription) {
301
+ this.hoverService.requestHover({
302
+ content: agentDescription,
303
+ target: agentLabel.current!,
304
+ position: 'right'
305
+ });
306
+ }
307
+ }}>
308
+ {this.getAgentLabel(node)}
309
+ </h3>
310
+ {inProgress && !waitingForInput && <span className='theia-ChatContentInProgress'>Generating</span>}
311
+ {inProgress && waitingForInput && <span className='theia-ChatContentInProgress'>Waiting for input</span>}
281
312
  <div className='theia-ChatNodeToolbar'>
282
313
  {!inProgress &&
283
314
  toolbarContributions.length > 0 &&
284
315
  toolbarContributions.map(action =>
285
316
  <span
317
+ key={action.commandId}
286
318
  className={`theia-ChatNodeToolbarAction ${action.icon}`}
287
319
  title={action.tooltip}
288
320
  onClick={e => {
@@ -308,8 +340,14 @@ export class ChatViewTreeWidget extends TreeWidget {
308
340
  // TODO find user name
309
341
  return 'You';
310
342
  }
311
- const agent = node.response.agentId ? this.chatAgentService.getAgent(node.response.agentId) : undefined;
312
- return agent?.name ?? 'AI';
343
+ return this.getAgent(node)?.name ?? 'AI';
344
+ }
345
+
346
+ private getAgent(node: RequestNode | ResponseNode): ChatAgent | undefined {
347
+ if (isRequestNode(node)) {
348
+ return undefined;
349
+ }
350
+ return node.response.agentId ? this.chatAgentService.getAgent(node.response.agentId) : undefined;
313
351
  }
314
352
 
315
353
  private getAgentIconClassName(node: RequestNode | ResponseNode): string | undefined {
@@ -331,7 +369,13 @@ export class ChatViewTreeWidget extends TreeWidget {
331
369
  }
332
370
 
333
371
  private renderChatRequest(node: RequestNode): React.ReactNode {
334
- return <ChatRequestRender node={node} />;
372
+ return <ChatRequestRender
373
+ node={node}
374
+ hoverService={this.hoverService}
375
+ chatAgentService={this.chatAgentService}
376
+ variableService={this.variableService}
377
+ openerService={this.openerService}
378
+ />;
335
379
  }
336
380
 
337
381
  private renderChatResponse(node: ResponseNode): React.ReactNode {
@@ -339,12 +383,28 @@ export class ChatViewTreeWidget extends TreeWidget {
339
383
  <div className={'theia-ResponseNode'}>
340
384
  {!node.response.isComplete
341
385
  && node.response.response.content.length === 0
342
- && node.response.progressMessages.map((c, i) =>
343
- <ProgressMessage {...c} key={`${node.id}-progress-${i}`} />
344
- )}
386
+ && node.response.progressMessages
387
+ .filter(c => c.show === 'untilFirstContent')
388
+ .map((c, i) =>
389
+ <ProgressMessage {...c} key={`${node.id}-progress-untilFirstContent-${i}`} />
390
+ )
391
+ }
345
392
  {node.response.response.content.map((c, i) =>
346
393
  <div className='theia-ResponseNode-Content' key={`${node.id}-content-${i}`}>{this.getChatResponsePartRenderer(c, node)}</div>
347
394
  )}
395
+ {!node.response.isComplete
396
+ && node.response.progressMessages
397
+ .filter(c => c.show === 'whileIncomplete')
398
+ .map((c, i) =>
399
+ <ProgressMessage {...c} key={`${node.id}-progress-whileIncomplete-${i}`} />
400
+ )
401
+ }
402
+ {node.response.progressMessages
403
+ .filter(c => c.show === 'forever')
404
+ .map((c, i) =>
405
+ <ProgressMessage {...c} key={`${node.id}-progress-afterComplete-${i}`} />
406
+ )
407
+ }
348
408
  </div>
349
409
  );
350
410
  }
@@ -375,11 +435,80 @@ export class ChatViewTreeWidget extends TreeWidget {
375
435
  }
376
436
  }
377
437
 
378
- const ChatRequestRender = ({ node }: { node: RequestNode }) => {
379
- const text = node.request.request.displayText ?? node.request.request.text;
380
- const ref = useMarkdownRendering(text);
438
+ const ChatRequestRender = (
439
+ {
440
+ node, hoverService, chatAgentService, variableService, openerService
441
+ }: {
442
+ node: RequestNode,
443
+ hoverService: HoverService,
444
+ chatAgentService: ChatAgentService,
445
+ variableService: AIVariableService,
446
+ openerService: OpenerService
447
+ }) => {
448
+ const parts = node.request.message.parts;
449
+ return (
450
+ <div className="theia-RequestNode">
451
+ <p>
452
+ {parts.map((part, index) => {
453
+ if (part instanceof ParsedChatRequestAgentPart || part instanceof ParsedChatRequestVariablePart) {
454
+ let description = undefined;
455
+ let className = '';
456
+ if (part instanceof ParsedChatRequestAgentPart) {
457
+ description = chatAgentService.getAgent(part.agentId)?.description;
458
+ className = 'theia-RequestNode-AgentLabel';
459
+ } else if (part instanceof ParsedChatRequestVariablePart) {
460
+ description = variableService.getVariable(part.variableName)?.description;
461
+ className = 'theia-RequestNode-VariableLabel';
462
+ }
463
+ return (
464
+ <HoverableLabel
465
+ key={index}
466
+ text={part.text}
467
+ description={description}
468
+ hoverService={hoverService}
469
+ className={className}
470
+ />
471
+ );
472
+ } else {
473
+ // maintain the leading and trailing spaces with explicit `&nbsp;`, otherwise they would get trimmed by the markdown renderer
474
+ const ref = useMarkdownRendering(part.text.replace(/^\s|\s$/g, '&nbsp;'), openerService, true);
475
+ return (
476
+ <span key={index} ref={ref}></span>
477
+ );
478
+ }
479
+ })}
480
+ </p>
481
+ </div>
482
+ );
483
+ };
381
484
 
382
- return <div className={'theia-RequestNode'} ref={ref}></div>;
485
+ const HoverableLabel = (
486
+ {
487
+ text, description, hoverService, className
488
+ }: {
489
+ text: string,
490
+ description?: string,
491
+ hoverService: HoverService,
492
+ className: string
493
+ }) => {
494
+ const spanRef = React.createRef<HTMLSpanElement>();
495
+ return (
496
+ <span
497
+ className={className}
498
+ ref={spanRef}
499
+ onMouseEnter={() => {
500
+ if (description) {
501
+ hoverService.requestHover({
502
+ content: description,
503
+ target: spanRef.current!,
504
+ position: 'right'
505
+ });
506
+ }
507
+ }}
508
+ >
509
+ {text}
510
+ </span>
511
+ );
383
512
  };
384
513
 
385
514
  const ProgressMessage = (c: ChatProgressMessage) => (
@@ -39,7 +39,6 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
39
39
  private providers: ContributionProvider<ToolProvider>;
40
40
 
41
41
  onStart(_app: FrontendApplication): MaybePromise<void> {
42
- console.log('ChatViewLanguageContribution started');
43
42
  monaco.languages.register({ id: CHAT_VIEW_LANGUAGE_ID, extensions: [CHAT_VIEW_LANGUAGE_EXTENSION] });
44
43
 
45
44
  monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
@@ -14,8 +14,8 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
  import { CommandService, deepClone, Emitter, Event, MessageService } from '@theia/core';
17
- import { ChatRequest, ChatRequestModel, ChatRequestModelImpl, ChatService, ChatSession } from '@theia/ai-chat';
18
- import { BaseWidget, codicon, ExtractableWidget, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
17
+ import { ChatRequest, ChatRequestModel, ChatService, ChatSession } from '@theia/ai-chat';
18
+ import { BaseWidget, codicon, ExtractableWidget, Message, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
19
19
  import { nls } from '@theia/core/lib/common/nls';
20
20
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
21
21
  import { AIChatInputWidget } from './chat-input-widget';
@@ -93,6 +93,8 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
93
93
  this.inputWidget.onQuery = this.onQuery.bind(this);
94
94
  this.inputWidget.onCancel = this.onCancel.bind(this);
95
95
  this.inputWidget.chatModel = this.chatSession.model;
96
+ this.inputWidget.onDeleteChangeSet = this.onDeleteChangeSet.bind(this);
97
+ this.inputWidget.onDeleteChangeSetElement = this.onDeleteChangeSetElement.bind(this);
96
98
  this.treeWidget.trackChatModel(this.chatSession.model);
97
99
 
98
100
  this.initListeners();
@@ -125,6 +127,11 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
125
127
  );
126
128
  }
127
129
 
130
+ protected override onActivateRequest(msg: Message): void {
131
+ super.onActivateRequest(msg);
132
+ this.inputWidget.activate();
133
+ }
134
+
128
135
  storeState(): object {
129
136
  return this.state;
130
137
  }
@@ -160,7 +167,7 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
160
167
  const requestProgress = await this.chatService.sendRequest(this.chatSession.id, chatRequest);
161
168
  requestProgress?.responseCompleted.then(responseModel => {
162
169
  if (responseModel.isError) {
163
- this.messageService.error(responseModel.errorObject?.message ?? 'An error occurred druring chat service invocation.');
170
+ this.messageService.error(responseModel.errorObject?.message ?? 'An error occurred during chat service invocation.');
164
171
  }
165
172
  });
166
173
  if (!requestProgress) {
@@ -171,9 +178,15 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
171
178
  }
172
179
 
173
180
  protected onCancel(requestModel: ChatRequestModel): void {
174
- // TODO we should pass a cancellation token with the request (or retrieve one from the request invocation) so we can cleanly cancel here
175
- // For now we cancel manually via casting
176
- (requestModel as ChatRequestModelImpl).response.cancel();
181
+ this.chatService.cancelRequest(requestModel.session.id, requestModel.id);
182
+ }
183
+
184
+ protected onDeleteChangeSet(sessionId: string): void {
185
+ this.chatService.deleteChangeSet(sessionId);
186
+ }
187
+
188
+ protected onDeleteChangeSetElement(sessionId: string, index: number): void {
189
+ this.chatService.deleteChangeSetElement(sessionId, index);
177
190
  }
178
191
 
179
192
  lock(): void {