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

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 (69) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +29 -1
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +158 -2
  4. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  5. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.js +8 -0
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/chat-input-widget.d.ts +9 -6
  9. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  10. package/lib/browser/chat-input-widget.js +181 -111
  11. package/lib/browser/chat-input-widget.js.map +1 -1
  12. package/lib/browser/chat-node-toolbar-action-contribution.d.ts +1 -0
  13. package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
  14. package/lib/browser/chat-node-toolbar-action-contribution.js +13 -0
  15. package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
  16. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +14 -0
  17. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -0
  18. package/lib/browser/chat-response-renderer/delegation-response-renderer.js +144 -0
  19. package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -0
  20. package/lib/browser/chat-response-renderer/index.d.ts +2 -0
  21. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  22. package/lib/browser/chat-response-renderer/index.js +2 -0
  23. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  24. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +17 -0
  25. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -0
  26. package/lib/browser/chat-response-renderer/tool-confirmation.js +120 -0
  27. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -0
  28. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -1
  29. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  30. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +83 -19
  31. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  32. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
  33. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
  34. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +9 -0
  35. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  36. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +8 -0
  37. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  38. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +72 -3
  39. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  40. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
  41. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
  42. package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
  43. package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
  44. package/lib/browser/chat-view-commands.d.ts +1 -0
  45. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  46. package/lib/browser/chat-view-commands.js +5 -0
  47. package/lib/browser/chat-view-commands.js.map +1 -1
  48. package/lib/browser/chat-view-contribution.js +2 -1
  49. package/lib/browser/chat-view-contribution.js.map +1 -1
  50. package/lib/browser/chat-view-widget.d.ts +6 -3
  51. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  52. package/lib/browser/chat-view-widget.js +20 -10
  53. package/lib/browser/chat-view-widget.js.map +1 -1
  54. package/package.json +10 -10
  55. package/src/browser/ai-chat-ui-contribution.ts +166 -5
  56. package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
  57. package/src/browser/chat-input-widget.tsx +280 -170
  58. package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
  59. package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
  60. package/src/browser/chat-response-renderer/index.ts +2 -0
  61. package/src/browser/chat-response-renderer/tool-confirmation.tsx +173 -0
  62. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +115 -19
  63. package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +16 -1
  64. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +89 -5
  65. package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
  66. package/src/browser/chat-view-commands.ts +6 -0
  67. package/src/browser/chat-view-contribution.ts +1 -1
  68. package/src/browser/chat-view-widget.tsx +25 -12
  69. package/src/browser/style/index.css +350 -2
@@ -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,12 @@ 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
+
145
+ protected lastScrollTop = 0;
146
+
139
147
  set shouldScrollToEnd(shouldScrollToEnd: boolean) {
140
148
  this._shouldScrollToEnd = shouldScrollToEnd;
141
149
  this.shouldScrollToRow = this._shouldScrollToEnd;
@@ -178,8 +186,14 @@ export class ChatViewTreeWidget extends TreeWidget {
178
186
  widget.setEnabled(change);
179
187
  });
180
188
  this.update();
189
+ }),
190
+ this.onScroll(scrollEvent => {
191
+ this.handleScrollEvent(scrollEvent);
181
192
  })
182
193
  ]);
194
+
195
+ // Initialize lastScrollTop with current scroll position
196
+ this.lastScrollTop = this.getCurrentScrollTop(undefined);
183
197
  }
184
198
 
185
199
  public setEnabled(enabled: boolean): void {
@@ -187,6 +201,72 @@ export class ChatViewTreeWidget extends TreeWidget {
187
201
  this.update();
188
202
  }
189
203
 
204
+ protected handleScrollEvent(scrollEvent: unknown): void {
205
+ // Get current scroll position
206
+ const currentScrollTop = this.getCurrentScrollTop(scrollEvent);
207
+ const isAtBottom = this.isScrolledToBottom();
208
+
209
+ // Determine scroll direction
210
+ const isScrollingUp = currentScrollTop < this.lastScrollTop;
211
+ const isScrollingDown = currentScrollTop > this.lastScrollTop;
212
+
213
+ // Handle scroll lock logic based on direction and position
214
+ // The key insight is that we only enable temporary lock when scrolling UP,
215
+ // and only disable it when scrolling DOWN to the bottom. This prevents
216
+ // the jitter when users try to scroll up by just a few pixels from the bottom.
217
+ if (this.shouldScrollToEnd) {
218
+ // Auto-scroll is enabled, check if we need to enable temporary lock
219
+ if (isScrollingUp) {
220
+ // User is scrolling up and not at bottom - enable temporary lock
221
+ this.setTemporaryScrollLock(true);
222
+ }
223
+ // Note: We don't disable temporary lock when scrolling down while shouldScrollToEnd is true
224
+ // because that would cause jitter. The lock will be disabled when user reaches bottom.
225
+ } else {
226
+ // Temporary lock is active, check if we should disable it
227
+ if (isScrollingDown && isAtBottom) {
228
+ // User scrolled back to bottom - disable temporary lock
229
+ this.setTemporaryScrollLock(false);
230
+ }
231
+ // Note: We don't change the lock state when scrolling up while locked,
232
+ // as the user is intentionally scrolling away from auto-scroll behavior.
233
+ }
234
+
235
+ // Update last scroll position for next comparison
236
+ this.lastScrollTop = currentScrollTop;
237
+ }
238
+
239
+ protected setTemporaryScrollLock(enabled: boolean): void {
240
+ // Immediately apply scroll lock changes without delay
241
+ this.onScrollLockChange?.(enabled);
242
+ }
243
+
244
+ protected getCurrentScrollTop(scrollEvent: unknown): number {
245
+ // For virtualized trees, try to get scroll position from the virtualized view
246
+ if (this.props.virtualized !== false && this.view) {
247
+ const scrollState = this.getVirtualizedScrollState();
248
+ if (scrollState !== undefined) {
249
+ return scrollState.scrollTop;
250
+ }
251
+ }
252
+
253
+ // Try to extract scroll position from the scroll event
254
+ if (scrollEvent && typeof scrollEvent === 'object' && 'scrollTop' in scrollEvent) {
255
+ const scrollEventWithScrollTop = scrollEvent as { scrollTop: unknown };
256
+ const scrollTop = scrollEventWithScrollTop.scrollTop;
257
+ if (typeof scrollTop === 'number' && !isNaN(scrollTop)) {
258
+ return scrollTop;
259
+ }
260
+ }
261
+
262
+ // Last resort: use DOM scroll position
263
+ if (this.node && typeof this.node.scrollTop === 'number') {
264
+ return this.node.scrollTop;
265
+ }
266
+
267
+ return 0;
268
+ }
269
+
190
270
  protected override renderTree(model: TreeModel): React.ReactNode {
191
271
  if (!this.isEnabled) {
192
272
  return this.renderDisabledMessage();
@@ -214,7 +294,8 @@ export class ChatViewTreeWidget extends TreeWidget {
214
294
  get request(): ChatRequestModel {
215
295
  return branch.get();
216
296
  },
217
- branch
297
+ branch,
298
+ sessionId: this.chatModelId
218
299
  };
219
300
  }
220
301
 
@@ -222,7 +303,8 @@ export class ChatViewTreeWidget extends TreeWidget {
222
303
  return {
223
304
  id: response.id,
224
305
  parent: this.model.root as CompositeTreeNode,
225
- response
306
+ response,
307
+ sessionId: this.chatModelId
226
308
  };
227
309
  }
228
310
 
@@ -292,6 +374,7 @@ export class ChatViewTreeWidget extends TreeWidget {
292
374
  protected async recreateModelTree(chatModel: ChatModel): Promise<void> {
293
375
  if (CompositeTreeNode.is(this.model.root)) {
294
376
  const nodes: TreeNode[] = [];
377
+ this.chatModelId = chatModel.id;
295
378
  chatModel.getBranches().forEach(branch => {
296
379
  const request = branch.get();
297
380
  nodes.push(this.mapRequestToNode(branch));
@@ -426,7 +509,8 @@ export class ChatViewTreeWidget extends TreeWidget {
426
509
  initialValue: editableNode.request.message.request.text,
427
510
  onQuery: async query => {
428
511
  editableNode.request.submitEdit({ text: query });
429
- }
512
+ },
513
+ branch: editableNode.branch
430
514
  });
431
515
 
432
516
  this.chatInputs.set(editableNode.id, widget);
@@ -0,0 +1,101 @@
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 { inject, injectable, named } from '@theia/core/shared/inversify';
18
+ import { ProgressMessage } from '../chat-progress-message';
19
+ import { ChatViewTreeWidget, ResponseNode } from './chat-view-tree-widget';
20
+ import * as React from '@theia/core/shared/react';
21
+ import { ContributionProvider } from '@theia/core';
22
+ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
23
+ import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution';
24
+ import { ChatResponseContent } from '@theia/ai-chat';
25
+ import { ContextMenuRenderer, TreeNode } from '@theia/core/lib/browser';
26
+ import { nls } from '@theia/core/lib/common/nls';
27
+
28
+ /**
29
+ * Subset of the ChatViewTreeWidget used to render ResponseNodes for delegated prompts.
30
+ */
31
+ @injectable()
32
+ export class SubChatWidget {
33
+
34
+ @inject(ContributionProvider) @named(ChatResponsePartRenderer)
35
+ protected readonly chatResponsePartRenderers: ContributionProvider<ChatResponsePartRenderer<ChatResponseContent>>;
36
+
37
+ @inject(ContributionProvider) @named(ChatNodeToolbarActionContribution)
38
+ protected readonly chatNodeToolbarActionContributions: ContributionProvider<ChatNodeToolbarActionContribution>;
39
+
40
+ @inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer;
41
+
42
+ renderChatResponse(node: ResponseNode): React.ReactNode {
43
+ return (
44
+ <div className={'theia-ResponseNode'}>
45
+ {!node.response.isComplete
46
+ && node.response.response.content.length === 0
47
+ && node.response.progressMessages
48
+ .filter(c => c.show === 'untilFirstContent')
49
+ .map((c, i) =>
50
+ <ProgressMessage {...c} key={`${node.id}-progress-untilFirstContent-${i}`} />
51
+ )
52
+ }
53
+ {node.response.response.content.map((c, i) =>
54
+ <div className='theia-ResponseNode-Content' key={`${node.id}-content-${i}`}>{this.getChatResponsePartRenderer(c, node)}</div>
55
+ )}
56
+ {!node.response.isComplete
57
+ && node.response.progressMessages
58
+ .filter(c => c.show === 'whileIncomplete')
59
+ .map((c, i) =>
60
+ <ProgressMessage {...c} key={`${node.id}-progress-whileIncomplete-${i}`} />
61
+ )
62
+ }
63
+ {node.response.progressMessages
64
+ .filter(c => c.show === 'forever')
65
+ .map((c, i) =>
66
+ <ProgressMessage {...c} key={`${node.id}-progress-afterComplete-${i}`} />
67
+ )
68
+ }
69
+ </div>
70
+ );
71
+ }
72
+
73
+ protected getChatResponsePartRenderer(content: ChatResponseContent, node: ResponseNode): React.ReactNode {
74
+ const renderer = this.chatResponsePartRenderers.getContributions().reduce<[number, ChatResponsePartRenderer<ChatResponseContent> | undefined]>(
75
+ (prev, current) => {
76
+ const prio = current.canHandle(content);
77
+ if (prio > prev[0]) {
78
+ return [prio, current];
79
+ } return prev;
80
+ },
81
+ [-1, undefined])[1];
82
+ if (!renderer) {
83
+ console.error('No renderer found for content', content);
84
+ return <div>{nls.localize('theia/ai/chat-ui/chat-view-tree-widget/noRenderer', 'Error: No renderer found')}</div>;
85
+ }
86
+ return renderer.render(content, node);
87
+ }
88
+
89
+ protected handleContextMenu(node: TreeNode | undefined, event: React.MouseEvent<HTMLElement>): void {
90
+ this.contextMenuRenderer.render({
91
+ menuPath: ChatViewTreeWidget.CONTEXT_MENU,
92
+ anchor: { x: event.clientX, y: event.clientY },
93
+ args: [node],
94
+ context: event.currentTarget
95
+ });
96
+ event.preventDefault();
97
+ }
98
+ }
99
+
100
+ export const SubChatWidgetFactory = Symbol('SubChatWidgetFactory');
101
+ export type SubChatWidgetFactory = () => SubChatWidget;
@@ -43,6 +43,12 @@ export namespace ChatCommands {
43
43
  id: 'ai-chat.new-with-task-context',
44
44
  };
45
45
 
46
+ export const AI_CHAT_INITIATE_SESSION_WITH_TASK_CONTEXT = Command.toLocalizedCommand({
47
+ id: 'ai-chat.initiate-session-with-task-context',
48
+ label: 'Task Context: Initiate Session',
49
+ category: CHAT_CATEGORY
50
+ }, undefined, CHAT_CATEGORY_KEY);
51
+
46
52
  export const AI_CHAT_SUMMARIZE_CURRENT_SESSION = Command.toLocalizedCommand({
47
53
  id: 'ai-chat-summary-current-session',
48
54
  iconClass: codicon('go-to-editing-session'),
@@ -117,7 +117,7 @@ export class ChatViewMenuContribution implements MenuContribution, CommandContri
117
117
 
118
118
  protected getCopyText(arg: RequestNode | ResponseNode): string {
119
119
  if (isRequestNode(arg)) {
120
- return arg.request.request.text;
120
+ return arg.request.request.text ?? '';
121
121
  } else if (isResponseNode(arg)) {
122
122
  return arg.response.response.asDisplayString();
123
123
  }
@@ -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
 
@@ -133,9 +136,6 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
133
136
  this.treeWidget.trackChatModel(this.chatSession.model);
134
137
  this.inputWidget.chatModel = this.chatSession.model;
135
138
  this.inputWidget.pinnedAgent = this.chatSession.pinnedAgent;
136
- if (event.focus) {
137
- this.show();
138
- }
139
139
  } else {
140
140
  console.warn(`Session with ${event.sessionId} not found.`);
141
141
  }
@@ -161,6 +161,8 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
161
161
  if (oldState.locked) {
162
162
  copy.locked = oldState.locked;
163
163
  }
164
+ // Don't restore temporary lock state as it should reset on restart
165
+ copy.temporaryLocked = false;
164
166
  this.state = copy;
165
167
  }
166
168
 
@@ -177,8 +179,8 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
177
179
  return this.onStateChangedEmitter.event;
178
180
  }
179
181
 
180
- protected async onQuery(query: string | ChatRequest): Promise<void> {
181
- const chatRequest: ChatRequest = typeof query === 'string' ? { text: query } : { ...query };
182
+ protected async onQuery(query?: string | ChatRequest): Promise<void> {
183
+ const chatRequest: ChatRequest = !query ? { text: '' } : typeof query === 'string' ? { text: query } : { ...query };
182
184
  if (chatRequest.text.length === 0) { return; }
183
185
 
184
186
  const requestProgress = await this.chatService.sendRequest(this.chatSession.id, chatRequest);
@@ -210,16 +212,27 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
210
212
  this.chatService.deleteChangeSet(sessionId);
211
213
  }
212
214
 
213
- protected onDeleteChangeSetElement(sessionId: string, index: number): void {
214
- this.chatService.deleteChangeSetElement(sessionId, index);
215
+ protected onDeleteChangeSetElement(sessionId: string, uri: URI): void {
216
+ this.chatService.deleteChangeSetElement(sessionId, uri);
217
+ }
218
+
219
+ protected onScrollLockChange(temporaryLocked: boolean): void {
220
+ this.setTemporaryLock(temporaryLocked);
215
221
  }
216
222
 
217
223
  lock(): void {
218
- this.state = { ...deepClone(this.state), locked: true };
224
+ this.state = { ...deepClone(this.state), locked: true, temporaryLocked: false };
219
225
  }
220
226
 
221
227
  unlock(): void {
222
- this.state = { ...deepClone(this.state), locked: false };
228
+ this.state = { ...deepClone(this.state), locked: false, temporaryLocked: false };
229
+ }
230
+
231
+ setTemporaryLock(locked: boolean): void {
232
+ // Only set temporary lock if not permanently locked
233
+ if (!this.state.locked) {
234
+ this.state = { ...deepClone(this.state), temporaryLocked: locked };
235
+ }
223
236
  }
224
237
 
225
238
  get isLocked(): boolean {