@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
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
|
|
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 { ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat/lib/common';
|
|
18
|
+
import { DisposableCollection, Emitter, Event } from '@theia/core';
|
|
19
|
+
import { codicon, DISABLED_CLASS, ReactWidget } from '@theia/core/lib/browser';
|
|
20
|
+
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
|
21
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
22
|
+
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
23
|
+
import * as React from '@theia/core/shared/react';
|
|
24
|
+
import '../../../src/browser/style/chat-find.css';
|
|
25
|
+
import { ChatFindMatch, ChatFindMatcher, ChatFindOptions } from './chat-find-matcher';
|
|
26
|
+
|
|
27
|
+
export interface ChatFindState {
|
|
28
|
+
regexp: RegExp | undefined;
|
|
29
|
+
matches: ChatFindMatch[];
|
|
30
|
+
current: ChatFindMatch | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The find bar of a chat view: owns query, options, matches and the current match, and tracks the
|
|
35
|
+
* chat model so matches follow streamed responses. Reveal and highlighting are done by the host
|
|
36
|
+
* (the chat tree widget) via {@link onDidRequestReveal} and {@link onDidChangeState}.
|
|
37
|
+
*/
|
|
38
|
+
@injectable()
|
|
39
|
+
export class ChatFindWidget extends ReactWidget {
|
|
40
|
+
|
|
41
|
+
static readonly ID = 'chat-find-widget';
|
|
42
|
+
protected static readonly RECOMPUTE_DELAY_MS = 100;
|
|
43
|
+
|
|
44
|
+
@inject(ChatFindMatcher)
|
|
45
|
+
protected matcher: ChatFindMatcher;
|
|
46
|
+
|
|
47
|
+
@inject(ContextKeyService)
|
|
48
|
+
protected contextKeyService: ContextKeyService;
|
|
49
|
+
|
|
50
|
+
protected readonly onDidChangeStateEmitter = new Emitter<ChatFindState>();
|
|
51
|
+
readonly onDidChangeState: Event<ChatFindState> = this.onDidChangeStateEmitter.event;
|
|
52
|
+
|
|
53
|
+
protected readonly onDidRequestRevealEmitter = new Emitter<ChatFindMatch>();
|
|
54
|
+
readonly onDidRequestReveal: Event<ChatFindMatch> = this.onDidRequestRevealEmitter.event;
|
|
55
|
+
|
|
56
|
+
/** Focused when the bar closes and the previously focused element is gone. */
|
|
57
|
+
fallbackFocusTarget: HTMLElement | undefined;
|
|
58
|
+
|
|
59
|
+
protected visibleKey: ContextKey<boolean>;
|
|
60
|
+
protected query = '';
|
|
61
|
+
protected options: ChatFindOptions = { matchCase: false, wholeWord: false, useRegex: false };
|
|
62
|
+
protected regexp: RegExp | undefined;
|
|
63
|
+
protected matches: ChatFindMatch[] = [];
|
|
64
|
+
protected currentIndex = -1;
|
|
65
|
+
protected chatModel: ChatModel | undefined;
|
|
66
|
+
protected nodeOrder = new Map<string, number>();
|
|
67
|
+
protected readonly toDisposeOnModelChange = new DisposableCollection();
|
|
68
|
+
protected recomputeTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
69
|
+
protected previouslyFocused: HTMLElement | undefined;
|
|
70
|
+
protected input: HTMLInputElement | undefined;
|
|
71
|
+
protected pendingFocus = false;
|
|
72
|
+
|
|
73
|
+
@postConstruct()
|
|
74
|
+
protected init(): void {
|
|
75
|
+
this.id = ChatFindWidget.ID;
|
|
76
|
+
this.addClass('theia-chat-find-widget');
|
|
77
|
+
this.visibleKey = this.contextKeyService.createKey<boolean>('chatFindVisible', false);
|
|
78
|
+
// No perfect-scrollbar: its `.ps { overflow: hidden }` would clip the bar, which overflows this zero-height host.
|
|
79
|
+
this.scrollOptions = undefined;
|
|
80
|
+
this.toDispose.pushAll([this.onDidChangeStateEmitter, this.onDidRequestRevealEmitter, this.toDisposeOnModelChange]);
|
|
81
|
+
this.hide();
|
|
82
|
+
this.update();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override dispose(): void {
|
|
86
|
+
if (this.recomputeTimeout !== undefined) {
|
|
87
|
+
clearTimeout(this.recomputeTimeout);
|
|
88
|
+
this.recomputeTimeout = undefined;
|
|
89
|
+
}
|
|
90
|
+
this.hideBar();
|
|
91
|
+
super.dispose();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get isOpen(): boolean {
|
|
95
|
+
return !this.isHidden;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get state(): ChatFindState {
|
|
99
|
+
return { regexp: this.regexp, matches: this.matches, current: this.currentMatch };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get currentMatch(): ChatFindMatch | undefined {
|
|
103
|
+
return this.matches[this.currentIndex];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether there is anything to search. A session without requests renders the welcome screen (which hosts
|
|
108
|
+
* the session list), so the find bar has nothing to search and stays closed there.
|
|
109
|
+
*/
|
|
110
|
+
get canFind(): boolean {
|
|
111
|
+
return this.chatModel !== undefined && !this.chatModel.isEmpty();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Track a chat model; query and options are kept, the position resets. Like the find widget of an editor, the bar
|
|
116
|
+
* belongs to one session: it closes when another session is tracked, and `Ctrl+F` brings the query back.
|
|
117
|
+
*/
|
|
118
|
+
setChatModel(model: ChatModel | undefined): void {
|
|
119
|
+
if (model !== this.chatModel) {
|
|
120
|
+
// Not `dismiss()`: the user just picked another session, so focus must stay where it is.
|
|
121
|
+
this.hideBar();
|
|
122
|
+
}
|
|
123
|
+
this.toDisposeOnModelChange.dispose();
|
|
124
|
+
this.chatModel = model;
|
|
125
|
+
this.currentIndex = -1;
|
|
126
|
+
if (model) {
|
|
127
|
+
this.toDisposeOnModelChange.push(model.onDidChange(event => this.handleModelChange(event)));
|
|
128
|
+
model.getRequests().forEach(request => this.trackResponse(request));
|
|
129
|
+
}
|
|
130
|
+
this.recompute(false);
|
|
131
|
+
this.closeWhenNothingToFind();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
open(): void {
|
|
135
|
+
if (!this.canFind) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const active = document.activeElement;
|
|
139
|
+
if (!this.isOpen && active instanceof HTMLElement && !this.node.contains(active)) {
|
|
140
|
+
this.previouslyFocused = active;
|
|
141
|
+
}
|
|
142
|
+
this.show();
|
|
143
|
+
this.visibleKey.set(true);
|
|
144
|
+
this.recompute(false);
|
|
145
|
+
this.focusInput();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Closes the bar and returns focus to where it was before the bar opened. */
|
|
149
|
+
dismiss(): void {
|
|
150
|
+
if (!this.isOpen) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const target = this.previouslyFocused?.isConnected ? this.previouslyFocused : this.fallbackFocusTarget;
|
|
154
|
+
this.hideBar();
|
|
155
|
+
target?.focus();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Closes the bar and clears the highlights without touching focus. */
|
|
159
|
+
protected hideBar(): void {
|
|
160
|
+
if (this.isDisposed || !this.isOpen) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.hide();
|
|
164
|
+
this.visibleKey.set(false);
|
|
165
|
+
this.previouslyFocused = undefined;
|
|
166
|
+
this.onDidChangeStateEmitter.fire({ regexp: undefined, matches: [], current: undefined });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setQuery(query: string): void {
|
|
170
|
+
if (query === this.query) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.query = query;
|
|
174
|
+
this.recompute(true);
|
|
175
|
+
this.revealCurrent();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
toggleOption(option: keyof ChatFindOptions): void {
|
|
179
|
+
this.options = { ...this.options, [option]: !this.options[option] };
|
|
180
|
+
this.recompute(true);
|
|
181
|
+
this.revealCurrent();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
next(): void {
|
|
185
|
+
this.step(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
previous(): void {
|
|
189
|
+
this.step(-1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
protected step(delta: number): void {
|
|
193
|
+
if (this.matches.length === 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.currentIndex = (this.currentIndex + delta + this.matches.length) % this.matches.length;
|
|
197
|
+
this.update();
|
|
198
|
+
this.fireStateIfOpen();
|
|
199
|
+
this.revealCurrent();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
protected revealCurrent(): void {
|
|
203
|
+
const current = this.currentMatch;
|
|
204
|
+
if (current) {
|
|
205
|
+
this.onDidRequestRevealEmitter.fire(current);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
protected handleModelChange(event: ChatChangeEvent): void {
|
|
210
|
+
if (event.kind === 'addRequest') {
|
|
211
|
+
this.trackResponse(event.request);
|
|
212
|
+
}
|
|
213
|
+
this.scheduleRecompute();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
protected trackResponse(request: ChatRequestModel): void {
|
|
217
|
+
if (!request.response.isComplete) {
|
|
218
|
+
this.toDisposeOnModelChange.push(request.response.onDidChange(() => this.scheduleRecompute()));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
protected scheduleRecompute(): void {
|
|
223
|
+
// A closed bar keeps its query but does not follow the model; `open()` recomputes.
|
|
224
|
+
if (!this.isOpen || this.recomputeTimeout !== undefined) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.recomputeTimeout = setTimeout(() => {
|
|
228
|
+
this.recomputeTimeout = undefined;
|
|
229
|
+
this.recompute(false);
|
|
230
|
+
}, ChatFindWidget.RECOMPUTE_DELAY_MS);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Recompute matches. The current match is kept by identity when it still exists; otherwise (or when
|
|
235
|
+
* `moveToNearest` is set after a query change) the first match at or after the previous position becomes current.
|
|
236
|
+
* While the bar is closed there are no matches.
|
|
237
|
+
*/
|
|
238
|
+
protected recompute(moveToNearest: boolean): void {
|
|
239
|
+
const previous = this.currentMatch;
|
|
240
|
+
this.regexp = this.matcher.createRegExp(this.query, this.options);
|
|
241
|
+
this.nodeOrder = this.computeNodeOrder();
|
|
242
|
+
this.matches = this.isOpen && this.chatModel && this.regexp ? this.matcher.findMatches(this.chatModel, this.regexp) : [];
|
|
243
|
+
this.currentIndex = this.pickCurrentIndex(previous, moveToNearest);
|
|
244
|
+
this.update();
|
|
245
|
+
this.fireStateIfOpen();
|
|
246
|
+
this.closeWhenNothingToFind();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Closes the bar once the tracked session has no requests left to search. */
|
|
250
|
+
protected closeWhenNothingToFind(): void {
|
|
251
|
+
if (this.isOpen && !this.canFind) {
|
|
252
|
+
this.dismiss();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected fireStateIfOpen(): void {
|
|
257
|
+
if (this.isOpen) {
|
|
258
|
+
this.onDidChangeStateEmitter.fire(this.state);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
protected computeNodeOrder(): Map<string, number> {
|
|
263
|
+
const order = new Map<string, number>();
|
|
264
|
+
this.chatModel?.getRequests().forEach((request, index) => {
|
|
265
|
+
order.set(request.id, 2 * index);
|
|
266
|
+
order.set(request.response.id, 2 * index + 1);
|
|
267
|
+
});
|
|
268
|
+
return order;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected pickCurrentIndex(previous: ChatFindMatch | undefined, moveToNearest: boolean): number {
|
|
272
|
+
if (this.matches.length === 0) {
|
|
273
|
+
return -1;
|
|
274
|
+
}
|
|
275
|
+
if (!previous) {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
if (!moveToNearest) {
|
|
279
|
+
const key = ChatFindMatch.key(previous);
|
|
280
|
+
const same = this.matches.findIndex(match => ChatFindMatch.key(match) === key);
|
|
281
|
+
if (same >= 0) {
|
|
282
|
+
return same;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const nearest = this.matches.findIndex(match => this.compare(match, previous) >= 0);
|
|
286
|
+
return nearest >= 0 ? nearest : 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Document order of two matches: row order, then content index, then offset. */
|
|
290
|
+
protected compare(a: ChatFindMatch, b: ChatFindMatch): number {
|
|
291
|
+
const rowA = this.nodeOrder.get(a.nodeId) ?? -1;
|
|
292
|
+
const rowB = this.nodeOrder.get(b.nodeId) ?? -1;
|
|
293
|
+
if (rowA !== rowB) {
|
|
294
|
+
return rowA - rowB;
|
|
295
|
+
}
|
|
296
|
+
const contentA = a.contentIndex ?? -1;
|
|
297
|
+
const contentB = b.contentIndex ?? -1;
|
|
298
|
+
if (contentA !== contentB) {
|
|
299
|
+
return contentA - contentB;
|
|
300
|
+
}
|
|
301
|
+
return a.start - b.start;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
protected focusInput(): void {
|
|
305
|
+
if (this.input) {
|
|
306
|
+
this.input.focus();
|
|
307
|
+
this.input.select();
|
|
308
|
+
} else {
|
|
309
|
+
this.pendingFocus = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected setInput = (input: HTMLInputElement | null): void => {
|
|
314
|
+
this.input = input ?? undefined;
|
|
315
|
+
if (input && this.pendingFocus) {
|
|
316
|
+
this.pendingFocus = false;
|
|
317
|
+
input.focus();
|
|
318
|
+
input.select();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
protected handleInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
323
|
+
this.setQuery(event.target.value);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
protected handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
327
|
+
if (event.key === 'Enter') {
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
event.stopPropagation();
|
|
330
|
+
if (event.shiftKey) {
|
|
331
|
+
this.previous();
|
|
332
|
+
} else {
|
|
333
|
+
this.next();
|
|
334
|
+
}
|
|
335
|
+
} else if (event.key === 'Escape') {
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
event.stopPropagation();
|
|
338
|
+
this.dismiss();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
protected handleToggleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
|
343
|
+
const option = event.currentTarget.dataset.option as keyof ChatFindOptions | undefined;
|
|
344
|
+
if (option) {
|
|
345
|
+
this.toggleOption(option);
|
|
346
|
+
this.input?.focus();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
protected handlePreviousClick = (): void => {
|
|
351
|
+
this.previous();
|
|
352
|
+
this.input?.focus();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
protected handleNextClick = (): void => {
|
|
356
|
+
this.next();
|
|
357
|
+
this.input?.focus();
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
protected handleCloseClick = (): void => {
|
|
361
|
+
this.dismiss();
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
protected render(): React.ReactNode {
|
|
365
|
+
const hasQuery = this.query.length > 0;
|
|
366
|
+
const count = this.matches.length;
|
|
367
|
+
const noResults = hasQuery && count === 0;
|
|
368
|
+
const label = count > 0
|
|
369
|
+
? nls.localizeByDefault('{0} of {1}', this.currentIndex + 1, count)
|
|
370
|
+
: nls.localizeByDefault('No results');
|
|
371
|
+
return <div className='theia-chat-find-bar' role='search'>
|
|
372
|
+
<div className={`theia-chat-find-input-box${noResults ? ' no-results' : ''}`}>
|
|
373
|
+
<input
|
|
374
|
+
ref={this.setInput}
|
|
375
|
+
type='text'
|
|
376
|
+
className='theia-input'
|
|
377
|
+
spellCheck={false}
|
|
378
|
+
defaultValue={this.query}
|
|
379
|
+
placeholder={nls.localizeByDefault('Find')}
|
|
380
|
+
title={nls.localizeByDefault('Find')}
|
|
381
|
+
aria-label={nls.localizeByDefault('Find')}
|
|
382
|
+
onChange={this.handleInputChange}
|
|
383
|
+
onKeyDown={this.handleInputKeyDown}
|
|
384
|
+
/>
|
|
385
|
+
{this.renderToggle('matchCase', 'case-sensitive', nls.localizeByDefault('Match Case'))}
|
|
386
|
+
{this.renderToggle('wholeWord', 'whole-word', nls.localizeByDefault('Match Whole Word'))}
|
|
387
|
+
{this.renderToggle('useRegex', 'regex', nls.localizeByDefault('Use Regular Expression'))}
|
|
388
|
+
</div>
|
|
389
|
+
<span className={`theia-chat-find-count${noResults ? ' no-results' : ''}`} aria-live='polite'>{label}</span>
|
|
390
|
+
{this.renderButton('arrow-up', nls.localizeByDefault('Previous Match'), count > 0, this.handlePreviousClick)}
|
|
391
|
+
{this.renderButton('arrow-down', nls.localizeByDefault('Next Match'), count > 0, this.handleNextClick)}
|
|
392
|
+
{this.renderButton('close', nls.localizeByDefault('Close'), true, this.handleCloseClick)}
|
|
393
|
+
</div>;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
protected renderToggle(option: keyof ChatFindOptions, icon: string, title: string): React.ReactNode {
|
|
397
|
+
const enabled = this.options[option];
|
|
398
|
+
return <button
|
|
399
|
+
key={option}
|
|
400
|
+
data-option={option}
|
|
401
|
+
className={`theia-chat-find-option ${codicon(icon, true)}${enabled ? ' enabled' : ''}`}
|
|
402
|
+
title={title}
|
|
403
|
+
aria-label={title}
|
|
404
|
+
aria-pressed={enabled}
|
|
405
|
+
onClick={this.handleToggleClick}
|
|
406
|
+
/>;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
protected renderButton(icon: string, title: string, enabled: boolean, onClick: () => void): React.ReactNode {
|
|
410
|
+
return <button
|
|
411
|
+
className={`theia-chat-find-option ${codicon(icon, true)}${enabled ? '' : ' ' + DISABLED_CLASS}`}
|
|
412
|
+
title={title}
|
|
413
|
+
aria-label={title}
|
|
414
|
+
disabled={!enabled}
|
|
415
|
+
onClick={onClick}
|
|
416
|
+
/>;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { computeRevealScrollDelta } from './chat-input-scroll-util';
|
|
19
|
+
|
|
20
|
+
describe('computeRevealScrollDelta', () => {
|
|
21
|
+
const viewportHeight = 240;
|
|
22
|
+
|
|
23
|
+
it('returns 0 when the row is fully visible', () => {
|
|
24
|
+
expect(computeRevealScrollDelta(100, 120, viewportHeight)).to.equal(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns 0 when the row touches the bottom edge', () => {
|
|
28
|
+
expect(computeRevealScrollDelta(220, 240, viewportHeight)).to.equal(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('scrolls up by the overshoot when the row is above the viewport', () => {
|
|
32
|
+
expect(computeRevealScrollDelta(-346, -326, viewportHeight)).to.equal(-346);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('scrolls down by the overshoot when the row is below the viewport', () => {
|
|
36
|
+
expect(computeRevealScrollDelta(560, 580, viewportHeight)).to.equal(340);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('scrolls down when the row is only partially below the viewport', () => {
|
|
40
|
+
expect(computeRevealScrollDelta(230, 250, viewportHeight)).to.equal(10);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
|
|
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
|
+
/**
|
|
18
|
+
* Computes how far a scroll container must scroll so that the vertical range `[rowTop, rowBottom]`,
|
|
19
|
+
* given in pixels relative to the top edge of the container's visible area, becomes fully visible.
|
|
20
|
+
* Negative values scroll up, positive values scroll down, `0` means the range is already visible.
|
|
21
|
+
*/
|
|
22
|
+
export function computeRevealScrollDelta(rowTop: number, rowBottom: number, viewportHeight: number): number {
|
|
23
|
+
if (rowTop < 0) {
|
|
24
|
+
return rowTop;
|
|
25
|
+
}
|
|
26
|
+
if (rowBottom > viewportHeight) {
|
|
27
|
+
return rowBottom - viewportHeight;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
@@ -24,6 +24,7 @@ FrontendApplicationConfigProvider.set({});
|
|
|
24
24
|
import 'reflect-metadata';
|
|
25
25
|
|
|
26
26
|
import { expect } from 'chai';
|
|
27
|
+
import { ReasoningLevel, ReasoningSettings, ReasoningSupport } from '@theia/ai-core';
|
|
27
28
|
import { AIChatInputWidget } from './chat-input-widget';
|
|
28
29
|
|
|
29
30
|
disableJSDOM();
|
|
@@ -44,6 +45,16 @@ class TestChatInputWidget extends AIChatInputWidget {
|
|
|
44
45
|
return this.refreshCapabilities();
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
setReasoningState(support: ReasoningSupport, saved?: ReasoningSettings): void {
|
|
49
|
+
this.currentReasoningSupport = support;
|
|
50
|
+
this.savedReasoning = saved;
|
|
51
|
+
(this as unknown as { chatService: unknown }).chatService = { getSessions: () => [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
currentReasoningLevelForTest(): ReasoningLevel | undefined {
|
|
55
|
+
return this.getCurrentReasoningLevel();
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
protected override async updateCapabilitiesForAgent(agentId: string, modeId?: string, preserveOverrides?: boolean): Promise<void> {
|
|
48
59
|
this.updateCalls.push({ agentId, modeId, preserveOverrides });
|
|
49
60
|
}
|
|
@@ -57,6 +68,24 @@ describe('AIChatInputWidget', () => {
|
|
|
57
68
|
before(() => disableJSDOM = enableJSDOM());
|
|
58
69
|
after(() => disableJSDOM());
|
|
59
70
|
|
|
71
|
+
describe('getCurrentReasoningLevel', () => {
|
|
72
|
+
const oSeries: ReasoningSupport = { supportedLevels: ['off', 'low', 'medium', 'high', 'auto'], defaultLevel: 'auto' };
|
|
73
|
+
|
|
74
|
+
it('clamps a persisted level the current model does not support to the nearest supported one', () => {
|
|
75
|
+
const widget = new TestChatInputWidget();
|
|
76
|
+
widget.setReasoningState(oSeries, { level: 'minimal' });
|
|
77
|
+
|
|
78
|
+
expect(widget.currentReasoningLevelForTest()).to.equal('low');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('keeps a persisted level the current model supports', () => {
|
|
82
|
+
const widget = new TestChatInputWidget();
|
|
83
|
+
widget.setReasoningState(oSeries, { level: 'high' });
|
|
84
|
+
|
|
85
|
+
expect(widget.currentReasoningLevelForTest()).to.equal('high');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
60
89
|
describe('refreshCapabilities', () => {
|
|
61
90
|
it('preserves capability selections while reloading prompt-template capabilities', async () => {
|
|
62
91
|
const widget = new TestChatInputWidget();
|