@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
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
import { mergeReasoningSettings } from '@theia/ai-core/lib/browser/frontend-language-model-service';
|
|
29
29
|
import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
|
|
30
30
|
import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
|
|
31
|
-
import { AI_SHOW_SETTINGS_COMMAND,
|
|
32
|
-
import { AISettingsService, PromptService } from '@theia/ai-core/lib/common';
|
|
31
|
+
import { AI_SHOW_SETTINGS_COMMAND, AIActivationService, FavoriteModelsService, FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
32
|
+
import { AISettingsService, groupModelsByProvider, LanguageModelAliasRegistry, PromptService } from '@theia/ai-core/lib/common';
|
|
33
33
|
import { CommandService, DisposableCollection, Emitter, InMemoryResources, MessageService, URI, nls, Disposable, ILogger } from '@theia/core';
|
|
34
34
|
import { ContextMenuRenderer, HoverService, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
|
|
35
35
|
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
|
|
@@ -39,15 +39,15 @@ import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
|
|
39
39
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
40
40
|
import { inject, injectable, optional, postConstruct, named } from '@theia/core/shared/inversify';
|
|
41
41
|
import * as React from '@theia/core/shared/react';
|
|
42
|
-
import { IMouseEvent,
|
|
42
|
+
import { IMouseEvent, IPosition } from '@theia/monaco-editor-core';
|
|
43
43
|
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
44
44
|
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
45
45
|
import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
|
|
46
46
|
import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
|
|
47
|
+
import { computeRevealScrollDelta } from './chat-input-scroll-util';
|
|
47
48
|
import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
|
|
48
49
|
import { ContextVariablePicker } from './context-variable-picker';
|
|
49
50
|
import { TASK_CONTEXT_VARIABLE } from '@theia/ai-chat/lib/browser/task-context-variable';
|
|
50
|
-
import { IModelDeltaDecoration } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
|
51
51
|
import { EditorOption } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
|
|
52
52
|
import { SuggestController } from '@theia/monaco-editor-core/esm/vs/editor/contrib/suggest/browser/suggestController';
|
|
53
53
|
import { ChatInputHistoryService, ChatInputNavigationState } from './chat-input-history';
|
|
@@ -84,6 +84,13 @@ type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
|
|
|
84
84
|
type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
|
|
85
85
|
type OpenContextElement = (request: AIVariableResolutionRequest) => unknown;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Id of the Models category of the AI Configuration view (`AiConfigurationCategoryId.MODELS`), used to
|
|
89
|
+
* open it from the model selector. Not imported: that id is declared in `@theia/ai-core-ui`, which this
|
|
90
|
+
* package does not depend on.
|
|
91
|
+
*/
|
|
92
|
+
const AI_CONFIGURATION_MODELS_CATEGORY_ID = 'models';
|
|
93
|
+
|
|
87
94
|
export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
|
|
88
95
|
export interface AIChatInputConfiguration {
|
|
89
96
|
showContext?: boolean;
|
|
@@ -182,6 +189,12 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
182
189
|
@inject(FrontendLanguageModelRegistry)
|
|
183
190
|
protected readonly languageModelRegistry: FrontendLanguageModelRegistry;
|
|
184
191
|
|
|
192
|
+
@inject(FavoriteModelsService)
|
|
193
|
+
protected readonly favoriteModels: FavoriteModelsService;
|
|
194
|
+
|
|
195
|
+
@inject(LanguageModelAliasRegistry)
|
|
196
|
+
protected readonly aliasRegistry: LanguageModelAliasRegistry;
|
|
197
|
+
|
|
185
198
|
@inject(PreferenceService) @optional()
|
|
186
199
|
protected readonly preferenceService: PreferenceService | undefined;
|
|
187
200
|
|
|
@@ -313,21 +326,20 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
313
326
|
* Resolves the reasoning level to display in the selector. Priority: session override →
|
|
314
327
|
* persisted per-agent selection (from {@link AISettingsService}) →
|
|
315
328
|
* `ai-features.reasoning.defaults` preference entry matching the current model/agent →
|
|
316
|
-
* model's declared default → `'off'`.
|
|
329
|
+
* model's declared default → `'off'`. The result is clamped to the model's supported levels, matching
|
|
330
|
+
* what the frontend language model service sends.
|
|
317
331
|
*/
|
|
318
332
|
protected getCurrentReasoningLevel(): ReasoningLevel | undefined {
|
|
319
333
|
if (!this.currentReasoningSupport) {
|
|
320
334
|
return undefined;
|
|
321
335
|
}
|
|
322
336
|
const session = this.chatService.getSessions().find(s => s.model.id === this._chatModel?.id);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
return this.resolvePreferenceReasoningLevel() ?? this.currentReasoningSupport.defaultLevel ?? 'off';
|
|
337
|
+
const level = session?.model.settings?.commonSettings?.reasoning?.level
|
|
338
|
+
?? this.savedReasoning?.level
|
|
339
|
+
?? this.resolvePreferenceReasoningLevel()
|
|
340
|
+
?? this.currentReasoningSupport.defaultLevel
|
|
341
|
+
?? 'off';
|
|
342
|
+
return ReasoningSupport.clampLevel(this.currentReasoningSupport, level);
|
|
331
343
|
}
|
|
332
344
|
|
|
333
345
|
protected resolvePreferenceReasoningLevel(): ReasoningLevel | undefined {
|
|
@@ -586,6 +598,17 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
586
598
|
return session?.model.settings?.commonSettings?.modelId;
|
|
587
599
|
}
|
|
588
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Opens the Models page of the AI Configuration view, which is where the models this list shows are
|
|
603
|
+
* chosen. The target is the id of that view's Models category (`AiConfigurationCategoryId.MODELS`),
|
|
604
|
+
* passed as a string because this package does not depend on the package that declares it.
|
|
605
|
+
*/
|
|
606
|
+
protected openModelConfiguration = (): void => {
|
|
607
|
+
this.commandService.executeCommand(AI_SHOW_SETTINGS_COMMAND.id, AI_CONFIGURATION_MODELS_CATEGORY_ID).catch(error => {
|
|
608
|
+
this.logger.error(`Failed to execute '${AI_SHOW_SETTINGS_COMMAND.id}' from the model selector`, error);
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
|
|
589
612
|
/** Sets (or clears, with `undefined`) the per-session model override for the active session. */
|
|
590
613
|
protected handleSessionModelChange = (modelId: string | undefined): void => {
|
|
591
614
|
const session = this.chatService.getSessions().find(s => s.model.id === this._chatModel?.id);
|
|
@@ -609,16 +632,27 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
609
632
|
this.update();
|
|
610
633
|
};
|
|
611
634
|
|
|
635
|
+
/**
|
|
636
|
+
* The models the selector offers: the favorites, i.e. the newest models of each provider plus
|
|
637
|
+
* whatever the user starred, and the session's own model even when it is neither. Discovery
|
|
638
|
+
* registers every release a provider offers, which is a list far too long to pick from here; the
|
|
639
|
+
* provider's page in the AI Configuration view is where the rest can be browsed and starred.
|
|
640
|
+
*/
|
|
641
|
+
protected getSelectableModels(currentModelId: string | undefined): LanguageModel[] {
|
|
642
|
+
return this.availableModels.filter(model => this.favoriteModels.isFavorite(model.id) || model.id === currentModelId);
|
|
643
|
+
}
|
|
644
|
+
|
|
612
645
|
/** Builds the props for the per-session model selector. */
|
|
613
646
|
protected getModelSelectorProps(): ModelSelectorWidgetProps {
|
|
614
647
|
const currentModelId = this.getSessionModelOverride();
|
|
615
648
|
const defaultLabel = this.resolvedDefaultLabel
|
|
616
649
|
?? nls.localize('theia/ai/chat-ui/agentDefaultModel', 'agent default');
|
|
617
650
|
return {
|
|
618
|
-
models: this.
|
|
651
|
+
models: this.getSelectableModels(currentModelId),
|
|
619
652
|
currentModelId,
|
|
620
653
|
defaultLabel,
|
|
621
654
|
onModelChange: this.handleSessionModelChange,
|
|
655
|
+
onConfigure: this.openModelConfiguration,
|
|
622
656
|
};
|
|
623
657
|
}
|
|
624
658
|
|
|
@@ -1113,6 +1147,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
1113
1147
|
this.updateReasoningSupport(this.receivingAgent.agentId);
|
|
1114
1148
|
}
|
|
1115
1149
|
}));
|
|
1150
|
+
this.toDispose.push(this.favoriteModels.onDidChange(() => this.update()));
|
|
1151
|
+
// An agent's default is usually an alias, so editing which model the alias points at changes what
|
|
1152
|
+
// "Default" resolves to and with it the model-dependent state. The registry's own change event
|
|
1153
|
+
// does not cover this: the models it holds are the same ones, only the alias moved.
|
|
1154
|
+
this.toDispose.push(this.aliasRegistry.onDidChange(() => {
|
|
1155
|
+
this.updateResolvedDefaultModel();
|
|
1156
|
+
this.updateReasoningSupport(this.receivingAgent?.agentId);
|
|
1157
|
+
}));
|
|
1116
1158
|
// When the agent's model is changed in the AI configuration, refresh the selector's resolved
|
|
1117
1159
|
// default and the model-dependent state (reasoning support, context size, server tools, vendor).
|
|
1118
1160
|
this.toDispose.push(this.aiSettingsService.onDidChange(() => {
|
|
@@ -1849,6 +1891,8 @@ interface ModelSelectorWidgetProps {
|
|
|
1849
1891
|
defaultLabel: string;
|
|
1850
1892
|
/** Set the session override (or clear it with `undefined`). */
|
|
1851
1893
|
onModelChange: (modelId: string | undefined) => void;
|
|
1894
|
+
/** Opens the page that decides which models this list shows. */
|
|
1895
|
+
onConfigure: () => void;
|
|
1852
1896
|
}
|
|
1853
1897
|
|
|
1854
1898
|
interface ChatInputProperties {
|
|
@@ -2074,37 +2118,28 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
2074
2118
|
props.contextMenuCallback(e.event)
|
|
2075
2119
|
);
|
|
2076
2120
|
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2121
|
+
// The editor is laid out at its full content height and the surrounding container
|
|
2122
|
+
// scrolls, so Monaco cannot reveal the cursor itself. Scroll the container to the
|
|
2123
|
+
// visual row of the cursor; for wrapped lines this differs from the start of the model line.
|
|
2124
|
+
// `getTopForPosition` is content-absolute, so subtract the editor's own scroll offset,
|
|
2125
|
+
// which is non-zero while `automaticLayout` has the editor laid out at a clamped height.
|
|
2126
|
+
const revealCursor = (position: IPosition) => {
|
|
2127
|
+
const container = editorContainerRef.current;
|
|
2128
|
+
const control = editor.getControl();
|
|
2129
|
+
const editorNode = control.getDomNode();
|
|
2130
|
+
if (!container || !editorNode) {
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const rowTop = editorNode.getBoundingClientRect().top - container.getBoundingClientRect().top
|
|
2134
|
+
+ control.getTopForPosition(position.lineNumber, position.column) - control.getScrollTop();
|
|
2135
|
+
const rowBottom = rowTop + control.getOption(EditorOption.lineHeight);
|
|
2136
|
+
const delta = computeRevealScrollDelta(rowTop, rowBottom, container.clientHeight);
|
|
2137
|
+
if (delta !== 0) {
|
|
2138
|
+
container.scrollTop += delta;
|
|
2092
2139
|
}
|
|
2093
|
-
|
|
2094
|
-
const lineNumbers = model.getAllDecorations().filter(predicate => predicate.options.description?.startsWith('line-number-'));
|
|
2095
|
-
editor.getControl().removeDecorations(lineNumbers.map(d => d.id));
|
|
2096
|
-
editor.getControl().createDecorationsCollection(decorations);
|
|
2097
2140
|
};
|
|
2098
2141
|
|
|
2099
|
-
editor.getControl().
|
|
2100
|
-
updateLineCounts();
|
|
2101
|
-
});
|
|
2102
|
-
|
|
2103
|
-
editor.getControl().onDidChangeCursorPosition(e => {
|
|
2104
|
-
const lineNumber = e.position.lineNumber;
|
|
2105
|
-
const line = editor.getControl().getDomNode()?.querySelector(`.line-number-${lineNumber}`);
|
|
2106
|
-
line?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
|
2107
|
-
});
|
|
2142
|
+
editor.getControl().onDidChangeCursorPosition(e => revealCursor(e.position));
|
|
2108
2143
|
|
|
2109
2144
|
editorRef.current = editor;
|
|
2110
2145
|
props.setEditorRef(editor);
|
|
@@ -2112,8 +2147,6 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
2112
2147
|
if (props.initialValue) {
|
|
2113
2148
|
setValue(props.initialValue);
|
|
2114
2149
|
}
|
|
2115
|
-
|
|
2116
|
-
updateLineCounts();
|
|
2117
2150
|
};
|
|
2118
2151
|
createInputElement();
|
|
2119
2152
|
|
|
@@ -2609,6 +2642,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({
|
|
|
2609
2642
|
currentModelId={modelSelectorProps.currentModelId}
|
|
2610
2643
|
defaultLabel={modelSelectorProps.defaultLabel}
|
|
2611
2644
|
onModelChange={modelSelectorProps.onModelChange}
|
|
2645
|
+
onConfigure={modelSelectorProps.onConfigure}
|
|
2612
2646
|
disabled={!isEnabled}
|
|
2613
2647
|
hoverService={hoverService}
|
|
2614
2648
|
/>
|
|
@@ -2777,6 +2811,8 @@ interface ChatModelSelectorProps {
|
|
|
2777
2811
|
currentModelId?: string;
|
|
2778
2812
|
defaultLabel: string;
|
|
2779
2813
|
onModelChange: (modelId: string | undefined) => void;
|
|
2814
|
+
/** Opens the page that decides which models this list shows. */
|
|
2815
|
+
onConfigure: () => void;
|
|
2780
2816
|
disabled?: boolean;
|
|
2781
2817
|
hoverService: HoverService;
|
|
2782
2818
|
}
|
|
@@ -2784,33 +2820,51 @@ interface ChatModelSelectorProps {
|
|
|
2784
2820
|
/**
|
|
2785
2821
|
* Per-session model selector. The first option ("Default") reverts to the agent's configured
|
|
2786
2822
|
* model that new sessions use; picking any other model overrides it for the current session only.
|
|
2823
|
+
*
|
|
2824
|
+
* It lists the models marked for it rather than every model a provider offers, and ends with the way
|
|
2825
|
+
* to change that, so a list this short is not mistaken for all there is.
|
|
2787
2826
|
*/
|
|
2788
2827
|
const ChatModelSelector: React.FunctionComponent<ChatModelSelectorProps> = React.memo(({
|
|
2789
|
-
models, currentModelId, defaultLabel, onModelChange, disabled, hoverService
|
|
2828
|
+
models, currentModelId, defaultLabel, onModelChange, onConfigure, disabled, hoverService
|
|
2790
2829
|
}) => {
|
|
2791
2830
|
// Sentinel value for the "use the agent default" option (SelectComponent needs a non-empty value).
|
|
2792
2831
|
const defaultValueId = '__default__';
|
|
2832
|
+
// …and for the entry that opens the configuration instead of selecting anything.
|
|
2833
|
+
const configureValueId = '__configure__';
|
|
2834
|
+
// Picking the configure entry leaves the model as it was, but the select has already taken it as its
|
|
2835
|
+
// selection; remounting it puts the session's own model back in the field.
|
|
2836
|
+
const [selectorGeneration, setSelectorGeneration] = React.useState(0);
|
|
2793
2837
|
const isOverridden = !!currentModelId;
|
|
2794
2838
|
// The override points at a model that is no longer ready/available. Guard on a loaded model list so
|
|
2795
2839
|
// the override is not flagged as unavailable during the initial (still empty) load.
|
|
2796
2840
|
const isUnavailable = isOverridden && models.length > 0
|
|
2797
2841
|
&& !models.some(model => model.id === currentModelId && model.status.status === 'ready');
|
|
2798
2842
|
const options: SelectOption[] = React.useMemo(() => {
|
|
2799
|
-
const readyModels = models.filter(model => model.status.status === 'ready')
|
|
2800
|
-
// Stable, predictable order: the registry adds models as their async metadata resolves.
|
|
2801
|
-
.sort((left, right) => left.id.localeCompare(right.id));
|
|
2802
2843
|
const opts: SelectOption[] = [
|
|
2803
2844
|
{
|
|
2804
2845
|
value: defaultValueId,
|
|
2805
2846
|
label: nls.localizeByDefault('Default'),
|
|
2806
2847
|
detail: defaultLabel
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2848
|
+
}
|
|
2849
|
+
];
|
|
2850
|
+
// A rule between the providers, their models newest first: the models of one provider belong
|
|
2851
|
+
// together, and the registry adds them in whatever order their metadata resolves. Models whose
|
|
2852
|
+
// provider reports no release date fall back to alphabetical.
|
|
2853
|
+
groupModelsByProvider(models.filter(model => model.status.status === 'ready')).forEach(({ models: providerModels }) => {
|
|
2854
|
+
opts.push({ separator: true });
|
|
2855
|
+
opts.push(...providerModels.map(model => ({
|
|
2809
2856
|
value: model.id,
|
|
2810
2857
|
label: model.id,
|
|
2811
2858
|
detail: model.name && model.name !== model.id ? model.name : undefined
|
|
2812
|
-
}))
|
|
2813
|
-
|
|
2859
|
+
})));
|
|
2860
|
+
});
|
|
2861
|
+
// The list is deliberately short — it holds the models marked for it, not every model a provider
|
|
2862
|
+
// offers — so it says where the rest are rather than leaving the absence to be puzzled over.
|
|
2863
|
+
opts.push({ separator: true });
|
|
2864
|
+
opts.push({
|
|
2865
|
+
value: configureValueId,
|
|
2866
|
+
label: nls.localize('theia/ai/chat-ui/manageModels', 'Manage models…')
|
|
2867
|
+
});
|
|
2814
2868
|
// Keep the active override visible even if its model is no longer ready/available, so the
|
|
2815
2869
|
// selector reflects the stored modelId instead of silently falling back to "Default". Once we
|
|
2816
2870
|
// know it is unavailable, show it as a disabled (struck-through) entry that cannot be reselected.
|
|
@@ -2826,8 +2880,15 @@ const ChatModelSelector: React.FunctionComponent<ChatModelSelectorProps> = React
|
|
|
2826
2880
|
}, [models, defaultLabel, currentModelId, isUnavailable]);
|
|
2827
2881
|
|
|
2828
2882
|
const handleChange = React.useCallback(
|
|
2829
|
-
(option: SelectOption) =>
|
|
2830
|
-
|
|
2883
|
+
(option: SelectOption) => {
|
|
2884
|
+
if (option.value === configureValueId) {
|
|
2885
|
+
setSelectorGeneration(generation => generation + 1);
|
|
2886
|
+
onConfigure();
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
onModelChange(!option.value || option.value === defaultValueId ? undefined : option.value);
|
|
2890
|
+
},
|
|
2891
|
+
[onModelChange, onConfigure]
|
|
2831
2892
|
);
|
|
2832
2893
|
|
|
2833
2894
|
const title = isUnavailable
|
|
@@ -2839,6 +2900,7 @@ const ChatModelSelector: React.FunctionComponent<ChatModelSelectorProps> = React
|
|
|
2839
2900
|
return (
|
|
2840
2901
|
<span className='theia-ChatInput-ModelSelector-container' onMouseEnter={hoverHandler(hoverService, title)}>
|
|
2841
2902
|
<SelectComponent
|
|
2903
|
+
key={selectorGeneration}
|
|
2842
2904
|
className={`theia-ChatInput-ModelSelector${isOverridden ? ' session-override' : ''}${disabled ? ' disabled' : ''}`}
|
|
2843
2905
|
options={options}
|
|
2844
2906
|
defaultValue={currentModelId ?? defaultValueId}
|
|
@@ -2885,7 +2947,7 @@ const ReasoningSelector: React.FunctionComponent<ReasoningSelectorProps> = React
|
|
|
2885
2947
|
);
|
|
2886
2948
|
|
|
2887
2949
|
const title = nls.localizeByDefault('Reasoning');
|
|
2888
|
-
const effectiveLevel = currentLevel ?? reasoningSupport.defaultLevel ??
|
|
2950
|
+
const effectiveLevel = ReasoningSupport.clampLevel(reasoningSupport, currentLevel ?? reasoningSupport.defaultLevel ?? 'off');
|
|
2889
2951
|
|
|
2890
2952
|
return (
|
|
2891
2953
|
<span onMouseEnter={hoverHandler(hoverService, title)}>
|
|
@@ -3030,7 +3092,7 @@ function toUiElement(element: ChangeSetElement,
|
|
|
3030
3092
|
}
|
|
3031
3093
|
|
|
3032
3094
|
const ChangeSetElement: React.FC<ChangeSetUIElement> = element => (
|
|
3033
|
-
<li title={nls.
|
|
3095
|
+
<li title={nls.localizeByDefault('Open Diff')} onClick={() => element.openChange?.()}>
|
|
3034
3096
|
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
|
|
3035
3097
|
</div>
|
|
3036
3098
|
<div className='theia-ChatInput-ChangeSet-labelParts'>
|
|
@@ -32,6 +32,7 @@ import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
|
|
|
32
32
|
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
33
33
|
import { ChatViewTreeWidget, ResponseNode } from '../chat-tree-view/chat-view-tree-widget';
|
|
34
34
|
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
35
|
+
import { CodeWrapperEditors } from './code-wrapper-editors';
|
|
35
36
|
|
|
36
37
|
export const CodePartRendererAction = Symbol('CodePartRendererAction');
|
|
37
38
|
/**
|
|
@@ -249,11 +250,18 @@ export const CodeWrapper = (props: {
|
|
|
249
250
|
editor.document.textEditorModel.setValue(props.content);
|
|
250
251
|
editor.getControl().onContextMenu(e => props.contextMenuCallback(e.event));
|
|
251
252
|
editorRef.current = editor;
|
|
253
|
+
if (ref.current) {
|
|
254
|
+
CodeWrapperEditors.set(ref.current, editor);
|
|
255
|
+
}
|
|
252
256
|
};
|
|
253
257
|
|
|
254
258
|
React.useEffect(() => {
|
|
255
259
|
createInputElement();
|
|
260
|
+
const element = ref.current;
|
|
256
261
|
return () => {
|
|
262
|
+
if (element) {
|
|
263
|
+
CodeWrapperEditors.remove(element);
|
|
264
|
+
}
|
|
257
265
|
if (editorRef.current) {
|
|
258
266
|
editorRef.current.dispose();
|
|
259
267
|
}
|
|
@@ -268,5 +276,5 @@ export const CodeWrapper = (props: {
|
|
|
268
276
|
|
|
269
277
|
editorRef.current?.resizeToFit();
|
|
270
278
|
|
|
271
|
-
return <div className=
|
|
279
|
+
return <div className={CodeWrapperEditors.CLASS} ref={ref}></div>;
|
|
272
280
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
18
|
+
|
|
19
|
+
const editors = new WeakMap<Element, SimpleMonacoEditor>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The Monaco editors that code parts render into, keyed by their `.theia-CodeWrapper` element, so features that
|
|
23
|
+
* work on the chat DOM (e.g. find in chat) can reach the editor that renders a code block.
|
|
24
|
+
*/
|
|
25
|
+
export namespace CodeWrapperEditors {
|
|
26
|
+
|
|
27
|
+
export const CLASS = 'theia-CodeWrapper';
|
|
28
|
+
|
|
29
|
+
/** The editor rendered into `element`, once it has been created. */
|
|
30
|
+
export function get(element: Element): SimpleMonacoEditor | undefined {
|
|
31
|
+
return editors.get(element);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function set(element: Element, editor: SimpleMonacoEditor): void {
|
|
35
|
+
editors.set(element, editor);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function remove(element: Element): void {
|
|
39
|
+
editors.delete(element);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -148,11 +148,6 @@ export class DelegationToolRenderer implements ChatResponsePartRenderer<ToolCall
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
interface PendingInteraction {
|
|
152
|
-
contentPart: InteractiveContent & ChatResponseContent;
|
|
153
|
-
id: string;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
151
|
interface DelegatedChatProps {
|
|
157
152
|
invocation?: ChatRequestInvocation;
|
|
158
153
|
agentName: string;
|
|
@@ -167,20 +162,18 @@ interface DelegatedChatProps {
|
|
|
167
162
|
interface DelegatedChatState {
|
|
168
163
|
node?: ResponseNode;
|
|
169
164
|
isOpen: boolean;
|
|
170
|
-
pendingInteractions: PendingInteraction[];
|
|
171
165
|
}
|
|
172
166
|
|
|
173
167
|
class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatState> {
|
|
174
168
|
private widget: ReturnType<SubChatWidgetFactory>;
|
|
175
169
|
private toDispose = new DisposableCollection();
|
|
176
|
-
private
|
|
170
|
+
private unmounted = false;
|
|
177
171
|
|
|
178
172
|
constructor(props: DelegatedChatProps) {
|
|
179
173
|
super(props);
|
|
180
174
|
this.state = {
|
|
181
175
|
node: undefined,
|
|
182
|
-
isOpen: false
|
|
183
|
-
pendingInteractions: []
|
|
176
|
+
isOpen: false
|
|
184
177
|
};
|
|
185
178
|
this.widget = props.subChatWidgetFactory();
|
|
186
179
|
}
|
|
@@ -198,72 +191,64 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
|
|
|
198
191
|
private subscribeToInvocation(invocation?: ChatRequestInvocation): void {
|
|
199
192
|
this.toDispose.dispose();
|
|
200
193
|
this.toDispose = new DisposableCollection();
|
|
201
|
-
this.trackedInteractionIds.clear();
|
|
202
194
|
if (!invocation) {
|
|
203
195
|
return;
|
|
204
196
|
}
|
|
205
197
|
|
|
206
198
|
invocation.responseCreated.then(chatModel => {
|
|
199
|
+
// Guard against continuations of a superseded invocation or an unmounted
|
|
200
|
+
// component: they would show the old model's node and register listeners
|
|
201
|
+
// into an already-disposed collection (which silently keeps them alive).
|
|
202
|
+
if (this.unmounted || invocation !== this.props.invocation) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
207
205
|
const node = mapResponseToNode(chatModel, this.props.parentNode);
|
|
208
206
|
this.setState({ node });
|
|
209
207
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
this.toDispose.push(chatModel.
|
|
215
|
-
|
|
216
|
-
// Subscribe to interactionNeeded for push-based interaction tracking
|
|
217
|
-
this.toDispose.push(chatModel.onInteractionNeeded(contentPart => {
|
|
218
|
-
const id = contentPart.interactionId;
|
|
219
|
-
if (id && !this.trackedInteractionIds.has(id)
|
|
220
|
-
&& this.findConfirmationRenderer(contentPart)) {
|
|
221
|
-
this.trackedInteractionIds.add(id);
|
|
222
|
-
this.setState(prevState => ({
|
|
223
|
-
pendingInteractions: [
|
|
224
|
-
...prevState.pendingInteractions,
|
|
225
|
-
{ contentPart, id }
|
|
226
|
-
]
|
|
227
|
-
}));
|
|
228
|
-
}
|
|
229
|
-
}));
|
|
208
|
+
this.toDispose.push(chatModel.onDidChange(() => this.forceUpdate()));
|
|
209
|
+
// Pending interactions are derived from chatModel.pendingInteractions in
|
|
210
|
+
// render(); interactionNeeded does not imply onDidChange, so re-render
|
|
211
|
+
// explicitly when a new interaction is announced (#17952).
|
|
212
|
+
this.toDispose.push(chatModel.onInteractionNeeded(() => this.forceUpdate()));
|
|
230
213
|
}).catch(error => {
|
|
231
214
|
console.error('Failed to create delegated chat response:', error);
|
|
232
215
|
});
|
|
233
216
|
|
|
234
217
|
invocation.responseCompleted.then(() => {
|
|
235
|
-
this.
|
|
218
|
+
if (!this.unmounted) {
|
|
219
|
+
this.forceUpdate();
|
|
220
|
+
}
|
|
236
221
|
}).catch(error => {
|
|
237
222
|
console.error('Error in delegated chat response completion:', error);
|
|
238
|
-
this.
|
|
223
|
+
if (!this.unmounted) {
|
|
224
|
+
this.forceUpdate();
|
|
225
|
+
}
|
|
239
226
|
});
|
|
240
227
|
}
|
|
241
228
|
|
|
242
229
|
override componentWillUnmount(): void {
|
|
230
|
+
this.unmounted = true;
|
|
243
231
|
this.toDispose.dispose();
|
|
244
|
-
this.trackedInteractionIds.clear();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private removeResolvedInteractions(): void {
|
|
248
|
-
this.setState(prevState => ({
|
|
249
|
-
pendingInteractions: prevState.pendingInteractions.filter(p => !p.contentPart.isResolved)
|
|
250
|
-
}));
|
|
251
232
|
}
|
|
252
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The highest-priority renderer that implements renderConfirmation. Unlike plain
|
|
236
|
+
* rendering, this must not stop at the overall highest-priority renderer: a
|
|
237
|
+
* specialized renderer without renderConfirmation (e.g. the delegation renderer
|
|
238
|
+
* itself for nested delegations) would otherwise silently drop the interaction.
|
|
239
|
+
*/
|
|
253
240
|
private findConfirmationRenderer(contentPart: ChatResponseContent): ChatResponsePartRenderer<ChatResponseContent> | undefined {
|
|
254
|
-
|
|
255
|
-
(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return undefined;
|
|
241
|
+
return this.props.chatResponsePartRenderers.getContributions()
|
|
242
|
+
.filter(renderer => renderer.renderConfirmation)
|
|
243
|
+
.reduce<[number, ChatResponsePartRenderer<ChatResponseContent> | undefined]>(
|
|
244
|
+
(prev, current) => {
|
|
245
|
+
const prio = current.canHandle(contentPart);
|
|
246
|
+
if (prio > prev[0]) {
|
|
247
|
+
return [prio, current];
|
|
248
|
+
}
|
|
249
|
+
return prev;
|
|
250
|
+
},
|
|
251
|
+
[-1, undefined])[1];
|
|
267
252
|
}
|
|
268
253
|
|
|
269
254
|
private handleToggle = (event: React.SyntheticEvent<HTMLDetailsElement>): void => {
|
|
@@ -271,10 +256,43 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
|
|
|
271
256
|
this.setState({ isOpen: details.open });
|
|
272
257
|
};
|
|
273
258
|
|
|
274
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Clicking anywhere inside a <summary> toggles the <details> element by default,
|
|
261
|
+
* which would collapse/expand the block while the user operates the inline
|
|
262
|
+
* interaction UI. Suppress the toggle for such clicks — but keep nested
|
|
263
|
+
* <summary> elements working (e.g. the argument expanders of a confirmation
|
|
264
|
+
* card), whose own toggle is also the click's default action.
|
|
265
|
+
*/
|
|
266
|
+
private preventSummaryToggle = (event: React.MouseEvent): void => {
|
|
267
|
+
const target = event.target instanceof HTMLElement ? event.target : undefined;
|
|
268
|
+
const closestSummary = target?.closest('summary');
|
|
269
|
+
if (closestSummary && event.currentTarget.contains(closestSummary)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
private renderPendingInteractions(pendingInteractions: ReadonlyArray<InteractiveContent & ChatResponseContent>): React.ReactNode[] {
|
|
276
|
+
// Key by interactionId so a sibling interaction resolving does not remount the
|
|
277
|
+
// remaining ones (losing in-progress wizard state); disambiguate the rare
|
|
278
|
+
// duplicate ids (e.g. two pending questions with identical text) by occurrence.
|
|
279
|
+
const seenIds = new Map<string, number>();
|
|
280
|
+
return pendingInteractions.map(contentPart => {
|
|
281
|
+
const baseKey = contentPart.interactionId ?? 'interaction';
|
|
282
|
+
const occurrence = seenIds.get(baseKey) ?? 0;
|
|
283
|
+
seenIds.set(baseKey, occurrence + 1);
|
|
284
|
+
return this.renderInteractionConfirmation(contentPart, occurrence === 0 ? baseKey : `${baseKey}-${occurrence}`);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private renderInteractionConfirmation(contentPart: InteractiveContent & ChatResponseContent, key: string): React.ReactNode {
|
|
275
289
|
const renderer = this.findConfirmationRenderer(contentPart);
|
|
276
290
|
if (renderer && this.state.node) {
|
|
277
|
-
return
|
|
291
|
+
return (
|
|
292
|
+
<React.Fragment key={key}>
|
|
293
|
+
{renderer.renderConfirmation!(contentPart, this.state.node)}
|
|
294
|
+
</React.Fragment>
|
|
295
|
+
);
|
|
278
296
|
}
|
|
279
297
|
return undefined;
|
|
280
298
|
}
|
|
@@ -285,6 +303,12 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
|
|
|
285
303
|
const isComplete = this.state.node?.response.isComplete ?? false;
|
|
286
304
|
const isCanceled = this.state.node?.response.isCanceled ?? false;
|
|
287
305
|
const isError = this.state.node?.response.isError ?? false;
|
|
306
|
+
// Derived from the model on every render so pending interactions survive
|
|
307
|
+
// remounts and never outlive their resolution or the response (#17952). The
|
|
308
|
+
// list also covers interactions bubbled up from nested delegations, whose
|
|
309
|
+
// waiting state is not reflected in this response's isWaitingForInput.
|
|
310
|
+
const pendingInteractions = this.state.node?.response.pendingInteractions ?? [];
|
|
311
|
+
const isWaitingForInput = (this.state.node?.response.isWaitingForInput ?? false) || pendingInteractions.length > 0;
|
|
288
312
|
|
|
289
313
|
let statusIcon = '';
|
|
290
314
|
let statusText = '';
|
|
@@ -292,12 +316,16 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
|
|
|
292
316
|
if (isCanceled) {
|
|
293
317
|
statusIcon = 'codicon-close';
|
|
294
318
|
statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/canceled', 'canceled');
|
|
295
|
-
} else if (isComplete) {
|
|
296
|
-
statusIcon = 'codicon-check';
|
|
297
|
-
statusText = nls.localizeByDefault('completed');
|
|
298
319
|
} else if (isError) {
|
|
320
|
+
// error() also marks the response complete, so check isError first
|
|
299
321
|
statusIcon = 'codicon-error';
|
|
300
322
|
statusText = nls.localizeByDefault('error');
|
|
323
|
+
} else if (isComplete) {
|
|
324
|
+
statusIcon = 'codicon-check';
|
|
325
|
+
statusText = nls.localizeByDefault('completed');
|
|
326
|
+
} else if (isWaitingForInput) {
|
|
327
|
+
statusIcon = 'codicon-loading';
|
|
328
|
+
statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/waitingForInput', 'waiting for input');
|
|
301
329
|
} else {
|
|
302
330
|
statusIcon = 'codicon-loading';
|
|
303
331
|
statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/generating', 'generating...');
|
|
@@ -310,7 +338,7 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
|
|
|
310
338
|
statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/starting', 'starting...');
|
|
311
339
|
}
|
|
312
340
|
|
|
313
|
-
const { isOpen
|
|
341
|
+
const { isOpen } = this.state;
|
|
314
342
|
const showInteractionsInSummary = !isOpen && pendingInteractions.length > 0;
|
|
315
343
|
|
|
316
344
|
return (
|
|
@@ -336,10 +364,8 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
|
|
|
336
364
|
<span className={`delegation-toggle-arrow${isOpen ? ' open' : ''}`} />
|
|
337
365
|
</div>
|
|
338
366
|
{showInteractionsInSummary && (
|
|
339
|
-
<div className='delegation-pending-confirmations'>
|
|
340
|
-
{
|
|
341
|
-
this.renderInteractionConfirmation(contentPart, id)
|
|
342
|
-
)}
|
|
367
|
+
<div className='delegation-pending-confirmations' onClick={this.preventSummaryToggle}>
|
|
368
|
+
{this.renderPendingInteractions(pendingInteractions)}
|
|
343
369
|
</div>
|
|
344
370
|
)}
|
|
345
371
|
</summary>
|