@theia/ai-chat-ui 1.46.0-next.241

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 (123) hide show
  1. package/README.md +33 -0
  2. package/lib/browser/ai-chat-ui-contribution.d.ts +24 -0
  3. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -0
  4. package/lib/browser/ai-chat-ui-contribution.js +161 -0
  5. package/lib/browser/ai-chat-ui-contribution.js.map +1 -0
  6. package/lib/browser/ai-chat-ui-frontend-module.d.ts +5 -0
  7. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -0
  8. package/lib/browser/ai-chat-ui-frontend-module.js +87 -0
  9. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -0
  10. package/lib/browser/chat-input-widget.d.ts +32 -0
  11. package/lib/browser/chat-input-widget.d.ts.map +1 -0
  12. package/lib/browser/chat-input-widget.js +217 -0
  13. package/lib/browser/chat-input-widget.js.map +1 -0
  14. package/lib/browser/chat-node-toolbar-action-contribution.d.ts +47 -0
  15. package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -0
  16. package/lib/browser/chat-node-toolbar-action-contribution.js +25 -0
  17. package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -0
  18. package/lib/browser/chat-response-part-renderer.d.ts +10 -0
  19. package/lib/browser/chat-response-part-renderer.d.ts.map +1 -0
  20. package/lib/browser/chat-response-part-renderer.js +20 -0
  21. package/lib/browser/chat-response-part-renderer.js.map +1 -0
  22. package/lib/browser/chat-response-renderer/ai-editor-manager.d.ts +36 -0
  23. package/lib/browser/chat-response-renderer/ai-editor-manager.d.ts.map +1 -0
  24. package/lib/browser/chat-response-renderer/ai-editor-manager.js +184 -0
  25. package/lib/browser/chat-response-renderer/ai-editor-manager.js.map +1 -0
  26. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts +45 -0
  27. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -0
  28. package/lib/browser/chat-response-renderer/code-part-renderer.js +190 -0
  29. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -0
  30. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts +12 -0
  31. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts.map +1 -0
  32. package/lib/browser/chat-response-renderer/command-part-renderer.js +70 -0
  33. package/lib/browser/chat-response-renderer/command-part-renderer.js.map +1 -0
  34. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts +9 -0
  35. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts.map +1 -0
  36. package/lib/browser/chat-response-renderer/error-part-renderer.js +40 -0
  37. package/lib/browser/chat-response-renderer/error-part-renderer.js.map +1 -0
  38. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts +12 -0
  39. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts.map +1 -0
  40. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.js +54 -0
  41. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.js.map +1 -0
  42. package/lib/browser/chat-response-renderer/index.d.ts +9 -0
  43. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -0
  44. package/lib/browser/chat-response-renderer/index.js +27 -0
  45. package/lib/browser/chat-response-renderer/index.js.map +1 -0
  46. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +25 -0
  47. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -0
  48. package/lib/browser/chat-response-renderer/markdown-part-renderer.js +89 -0
  49. package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -0
  50. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +10 -0
  51. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -0
  52. package/lib/browser/chat-response-renderer/question-part-renderer.js +43 -0
  53. package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -0
  54. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts +9 -0
  55. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts.map +1 -0
  56. package/lib/browser/chat-response-renderer/text-part-renderer.js +41 -0
  57. package/lib/browser/chat-response-renderer/text-part-renderer.js.map +1 -0
  58. package/lib/browser/chat-response-renderer/text-part-renderer.spec.d.ts +2 -0
  59. package/lib/browser/chat-response-renderer/text-part-renderer.spec.d.ts.map +1 -0
  60. package/lib/browser/chat-response-renderer/text-part-renderer.spec.js +46 -0
  61. package/lib/browser/chat-response-renderer/text-part-renderer.spec.js.map +1 -0
  62. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +12 -0
  63. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -0
  64. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +81 -0
  65. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -0
  66. package/lib/browser/chat-tree-view/chat-view-tree-container.d.ts +4 -0
  67. package/lib/browser/chat-tree-view/chat-view-tree-container.d.ts.map +1 -0
  68. package/lib/browser/chat-tree-view/chat-view-tree-container.js +33 -0
  69. package/lib/browser/chat-tree-view/chat-view-tree-container.js.map +1 -0
  70. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +56 -0
  71. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -0
  72. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +388 -0
  73. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -0
  74. package/lib/browser/chat-tree-view/index.d.ts +3 -0
  75. package/lib/browser/chat-tree-view/index.d.ts.map +1 -0
  76. package/lib/browser/chat-tree-view/index.js +21 -0
  77. package/lib/browser/chat-tree-view/index.js.map +1 -0
  78. package/lib/browser/chat-view-commands.d.ts +8 -0
  79. package/lib/browser/chat-view-commands.d.ts.map +1 -0
  80. package/lib/browser/chat-view-commands.js +44 -0
  81. package/lib/browser/chat-view-commands.js.map +1 -0
  82. package/lib/browser/chat-view-contribution.d.ts +18 -0
  83. package/lib/browser/chat-view-contribution.d.ts.map +1 -0
  84. package/lib/browser/chat-view-contribution.js +153 -0
  85. package/lib/browser/chat-view-contribution.js.map +1 -0
  86. package/lib/browser/chat-view-language-contribution.d.ts +20 -0
  87. package/lib/browser/chat-view-language-contribution.d.ts.map +1 -0
  88. package/lib/browser/chat-view-language-contribution.js +98 -0
  89. package/lib/browser/chat-view-language-contribution.js.map +1 -0
  90. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts +11 -0
  91. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -0
  92. package/lib/browser/chat-view-widget-toolbar-contribution.js +65 -0
  93. package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -0
  94. package/lib/browser/chat-view-widget.d.ts +41 -0
  95. package/lib/browser/chat-view-widget.d.ts.map +1 -0
  96. package/lib/browser/chat-view-widget.js +182 -0
  97. package/lib/browser/chat-view-widget.js.map +1 -0
  98. package/package.json +59 -0
  99. package/src/browser/ai-chat-ui-contribution.ts +171 -0
  100. package/src/browser/ai-chat-ui-frontend-module.ts +105 -0
  101. package/src/browser/chat-input-widget.tsx +262 -0
  102. package/src/browser/chat-node-toolbar-action-contribution.ts +63 -0
  103. package/src/browser/chat-response-part-renderer.ts +25 -0
  104. package/src/browser/chat-response-renderer/ai-editor-manager.ts +183 -0
  105. package/src/browser/chat-response-renderer/code-part-renderer.tsx +211 -0
  106. package/src/browser/chat-response-renderer/command-part-renderer.tsx +60 -0
  107. package/src/browser/chat-response-renderer/error-part-renderer.tsx +35 -0
  108. package/src/browser/chat-response-renderer/horizontal-layout-part-renderer.tsx +59 -0
  109. package/src/browser/chat-response-renderer/index.ts +23 -0
  110. package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +92 -0
  111. package/src/browser/chat-response-renderer/question-part-renderer.tsx +59 -0
  112. package/src/browser/chat-response-renderer/text-part-renderer.spec.ts +50 -0
  113. package/src/browser/chat-response-renderer/text-part-renderer.tsx +35 -0
  114. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +89 -0
  115. package/src/browser/chat-tree-view/chat-view-tree-container.ts +32 -0
  116. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +526 -0
  117. package/src/browser/chat-tree-view/index.ts +18 -0
  118. package/src/browser/chat-view-commands.ts +45 -0
  119. package/src/browser/chat-view-contribution.ts +154 -0
  120. package/src/browser/chat-view-language-contribution.ts +141 -0
  121. package/src/browser/chat-view-widget-toolbar-contribution.tsx +54 -0
  122. package/src/browser/chat-view-widget.tsx +194 -0
  123. package/src/browser/style/index.css +415 -0
@@ -0,0 +1,262 @@
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 { ChatAgent, ChatAgentService, ChatModel, ChatRequestModel } from '@theia/ai-chat';
17
+ import { UntitledResourceResolver } from '@theia/core';
18
+ import { ContextMenuRenderer, Message, ReactWidget } from '@theia/core/lib/browser';
19
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
20
+ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
21
+ import * as React from '@theia/core/shared/react';
22
+ import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
23
+ import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
24
+ import { IMouseEvent } from '@theia/monaco-editor-core';
25
+
26
+ type Query = (query: string) => Promise<void>;
27
+ type Cancel = (requestModel: ChatRequestModel) => void;
28
+
29
+ @injectable()
30
+ export class AIChatInputWidget extends ReactWidget {
31
+ public static ID = 'chat-input-widget';
32
+ static readonly CONTEXT_MENU = ['chat-input-context-menu'];
33
+
34
+ @inject(ChatAgentService)
35
+ protected readonly agentService: ChatAgentService;
36
+
37
+ @inject(MonacoEditorProvider)
38
+ protected readonly editorProvider: MonacoEditorProvider;
39
+
40
+ @inject(UntitledResourceResolver)
41
+ protected readonly untitledResourceResolver: UntitledResourceResolver;
42
+
43
+ @inject(ContextMenuRenderer)
44
+ protected readonly contextMenuRenderer: ContextMenuRenderer;
45
+
46
+ protected isEnabled = false;
47
+
48
+ private _onQuery: Query;
49
+ set onQuery(query: Query) {
50
+ this._onQuery = query;
51
+ }
52
+ private _onCancel: Cancel;
53
+ set onCancel(cancel: Cancel) {
54
+ this._onCancel = cancel;
55
+ }
56
+ private _chatModel: ChatModel;
57
+ set chatModel(chatModel: ChatModel) {
58
+ this._chatModel = chatModel;
59
+ this.update();
60
+ }
61
+
62
+ @postConstruct()
63
+ protected init(): void {
64
+ this.id = AIChatInputWidget.ID;
65
+ this.title.closable = false;
66
+ this.update();
67
+ }
68
+ protected override onActivateRequest(msg: Message): void {
69
+ super.onActivateRequest(msg);
70
+ this.node.focus({ preventScroll: true });
71
+ }
72
+
73
+ protected getChatAgents(): ChatAgent[] {
74
+ return this.agentService.getAgents();
75
+ }
76
+
77
+ protected render(): React.ReactNode {
78
+ return (
79
+ <ChatInput
80
+ onQuery={this._onQuery.bind(this)}
81
+ onCancel={this._onCancel.bind(this)}
82
+ chatModel={this._chatModel}
83
+ getChatAgents={this.getChatAgents.bind(this)}
84
+ editorProvider={this.editorProvider}
85
+ untitledResourceResolver={this.untitledResourceResolver}
86
+ contextMenuCallback={this.handleContextMenu.bind(this)}
87
+ isEnabled={this.isEnabled}
88
+ />
89
+ );
90
+ }
91
+
92
+ public setEnabled(enabled: boolean): void {
93
+ this.isEnabled = enabled;
94
+ this.update();
95
+ }
96
+
97
+ protected handleContextMenu(event: IMouseEvent): void {
98
+ this.contextMenuRenderer.render({
99
+ menuPath: AIChatInputWidget.CONTEXT_MENU,
100
+ anchor: { x: event.posx, y: event.posy },
101
+ });
102
+ event.preventDefault();
103
+ }
104
+
105
+ }
106
+
107
+ interface ChatInputProperties {
108
+ onCancel: (requestModel: ChatRequestModel) => void;
109
+ onQuery: (query: string) => void;
110
+ isEnabled?: boolean;
111
+ chatModel: ChatModel;
112
+ getChatAgents: () => ChatAgent[];
113
+ editorProvider: MonacoEditorProvider;
114
+ untitledResourceResolver: UntitledResourceResolver;
115
+ contextMenuCallback: (event: IMouseEvent) => void;
116
+ }
117
+ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
118
+
119
+ const [inProgress, setInProgress] = React.useState(false);
120
+ // eslint-disable-next-line no-null/no-null
121
+ const editorContainerRef = React.useRef<HTMLDivElement | null>(null);
122
+ // eslint-disable-next-line no-null/no-null
123
+ const placeholderRef = React.useRef<HTMLDivElement | null>(null);
124
+ const editorRef = React.useRef<MonacoEditor | undefined>(undefined);
125
+ const allRequests = props.chatModel.getRequests();
126
+ const lastRequest = allRequests.length === 0 ? undefined : allRequests[allRequests.length - 1];
127
+
128
+ const createInputElement = async () => {
129
+ const paddingTop = 8;
130
+ const lineHeight = 20;
131
+ const maxHeight = 240;
132
+ const resource = await props.untitledResourceResolver.createUntitledResource('', CHAT_VIEW_LANGUAGE_EXTENSION);
133
+ const editor = await props.editorProvider.createInline(resource.uri, editorContainerRef.current!, {
134
+ language: CHAT_VIEW_LANGUAGE_EXTENSION,
135
+ // Disable code lens, inlay hints and hover support to avoid console errors from other contributions
136
+ codeLens: false,
137
+ inlayHints: { enabled: 'off' },
138
+ hover: { enabled: false },
139
+ autoSizing: false, // we handle the sizing ourselves
140
+ scrollBeyondLastLine: false,
141
+ scrollBeyondLastColumn: 0,
142
+ minHeight: 1,
143
+ fontFamily: 'var(--theia-ui-font-family)',
144
+ fontSize: 13,
145
+ cursorWidth: 1,
146
+ maxHeight: -1,
147
+ scrollbar: { horizontal: 'hidden' },
148
+ automaticLayout: true,
149
+ lineNumbers: 'off',
150
+ lineHeight,
151
+ padding: { top: paddingTop },
152
+ suggest: {
153
+ showIcons: true,
154
+ showSnippets: false,
155
+ showWords: false,
156
+ showStatusBar: false,
157
+ insertMode: 'replace',
158
+ },
159
+ bracketPairColorization: { enabled: false },
160
+ wrappingStrategy: 'advanced',
161
+ stickyScroll: { enabled: false },
162
+ });
163
+
164
+ if (editorContainerRef.current) {
165
+ editorContainerRef.current.style.height = (lineHeight + (2 * paddingTop)) + 'px';
166
+ }
167
+
168
+ const updateEditorHeight = () => {
169
+ if (editorContainerRef.current) {
170
+ const contentHeight = editor.getControl().getContentHeight() + paddingTop;
171
+ editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeight)}px`;
172
+ }
173
+ };
174
+ editor.getControl().onDidChangeModelContent(updateEditorHeight);
175
+ const resizeObserver = new ResizeObserver(updateEditorHeight);
176
+ if (editorContainerRef.current) {
177
+ resizeObserver.observe(editorContainerRef.current);
178
+ }
179
+ editor.getControl().onDidDispose(() => {
180
+ resizeObserver.disconnect();
181
+ });
182
+
183
+ editor.getControl().onContextMenu(e =>
184
+ props.contextMenuCallback(e.event)
185
+ );
186
+
187
+ editorRef.current = editor;
188
+ };
189
+
190
+ React.useEffect(() => {
191
+ createInputElement();
192
+ return () => {
193
+ if (editorRef.current) {
194
+ editorRef.current.dispose();
195
+ }
196
+ };
197
+ }, []);
198
+
199
+ React.useEffect(() => {
200
+ const listener = lastRequest?.response.onDidChange(() => {
201
+ if (lastRequest.response.isCanceled || lastRequest.response.isComplete || lastRequest.response.isError) {
202
+ setInProgress(false);
203
+ }
204
+ });
205
+ return () => listener?.dispose();
206
+ }, [lastRequest]);
207
+
208
+ function submit(value: string): void {
209
+ setInProgress(true);
210
+ props.onQuery(value);
211
+ if (editorRef.current) {
212
+ editorRef.current.document.textEditorModel.setValue('');
213
+ }
214
+ }
215
+
216
+ const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
217
+ if (!props.isEnabled) {
218
+ return;
219
+ }
220
+ if (event.key === 'Enter' && !event.shiftKey) {
221
+ event.preventDefault();
222
+ submit(editorRef.current?.document.textEditorModel.getValue() || '');
223
+ }
224
+ }, [props.isEnabled]);
225
+
226
+ const handleInputFocus = () => {
227
+ placeholderRef.current?.classList.add('hidden');
228
+ };
229
+
230
+ const handleInputBlur = () => {
231
+ if (!editorRef.current?.getControl().getValue()) {
232
+ placeholderRef.current?.classList.remove('hidden');
233
+ }
234
+ };
235
+
236
+ return <div className='theia-ChatInput'>
237
+ <div className='theia-ChatInput-Editor-Box'>
238
+ <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
239
+ <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>Ask a question</div>
240
+ </div>
241
+ </div>
242
+ <div className="theia-ChatInputOptions">
243
+ {
244
+ inProgress ? <span
245
+ className="codicon codicon-stop-circle option"
246
+ title="Cancel (Esc)"
247
+ onClick={() => {
248
+ if (lastRequest) {
249
+ props.onCancel(lastRequest);
250
+ }
251
+ setInProgress(false);
252
+ }} /> :
253
+ <span
254
+ className="codicon codicon-send option"
255
+ title="Send (Enter)"
256
+ onClick={!props.isEnabled ? undefined : () => submit(editorRef.current?.document.textEditorModel.getValue() || '')}
257
+ style={{ cursor: !props.isEnabled ? 'default' : 'pointer', opacity: !props.isEnabled ? 0.5 : 1 }}
258
+ />
259
+ }
260
+ </div>
261
+ </div>;
262
+ };
@@ -0,0 +1,63 @@
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 { RequestNode, ResponseNode } from './chat-tree-view';
17
+
18
+ export interface ChatNodeToolbarAction {
19
+ /**
20
+ * The command to execute when the item is selected. The handler will receive the `RequestNode` or `ResponseNode` as first argument.
21
+ */
22
+ commandId: string;
23
+ /**
24
+ * Icon class name(s) for the item (e.g. 'codicon codicon-feedback').
25
+ */
26
+ icon: string;
27
+ /**
28
+ * Priority among the items. Can be negative. The smaller the number the left-most the item will be placed in the toolbar. It is `0` by default.
29
+ */
30
+ priority?: number;
31
+ /**
32
+ * Optional tooltip for the item.
33
+ */
34
+ tooltip?: string;
35
+ }
36
+
37
+ /**
38
+ * Clients implement this interface if they want to contribute to the toolbar of chat nodes.
39
+ *
40
+ * ### Example
41
+ * ```ts
42
+ * bind(ChatNodeToolbarActionContribution).toDynamicValue(context => ({
43
+ * getToolbarActions: (args: RequestNode | ResponseNode) => {
44
+ * if (isResponseNode(args)) {
45
+ * return [{
46
+ * commandId: 'core.about',
47
+ * icon: 'codicon codicon-feedback',
48
+ * tooltip: 'Show about dialog on response nodes'
49
+ * }];
50
+ * } else {
51
+ * return [];
52
+ * }
53
+ * }
54
+ * }));
55
+ * ```
56
+ */
57
+ export const ChatNodeToolbarActionContribution = Symbol('ChatNodeToolbarActionContribution');
58
+ export interface ChatNodeToolbarActionContribution {
59
+ /**
60
+ * Returns the toolbar actions for the given node.
61
+ */
62
+ getToolbarActions(node: RequestNode | ResponseNode): ChatNodeToolbarAction[];
63
+ }
@@ -0,0 +1,25 @@
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
+
17
+ import { ChatResponseContent } from '@theia/ai-chat/lib/common';
18
+ import { ReactNode } from '@theia/core/shared/react';
19
+ import { ResponseNode } from './chat-tree-view/chat-view-tree-widget';
20
+
21
+ export const ChatResponsePartRenderer = Symbol('ChatResponsePartRenderer');
22
+ export interface ChatResponsePartRenderer<T extends ChatResponseContent> {
23
+ canHandle(response: ChatResponseContent): number;
24
+ render(response: T, parentNode: ResponseNode): ReactNode;
25
+ }
@@ -0,0 +1,183 @@
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
+
17
+ import { CancellationToken, ContributionProvider, Prioritizeable, RecursivePartial, URI } from '@theia/core';
18
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
19
+ import { EditorOpenerOptions, EditorWidget, Range } from '@theia/editor/lib/browser';
20
+
21
+ import { EditorPreviewManager } from '@theia/editor-preview/lib/browser/editor-preview-manager';
22
+ import { DocumentSymbol } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
23
+ import { TextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
24
+ import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
25
+ import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
26
+ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
27
+ import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
28
+
29
+ /** Regex to match GitHub-style position and range declaration with line (L) and column (C) */
30
+ export const LOCATION_REGEX = /#L(\d+)?(?:C(\d+))?(?:-L(\d+)?(?:C(\d+))?)?$/;
31
+
32
+ export const AIEditorSelectionResolver = Symbol('AIEditorSelectionResolver');
33
+ export interface AIEditorSelectionResolver {
34
+ /**
35
+ * The priority of the resolver. A higher value resolver will be called before others.
36
+ */
37
+ priority?: number;
38
+ resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise<RecursivePartial<Range> | undefined>
39
+ }
40
+
41
+ @injectable()
42
+ export class GitHubSelectionResolver implements AIEditorSelectionResolver {
43
+ priority = 100;
44
+
45
+ async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise<RecursivePartial<Range> | undefined> {
46
+ if (!uri) {
47
+ return;
48
+ }
49
+ // We allow the GitHub syntax of selecting a range in markdown 'L1', 'L1-L2' 'L1-C1_L2-C2' (starting at line 1 and column 1)
50
+ const match = uri?.toString().match(LOCATION_REGEX);
51
+ if (!match) {
52
+ return;
53
+ }
54
+ // we need to adapt the position information from one-based (in GitHub) to zero-based (in Theia)
55
+ const startLine = match[1] ? parseInt(match[1], 10) - 1 : undefined;
56
+ // if no start column is given, we assume the start of the line
57
+ const startColumn = match[2] ? parseInt(match[2], 10) - 1 : 0;
58
+ const endLine = match[3] ? parseInt(match[3], 10) - 1 : undefined;
59
+ // if no end column is given, we assume the end of the line
60
+ const endColumn = match[4] ? parseInt(match[4], 10) - 1 : endLine ? widget.editor.document.getLineMaxColumn(endLine) : undefined;
61
+
62
+ return {
63
+ start: { line: startLine, character: startColumn },
64
+ end: { line: endLine, character: endColumn }
65
+ };
66
+ }
67
+ }
68
+
69
+ @injectable()
70
+ export class TypeDocSymbolSelectionResolver implements AIEditorSelectionResolver {
71
+ priority = 50;
72
+
73
+ @inject(MonacoToProtocolConverter) protected readonly m2p: MonacoToProtocolConverter;
74
+
75
+ async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise<RecursivePartial<Range> | undefined> {
76
+ if (!uri) {
77
+ return;
78
+ }
79
+ const editor = MonacoEditor.get(widget);
80
+ const monacoEditor = editor?.getControl();
81
+ if (!monacoEditor) {
82
+ return;
83
+ }
84
+ const symbolPath = this.findSymbolPath(uri);
85
+ if (!symbolPath) {
86
+ return;
87
+ }
88
+ const textModel = monacoEditor.getModel() as unknown as TextModel;
89
+ if (!textModel) {
90
+ return;
91
+ }
92
+
93
+ // try to find the symbol through the document symbol provider
94
+ // support referencing nested symbols by separating a dot path similar to TypeDoc
95
+ for (const provider of StandaloneServices.get(ILanguageFeaturesService).documentSymbolProvider.ordered(textModel)) {
96
+ const symbols = await provider.provideDocumentSymbols(textModel, CancellationToken.None);
97
+ const match = this.findSymbolByPath(symbols ?? [], symbolPath);
98
+ if (match) {
99
+ return this.m2p.asRange(match.selectionRange);
100
+ }
101
+ }
102
+ }
103
+
104
+ protected findSymbolPath(uri: URI): string[] | undefined {
105
+ return uri.fragment.split('.');
106
+ }
107
+
108
+ protected findSymbolByPath(symbols: DocumentSymbol[], symbolPath: string[]): DocumentSymbol | undefined {
109
+ if (!symbols || symbolPath.length === 0) {
110
+ return undefined;
111
+ }
112
+ let matchedSymbol: DocumentSymbol | undefined = undefined;
113
+ let currentSymbols = symbols;
114
+ for (const part of symbolPath) {
115
+ matchedSymbol = currentSymbols.find(symbol => symbol.name === part);
116
+ if (!matchedSymbol) {
117
+ return undefined;
118
+ }
119
+ currentSymbols = matchedSymbol.children || [];
120
+ }
121
+ return matchedSymbol;
122
+ }
123
+ }
124
+
125
+ @injectable()
126
+ export class TextFragmentSelectionResolver implements AIEditorSelectionResolver {
127
+ async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise<RecursivePartial<Range> | undefined> {
128
+ if (!uri) {
129
+ return;
130
+ }
131
+ const fragment = this.findFragment(uri);
132
+ if (!fragment) {
133
+ return;
134
+ }
135
+ const matches = widget.editor.document.findMatches?.({ isRegex: false, matchCase: false, matchWholeWord: false, searchString: fragment }) ?? [];
136
+ if (matches.length > 0) {
137
+ return {
138
+ start: {
139
+ line: matches[0].range.start.line - 1,
140
+ character: matches[0].range.start.character - 1
141
+ },
142
+ end: {
143
+ line: matches[0].range.end.line - 1,
144
+ character: matches[0].range.end.character - 1
145
+ }
146
+ };
147
+ }
148
+ }
149
+
150
+ protected findFragment(uri: URI): string | undefined {
151
+ return uri.fragment;
152
+ }
153
+ }
154
+
155
+ @injectable()
156
+ export class AIEditorManager extends EditorPreviewManager {
157
+ @inject(ContributionProvider) @named(AIEditorSelectionResolver)
158
+ protected readonly resolvers: ContributionProvider<AIEditorSelectionResolver>;
159
+
160
+ protected override async revealSelection(widget: EditorWidget, options: EditorOpenerOptions = {}, uri?: URI): Promise<void> {
161
+ if (!options.selection) {
162
+ options.selection = await this.resolveSelection(options, widget, uri);
163
+ }
164
+ super.revealSelection(widget, options, uri);
165
+ }
166
+
167
+ protected async resolveSelection(options: EditorOpenerOptions, widget: EditorWidget, uri: URI | undefined): Promise<RecursivePartial<Range> | undefined> {
168
+ if (!options.selection) {
169
+ const orderedResolvers = Prioritizeable.prioritizeAllSync(this.resolvers.getContributions(), resolver => resolver.priority ?? 1);
170
+ for (const linkResolver of orderedResolvers) {
171
+ try {
172
+ const selection = await linkResolver.value.resolveSelection(widget, options, uri);
173
+ if (selection) {
174
+ return selection;
175
+ }
176
+ } catch (error) {
177
+ console.error(error);
178
+ }
179
+ }
180
+ }
181
+ return undefined;
182
+ }
183
+ }