@theia/ai-chat-ui 1.76.0-next.7 → 1.76.0
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.
- package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.js +6 -0
- package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
- package/lib/browser/chat-find/chat-find-contribution.d.ts +18 -0
- package/lib/browser/chat-find/chat-find-contribution.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-contribution.js +76 -0
- package/lib/browser/chat-find/chat-find-contribution.js.map +1 -0
- package/lib/browser/chat-find/chat-find-highlighter.d.ts +114 -0
- package/lib/browser/chat-find/chat-find-highlighter.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-highlighter.js +353 -0
- package/lib/browser/chat-find/chat-find-highlighter.js.map +1 -0
- package/lib/browser/chat-find/chat-find-highlighter.spec.d.ts +2 -0
- package/lib/browser/chat-find/chat-find-highlighter.spec.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-highlighter.spec.js +283 -0
- package/lib/browser/chat-find/chat-find-highlighter.spec.js.map +1 -0
- package/lib/browser/chat-find/chat-find-matcher.d.ts +72 -0
- package/lib/browser/chat-find/chat-find-matcher.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-matcher.js +168 -0
- package/lib/browser/chat-find/chat-find-matcher.js.map +1 -0
- package/lib/browser/chat-find/chat-find-matcher.spec.d.ts +2 -0
- package/lib/browser/chat-find/chat-find-matcher.spec.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-matcher.spec.js +145 -0
- package/lib/browser/chat-find/chat-find-matcher.spec.js.map +1 -0
- package/lib/browser/chat-find/chat-find-text.d.ts +20 -0
- package/lib/browser/chat-find/chat-find-text.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-text.js +56 -0
- package/lib/browser/chat-find/chat-find-text.js.map +1 -0
- package/lib/browser/chat-find/chat-find-widget.d.ts +96 -0
- package/lib/browser/chat-find/chat-find-widget.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-widget.js +332 -0
- package/lib/browser/chat-find/chat-find-widget.js.map +1 -0
- package/lib/browser/chat-find/chat-find-widget.spec.d.ts +2 -0
- package/lib/browser/chat-find/chat-find-widget.spec.d.ts.map +1 -0
- package/lib/browser/chat-find/chat-find-widget.spec.js +236 -0
- package/lib/browser/chat-find/chat-find-widget.spec.js.map +1 -0
- package/lib/browser/chat-input-scroll-util.d.ts +7 -0
- package/lib/browser/chat-input-scroll-util.d.ts.map +1 -0
- package/lib/browser/chat-input-scroll-util.js +33 -0
- package/lib/browser/chat-input-scroll-util.js.map +1 -0
- package/lib/browser/chat-input-scroll-util.spec.d.ts +2 -0
- package/lib/browser/chat-input-scroll-util.spec.d.ts.map +1 -0
- package/lib/browser/chat-input-scroll-util.spec.js +38 -0
- package/lib/browser/chat-input-scroll-util.spec.js.map +1 -0
- package/lib/browser/chat-input-widget.d.ts +21 -3
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +109 -50
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-input-widget.spec.js +21 -0
- package/lib/browser/chat-input-widget.spec.js.map +1 -1
- package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/code-part-renderer.js +9 -1
- package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/code-wrapper-editors.d.ts +13 -0
- package/lib/browser/chat-response-renderer/code-wrapper-editors.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/code-wrapper-editors.js +41 -0
- package/lib/browser/chat-response-renderer/code-wrapper-editors.js.map +1 -0
- package/lib/browser/chat-response-renderer/delegation-tool-renderer.js +74 -43
- package/lib/browser/chat-response-renderer/delegation-tool-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/index.d.ts +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/index.js +1 -0
- package/lib/browser/chat-response-renderer/index.js.map +1 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +9 -0
- package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js +34 -16
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/server-toolcall-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/server-toolcall-part-renderer.js +2 -2
- package/lib/browser/chat-response-renderer/server-toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/spinner.d.ts +3 -0
- package/lib/browser/chat-response-renderer/spinner.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/spinner.js +24 -0
- package/lib/browser/chat-response-renderer/spinner.js.map +1 -0
- package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts +9 -1
- package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/thinking-part-renderer.js +93 -2
- package/lib/browser/chat-response-renderer/thinking-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.d.ts +2 -0
- package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.js +84 -0
- package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.js.map +1 -0
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +2 -2
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.spec.js +16 -0
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.spec.js.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-utils.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-utils.js +7 -2
- package/lib/browser/chat-response-renderer/toolcall-utils.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-container.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-container.js +4 -0
- package/lib/browser/chat-tree-view/chat-view-tree-container.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +20 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js +88 -7
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/common/toolcall-utils.d.ts +11 -1
- package/lib/common/toolcall-utils.d.ts.map +1 -1
- package/lib/common/toolcall-utils.js +191 -9
- package/lib/common/toolcall-utils.js.map +1 -1
- package/lib/common/toolcall-utils.spec.js +92 -4
- package/lib/common/toolcall-utils.spec.js.map +1 -1
- package/package.json +13 -13
- package/src/browser/ai-chat-ui-frontend-module.ts +7 -0
- package/src/browser/chat-find/chat-find-contribution.ts +74 -0
- package/src/browser/chat-find/chat-find-highlighter.spec.ts +313 -0
- package/src/browser/chat-find/chat-find-highlighter.ts +385 -0
- package/src/browser/chat-find/chat-find-matcher.spec.ts +173 -0
- package/src/browser/chat-find/chat-find-matcher.ts +197 -0
- package/src/browser/chat-find/chat-find-text.ts +65 -0
- package/src/browser/chat-find/chat-find-widget.spec.tsx +261 -0
- package/src/browser/chat-find/chat-find-widget.tsx +418 -0
- package/src/browser/chat-input-scroll-util.spec.ts +42 -0
- package/src/browser/chat-input-scroll-util.ts +30 -0
- package/src/browser/chat-input-widget.spec.ts +29 -0
- package/src/browser/chat-input-widget.tsx +118 -56
- package/src/browser/chat-response-renderer/code-part-renderer.tsx +9 -1
- package/src/browser/chat-response-renderer/code-wrapper-editors.ts +41 -0
- package/src/browser/chat-response-renderer/delegation-tool-renderer.tsx +88 -62
- package/src/browser/chat-response-renderer/index.ts +1 -0
- package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +34 -17
- package/src/browser/chat-response-renderer/server-toolcall-part-renderer.tsx +2 -5
- package/src/browser/chat-response-renderer/spinner.tsx +22 -0
- package/src/browser/chat-response-renderer/thinking-part-renderer.spec.tsx +104 -0
- package/src/browser/chat-response-renderer/thinking-part-renderer.tsx +129 -9
- package/src/browser/chat-response-renderer/toolcall-part-renderer.spec.ts +16 -0
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +1 -4
- package/src/browser/chat-response-renderer/toolcall-utils.ts +5 -0
- package/src/browser/chat-tree-view/chat-view-tree-container.ts +4 -0
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +99 -6
- package/src/browser/style/chat-find.css +107 -0
- package/src/browser/style/index.css +75 -8
- package/src/common/toolcall-utils.spec.ts +113 -4
- package/src/common/toolcall-utils.ts +207 -9
|
@@ -22,6 +22,7 @@ export * from './markdown-part-renderer';
|
|
|
22
22
|
export * from './mcp-app-frame';
|
|
23
23
|
export * from './mermaid-part-renderer';
|
|
24
24
|
export * from './mermaid-rendering';
|
|
25
|
+
export * from './spinner';
|
|
25
26
|
export * from './text-part-renderer';
|
|
26
27
|
export * from './toolcall-part-renderer';
|
|
27
28
|
export * from './server-toolcall-part-renderer';
|
|
@@ -77,6 +77,39 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownCh
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export namespace MarkdownRendering {
|
|
81
|
+
/**
|
|
82
|
+
* Renders markdown into a detached fragment, exactly as {@link useMarkdownRendering} does before mounting it.
|
|
83
|
+
* Also used to read the rendered text of markdown that is not mounted, e.g. to search a chat session.
|
|
84
|
+
*/
|
|
85
|
+
export function renderToFragment(markdown: string, skipSurroundingParagraph: boolean = false, blockExternalResourceLoading: boolean = true): DocumentFragment {
|
|
86
|
+
const markdownIt = markdownit({ html: true }).use(markdownitemoji.full);
|
|
87
|
+
const template = document.createElement('template');
|
|
88
|
+
|
|
89
|
+
// markdownIt always puts the content in a paragraph element, so we remove it if we don't want that
|
|
90
|
+
const html = skipSurroundingParagraph ? markdownIt.render(markdown).replace(/^<p>|<\/p>|<p><\/p>$/g, '') : markdownIt.render(markdown);
|
|
91
|
+
|
|
92
|
+
template.innerHTML = DOMPurify.sanitize(html, {
|
|
93
|
+
// DOMPurify usually strips non http(s) links from hrefs
|
|
94
|
+
// but we want to allow them (see handleClick via OpenerService in useMarkdownRendering)
|
|
95
|
+
ALLOW_UNKNOWN_PROTOCOLS: true,
|
|
96
|
+
ADD_TAGS: ['iframe', 'frame'],
|
|
97
|
+
ADD_ATTR: ['src', 'srcset', 'srcdoc', 'poster', 'href', 'xlink:href', 'data']
|
|
98
|
+
});
|
|
99
|
+
// Active embedded content is always blocked; trusted content (blockExternalResourceLoading=false)
|
|
100
|
+
// still renders its external URL resources directly.
|
|
101
|
+
blockExternalResources(template.content, blockExternalResourceLoading);
|
|
102
|
+
return template.content;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Prepares the text of a chat request text part for inline rendering. */
|
|
106
|
+
export function prepareRequestText(text: string): string {
|
|
107
|
+
return text
|
|
108
|
+
.replace(/^[\r\n]+|[\r\n]+$/g, '') // remove excessive new lines
|
|
109
|
+
.replace(/(^ )/g, ' '); // enforce keeping space before
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
80
113
|
export interface DeclaredEventsEventListenerObject extends EventListenerObject {
|
|
81
114
|
handledEvents?: (keyof HTMLElementEventMap)[];
|
|
82
115
|
}
|
|
@@ -113,24 +146,8 @@ export const useMarkdownRendering = (
|
|
|
113
146
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
114
147
|
const markdownString = typeof markdown === 'string' ? markdown : markdown.value;
|
|
115
148
|
useEffect(() => {
|
|
116
|
-
const markdownIt = markdownit({ html: true }).use(markdownitemoji.full);
|
|
117
149
|
const host = document.createElement('div');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// markdownIt always puts the content in a paragraph element, so we remove it if we don't want that
|
|
121
|
-
const html = skipSurroundingParagraph ? markdownIt.render(markdownString).replace(/^<p>|<\/p>|<p><\/p>$/g, '') : markdownIt.render(markdownString);
|
|
122
|
-
|
|
123
|
-
template.innerHTML = DOMPurify.sanitize(html, {
|
|
124
|
-
// DOMPurify usually strips non http(s) links from hrefs
|
|
125
|
-
// but we want to allow them (see handleClick via OpenerService below)
|
|
126
|
-
ALLOW_UNKNOWN_PROTOCOLS: true,
|
|
127
|
-
ADD_TAGS: ['iframe', 'frame'],
|
|
128
|
-
ADD_ATTR: ['src', 'srcset', 'srcdoc', 'poster', 'href', 'xlink:href', 'data']
|
|
129
|
-
});
|
|
130
|
-
// Active embedded content is always blocked; trusted content (blockExternalResourceLoading=false)
|
|
131
|
-
// still renders its external URL resources directly.
|
|
132
|
-
blockExternalResources(template.content, blockExternalResourceLoading);
|
|
133
|
-
host.appendChild(template.content);
|
|
150
|
+
host.appendChild(MarkdownRendering.renderToFragment(markdownString, skipSurroundingParagraph, blockExternalResourceLoading));
|
|
134
151
|
while (ref?.current?.firstChild) {
|
|
135
152
|
ref.current.removeChild(ref.current.firstChild);
|
|
136
153
|
}
|
|
@@ -20,9 +20,10 @@ import { ChatResponseContent, ServerToolCallChatResponseContent } from '@theia/a
|
|
|
20
20
|
import * as React from '@theia/core/shared/react';
|
|
21
21
|
import { ReactNode } from '@theia/core/shared/react';
|
|
22
22
|
import { nls } from '@theia/core/lib/common/nls';
|
|
23
|
-
import {
|
|
23
|
+
import { HoverService, OpenerService } from '@theia/core/lib/browser';
|
|
24
24
|
import { renderToolCallResult } from './toolcall-result';
|
|
25
25
|
import { condenseArguments, formatArgsForTooltip } from './toolcall-utils';
|
|
26
|
+
import { Spinner } from './spinner';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Renders a {@link ServerToolCallChatResponseContent}: a tool the provider executed on its own
|
|
@@ -90,10 +91,6 @@ export class ServerToolCallPartRenderer implements ChatResponsePartRenderer<Serv
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const Spinner = () => (
|
|
94
|
-
<span className={`${codicon('loading')} theia-animation-spin`}></span>
|
|
95
|
-
);
|
|
96
|
-
|
|
97
94
|
interface ServerToolCallContentProps {
|
|
98
95
|
response: ServerToolCallChatResponseContent;
|
|
99
96
|
getArgumentsLabel: (args: string | undefined) => string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 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 { codicon } from '@theia/core/lib/browser';
|
|
18
|
+
|
|
19
|
+
/** Spinning indicator shared by the chat response part renderers to signal ongoing activity. */
|
|
20
|
+
export const Spinner = () => (
|
|
21
|
+
<span className={`${codicon('loading')} theia-animation-spin`}></span>
|
|
22
|
+
);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
|
|
19
|
+
let disableJSDOM = enableJSDOM();
|
|
20
|
+
|
|
21
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
22
|
+
FrontendApplicationConfigProvider.set({});
|
|
23
|
+
|
|
24
|
+
import { ChatResponseContent, ThinkingChatResponseContent } from '@theia/ai-chat';
|
|
25
|
+
import { expect } from 'chai';
|
|
26
|
+
import * as React from '@theia/core/shared/react';
|
|
27
|
+
import { ResponseNode } from '../chat-tree-view';
|
|
28
|
+
import { ThinkingPartRenderer } from './thinking-part-renderer';
|
|
29
|
+
|
|
30
|
+
const thinking = (content: string): ThinkingChatResponseContent => ({
|
|
31
|
+
kind: 'thinking',
|
|
32
|
+
content,
|
|
33
|
+
signature: 'signature',
|
|
34
|
+
asString: () => content,
|
|
35
|
+
asDisplayString: () => content,
|
|
36
|
+
merge: () => true
|
|
37
|
+
} as unknown as ThinkingChatResponseContent);
|
|
38
|
+
|
|
39
|
+
const responseNode = (
|
|
40
|
+
content: ChatResponseContent[],
|
|
41
|
+
state: { isComplete?: boolean, isCanceled?: boolean, isError?: boolean } = {}
|
|
42
|
+
): ResponseNode => ({
|
|
43
|
+
id: 'response-1',
|
|
44
|
+
sessionId: 'session-1',
|
|
45
|
+
parent: undefined,
|
|
46
|
+
response: {
|
|
47
|
+
isComplete: false,
|
|
48
|
+
isCanceled: false,
|
|
49
|
+
isError: false,
|
|
50
|
+
...state,
|
|
51
|
+
response: { content }
|
|
52
|
+
}
|
|
53
|
+
} as unknown as ResponseNode);
|
|
54
|
+
|
|
55
|
+
/** The rendered element is a `ThinkingContent`; its `active` prop is what drives expand/collapse. */
|
|
56
|
+
const isActive = (node: React.ReactNode): boolean => (node as React.ReactElement<{ active: boolean }>).props.active;
|
|
57
|
+
|
|
58
|
+
disableJSDOM();
|
|
59
|
+
|
|
60
|
+
describe('ThinkingPartRenderer', () => {
|
|
61
|
+
|
|
62
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
63
|
+
after(() => disableJSDOM());
|
|
64
|
+
|
|
65
|
+
it('only handles thinking parts', () => {
|
|
66
|
+
const renderer = new ThinkingPartRenderer();
|
|
67
|
+
expect(renderer.canHandle(thinking('...'))).to.be.greaterThan(0);
|
|
68
|
+
expect(renderer.canHandle({ kind: 'text' } as ChatResponseContent)).to.be.lessThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('renders the thinking content', () => {
|
|
72
|
+
const renderer = new ThinkingPartRenderer();
|
|
73
|
+
const part = thinking('Let me check the imports first.');
|
|
74
|
+
const node = renderer.render(part, responseNode([part]));
|
|
75
|
+
expect(JSON.stringify(node)).to.contain('Let me check the imports first.');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('is active while it is the last part of a running response', () => {
|
|
79
|
+
const renderer = new ThinkingPartRenderer();
|
|
80
|
+
const part = thinking('...');
|
|
81
|
+
expect(isActive(renderer.render(part, responseNode([part])))).to.be.true;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('is inactive once a later part has arrived', () => {
|
|
85
|
+
const renderer = new ThinkingPartRenderer();
|
|
86
|
+
const part = thinking('...');
|
|
87
|
+
const node = responseNode([part, { kind: 'text' } as ChatResponseContent]);
|
|
88
|
+
expect(isActive(renderer.render(part, node))).to.be.false;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('is inactive for a completed, canceled or failed response', () => {
|
|
92
|
+
const renderer = new ThinkingPartRenderer();
|
|
93
|
+
const part = thinking('...');
|
|
94
|
+
expect(isActive(renderer.render(part, responseNode([part], { isComplete: true })))).to.be.false;
|
|
95
|
+
expect(isActive(renderer.render(part, responseNode([part], { isCanceled: true })))).to.be.false;
|
|
96
|
+
expect(isActive(renderer.render(part, responseNode([part], { isError: true })))).to.be.false;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('is inactive without a parent node', () => {
|
|
100
|
+
const renderer = new ThinkingPartRenderer();
|
|
101
|
+
expect(isActive(renderer.render(thinking('...')))).to.be.false;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
});
|
|
@@ -19,6 +19,19 @@ import { injectable } from '@theia/core/shared/inversify';
|
|
|
19
19
|
import { ChatResponseContent, ThinkingChatResponseContent } from '@theia/ai-chat/lib/common';
|
|
20
20
|
import { ReactNode } from '@theia/core/shared/react';
|
|
21
21
|
import { nls } from '@theia/core/lib/common/nls';
|
|
22
|
+
import * as React from '@theia/core/shared/react';
|
|
23
|
+
import { ResponseNode } from '../chat-tree-view';
|
|
24
|
+
import { Spinner } from './spinner';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Number of trailing characters of the reasoning the live preview keeps at least. Far more than its
|
|
28
|
+
* few visible lines, so that the dropped text is always well above them, while the browser never has
|
|
29
|
+
* to lay out the whole reasoning.
|
|
30
|
+
*/
|
|
31
|
+
const PREVIEW_LENGTH = 2000;
|
|
32
|
+
|
|
33
|
+
/** Duration in milliseconds of the slide that moves the live preview up when a new line starts. */
|
|
34
|
+
const PREVIEW_SCROLL_DURATION = 600;
|
|
22
35
|
|
|
23
36
|
@injectable()
|
|
24
37
|
export class ThinkingPartRenderer implements ChatResponsePartRenderer<ThinkingChatResponseContent> {
|
|
@@ -30,14 +43,121 @@ export class ThinkingPartRenderer implements ChatResponsePartRenderer<ThinkingCh
|
|
|
30
43
|
return -1;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
render(response: ThinkingChatResponseContent): ReactNode {
|
|
34
|
-
return (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
render(response: ThinkingChatResponseContent, parentNode?: ResponseNode): ReactNode {
|
|
47
|
+
return <ThinkingContent response={response} active={this.isThinking(response, parentNode)} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A thinking part is still being written iff it is the last content part of a response that is
|
|
52
|
+
* still running: as soon as the answer text or a tool call arrives, a new part becomes the last
|
|
53
|
+
* one. `ThinkingChatResponseContent` carries no completion flag of its own, and chunks are
|
|
54
|
+
* merged into the existing part, so its position in the response is the reliable signal.
|
|
55
|
+
*/
|
|
56
|
+
protected isThinking(response: ThinkingChatResponseContent, parentNode?: ResponseNode): boolean {
|
|
57
|
+
const model = parentNode?.response;
|
|
58
|
+
if (!model || model.isComplete || model.isCanceled || model.isError) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const contents = model.response.content;
|
|
62
|
+
return contents[contents.length - 1] === response;
|
|
42
63
|
}
|
|
43
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Renders the reasoning of an agent. While the agent is still thinking, a spinner and a preview of
|
|
68
|
+
* the last few lines of the streaming reasoning show that it is working, without the reasoning
|
|
69
|
+
* taking over the response. Once the thinking is done, the full reasoning becomes available in a
|
|
70
|
+
* collapsed block the user can expand.
|
|
71
|
+
*/
|
|
72
|
+
const ThinkingContent: React.FC<{ response: ThinkingChatResponseContent, active: boolean }> = ({ response, active }) => active
|
|
73
|
+
? <div className='theia-thinking active'>
|
|
74
|
+
<div className='theia-thinking-status'>
|
|
75
|
+
<Spinner /> {nls.localizeByDefault('Thinking')}
|
|
76
|
+
</div>
|
|
77
|
+
<ThinkingPreview content={response.content} />
|
|
78
|
+
</div>
|
|
79
|
+
: <div className='theia-thinking'>
|
|
80
|
+
<details>
|
|
81
|
+
<summary>{nls.localizeByDefault('Thinking')}</summary>
|
|
82
|
+
<pre>{response.content}</pre>
|
|
83
|
+
</details>
|
|
84
|
+
</div>;
|
|
85
|
+
|
|
86
|
+
interface PreviewParagraph {
|
|
87
|
+
/** Offset of the paragraph in the reasoning; stays the same while the reasoning grows. */
|
|
88
|
+
start: number;
|
|
89
|
+
text: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Splits the tail of the reasoning into its non-empty paragraphs. Only whole paragraphs are dropped
|
|
94
|
+
* from the start, never parts of one, so that the visible lines never re-wrap as the reasoning grows.
|
|
95
|
+
*/
|
|
96
|
+
const previewParagraphs = (content: string): PreviewParagraph[] => {
|
|
97
|
+
const cut = content.length - PREVIEW_LENGTH;
|
|
98
|
+
let start = cut > 0 ? content.lastIndexOf('\n', cut) + 1 : 0;
|
|
99
|
+
const paragraphs: PreviewParagraph[] = [];
|
|
100
|
+
for (const text of content.slice(start).split('\n')) {
|
|
101
|
+
if (text.trim()) {
|
|
102
|
+
paragraphs.push({ start, text });
|
|
103
|
+
}
|
|
104
|
+
start += text.length + 1;
|
|
105
|
+
}
|
|
106
|
+
return paragraphs;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The last few lines of the reasoning as it streams in, hidden from assistive technology because the
|
|
111
|
+
* streaming text carries no information on its own. The text is anchored to the bottom, so the newest
|
|
112
|
+
* reasoning always stays visible, while the oldest line dissolves at the top.
|
|
113
|
+
*
|
|
114
|
+
* New text within a line simply appears. When a new line starts, the lines slide up instead of jumping,
|
|
115
|
+
* and a line starting mid-slide continues from the current position rather than restarting.
|
|
116
|
+
*/
|
|
117
|
+
const ThinkingPreview: React.FC<{ content: string }> = ({ content }) => {
|
|
118
|
+
const boxRef = React.useRef<HTMLDivElement | undefined>(undefined);
|
|
119
|
+
const textRef = React.useRef<HTMLDivElement | undefined>(undefined);
|
|
120
|
+
// The last paragraph of the previous render and its position; how far it has moved up since is
|
|
121
|
+
// how far the lines have to slide.
|
|
122
|
+
const anchorRef = React.useRef<{ start: number, top: number } | undefined>(undefined);
|
|
123
|
+
const [overflowing, setOverflowing] = React.useState(false);
|
|
124
|
+
const paragraphs = React.useMemo(() => previewParagraphs(content), [content]);
|
|
125
|
+
|
|
126
|
+
React.useLayoutEffect(() => {
|
|
127
|
+
const box = boxRef.current;
|
|
128
|
+
const text = textRef.current;
|
|
129
|
+
const last = text?.lastElementChild;
|
|
130
|
+
if (!box || !text || !(last instanceof HTMLElement)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setOverflowing(text.offsetHeight > box.clientHeight);
|
|
134
|
+
const previous = anchorRef.current;
|
|
135
|
+
anchorRef.current = { start: Number(last.dataset.start), top: last.offsetTop };
|
|
136
|
+
const anchor = previous && text.querySelector<HTMLElement>(`[data-start="${previous.start}"]`);
|
|
137
|
+
if (!previous || !anchor || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const shift = Math.min(previous.top - anchor.offsetTop, box.clientHeight);
|
|
141
|
+
if (shift <= 0) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Where a running slide currently is, so that the next one picks up from there.
|
|
145
|
+
const transform = getComputedStyle(text).transform;
|
|
146
|
+
const currentOffset = transform === 'none' ? 0 : new DOMMatrixReadOnly(transform).m42;
|
|
147
|
+
text.getAnimations().forEach(animation => animation.cancel());
|
|
148
|
+
text.animate(
|
|
149
|
+
[{ transform: `translateY(${currentOffset + shift}px)` }, { transform: 'none' }],
|
|
150
|
+
{ duration: PREVIEW_SCROLL_DURATION, easing: 'ease-out' }
|
|
151
|
+
);
|
|
152
|
+
}, [paragraphs]);
|
|
153
|
+
|
|
154
|
+
return <div
|
|
155
|
+
className={`theia-thinking-preview${overflowing ? ' overflowing' : ''}`}
|
|
156
|
+
aria-hidden={true}
|
|
157
|
+
ref={(element: HTMLDivElement | null) => { boxRef.current = element ?? undefined; }}
|
|
158
|
+
>
|
|
159
|
+
<div className='theia-thinking-preview-text' ref={(element: HTMLDivElement | null) => { textRef.current = element ?? undefined; }}>
|
|
160
|
+
{paragraphs.map(paragraph => <div key={paragraph.start} data-start={paragraph.start}>{paragraph.text}</div>)}
|
|
161
|
+
</div>
|
|
162
|
+
</div>;
|
|
163
|
+
};
|
|
@@ -40,6 +40,22 @@ describe('condenseArguments', () => {
|
|
|
40
40
|
expect(condenseArguments('{invalid}')).to.equal('{invalid}');
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it('returns the raw string without attempting JSON.parse while an arguments object is still streaming', () => {
|
|
44
|
+
const originalParse = JSON.parse;
|
|
45
|
+
let parseCalls = 0;
|
|
46
|
+
JSON.parse = ((text: string, reviver?: Parameters<typeof originalParse>[1]) => {
|
|
47
|
+
parseCalls++;
|
|
48
|
+
return originalParse(text, reviver);
|
|
49
|
+
}) as typeof JSON.parse;
|
|
50
|
+
try {
|
|
51
|
+
const streaming = '{"path": "src/index.ts", "content": "partial';
|
|
52
|
+
expect(condenseArguments(streaming)).to.equal(streaming);
|
|
53
|
+
expect(parseCalls).to.equal(0);
|
|
54
|
+
} finally {
|
|
55
|
+
JSON.parse = originalParse;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
43
59
|
it('condenses single string parameter as value only', () => {
|
|
44
60
|
const result = condenseArguments('{"query": "search term"}');
|
|
45
61
|
expect(result).to.equal('search term');
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
DENY_LATEST_TOOL_CONFIRMATION_COMMAND
|
|
34
34
|
} from '../tool-confirmation-keybinding-contribution';
|
|
35
35
|
import { condenseArguments, formatArgsForTooltip } from './toolcall-utils';
|
|
36
|
+
import { Spinner } from './spinner';
|
|
36
37
|
|
|
37
38
|
@injectable()
|
|
38
39
|
export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {
|
|
@@ -165,10 +166,6 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
const Spinner = () => (
|
|
169
|
-
<span className={`${codicon('loading')} theia-animation-spin`}></span>
|
|
170
|
-
);
|
|
171
|
-
|
|
172
169
|
interface ToolCallContentProps {
|
|
173
170
|
response: ToolCallChatResponseContent;
|
|
174
171
|
confirmationMode: ToolConfirmationMode;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
|
|
18
|
+
import { mayBeCompleteJson } from '../../common/toolcall-utils';
|
|
18
19
|
|
|
19
20
|
export { extractJsonStringField } from '../../common/toolcall-utils';
|
|
20
21
|
|
|
@@ -168,6 +169,10 @@ export function condenseArguments(args: string): string | undefined {
|
|
|
168
169
|
if (!args || !args.trim() || args.trim() === '{}') {
|
|
169
170
|
return undefined;
|
|
170
171
|
}
|
|
172
|
+
if (!mayBeCompleteJson(args)) {
|
|
173
|
+
// still streaming: parsing could only fail, so show the raw text right away
|
|
174
|
+
return truncateString(args, MAX_CONDENSED_LENGTH);
|
|
175
|
+
}
|
|
171
176
|
let parsed: unknown;
|
|
172
177
|
try {
|
|
173
178
|
parsed = JSON.parse(args);
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { createTreeContainer, TreeProps } from '@theia/core/lib/browser';
|
|
18
18
|
import { interfaces } from '@theia/core/shared/inversify';
|
|
19
|
+
import { ChatFindHighlighter } from '../chat-find/chat-find-highlighter';
|
|
20
|
+
import { ChatFindWidget } from '../chat-find/chat-find-widget';
|
|
19
21
|
import { ChatViewTreeWidget } from './chat-view-tree-widget';
|
|
20
22
|
|
|
21
23
|
const CHAT_VIEW_TREE_PROPS = {
|
|
@@ -28,5 +30,7 @@ export function createChatViewTreeWidget(parent: interfaces.Container): ChatView
|
|
|
28
30
|
props: CHAT_VIEW_TREE_PROPS,
|
|
29
31
|
widget: ChatViewTreeWidget,
|
|
30
32
|
});
|
|
33
|
+
child.bind(ChatFindWidget).toSelf().inSingletonScope();
|
|
34
|
+
child.bind(ChatFindHighlighter).toSelf().inSingletonScope();
|
|
31
35
|
return child.get(ChatViewTreeWidget);
|
|
32
36
|
}
|
|
@@ -38,16 +38,21 @@ import {
|
|
|
38
38
|
HoverService,
|
|
39
39
|
Key,
|
|
40
40
|
KeyCode,
|
|
41
|
+
Message,
|
|
41
42
|
NodeProps,
|
|
42
43
|
OpenerService,
|
|
43
44
|
TreeModel,
|
|
44
45
|
TreeNode,
|
|
45
46
|
TreeProps,
|
|
46
47
|
TreeWidget,
|
|
48
|
+
UnsafeWidgetUtilities,
|
|
47
49
|
Widget,
|
|
48
50
|
type ReactWidget
|
|
49
51
|
} from '@theia/core/lib/browser';
|
|
50
52
|
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
|
53
|
+
import { ChatFindHighlighter } from '../chat-find/chat-find-highlighter';
|
|
54
|
+
import { ChatFindMatch } from '../chat-find/chat-find-matcher';
|
|
55
|
+
import { ChatFindWidget } from '../chat-find/chat-find-widget';
|
|
51
56
|
import { nls } from '@theia/core/lib/common/nls';
|
|
52
57
|
import {
|
|
53
58
|
inject,
|
|
@@ -61,7 +66,7 @@ import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
|
|
61
66
|
import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution';
|
|
62
67
|
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
63
68
|
import { formatTokenCount } from '../chat-token-usage-indicator-util';
|
|
64
|
-
import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';
|
|
69
|
+
import { MarkdownRendering, useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';
|
|
65
70
|
import { ProgressMessage } from '../chat-progress-message';
|
|
66
71
|
import { AIChatTreeInputFactory, type AIChatTreeInputWidget } from './chat-view-tree-input-widget';
|
|
67
72
|
import { PromptVariantBadge } from './prompt-variant-badge';
|
|
@@ -150,6 +155,12 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
150
155
|
|
|
151
156
|
protected chatResponseFocusKey: ContextKey<boolean>;
|
|
152
157
|
|
|
158
|
+
@inject(ChatFindWidget)
|
|
159
|
+
protected readonly findWidget: ChatFindWidget;
|
|
160
|
+
|
|
161
|
+
@inject(ChatFindHighlighter)
|
|
162
|
+
protected readonly findHighlighter: ChatFindHighlighter;
|
|
163
|
+
|
|
153
164
|
protected readonly onDidSubmitEditEmitter = new Emitter<ChatRequest>();
|
|
154
165
|
onDidSubmitEdit = this.onDidSubmitEditEmitter.event;
|
|
155
166
|
|
|
@@ -208,8 +219,14 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
208
219
|
this.chatResponseFocusKey = this.contextKeyService.createKey<boolean>('chatResponseFocus', false);
|
|
209
220
|
this.node.setAttribute('tabindex', '0');
|
|
210
221
|
this.node.setAttribute('aria-label', nls.localize('theia/ai/chat-ui/chatResponses', 'Chat responses'));
|
|
211
|
-
|
|
212
|
-
this.
|
|
222
|
+
|
|
223
|
+
this.findWidget.fallbackFocusTarget = this.node;
|
|
224
|
+
this.toDispose.pushAll([
|
|
225
|
+
this.findWidget,
|
|
226
|
+
this.findHighlighter,
|
|
227
|
+
this.findWidget.onDidChangeState(state => this.findHighlighter.update(this.node, state.regexp, state.matches, state.current)),
|
|
228
|
+
this.findWidget.onDidRequestReveal(match => this.revealFindMatch(match))
|
|
229
|
+
]);
|
|
213
230
|
|
|
214
231
|
this.toDispose.pushAll([
|
|
215
232
|
this.toDisposeOnChatModelChange,
|
|
@@ -245,9 +262,79 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
245
262
|
|
|
246
263
|
public setEnabled(enabled: boolean): void {
|
|
247
264
|
this.isEnabled = enabled;
|
|
265
|
+
if (!enabled) {
|
|
266
|
+
this.findWidget.dismiss();
|
|
267
|
+
}
|
|
248
268
|
this.update();
|
|
249
269
|
}
|
|
250
270
|
|
|
271
|
+
/** Opens the find bar over the response tree (or refocuses it when already open). */
|
|
272
|
+
showFind(): void {
|
|
273
|
+
this.findWidget.open();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
hideFind(): void {
|
|
277
|
+
this.findWidget.dismiss();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get isFindVisible(): boolean {
|
|
281
|
+
return this.findWidget.isOpen;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Whether the tracked session has content to search; false while the welcome/session-list screen shows. */
|
|
285
|
+
get canFind(): boolean {
|
|
286
|
+
return this.findWidget.canFind;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reveals a match: mounts the owning row via virtuoso when it is not rendered yet, then re-applies the highlights
|
|
291
|
+
* and scrolls the matched text itself into view (rows can be taller than the viewport).
|
|
292
|
+
*/
|
|
293
|
+
protected revealFindMatch(match: ChatFindMatch): void {
|
|
294
|
+
const row = this.rows.get(match.nodeId);
|
|
295
|
+
if (row === undefined) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const settle = (): void => {
|
|
299
|
+
this.findHighlighter.refresh();
|
|
300
|
+
this.findHighlighter.scrollCurrentIntoView(this.node);
|
|
301
|
+
};
|
|
302
|
+
if (this.findHighlighter.findRow(this.node, match.nodeId)) {
|
|
303
|
+
requestAnimationFrame(settle);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.view?.list?.scrollIntoView({
|
|
307
|
+
index: row.index,
|
|
308
|
+
align: 'center',
|
|
309
|
+
done: settle
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected override onAfterAttach(msg: Message): void {
|
|
314
|
+
super.onAfterAttach(msg);
|
|
315
|
+
// Registered per attach, not in `init()`: `addEventListener` disposes on detach, so listeners registered
|
|
316
|
+
// once would be lost the first time the view is moved (e.g. to the main area), leaving `chatResponseFocus`
|
|
317
|
+
// stuck and every keybinding scoped to it dead until the page is reloaded.
|
|
318
|
+
this.addEventListener(this.node, 'focusin', () => this.chatResponseFocusKey.set(true));
|
|
319
|
+
this.addEventListener(this.node, 'focusout', () => this.chatResponseFocusKey.set(false));
|
|
320
|
+
// The tree node is a React root, so the find bar is attached as a sibling right before it,
|
|
321
|
+
// the same way the core TreeWidget hosts its SearchBox.
|
|
322
|
+
if (this.findWidget.isAttached) {
|
|
323
|
+
Widget.detach(this.findWidget);
|
|
324
|
+
}
|
|
325
|
+
if (this.node.parentElement) {
|
|
326
|
+
UnsafeWidgetUtilities.attach(this.findWidget, this.node.parentElement, this.node);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
protected override onBeforeDetach(msg: Message): void {
|
|
331
|
+
this.chatResponseFocusKey.set(false);
|
|
332
|
+
if (this.findWidget.isAttached) {
|
|
333
|
+
Widget.detach(this.findWidget);
|
|
334
|
+
}
|
|
335
|
+
super.onBeforeDetach(msg);
|
|
336
|
+
}
|
|
337
|
+
|
|
251
338
|
/** Toggles auto-scroll and the scroll-to-bottom button based on whether the viewport includes the bottom of the list. */
|
|
252
339
|
protected handleAtBottomStateChange(isAtBottom: boolean): void {
|
|
253
340
|
if (isAtBottom !== this.atBottom) {
|
|
@@ -390,6 +477,7 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
390
477
|
public trackChatModel(chatModel: ChatModel): void {
|
|
391
478
|
this.toDisposeOnChatModelChange.dispose();
|
|
392
479
|
this.recreateModelTree(chatModel);
|
|
480
|
+
this.findWidget.setChatModel(chatModel);
|
|
393
481
|
|
|
394
482
|
chatModel.getRequests().forEach(request => {
|
|
395
483
|
if (!request.response.isComplete) {
|
|
@@ -439,6 +527,12 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
439
527
|
}
|
|
440
528
|
}
|
|
441
529
|
|
|
530
|
+
protected override doUpdateRows(): void {
|
|
531
|
+
super.doUpdateRows();
|
|
532
|
+
// Follow new requests and responses to the end of the chat, unless auto-scroll is locked.
|
|
533
|
+
this.scheduleUpdateScrollToRow();
|
|
534
|
+
}
|
|
535
|
+
|
|
442
536
|
protected override getScrollToRow(): number | undefined {
|
|
443
537
|
// Only scroll to end if auto-scroll is enabled (not locked)
|
|
444
538
|
if (this.shouldScrollToEnd) {
|
|
@@ -478,6 +572,7 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
478
572
|
return <React.Fragment key={node.id}>
|
|
479
573
|
<div
|
|
480
574
|
className='theia-ChatNode'
|
|
575
|
+
data-node-id={node.id}
|
|
481
576
|
role='article'
|
|
482
577
|
aria-label={ariaLabel}
|
|
483
578
|
onContextMenu={e => this.handleContextMenu(node, e)}
|
|
@@ -880,9 +975,7 @@ export const ChatRequestRender = (
|
|
|
880
975
|
);
|
|
881
976
|
} else {
|
|
882
977
|
const ref = useMarkdownRendering(
|
|
883
|
-
part.text
|
|
884
|
-
.replace(/^[\r\n]+|[\r\n]+$/g, '') // remove excessive new lines
|
|
885
|
-
.replace(/(^ )/g, ' '), // enforce keeping space before
|
|
978
|
+
MarkdownRendering.prepareRequestText(part.text),
|
|
886
979
|
openerService,
|
|
887
980
|
true,
|
|
888
981
|
undefined,
|