@theia/ai-chat-ui 1.63.0-next.0 → 1.63.0-next.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/lib/browser/ai-chat-ui-contribution.js +2 -2
  2. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  3. package/lib/browser/chat-input-widget.d.ts +3 -1
  4. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  5. package/lib/browser/chat-input-widget.js +101 -90
  6. package/lib/browser/chat-input-widget.js.map +1 -1
  7. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  8. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  9. package/lib/browser/chat-response-renderer/index.js +1 -0
  10. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  11. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +17 -0
  12. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -0
  13. package/lib/browser/chat-response-renderer/tool-confirmation.js +120 -0
  14. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -0
  15. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -1
  16. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  17. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +83 -19
  18. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  19. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
  20. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
  21. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +9 -0
  22. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  23. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +6 -0
  24. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  25. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +30 -3
  26. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  27. package/lib/browser/chat-view-widget.d.ts +5 -2
  28. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  29. package/lib/browser/chat-view-widget.js +19 -6
  30. package/lib/browser/chat-view-widget.js.map +1 -1
  31. package/package.json +10 -10
  32. package/src/browser/ai-chat-ui-contribution.ts +2 -2
  33. package/src/browser/chat-input-widget.tsx +171 -137
  34. package/src/browser/chat-response-renderer/index.ts +1 -0
  35. package/src/browser/chat-response-renderer/tool-confirmation.tsx +173 -0
  36. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +115 -19
  37. package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +16 -1
  38. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +39 -5
  39. package/src/browser/chat-view-widget.tsx +23 -7
  40. package/src/browser/style/index.css +173 -0
@@ -23,3 +23,4 @@ export * from './text-part-renderer';
23
23
  export * from './toolcall-part-renderer';
24
24
  export * from './thinking-part-renderer';
25
25
  export * from './progress-part-renderer';
26
+ export * from './tool-confirmation';
@@ -0,0 +1,173 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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 * as React from '@theia/core/shared/react';
18
+ import { nls } from '@theia/core/lib/common/nls';
19
+ import { codicon } from '@theia/core/lib/browser';
20
+ import { ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
21
+
22
+ /**
23
+ * States the tool confirmation component can be in
24
+ */
25
+ export type ToolConfirmationState = 'waiting' | 'approved' | 'denied';
26
+
27
+ export interface ToolConfirmationProps {
28
+ response: ToolCallChatResponseContent;
29
+ onApprove: (mode?: 'once' | 'session' | 'forever') => void;
30
+ onDeny: (mode?: 'once' | 'session' | 'forever') => void;
31
+ }
32
+
33
+ /**
34
+ * Component that displays approval/denial buttons for tool execution
35
+ */
36
+ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, onApprove, onDeny }) => {
37
+ const [state, setState] = React.useState<ToolConfirmationState>('waiting');
38
+ // Track selected mode for each action
39
+ const [approveMode, setApproveMode] = React.useState<'once' | 'session' | 'forever'>('once');
40
+ const [denyMode, setDenyMode] = React.useState<'once' | 'session' | 'forever'>('once');
41
+ const [dropdownOpen, setDropdownOpen] = React.useState<'approve' | 'deny' | undefined>(undefined);
42
+
43
+ const handleApprove = React.useCallback(() => {
44
+ setState('approved');
45
+ onApprove(approveMode);
46
+ }, [onApprove, approveMode]);
47
+
48
+ const handleDeny = React.useCallback(() => {
49
+ setState('denied');
50
+ onDeny(denyMode);
51
+ }, [onDeny, denyMode]);
52
+
53
+ if (state === 'approved') {
54
+ return (
55
+ <div className="theia-tool-confirmation-status approved">
56
+ <span className={codicon('check')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/approved', 'Tool execution approved')}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ if (state === 'denied') {
62
+ return (
63
+ <div className="theia-tool-confirmation-status denied">
64
+ <span className={codicon('close')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/denied', 'Tool execution denied')}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ // Helper for dropdown options
70
+ const MODES: Array<'once' | 'session' | 'forever'> = ['once', 'session', 'forever'];
71
+ // Unified labels for both main button and dropdown, as requested
72
+ const modeLabel = (type: 'approve' | 'deny', mode: 'once' | 'session' | 'forever') => {
73
+ if (type === 'approve') {
74
+ switch (mode) {
75
+ case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve', 'Approve');
76
+ case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-session', 'Approve for this Chat');
77
+ case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-forever', 'Always Approve');
78
+ }
79
+ } else {
80
+ switch (mode) {
81
+ case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny', 'Deny');
82
+ case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-session', 'Deny for this Chat');
83
+ case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-forever', 'Always Deny');
84
+ }
85
+ }
86
+ };
87
+ // Main button label is always the same as the dropdown label for the selected mode
88
+ const mainButtonLabel = modeLabel; // Use the same function for both
89
+
90
+ // Tooltips for dropdown options
91
+ const modeTooltip = (type: 'approve' | 'deny', mode: 'once' | 'session' | 'forever') => {
92
+ if (type === 'approve') {
93
+ switch (mode) {
94
+ case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-tooltip', 'Approve this tool call once');
95
+ case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-session-tooltip', 'Approve all calls of this tool for this chat session');
96
+ case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-forever-tooltip', 'Always approve this tool');
97
+ }
98
+ } else {
99
+ switch (mode) {
100
+ case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-tooltip', 'Deny this tool call once');
101
+ case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-session-tooltip', 'Deny all calls of this tool for this chat session');
102
+ case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-forever-tooltip', 'Always deny this tool');
103
+ }
104
+ }
105
+ };
106
+
107
+ // Split button for approve/deny
108
+ const renderSplitButton = (type: 'approve' | 'deny') => {
109
+ const selectedMode = type === 'approve' ? approveMode : denyMode;
110
+ const setMode = type === 'approve' ? setApproveMode : setDenyMode;
111
+ const handleMain = type === 'approve' ? handleApprove : handleDeny;
112
+ const otherModes = MODES.filter(m => m !== selectedMode);
113
+ return (
114
+ <div className={`theia-tool-confirmation-split-button ${type}`}
115
+ style={{ display: 'inline-flex', position: 'relative' }}>
116
+ <button
117
+ className={`theia-button ${type === 'approve' ? 'primary' : 'secondary'} theia-tool-confirmation-main-btn`}
118
+ onClick={handleMain}
119
+ >
120
+ {mainButtonLabel(type, selectedMode)}
121
+ </button>
122
+ <button
123
+ className={`theia-button ${type === 'approve' ? 'primary' : 'secondary'} theia-tool-confirmation-chevron-btn`}
124
+ onClick={() => setDropdownOpen(dropdownOpen === type ? undefined : type)}
125
+ aria-haspopup="true"
126
+ aria-expanded={dropdownOpen === type}
127
+ tabIndex={0}
128
+ title={type === 'approve' ? 'More Approve Options' : 'More Deny Options'}
129
+ >
130
+ <span className={codicon('chevron-down')}></span>
131
+ </button>
132
+ {dropdownOpen === type && (
133
+ <ul
134
+ className="theia-tool-confirmation-dropdown-menu"
135
+ onMouseLeave={() => setDropdownOpen(undefined)}
136
+ >
137
+ {otherModes.map(mode => (
138
+ <li
139
+ key={mode}
140
+ className="theia-tool-confirmation-dropdown-item"
141
+ onClick={() => {
142
+ setMode(mode);
143
+ setDropdownOpen(undefined);
144
+ }}
145
+ title={modeTooltip(type, mode)}
146
+ >
147
+ {modeLabel(type, mode)}
148
+ </li>
149
+ ))}
150
+ </ul>
151
+ )}
152
+ </div>
153
+ );
154
+ };
155
+
156
+ return (
157
+ <div className="theia-tool-confirmation">
158
+ <div className="theia-tool-confirmation-header">
159
+ <span className={codicon('shield')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/header', 'Confirm Tool Execution')}
160
+ </div>
161
+ <div className="theia-tool-confirmation-info">
162
+ <div className="theia-tool-confirmation-name">
163
+ <span className="label">{nls.localize('theia/ai/chat-ui/toolconfirmation/tool', 'Tool')}:</span>
164
+ <span className="value">{response.name}</span>
165
+ </div>
166
+ </div>
167
+ <div className="theia-tool-confirmation-actions">
168
+ {renderSplitButton('deny')}
169
+ {renderSplitButton('approve')}
170
+ </div>
171
+ </div>
172
+ );
173
+ };
@@ -15,15 +15,22 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
18
- import { injectable } from '@theia/core/shared/inversify';
18
+ import { inject, injectable } from '@theia/core/shared/inversify';
19
19
  import { ChatResponseContent, ToolCallChatResponseContent } 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 { codicon } from '@theia/core/lib/browser';
22
23
  import * as React from '@theia/core/shared/react';
24
+ import { ToolConfirmation, ToolConfirmationState } from './tool-confirmation';
25
+ import { ToolConfirmationManager, ToolConfirmationMode } from '@theia/ai-chat/lib/browser/chat-tool-preferences';
26
+ import { ResponseNode } from '../chat-tree-view';
23
27
 
24
28
  @injectable()
25
29
  export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {
26
30
 
31
+ @inject(ToolConfirmationManager)
32
+ protected toolConfirmationManager: ToolConfirmationManager;
33
+
27
34
  canHandle(response: ChatResponseContent): number {
28
35
  if (ToolCallChatResponseContent.is(response)) {
29
36
  return 10;
@@ -31,23 +38,20 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
31
38
  return -1;
32
39
  }
33
40
 
34
- render(response: ToolCallChatResponseContent): ReactNode {
35
- return (
36
- <h4 className='theia-toolCall'>
37
- {response.finished ? (
38
- <details>
39
- <summary>{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
40
- ({this.renderCollapsibleArguments(response.arguments)})
41
- </summary>
42
- <pre>{this.tryPrettyPrintJson(response)}</pre>
43
- </details>
44
- ) : (
45
- <span>
46
- <Spinner /> {nls.localizeByDefault('Running')} {response.name}({this.renderCollapsibleArguments(response.arguments)})
47
- </span>
48
- )}
49
- </h4>
50
- );
41
+ render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
42
+ const chatId = parentNode.sessionId;
43
+ const confirmationMode = response.name ? this.getToolConfirmationSettings(response.name, chatId) : ToolConfirmationMode.DISABLED;
44
+ return <ToolCallContent
45
+ response={response}
46
+ confirmationMode={confirmationMode}
47
+ toolConfirmationManager={this.toolConfirmationManager}
48
+ chatId={chatId}
49
+ renderCollapsibleArguments={this.renderCollapsibleArguments.bind(this)}
50
+ tryPrettyPrintJson={this.tryPrettyPrintJson.bind(this)} />;
51
+ }
52
+
53
+ protected getToolConfirmationSettings(responseId: string, chatId: string): ToolConfirmationMode {
54
+ return this.toolConfirmationManager.getConfirmationMode(responseId, chatId);
51
55
  }
52
56
 
53
57
  protected renderCollapsibleArguments(args: string | undefined): ReactNode {
@@ -97,5 +101,97 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
97
101
  }
98
102
 
99
103
  const Spinner = () => (
100
- <i className="fa fa-spinner fa-spin"></i>
104
+ <span className={codicon('loading')}></span>
101
105
  );
106
+
107
+ interface ToolCallContentProps {
108
+ response: ToolCallChatResponseContent;
109
+ confirmationMode: ToolConfirmationMode;
110
+ toolConfirmationManager: ToolConfirmationManager;
111
+ chatId: string;
112
+ renderCollapsibleArguments: (args: string | undefined) => ReactNode;
113
+ tryPrettyPrintJson: (response: ToolCallChatResponseContent) => string | undefined;
114
+ }
115
+
116
+ /**
117
+ * A function component to handle tool call rendering and confirmation
118
+ */
119
+ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId, tryPrettyPrintJson, renderCollapsibleArguments }) => {
120
+ const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
121
+
122
+ React.useEffect(() => {
123
+ if (confirmationMode === ToolConfirmationMode.YOLO) {
124
+ response.confirm();
125
+ setConfirmationState('approved');
126
+ return;
127
+ } else if (confirmationMode === ToolConfirmationMode.DISABLED) {
128
+ response.deny();
129
+ setConfirmationState('denied');
130
+ return;
131
+ }
132
+ response.confirmed.then(
133
+ confirmed => {
134
+ if (confirmed === true) {
135
+ setConfirmationState('approved');
136
+ } else {
137
+ setConfirmationState('denied');
138
+ }
139
+ }
140
+ )
141
+ .catch(() => {
142
+ setConfirmationState('denied');
143
+ });
144
+ }, [response, confirmationMode]);
145
+
146
+ const handleApprove = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
147
+ if (mode === 'forever' && response.name) {
148
+ toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.YOLO);
149
+ } else if (mode === 'session' && response.name) {
150
+ toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.YOLO, chatId);
151
+ }
152
+ response.confirm();
153
+ }, [response, toolConfirmationManager, chatId]);
154
+
155
+ const handleDeny = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
156
+ if (mode === 'forever' && response.name) {
157
+ toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.DISABLED);
158
+ } else if (mode === 'session' && response.name) {
159
+ toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.DISABLED, chatId);
160
+ }
161
+ response.deny();
162
+ }, [response, toolConfirmationManager, chatId]);
163
+
164
+ return (
165
+ <div className='theia-toolCall'>
166
+ <h4>
167
+ {confirmationState === 'denied' ? (
168
+ <span className="theia-tool-denied">
169
+ <span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolcall-part-renderer/denied', 'Execution denied')}: {response.name}
170
+ </span>
171
+ ) : response.finished ? (
172
+ <details>
173
+ <summary>{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
174
+ ({renderCollapsibleArguments(response.arguments)})
175
+ </summary>
176
+ <pre>{tryPrettyPrintJson(response)}</pre>
177
+ </details>
178
+ ) : (
179
+ confirmationState === 'approved' && (
180
+ <span>
181
+ <Spinner /> {nls.localizeByDefault('Running')} {response.name}
182
+ </span>
183
+ )
184
+ )}
185
+ </h4>
186
+
187
+ {/* Show confirmation UI when waiting for approval */}
188
+ {confirmationState === 'waiting' && (
189
+ <ToolConfirmation
190
+ response={response}
191
+ onApprove={handleApprove}
192
+ onDeny={handleDeny}
193
+ />
194
+ )}
195
+ </div>
196
+ );
197
+ };
@@ -19,7 +19,7 @@ import { AIChatInputWidget, type AIChatInputConfiguration } from '../chat-input-
19
19
  import type { EditableRequestNode } from './chat-view-tree-widget';
20
20
  import { URI } from '@theia/core';
21
21
  import { CHAT_VIEW_LANGUAGE_EXTENSION } from '../chat-view-language-contribution';
22
- import type { ChatRequestModel, EditableChatRequestModel } from '@theia/ai-chat';
22
+ import type { ChatRequestModel, EditableChatRequestModel, ChatHierarchyBranch } from '@theia/ai-chat';
23
23
  import type { AIVariableResolutionRequest } from '@theia/ai-core';
24
24
  import { Key } from '@theia/core/lib/browser';
25
25
 
@@ -29,6 +29,10 @@ export interface AIChatTreeInputConfiguration extends AIChatInputConfiguration {
29
29
  export const AIChatTreeInputArgs = Symbol('AIChatTreeInputArgs');
30
30
  export interface AIChatTreeInputArgs {
31
31
  node: EditableRequestNode;
32
+ /**
33
+ * The branch of the chat tree for this request node (used by the input widget for state tracking).
34
+ */
35
+ branch?: ChatHierarchyBranch;
32
36
  initialValue?: string;
33
37
  onQuery: (query: string) => Promise<void>;
34
38
  onUnpin?: () => void;
@@ -60,6 +64,13 @@ export class AIChatTreeInputWidget extends AIChatInputWidget {
60
64
  @postConstruct()
61
65
  protected override init(): void {
62
66
  super.init();
67
+ this.updateBranch();
68
+
69
+ const request = this.requestNode.request;
70
+ this.toDispose.push(request.session.onDidChange(() => {
71
+ this.updateBranch();
72
+ }));
73
+
63
74
  this.addKeyListener(this.node, Key.ESCAPE, () => {
64
75
  this.request.cancelEdit();
65
76
  });
@@ -71,6 +82,10 @@ export class AIChatTreeInputWidget extends AIChatInputWidget {
71
82
  });
72
83
  }
73
84
 
85
+ protected updateBranch(): void {
86
+ this.branch = this.args.branch ?? this.requestNode.branch;
87
+ }
88
+
74
89
  protected override getResourceUri(): URI {
75
90
  return new URI(`ai-chat:/${this.requestNode.id}-input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
76
91
  }
@@ -64,7 +64,8 @@ import { AIChatTreeInputFactory, type AIChatTreeInputWidget } from './chat-view-
64
64
  // TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model
65
65
  export interface RequestNode extends TreeNode {
66
66
  request: ChatRequestModel,
67
- branch: ChatHierarchyBranch
67
+ branch: ChatHierarchyBranch,
68
+ sessionId: string
68
69
  }
69
70
  export const isRequestNode = (node: TreeNode): node is RequestNode => 'request' in node;
70
71
 
@@ -75,7 +76,8 @@ export const isEditableRequestNode = (node: TreeNode): node is EditableRequestNo
75
76
 
76
77
  // TODO Instead of directly operating on the ChatResponseModel we could use an intermediate view model
77
78
  export interface ResponseNode extends TreeNode {
78
- response: ChatResponseModel
79
+ response: ChatResponseModel,
80
+ sessionId: string
79
81
  }
80
82
  export const isResponseNode = (node: TreeNode): node is ResponseNode => 'response' in node;
81
83
 
@@ -136,6 +138,10 @@ export class ChatViewTreeWidget extends TreeWidget {
136
138
 
137
139
  protected isEnabled = false;
138
140
 
141
+ protected chatModelId: string;
142
+
143
+ onScrollLockChange?: (temporaryLocked: boolean) => void;
144
+
139
145
  set shouldScrollToEnd(shouldScrollToEnd: boolean) {
140
146
  this._shouldScrollToEnd = shouldScrollToEnd;
141
147
  this.shouldScrollToRow = this._shouldScrollToEnd;
@@ -178,6 +184,9 @@ export class ChatViewTreeWidget extends TreeWidget {
178
184
  widget.setEnabled(change);
179
185
  });
180
186
  this.update();
187
+ }),
188
+ this.onScroll(scrollEvent => {
189
+ this.handleScrollEvent(scrollEvent);
181
190
  })
182
191
  ]);
183
192
  }
@@ -187,6 +196,27 @@ export class ChatViewTreeWidget extends TreeWidget {
187
196
  this.update();
188
197
  }
189
198
 
199
+ protected handleScrollEvent(_scrollEvent: unknown): void {
200
+ // Check if we're at the bottom of the view
201
+ const isAtBottom = this.isScrolledToBottom();
202
+
203
+ // Only handle temporary scroll lock if auto-scroll is currently enabled
204
+ if (this.shouldScrollToEnd) {
205
+ if (!isAtBottom) {
206
+ // User scrolled away from bottom, enable temporary lock
207
+ this.setTemporaryScrollLock(true);
208
+ }
209
+ } else if (isAtBottom) {
210
+ // User scrolled back to bottom, disable temporary lock
211
+ this.setTemporaryScrollLock(false);
212
+ }
213
+ }
214
+
215
+ protected setTemporaryScrollLock(enabled: boolean): void {
216
+ // Immediately apply scroll lock changes without delay
217
+ this.onScrollLockChange?.(enabled);
218
+ }
219
+
190
220
  protected override renderTree(model: TreeModel): React.ReactNode {
191
221
  if (!this.isEnabled) {
192
222
  return this.renderDisabledMessage();
@@ -214,7 +244,8 @@ export class ChatViewTreeWidget extends TreeWidget {
214
244
  get request(): ChatRequestModel {
215
245
  return branch.get();
216
246
  },
217
- branch
247
+ branch,
248
+ sessionId: this.chatModelId
218
249
  };
219
250
  }
220
251
 
@@ -222,7 +253,8 @@ export class ChatViewTreeWidget extends TreeWidget {
222
253
  return {
223
254
  id: response.id,
224
255
  parent: this.model.root as CompositeTreeNode,
225
- response
256
+ response,
257
+ sessionId: this.chatModelId
226
258
  };
227
259
  }
228
260
 
@@ -292,6 +324,7 @@ export class ChatViewTreeWidget extends TreeWidget {
292
324
  protected async recreateModelTree(chatModel: ChatModel): Promise<void> {
293
325
  if (CompositeTreeNode.is(this.model.root)) {
294
326
  const nodes: TreeNode[] = [];
327
+ this.chatModelId = chatModel.id;
295
328
  chatModel.getBranches().forEach(branch => {
296
329
  const request = branch.get();
297
330
  nodes.push(this.mapRequestToNode(branch));
@@ -426,7 +459,8 @@ export class ChatViewTreeWidget extends TreeWidget {
426
459
  initialValue: editableNode.request.message.request.text,
427
460
  onQuery: async query => {
428
461
  editableNode.request.submitEdit({ text: query });
429
- }
462
+ },
463
+ branch: editableNode.branch
430
464
  });
431
465
 
432
466
  this.chatInputs.set(editableNode.id, widget);
@@ -13,7 +13,7 @@
13
13
  //
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
- import { CommandService, deepClone, Emitter, Event, MessageService } from '@theia/core';
16
+ import { CommandService, deepClone, Emitter, Event, MessageService, URI } from '@theia/core';
17
17
  import { ChatRequest, ChatRequestModel, ChatService, ChatSession, isActiveSessionChangedEvent, MutableChatModel } from '@theia/ai-chat';
18
18
  import { BaseWidget, codicon, ExtractableWidget, Message, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
19
19
  import { nls } from '@theia/core/lib/common/nls';
@@ -28,6 +28,7 @@ import { FrontendVariableService } from '@theia/ai-core/lib/browser';
28
28
  export namespace ChatViewWidget {
29
29
  export interface State {
30
30
  locked?: boolean;
31
+ temporaryLocked?: boolean;
31
32
  }
32
33
  }
33
34
 
@@ -60,7 +61,7 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
60
61
 
61
62
  protected chatSession: ChatSession;
62
63
 
63
- protected _state: ChatViewWidget.State = { locked: false };
64
+ protected _state: ChatViewWidget.State = { locked: false, temporaryLocked: false };
64
65
  protected readonly onStateChangedEmitter = new Emitter<ChatViewWidget.State>();
65
66
 
66
67
  secondaryWindow: Window | undefined;
@@ -87,7 +88,8 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
87
88
  this.treeWidget,
88
89
  this.inputWidget,
89
90
  this.onStateChanged(newState => {
90
- this.treeWidget.shouldScrollToEnd = !newState.locked;
91
+ const shouldScrollToEnd = !newState.locked && !newState.temporaryLocked;
92
+ this.treeWidget.shouldScrollToEnd = shouldScrollToEnd;
91
93
  this.update();
92
94
  })
93
95
  ]);
@@ -107,6 +109,7 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
107
109
  this.inputWidget.onDeleteChangeSet = this.onDeleteChangeSet.bind(this);
108
110
  this.inputWidget.onDeleteChangeSetElement = this.onDeleteChangeSetElement.bind(this);
109
111
  this.treeWidget.trackChatModel(this.chatSession.model);
112
+ this.treeWidget.onScrollLockChange = this.onScrollLockChange.bind(this);
110
113
 
111
114
  this.initListeners();
112
115
 
@@ -161,6 +164,8 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
161
164
  if (oldState.locked) {
162
165
  copy.locked = oldState.locked;
163
166
  }
167
+ // Don't restore temporary lock state as it should reset on restart
168
+ copy.temporaryLocked = false;
164
169
  this.state = copy;
165
170
  }
166
171
 
@@ -210,16 +215,27 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
210
215
  this.chatService.deleteChangeSet(sessionId);
211
216
  }
212
217
 
213
- protected onDeleteChangeSetElement(sessionId: string, index: number): void {
214
- this.chatService.deleteChangeSetElement(sessionId, index);
218
+ protected onDeleteChangeSetElement(sessionId: string, uri: URI): void {
219
+ this.chatService.deleteChangeSetElement(sessionId, uri);
220
+ }
221
+
222
+ protected onScrollLockChange(temporaryLocked: boolean): void {
223
+ this.setTemporaryLock(temporaryLocked);
215
224
  }
216
225
 
217
226
  lock(): void {
218
- this.state = { ...deepClone(this.state), locked: true };
227
+ this.state = { ...deepClone(this.state), locked: true, temporaryLocked: false };
219
228
  }
220
229
 
221
230
  unlock(): void {
222
- this.state = { ...deepClone(this.state), locked: false };
231
+ this.state = { ...deepClone(this.state), locked: false, temporaryLocked: false };
232
+ }
233
+
234
+ setTemporaryLock(locked: boolean): void {
235
+ // Only set temporary lock if not permanently locked
236
+ if (!this.state.locked) {
237
+ this.state = { ...deepClone(this.state), temporaryLocked: locked };
238
+ }
223
239
  }
224
240
 
225
241
  get isLocked(): boolean {