@theia/ai-chat-ui 1.68.0-next.8 → 1.68.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.
Files changed (132) hide show
  1. package/README.md +33 -0
  2. package/lib/browser/ai-chat-ui-contribution.d.ts +2 -1
  3. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  4. package/lib/browser/ai-chat-ui-contribution.js +41 -32
  5. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  7. package/lib/browser/ai-chat-ui-frontend-module.js +15 -6
  8. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  9. package/lib/browser/change-set-actions/change-set-accept-action.d.ts +0 -1
  10. package/lib/browser/change-set-actions/change-set-accept-action.d.ts.map +1 -1
  11. package/lib/browser/change-set-actions/change-set-action-service.d.ts +0 -1
  12. package/lib/browser/change-set-actions/change-set-action-service.d.ts.map +1 -1
  13. package/lib/browser/change-set-actions/change-set-action-service.js +2 -2
  14. package/lib/browser/change-set-actions/change-set-action-service.js.map +1 -1
  15. package/lib/browser/chat-focus-contribution.d.ts +14 -0
  16. package/lib/browser/chat-focus-contribution.d.ts.map +1 -0
  17. package/lib/browser/chat-focus-contribution.js +102 -0
  18. package/lib/browser/chat-focus-contribution.js.map +1 -0
  19. package/lib/browser/chat-input-agent-suggestions.d.ts +0 -1
  20. package/lib/browser/chat-input-agent-suggestions.d.ts.map +1 -1
  21. package/lib/browser/chat-input-agent-suggestions.js +1 -1
  22. package/lib/browser/chat-input-agent-suggestions.js.map +1 -1
  23. package/lib/browser/chat-input-history-contribution.js +6 -10
  24. package/lib/browser/chat-input-history-contribution.js.map +1 -1
  25. package/lib/browser/chat-input-mode-contribution.js +1 -2
  26. package/lib/browser/chat-input-mode-contribution.js.map +1 -1
  27. package/lib/browser/chat-input-widget.d.ts +0 -1
  28. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  29. package/lib/browser/chat-input-widget.js +106 -96
  30. package/lib/browser/chat-input-widget.js.map +1 -1
  31. package/lib/browser/chat-progress-message.d.ts +0 -1
  32. package/lib/browser/chat-progress-message.d.ts.map +1 -1
  33. package/lib/browser/chat-response-part-renderer.d.ts +0 -1
  34. package/lib/browser/chat-response-part-renderer.d.ts.map +1 -1
  35. package/lib/browser/chat-response-renderer/ai-selection-resolver.js +4 -5
  36. package/lib/browser/chat-response-renderer/ai-selection-resolver.js.map +1 -1
  37. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts +0 -1
  38. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
  39. package/lib/browser/chat-response-renderer/code-part-renderer.js +6 -9
  40. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  41. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts +0 -1
  42. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts.map +1 -1
  43. package/lib/browser/chat-response-renderer/command-part-renderer.js +7 -4
  44. package/lib/browser/chat-response-renderer/command-part-renderer.js.map +1 -1
  45. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +0 -1
  46. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -1
  47. package/lib/browser/chat-response-renderer/delegation-response-renderer.js +3 -4
  48. package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -1
  49. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts +0 -1
  50. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts.map +1 -1
  51. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts +0 -1
  52. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts.map +1 -1
  53. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  54. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  55. package/lib/browser/chat-response-renderer/index.js +1 -0
  56. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  57. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +0 -1
  58. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
  59. package/lib/browser/chat-response-renderer/markdown-part-renderer.js +7 -9
  60. package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
  61. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.d.ts +17 -0
  62. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.d.ts.map +1 -0
  63. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.js +56 -0
  64. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.js.map +1 -0
  65. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts +0 -1
  66. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts.map +1 -1
  67. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +0 -1
  68. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -1
  69. package/lib/browser/chat-response-renderer/question-part-renderer.js +6 -9
  70. package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -1
  71. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts +0 -1
  72. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts.map +1 -1
  73. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts +0 -1
  74. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts.map +1 -1
  75. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +29 -10
  76. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  77. package/lib/browser/chat-response-renderer/tool-confirmation.js +219 -65
  78. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  79. package/lib/browser/chat-response-renderer/tool-confirmation.spec.d.ts +2 -0
  80. package/lib/browser/chat-response-renderer/tool-confirmation.spec.d.ts.map +1 -0
  81. package/lib/browser/chat-response-renderer/tool-confirmation.spec.js +100 -0
  82. package/lib/browser/chat-response-renderer/tool-confirmation.spec.js.map +1 -0
  83. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -3
  84. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  85. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +21 -11
  86. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  87. package/lib/browser/chat-response-renderer/unknown-part-renderer.d.ts +0 -1
  88. package/lib/browser/chat-response-renderer/unknown-part-renderer.d.ts.map +1 -1
  89. package/lib/browser/chat-response-renderer/unknown-part-renderer.js +1 -2
  90. package/lib/browser/chat-response-renderer/unknown-part-renderer.js.map +1 -1
  91. package/lib/browser/chat-tree-view/chat-view-tree-container.js +1 -2
  92. package/lib/browser/chat-tree-view/chat-view-tree-container.js.map +1 -1
  93. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +2 -3
  94. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  95. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +3 -1
  96. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  97. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +49 -42
  98. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  99. package/lib/browser/chat-tree-view/prompt-variant-badge.d.ts +0 -1
  100. package/lib/browser/chat-tree-view/prompt-variant-badge.d.ts.map +1 -1
  101. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +0 -1
  102. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -1
  103. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  104. package/lib/browser/chat-view-commands.js +2 -2
  105. package/lib/browser/chat-view-commands.js.map +1 -1
  106. package/lib/browser/chat-view-contribution.js +7 -7
  107. package/lib/browser/chat-view-contribution.js.map +1 -1
  108. package/lib/browser/chat-view-language-contribution.js +3 -4
  109. package/lib/browser/chat-view-language-contribution.js.map +1 -1
  110. package/lib/browser/chat-view-widget.js +9 -10
  111. package/lib/browser/chat-view-widget.js.map +1 -1
  112. package/lib/browser/context-variable-picker.js +9 -13
  113. package/lib/browser/context-variable-picker.js.map +1 -1
  114. package/lib/browser/session-storage-preference-renderer.d.ts +127 -0
  115. package/lib/browser/session-storage-preference-renderer.d.ts.map +1 -0
  116. package/lib/browser/session-storage-preference-renderer.js +490 -0
  117. package/lib/browser/session-storage-preference-renderer.js.map +1 -0
  118. package/package.json +12 -11
  119. package/src/browser/ai-chat-ui-contribution.ts +19 -7
  120. package/src/browser/ai-chat-ui-frontend-module.ts +13 -0
  121. package/src/browser/chat-focus-contribution.ts +102 -0
  122. package/src/browser/chat-input-widget.tsx +99 -27
  123. package/src/browser/chat-response-renderer/index.ts +1 -0
  124. package/src/browser/chat-response-renderer/not-available-toolcall-renderer.tsx +57 -0
  125. package/src/browser/chat-response-renderer/tool-confirmation.spec.ts +108 -0
  126. package/src/browser/chat-response-renderer/tool-confirmation.tsx +379 -95
  127. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +27 -13
  128. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +33 -5
  129. package/src/browser/chat-view-commands.ts +2 -2
  130. package/src/browser/session-storage-preference-renderer.ts +657 -0
  131. package/src/browser/style/index.css +73 -26
  132. package/src/browser/style/session-storage-preference.css +134 -0
@@ -0,0 +1,657 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import '../../src/browser/style/session-storage-preference.css';
18
+
19
+ import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
20
+ import { codicon } from '@theia/core/lib/browser';
21
+ import { Path } from '@theia/core/lib/common/path';
22
+ import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
23
+ import {
24
+ PreferenceLeafNodeRenderer,
25
+ PreferenceNodeRenderer
26
+ } from '@theia/preferences/lib/browser/views/components/preference-node-renderer';
27
+ import { PreferenceLeafNodeRendererContribution } from '@theia/preferences/lib/browser/views/components/preference-node-renderer-creator';
28
+ import { Preference } from '@theia/preferences/lib/browser/util/preference-types';
29
+ import * as React from '@theia/core/shared/react';
30
+ import { createRoot, Root } from '@theia/core/shared/react-dom/client';
31
+ import {
32
+ SessionStorageScope,
33
+ SessionStorageTypeDetails,
34
+ SessionStorageValue
35
+ } from '@theia/ai-chat/lib/common/ai-chat-preferences';
36
+ import { SessionStorageDefaultsProvider } from '@theia/ai-chat/lib/browser/session-storage-defaults-provider';
37
+ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
38
+ import debounce = require('@theia/core/shared/lodash.debounce');
39
+
40
+ /**
41
+ * Callbacks provided by the renderer to delegates for preference management.
42
+ */
43
+ export interface InputRowDelegateContext {
44
+ getValue(): SessionStorageValue;
45
+ /** Set preference with debounce. Pass the full value; defaults will be stripped automatically. */
46
+ setPreferenceDebounced(value: SessionStorageValue): void;
47
+ /** Set preference immediately. Pass the full value; defaults will be stripped automatically. */
48
+ setPreferenceImmediately(value: SessionStorageValue): void;
49
+ updateResetButtonStates(): void;
50
+ handleValueChange(): void;
51
+ flushDebouncedPreference(): void;
52
+ }
53
+
54
+ /**
55
+ * Delegate interface that encapsulates the behavior for a path input row,
56
+ * avoiding fieldId-based switching in the renderer.
57
+ */
58
+ export interface InputRowDelegate {
59
+ /** Get the input element for this row */
60
+ getInput(): HTMLInputElement | undefined;
61
+ /** Set the input element for this row */
62
+ setInput(input: HTMLInputElement): void;
63
+ /** Get the default value for this path */
64
+ getDefaultValue(): string;
65
+ /** Check if this input should be active for the given scope */
66
+ isActiveForScope(scope: SessionStorageScope): boolean;
67
+ /** Get the current path value from a preference value */
68
+ getPathFromValue(value: SessionStorageValue): string;
69
+ /** Handle input change event */
70
+ handleInputChange(): void;
71
+ /** Handle input blur event */
72
+ handleInputBlur(): void;
73
+ /** Handle reset button click */
74
+ handleReset(): void;
75
+ /** Update the reset button disabled state */
76
+ updateResetButtonState(currentValue: SessionStorageValue): void;
77
+ /** Sync input value from preference if not focused */
78
+ syncFromPreference(value: SessionStorageValue): void;
79
+ /** Set the message element for this row (used for errors and info) */
80
+ setMessageElement(element: HTMLElement): void;
81
+ /** Get the message element for this row */
82
+ getMessageElement(): HTMLElement | undefined;
83
+ /** Set the row element for this input */
84
+ setRow(row: HTMLElement): void;
85
+ /** Get the row element for this input */
86
+ getRow(): HTMLElement | undefined;
87
+ /** Validate the input value. Returns error message or undefined if valid */
88
+ validate(value: string): string | undefined;
89
+ /** Get the info message to show when this path is not active for the current scope */
90
+ getInactiveInfoMessage(): string;
91
+ }
92
+
93
+ @injectable()
94
+ export class SessionStoragePreferenceRenderer extends PreferenceLeafNodeRenderer<SessionStorageValue, HTMLDivElement> {
95
+
96
+ @inject(SessionStorageDefaultsProvider)
97
+ protected readonly defaultsProvider: SessionStorageDefaultsProvider;
98
+
99
+ @inject(WorkspaceService)
100
+ protected readonly workspaceService: WorkspaceService;
101
+
102
+ protected scopeSelectRef = React.createRef<SelectComponent>();
103
+ protected expandedSection: HTMLDivElement;
104
+ protected disclosureButton: HTMLElement;
105
+ protected isExpanded = false;
106
+ protected reactRoot: Root;
107
+
108
+ protected delegateContext: InputRowDelegateContext = this.createDelegateContext();
109
+ protected workspacePathDelegate: InputRowDelegate = this.createWorkspacePathDelegate();
110
+ protected globalPathDelegate: InputRowDelegate = this.createGlobalPathDelegate();
111
+
112
+ /**
113
+ * Initialize dynamic defaults asynchronously. This is called lazily when the UI is created,
114
+ * not in @postConstruct() to avoid issues with InversifyJS synchronous instantiation.
115
+ */
116
+ protected async initDynamicDefaults(): Promise<void> {
117
+ await this.defaultsProvider.initialize();
118
+ this.updatePlaceholders();
119
+ }
120
+
121
+ protected updatePlaceholders(): void {
122
+ // Update placeholder texts with computed defaults
123
+ const workspaceInput = this.workspacePathDelegate.getInput();
124
+ if (workspaceInput) {
125
+ workspaceInput.placeholder = this.defaultsProvider.getDefaultWorkspacePath();
126
+ }
127
+ const globalInput = this.globalPathDelegate.getInput();
128
+ if (globalInput) {
129
+ globalInput.placeholder = this.defaultsProvider.getDefaultGlobalPath();
130
+ }
131
+ }
132
+
133
+ protected createDelegateContext(): InputRowDelegateContext {
134
+ return {
135
+ getValue: () => this.getValueWithDefaults(),
136
+ setPreferenceDebounced: value => this.debouncedSetPreference(this.stripDefaultValues(value) as SessionStorageValue | undefined),
137
+ setPreferenceImmediately: value => this.setPreferenceImmediately(this.stripDefaultValues(value) as SessionStorageValue | undefined),
138
+ updateResetButtonStates: () => this.updateResetButtonStates(),
139
+ handleValueChange: () => this.handleValueChange(),
140
+ flushDebouncedPreference: () => this.debouncedSetPreference.flush()
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get the current preference value merged with dynamic defaults.
146
+ * This handles the case where sparse serialization stores only non-default properties.
147
+ */
148
+ protected getValueWithDefaults(): SessionStorageValue {
149
+ return this.defaultsProvider.mergeWithDefaults(this.getValue() ?? undefined);
150
+ }
151
+
152
+ /**
153
+ * Strip properties that have their default values from the preference object.
154
+ * This ensures that only non-default values are written to settings.json.
155
+ * Returns undefined if all values are at their defaults (so the preference is removed entirely).
156
+ */
157
+ protected stripDefaultValues(value: SessionStorageValue): Partial<SessionStorageValue> | undefined {
158
+ const defaultScope: SessionStorageScope = 'workspace';
159
+ const result: Partial<SessionStorageValue> = {};
160
+ let hasNonDefault = false;
161
+
162
+ if (value.scope !== defaultScope) {
163
+ result.scope = value.scope;
164
+ hasNonDefault = true;
165
+ }
166
+
167
+ if (value.workspacePath !== this.defaultsProvider.getDefaultWorkspacePath()) {
168
+ result.workspacePath = value.workspacePath;
169
+ hasNonDefault = true;
170
+ }
171
+
172
+ if (value.globalPath !== this.defaultsProvider.getDefaultGlobalPath()) {
173
+ result.globalPath = value.globalPath;
174
+ hasNonDefault = true;
175
+ }
176
+
177
+ return hasNonDefault ? result : undefined;
178
+ }
179
+
180
+ protected createWorkspacePathDelegate(): InputRowDelegate {
181
+ return this.createPathDelegate(
182
+ () => this.defaultsProvider.getDefaultWorkspacePath(),
183
+ scope => scope === 'workspace',
184
+ value => value.workspacePath,
185
+ (currentValue, newInputValue) => ({ ...currentValue, workspacePath: newInputValue }),
186
+ (value: string) => {
187
+ if (!value.trim()) {
188
+ return SessionStorageValue.Labels.pathRequired();
189
+ }
190
+ const path = new Path(value);
191
+ if (path.isAbsolute) {
192
+ return SessionStorageValue.Labels.workspacePathInvalidRelative();
193
+ }
194
+ // Check for workspace escape: normalize and check for leading ..
195
+ const normalized = path.normalize();
196
+ if (normalized.toString().startsWith('..')) {
197
+ return SessionStorageValue.Labels.workspacePathEscapesWorkspace();
198
+ }
199
+ return undefined;
200
+ },
201
+ () => SessionStorageValue.Labels.pathNotUsedForScope(SessionStorageValue.Labels.scopeGlobal())
202
+ );
203
+ }
204
+
205
+ protected createGlobalPathDelegate(): InputRowDelegate {
206
+ return this.createPathDelegate(
207
+ () => this.defaultsProvider.getDefaultGlobalPath(),
208
+ // Global path is active when scope is 'global' OR when no workspace is available
209
+ // (since ChatSessionStoreImpl falls back to global storage without a workspace)
210
+ scope => scope === 'global' || this.workspaceService.tryGetRoots().length === 0,
211
+ value => value.globalPath,
212
+ (currentValue, newInputValue) => ({ ...currentValue, globalPath: newInputValue }),
213
+ (value: string) => {
214
+ if (!value.trim()) {
215
+ return SessionStorageValue.Labels.pathRequired();
216
+ }
217
+ const path = new Path(value);
218
+ if (!path.isAbsolute) {
219
+ return SessionStorageValue.Labels.globalPathInvalidAbsolute();
220
+ }
221
+ return undefined;
222
+ },
223
+ () => SessionStorageValue.Labels.pathNotUsedForScope(SessionStorageValue.Labels.scopeWorkspace())
224
+ );
225
+ }
226
+
227
+ /** Create a delegate to handle UI interactions in the storage path input for one of the supported scopes. */
228
+ protected createPathDelegate(
229
+ getDefaultValue: () => string,
230
+ isActiveForScope: (scope: SessionStorageScope) => boolean,
231
+ getPathFromValue: (value: SessionStorageValue) => string,
232
+ updatePreferenceValue: (currentValue: SessionStorageValue, newInputValue: string) => SessionStorageValue,
233
+ validateFn: (value: string) => string | undefined,
234
+ getInactiveInfoMessage: () => string
235
+ ): InputRowDelegate {
236
+ let input: HTMLInputElement | undefined;
237
+ let messageElement: HTMLElement | undefined;
238
+ let row: HTMLElement | undefined;
239
+ const context = this.delegateContext;
240
+
241
+ const showMessage = (message: string, severity: 'error' | 'info'): void => {
242
+ if (messageElement && row) {
243
+ messageElement.textContent = message;
244
+ messageElement.classList.remove('error', 'info');
245
+ messageElement.classList.add(severity);
246
+ if (!messageElement.parentElement) {
247
+ row.appendChild(messageElement);
248
+ }
249
+ }
250
+ };
251
+
252
+ const hideMessage = (): void => {
253
+ messageElement?.remove();
254
+ };
255
+
256
+ /** Update the message based on validation and scope */
257
+ const updateMessage = (scope: SessionStorageScope): void => {
258
+ if (!input) {
259
+ return;
260
+ }
261
+ const error = validateFn(input.value);
262
+ if (error) {
263
+ showMessage(error, 'error');
264
+ } else if (!isActiveForScope(scope)) {
265
+ showMessage(getInactiveInfoMessage(), 'info');
266
+ } else {
267
+ hideMessage();
268
+ }
269
+ };
270
+
271
+ return {
272
+ getInput: () => input,
273
+ setInput: (element: HTMLInputElement) => { input = element; },
274
+ getDefaultValue,
275
+ isActiveForScope,
276
+ getPathFromValue,
277
+ setMessageElement: (element: HTMLElement) => { messageElement = element; },
278
+ getMessageElement: () => messageElement,
279
+ getInactiveInfoMessage,
280
+ setRow: (element: HTMLElement) => { row = element; },
281
+ getRow: () => row,
282
+ validate: validateFn,
283
+
284
+ handleInputChange: () => {
285
+ if (!input) {
286
+ return;
287
+ }
288
+ const currentValue = context.getValue();
289
+ const error = validateFn(input.value);
290
+ if (error) {
291
+ showMessage(error, 'error');
292
+ } else {
293
+ // Update message (may show info or hide)
294
+ updateMessage(currentValue.scope);
295
+ // Only update preference if valid
296
+ const newValue = updatePreferenceValue(currentValue, input.value);
297
+ context.setPreferenceDebounced(newValue);
298
+ }
299
+ context.updateResetButtonStates();
300
+ },
301
+
302
+ handleInputBlur: () => {
303
+ context.flushDebouncedPreference();
304
+ context.handleValueChange();
305
+ },
306
+
307
+ handleReset: () => {
308
+ if (!input) {
309
+ return;
310
+ }
311
+ const defaultValue = getDefaultValue();
312
+ input.value = defaultValue;
313
+
314
+ const currentValue = context.getValue();
315
+ updateMessage(currentValue.scope);
316
+
317
+ const newValue = updatePreferenceValue(currentValue, defaultValue);
318
+ context.setPreferenceImmediately(newValue);
319
+ context.updateResetButtonStates();
320
+ },
321
+
322
+ updateResetButtonState: (currentValue: SessionStorageValue) => {
323
+ if (!input) {
324
+ return;
325
+ }
326
+ const resetButton = row?.querySelector('.session-storage-reset-button') as HTMLButtonElement | null;
327
+ if (resetButton) {
328
+ const isDefault = input.value === getDefaultValue();
329
+ resetButton.disabled = isDefault;
330
+ }
331
+ },
332
+
333
+ syncFromPreference: (value: SessionStorageValue) => {
334
+ if (input) {
335
+ if (document.activeElement !== input) {
336
+ input.value = getPathFromValue(value);
337
+ }
338
+ updateMessage(value.scope);
339
+ }
340
+ }
341
+ };
342
+ }
343
+
344
+ protected get scopeOptions(): SelectOption[] {
345
+ return [
346
+ {
347
+ value: 'workspace',
348
+ label: SessionStorageValue.Labels.scopeWorkspace(),
349
+ detail: 'default',
350
+ description: SessionStorageValue.Labels.scopeWorkspaceDescription()
351
+ },
352
+ {
353
+ value: 'global',
354
+ label: SessionStorageValue.Labels.scopeGlobal(),
355
+ description: SessionStorageValue.Labels.scopeGlobalDescription()
356
+ }
357
+ ];
358
+ }
359
+
360
+ protected createInteractable(parent: HTMLElement): void {
361
+ const hasWorkspace = this.workspaceService.tryGetRoots().length > 0;
362
+
363
+ if (hasWorkspace) {
364
+ this.createFullInteractable(parent);
365
+ } else {
366
+ this.createSimplifiedInteractable(parent);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Create the full UI with scope dropdown, disclosure button, and expandable path settings.
372
+ * Used when a workspace is available.
373
+ */
374
+ protected createFullInteractable(parent: HTMLElement): void {
375
+ // Initialize dynamic defaults asynchronously - will update UI when ready
376
+ this.initDynamicDefaults();
377
+
378
+ const container = document.createElement('div');
379
+ container.classList.add('session-storage-preference-container');
380
+ this.interactable = container;
381
+
382
+ // Scope dropdown section
383
+ const scopeSection = document.createElement('div');
384
+ scopeSection.classList.add('session-storage-scope-section');
385
+
386
+ const scopeSelectContainer = document.createElement('div');
387
+ scopeSelectContainer.classList.add('session-storage-scope-select');
388
+ scopeSection.appendChild(scopeSelectContainer);
389
+
390
+ const currentValue = this.getValue();
391
+ const selectedIndex = currentValue?.scope === 'global' ? 1 : 0;
392
+
393
+ const selectComponent = React.createElement(SelectComponent, {
394
+ options: this.scopeOptions,
395
+ defaultValue: selectedIndex,
396
+ onChange: (option, index) => this.onScopeSelectionChange(option.value as SessionStorageScope),
397
+ ref: this.scopeSelectRef
398
+ });
399
+
400
+ this.reactRoot = createRoot(scopeSelectContainer);
401
+ this.reactRoot.render(selectComponent);
402
+
403
+ container.appendChild(scopeSection);
404
+
405
+ // Disclosure triangle and expanded section
406
+ const disclosureRow = document.createElement('div');
407
+ disclosureRow.classList.add('session-storage-disclosure-row');
408
+
409
+ this.disclosureButton = document.createElement('span');
410
+ this.disclosureButton.classList.add('session-storage-disclosure-button');
411
+ this.disclosureButton.innerHTML = `<i class="${codicon('chevron-right')}"></i>`;
412
+ this.disclosureButton.onclick = () => this.toggleExpanded();
413
+ this.disclosureButton.setAttribute('role', 'button');
414
+ this.disclosureButton.setAttribute('tabindex', '0');
415
+ this.disclosureButton.onkeydown = e => {
416
+ if (e.key === 'Enter' || e.key === ' ') {
417
+ this.toggleExpanded();
418
+ e.preventDefault();
419
+ }
420
+ };
421
+ disclosureRow.appendChild(this.disclosureButton);
422
+
423
+ const disclosureLabel = document.createElement('span');
424
+ disclosureLabel.classList.add('session-storage-disclosure-label');
425
+ disclosureLabel.textContent = SessionStorageValue.Labels.pathSettings();
426
+ disclosureLabel.onclick = () => this.toggleExpanded();
427
+ disclosureRow.appendChild(disclosureLabel);
428
+
429
+ container.appendChild(disclosureRow);
430
+
431
+ // Expanded section with path inputs
432
+ this.expandedSection = document.createElement('div');
433
+ this.expandedSection.classList.add('session-storage-expanded-section', 'hidden');
434
+
435
+ // Workspace path input
436
+ const workspacePathRow = this.createPathInputRow(
437
+ this.workspacePathDelegate,
438
+ SessionStorageValue.Labels.workspacePathLabel(),
439
+ SessionStorageValue.Labels.workspacePathDescription(),
440
+ currentValue?.workspacePath ?? this.workspacePathDelegate.getDefaultValue(),
441
+ !this.workspacePathDelegate.isActiveForScope(currentValue?.scope ?? 'workspace'),
442
+ this.defaultsProvider.getDefaultWorkspacePath(),
443
+ true
444
+ );
445
+ this.expandedSection.appendChild(workspacePathRow);
446
+
447
+ // Global path input
448
+ const globalPathRow = this.createPathInputRow(
449
+ this.globalPathDelegate,
450
+ SessionStorageValue.Labels.globalPathLabel(),
451
+ SessionStorageValue.Labels.globalPathDescription(),
452
+ currentValue?.globalPath ?? this.globalPathDelegate.getDefaultValue(),
453
+ !this.globalPathDelegate.isActiveForScope(currentValue?.scope ?? 'workspace'),
454
+ this.defaultsProvider.getDefaultGlobalPath(),
455
+ true
456
+ );
457
+ this.expandedSection.appendChild(globalPathRow);
458
+
459
+ container.appendChild(this.expandedSection);
460
+
461
+ parent.appendChild(container);
462
+ }
463
+
464
+ /**
465
+ * Create a simplified UI with just the global storage path input.
466
+ * Used when no workspace is available - scope falls back to 'global'.
467
+ * No additional label/description needed since the preference title "Session Storage" is sufficient.
468
+ */
469
+ protected createSimplifiedInteractable(parent: HTMLElement): void {
470
+ // Initialize dynamic defaults asynchronously
471
+ this.initDynamicDefaults();
472
+
473
+ const container = document.createElement('div');
474
+ container.classList.add('session-storage-preference-container', 'session-storage-no-workspace', 'pref-input');
475
+ this.interactable = container;
476
+
477
+ const input = document.createElement('input');
478
+ input.type = 'text';
479
+ input.classList.add('theia-input', 'session-storage-path-input');
480
+ input.value = this.getValueWithDefaults().globalPath;
481
+ input.placeholder = this.defaultsProvider.getDefaultGlobalPath();
482
+ input.oninput = () => this.globalPathDelegate.handleInputChange();
483
+ input.onblur = () => this.globalPathDelegate.handleInputBlur();
484
+
485
+ this.globalPathDelegate.setInput(input);
486
+
487
+ // Message element for validation errors
488
+ const messageElement = document.createElement('div');
489
+ messageElement.classList.add('session-storage-message');
490
+ this.globalPathDelegate.setMessageElement(messageElement);
491
+ this.globalPathDelegate.setRow(container);
492
+
493
+ container.appendChild(input);
494
+
495
+ parent.appendChild(container);
496
+ }
497
+
498
+ /** Create a row for input of the storage path for one of the supported scopes. */
499
+ protected createPathInputRow(
500
+ delegate: InputRowDelegate,
501
+ label: string,
502
+ description: string,
503
+ value: string,
504
+ isInactive: boolean,
505
+ placeholder: string,
506
+ showResetButton: boolean = true
507
+ ): HTMLDivElement {
508
+ const row = document.createElement('div');
509
+ row.classList.add('session-storage-path-row');
510
+
511
+ const labelElement = document.createElement('label');
512
+ labelElement.classList.add('session-storage-path-label');
513
+ labelElement.textContent = label;
514
+ row.appendChild(labelElement);
515
+
516
+ const descriptionElement = document.createElement('span');
517
+ descriptionElement.classList.add('session-storage-path-description');
518
+ descriptionElement.textContent = description;
519
+ row.appendChild(descriptionElement);
520
+
521
+ const inputContainer = document.createElement('div');
522
+ inputContainer.classList.add('session-storage-path-input-container');
523
+
524
+ const input = document.createElement('input');
525
+ input.type = 'text';
526
+ input.classList.add('theia-input', 'session-storage-path-input');
527
+ input.value = value;
528
+ input.placeholder = placeholder;
529
+ input.oninput = () => delegate.handleInputChange();
530
+ input.onblur = () => delegate.handleInputBlur();
531
+
532
+ delegate.setInput(input);
533
+
534
+ inputContainer.appendChild(input);
535
+
536
+ if (showResetButton) {
537
+ // Reset button
538
+ const resetButton = document.createElement('button');
539
+ resetButton.classList.add('theia-button', 'session-storage-reset-button');
540
+ resetButton.title = SessionStorageValue.Labels.resetToDefault();
541
+ resetButton.innerHTML = `<i class="${codicon('discard')}"></i>`;
542
+ resetButton.disabled = value === delegate.getDefaultValue();
543
+ resetButton.onclick = () => delegate.handleReset();
544
+ inputContainer.appendChild(resetButton);
545
+ }
546
+
547
+ // Message element for errors and info (with severity class)
548
+ const messageElement = document.createElement('div');
549
+ messageElement.classList.add('session-storage-message');
550
+ delegate.setMessageElement(messageElement);
551
+
552
+ delegate.setRow(row);
553
+
554
+ row.appendChild(inputContainer);
555
+
556
+ // Show info message initially if this path is not active for the current scope
557
+ if (isInactive) {
558
+ messageElement.textContent = delegate.getInactiveInfoMessage();
559
+ messageElement.classList.add('info');
560
+ row.appendChild(messageElement);
561
+ }
562
+
563
+ return row;
564
+ }
565
+
566
+ protected toggleExpanded(): void {
567
+ this.isExpanded = !this.isExpanded;
568
+ if (this.isExpanded) {
569
+ this.expandedSection.classList.remove('hidden');
570
+ this.disclosureButton.innerHTML = `<i class="${codicon('chevron-down')}"></i>`;
571
+ } else {
572
+ this.expandedSection.classList.add('hidden');
573
+ this.disclosureButton.innerHTML = `<i class="${codicon('chevron-right')}"></i>`;
574
+ }
575
+ }
576
+
577
+ protected onScopeSelectionChange(newScope: SessionStorageScope): void {
578
+ const currentValue = this.getValueWithDefaults();
579
+ const newValue: SessionStorageValue = {
580
+ ...currentValue,
581
+ scope: newScope
582
+ };
583
+ this.setPreferenceImmediately(this.stripDefaultValues(newValue) as SessionStorageValue | undefined);
584
+
585
+ this.updateInputActiveStates(newScope);
586
+ this.updateResetButtonStates();
587
+ }
588
+
589
+ protected updateInputActiveStates(scope: SessionStorageScope): void {
590
+ const value = this.getValueWithDefaults();
591
+ for (const delegate of this.getInputRowDelegates()) {
592
+ delegate.syncFromPreference(value);
593
+ }
594
+ }
595
+
596
+ protected getInputRowDelegates(): InputRowDelegate[] {
597
+ return [this.workspacePathDelegate, this.globalPathDelegate];
598
+ }
599
+
600
+ protected debouncedSetPreference = debounce((value: SessionStorageValue | undefined) => {
601
+ this.setPreferenceImmediately(value);
602
+ }, 500, { leading: false, trailing: true });
603
+
604
+ protected updateResetButtonStates(): void {
605
+ const currentValue = this.getValueWithDefaults();
606
+
607
+ for (const delegate of this.getInputRowDelegates()) {
608
+ delegate.updateResetButtonState(currentValue);
609
+ }
610
+ }
611
+
612
+ protected getFallbackValue(): SessionStorageValue {
613
+ return this.defaultsProvider.getDefaultValue();
614
+ }
615
+
616
+ protected doHandleValueChange(): void {
617
+ this.updateInspection();
618
+ const newValue = this.getValueWithDefaults();
619
+ this.updateModificationStatus();
620
+
621
+ // Update scope dropdown
622
+ if (this.scopeSelectRef.current) {
623
+ const selectedIndex = newValue.scope === 'global' ? 1 : 0;
624
+ this.scopeSelectRef.current.value = selectedIndex;
625
+ }
626
+
627
+ // Update path inputs if not focused, and update enabled states
628
+ for (const delegate of this.getInputRowDelegates()) {
629
+ delegate.syncFromPreference(newValue);
630
+ }
631
+
632
+ this.updateResetButtonStates();
633
+ }
634
+
635
+ override dispose(): void {
636
+ this.debouncedSetPreference.cancel();
637
+ if (this.reactRoot) {
638
+ this.reactRoot.unmount();
639
+ }
640
+ super.dispose();
641
+ }
642
+ }
643
+
644
+ @injectable()
645
+ export class SessionStoragePreferenceRendererContribution extends PreferenceLeafNodeRendererContribution {
646
+ static ID = 'session-storage-preference-renderer';
647
+ id = SessionStoragePreferenceRendererContribution.ID;
648
+
649
+ canHandleLeafNode(node: Preference.LeafNode): number {
650
+ const typeDetails = node.preference.data.typeDetails;
651
+ return SessionStorageTypeDetails.is(typeDetails) ? 10 : 0;
652
+ }
653
+
654
+ createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
655
+ return container.get(SessionStoragePreferenceRenderer);
656
+ }
657
+ }