@carbon/ai-chat-components 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +1 -0
  2. package/custom-elements.json +917 -258
  3. package/es/components/card/src/card-footer.scss.js +1 -1
  4. package/es/components/card/src/card-steps.scss.js +1 -1
  5. package/es/components/card/src/card.scss.js +1 -1
  6. package/es/components/chat-button/src/chat-button.scss.js +1 -1
  7. package/es/components/chat-shell/index.d.ts +2 -2
  8. package/es/components/chat-shell/index.js +2 -2
  9. package/es/components/chat-shell/src/{cds-aichat-panel.d.ts → panel.d.ts} +11 -2
  10. package/es/components/chat-shell/src/{cds-aichat-panel.js → panel.js} +79 -26
  11. package/es/components/chat-shell/src/panel.js.map +1 -0
  12. package/es/components/chat-shell/src/panel.scss.js +13 -0
  13. package/es/components/chat-shell/src/panel.scss.js.map +1 -0
  14. package/es/components/chat-shell/src/{cds-aichat-shell.d.ts → shell.d.ts} +20 -3
  15. package/es/components/chat-shell/src/{cds-aichat-shell.js → shell.js} +298 -97
  16. package/es/components/chat-shell/src/shell.js.map +1 -0
  17. package/es/components/chat-shell/src/shell.scss.js +13 -0
  18. package/es/components/chat-shell/src/shell.scss.js.map +1 -0
  19. package/es/components/chat-shell/src/workspace-manager-utils.d.ts +90 -0
  20. package/es/components/chat-shell/src/workspace-manager-utils.js +120 -0
  21. package/es/components/chat-shell/src/workspace-manager-utils.js.map +1 -0
  22. package/es/components/chat-shell/src/workspace-manager.d.ts +52 -8
  23. package/es/components/chat-shell/src/workspace-manager.js +330 -117
  24. package/es/components/chat-shell/src/workspace-manager.js.map +1 -1
  25. package/es/components/code-snippet/src/code-snippet.d.ts +27 -11
  26. package/es/components/code-snippet/src/code-snippet.scss.js +1 -1
  27. package/es/components/markdown/src/markdown.js +1 -1
  28. package/es/components/markdown/src/markdown.js.map +1 -1
  29. package/es/components/toolbar/src/toolbar.d.ts +4 -0
  30. package/es/components/toolbar/src/toolbar.js +62 -28
  31. package/es/components/toolbar/src/toolbar.js.map +1 -1
  32. package/es/components/toolbar/src/toolbar.scss.js +1 -1
  33. package/es/components/workspace-shell/src/workspace-shell-footer.js +6 -1
  34. package/es/components/workspace-shell/src/workspace-shell-footer.js.map +1 -1
  35. package/es/components/workspace-shell/src/workspace-shell-footer.scss.js +1 -1
  36. package/es/components/workspace-shell/src/workspace-shell.d.ts +0 -1
  37. package/es/components/workspace-shell/src/workspace-shell.js +0 -6
  38. package/es/components/workspace-shell/src/workspace-shell.js.map +1 -1
  39. package/es/components/workspace-shell/src/workspace-shell.scss.js +1 -1
  40. package/es/react/chat-shell.d.ts +3 -3
  41. package/es/react/chat-shell.js +4 -4
  42. package/es/react/chat-shell.js.map +1 -1
  43. package/es/react/panel.d.ts +3 -3
  44. package/es/react/panel.js +5 -4
  45. package/es/react/panel.js.map +1 -1
  46. package/es/react/toolbar.js +1 -1
  47. package/es/react/toolbar.js.map +1 -1
  48. package/es-custom/components/card/src/card-footer.scss.js +1 -1
  49. package/es-custom/components/card/src/card-steps.scss.js +1 -1
  50. package/es-custom/components/card/src/card.scss.js +1 -1
  51. package/es-custom/components/chat-button/src/chat-button.scss.js +1 -1
  52. package/es-custom/components/chat-shell/index.d.ts +2 -2
  53. package/es-custom/components/chat-shell/index.js +2 -2
  54. package/es-custom/components/chat-shell/src/{cds-aichat-panel.d.ts → panel.d.ts} +11 -2
  55. package/es-custom/components/chat-shell/src/{cds-aichat-panel.js → panel.js} +79 -26
  56. package/es-custom/components/chat-shell/src/panel.js.map +1 -0
  57. package/es-custom/components/chat-shell/src/panel.scss.js +13 -0
  58. package/es-custom/components/chat-shell/src/panel.scss.js.map +1 -0
  59. package/es-custom/components/chat-shell/src/{cds-aichat-shell.d.ts → shell.d.ts} +20 -3
  60. package/es-custom/components/chat-shell/src/{cds-aichat-shell.js → shell.js} +298 -97
  61. package/es-custom/components/chat-shell/src/shell.js.map +1 -0
  62. package/es-custom/components/chat-shell/src/shell.scss.js +13 -0
  63. package/es-custom/components/chat-shell/src/shell.scss.js.map +1 -0
  64. package/es-custom/components/chat-shell/src/workspace-manager-utils.d.ts +90 -0
  65. package/es-custom/components/chat-shell/src/workspace-manager-utils.js +120 -0
  66. package/es-custom/components/chat-shell/src/workspace-manager-utils.js.map +1 -0
  67. package/es-custom/components/chat-shell/src/workspace-manager.d.ts +52 -8
  68. package/es-custom/components/chat-shell/src/workspace-manager.js +330 -117
  69. package/es-custom/components/chat-shell/src/workspace-manager.js.map +1 -1
  70. package/es-custom/components/code-snippet/src/code-snippet.d.ts +27 -11
  71. package/es-custom/components/code-snippet/src/code-snippet.scss.js +1 -1
  72. package/es-custom/components/markdown/src/markdown.js +1 -1
  73. package/es-custom/components/markdown/src/markdown.js.map +1 -1
  74. package/es-custom/components/toolbar/src/toolbar.d.ts +4 -0
  75. package/es-custom/components/toolbar/src/toolbar.js +62 -28
  76. package/es-custom/components/toolbar/src/toolbar.js.map +1 -1
  77. package/es-custom/components/toolbar/src/toolbar.scss.js +1 -1
  78. package/es-custom/components/workspace-shell/src/workspace-shell-footer.js +6 -1
  79. package/es-custom/components/workspace-shell/src/workspace-shell-footer.js.map +1 -1
  80. package/es-custom/components/workspace-shell/src/workspace-shell-footer.scss.js +1 -1
  81. package/es-custom/components/workspace-shell/src/workspace-shell.d.ts +0 -1
  82. package/es-custom/components/workspace-shell/src/workspace-shell.js +0 -6
  83. package/es-custom/components/workspace-shell/src/workspace-shell.js.map +1 -1
  84. package/es-custom/components/workspace-shell/src/workspace-shell.scss.js +1 -1
  85. package/es-custom/react/chat-shell.d.ts +3 -3
  86. package/es-custom/react/chat-shell.js +4 -4
  87. package/es-custom/react/chat-shell.js.map +1 -1
  88. package/es-custom/react/panel.d.ts +3 -3
  89. package/es-custom/react/panel.js +5 -4
  90. package/es-custom/react/panel.js.map +1 -1
  91. package/es-custom/react/toolbar.js +1 -1
  92. package/es-custom/react/toolbar.js.map +1 -1
  93. package/package.json +13 -10
  94. package/es/components/chat-shell/src/cds-aichat-panel.js.map +0 -1
  95. package/es/components/chat-shell/src/cds-aichat-panel.scss.js +0 -13
  96. package/es/components/chat-shell/src/cds-aichat-panel.scss.js.map +0 -1
  97. package/es/components/chat-shell/src/cds-aichat-shell.js.map +0 -1
  98. package/es/components/chat-shell/src/cds-aichat-shell.scss.js +0 -13
  99. package/es/components/chat-shell/src/cds-aichat-shell.scss.js.map +0 -1
  100. package/es-custom/components/chat-shell/src/cds-aichat-panel.js.map +0 -1
  101. package/es-custom/components/chat-shell/src/cds-aichat-panel.scss.js +0 -13
  102. package/es-custom/components/chat-shell/src/cds-aichat-panel.scss.js.map +0 -1
  103. package/es-custom/components/chat-shell/src/cds-aichat-shell.js.map +0 -1
  104. package/es-custom/components/chat-shell/src/cds-aichat-shell.scss.js +0 -13
  105. package/es-custom/components/chat-shell/src/cds-aichat-shell.scss.js.map +0 -1
@@ -5,7 +5,8 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import throttle from 'lodash-es/throttle';
8
+ import throttle from 'lodash-es/throttle.js';
9
+ import { getInlineSizeFromEntry, shouldSkipWorkspaceUpdate, calculateRequiredWidth, isWideEnough, canHostGrow, hasSignificantWidthChange, areWorkspaceAttributesCorrect, getCssLengthFromProperty } from './workspace-manager-utils.js';
9
10
 
10
11
  /**
11
12
  * @license
@@ -18,6 +19,8 @@ import throttle from 'lodash-es/throttle';
18
19
  const WORKSPACE_MIN_WIDTH_FALLBACK = 640;
19
20
  const MESSAGES_MIN_WIDTH_FALLBACK = 320;
20
21
  const HISTORY_WIDTH_FALLBACK = 320;
22
+ const EXPANSION_POLL_INTERVAL_MS = 200;
23
+ const EXPANSION_THRESHOLD_PX = 1;
21
24
  /**
22
25
  * Manages workspace layout, responsive behavior, and transitions for cds-aichat-shell.
23
26
  * Handles switching between inline and panel modes based on available width,
@@ -32,7 +35,9 @@ class WorkspaceManager {
32
35
  inPanel: false,
33
36
  contentVisible: true,
34
37
  containerVisible: false,
38
+ isCheckingExpansion: false,
35
39
  isExpanding: false,
40
+ isCheckingContracting: false,
36
41
  isContracting: false,
37
42
  };
38
43
  this.lastKnownCssValues = {};
@@ -45,7 +50,6 @@ class WorkspaceManager {
45
50
  */
46
51
  connect() {
47
52
  if (this.config.showWorkspace) {
48
- this.observeHostWidth();
49
53
  this.handleShowWorkspaceEnabled();
50
54
  }
51
55
  this.observeCssProperties();
@@ -60,7 +64,7 @@ class WorkspaceManager {
60
64
  window.removeEventListener("resize", this.windowResizeHandler);
61
65
  }
62
66
  this.clearExpansionTimers();
63
- this.clearClosingTimer();
67
+ this.clearContractionTimers();
64
68
  this.cssPropertyObserver?.disconnect();
65
69
  }
66
70
  /**
@@ -83,7 +87,7 @@ class WorkspaceManager {
83
87
  // Handle showWorkspace changes
84
88
  if (newConfig.showWorkspace !== undefined) {
85
89
  if (newConfig.showWorkspace && !oldConfig.showWorkspace) {
86
- this.observeHostWidth();
90
+ // handleShowWorkspaceEnabled will call observeHostWidth at the right time
87
91
  this.handleShowWorkspaceEnabled();
88
92
  }
89
93
  else if (!newConfig.showWorkspace && oldConfig.showWorkspace) {
@@ -114,13 +118,41 @@ class WorkspaceManager {
114
118
  * Check if workspace should be rendered inline (side-by-side with messages).
115
119
  */
116
120
  shouldRenderInline() {
117
- return this.state.containerVisible && !this.state.inPanel;
121
+ // During checking phase, optimistically render inline
122
+ if (this.state.isCheckingExpansion) {
123
+ return (this.state.containerVisible &&
124
+ !this.state.isContracting &&
125
+ !this.state.isCheckingContracting);
126
+ }
127
+ // During expanding phase, always render inline (will be hidden with CSS)
128
+ if (this.state.isExpanding) {
129
+ return (this.state.containerVisible &&
130
+ !this.state.isContracting &&
131
+ !this.state.isCheckingContracting);
132
+ }
133
+ // During checking-contracting phase, keep rendering inline to maintain layout
134
+ if (this.state.isCheckingContracting) {
135
+ return this.state.containerVisible;
136
+ }
137
+ // During contracting phase, keep rendering inline to maintain layout
138
+ // Container will be removed from DOM at the very end
139
+ if (this.state.isContracting) {
140
+ return this.state.containerVisible;
141
+ }
142
+ // In stable state, render inline if not in panel mode
143
+ const returnValue = this.state.containerVisible && !this.state.inPanel;
144
+ return returnValue;
118
145
  }
119
146
  /**
120
147
  * Check if workspace should be rendered as a panel (overlay).
121
148
  */
122
149
  shouldRenderPanel() {
123
- return this.state.containerVisible && this.state.inPanel;
150
+ return (this.state.containerVisible &&
151
+ this.state.inPanel &&
152
+ !this.state.isCheckingExpansion &&
153
+ !this.state.isExpanding &&
154
+ !this.state.isCheckingContracting &&
155
+ !this.state.isContracting);
124
156
  }
125
157
  // ========== Private Methods ==========
126
158
  observeHostWidth() {
@@ -148,10 +180,13 @@ class WorkspaceManager {
148
180
  }
149
181
  createHostResizeObserver() {
150
182
  this.hostResizeObserver = new ResizeObserver((entries) => {
151
- for (const entry of entries) {
152
- const inlineSize = this.getInlineSizeFromEntry(entry);
153
- this.throttledHandleHostResize(inlineSize);
154
- }
183
+ // Use requestAnimationFrame to avoid ResizeObserver loop errors
184
+ requestAnimationFrame(() => {
185
+ for (const entry of entries) {
186
+ const inlineSize = getInlineSizeFromEntry(entry);
187
+ this.throttledHandleHostResize(inlineSize);
188
+ }
189
+ });
155
190
  });
156
191
  this.hostResizeObserver.observe(this.hostElement);
157
192
  }
@@ -167,6 +202,10 @@ class WorkspaceManager {
167
202
  this.setWorkspaceInPanel(false);
168
203
  return;
169
204
  }
205
+ // Don't perform initial measurement during expansion/contraction
206
+ if (this.state.isExpanding || this.state.isContracting) {
207
+ return;
208
+ }
170
209
  this.handleHostResize(currentWidth);
171
210
  }
172
211
  handleHostResize(inlineSize) {
@@ -174,6 +213,10 @@ class WorkspaceManager {
174
213
  this.trackExpectedExpansion(inlineSize);
175
214
  return;
176
215
  }
216
+ if (this.state.isCheckingContracting) {
217
+ this.trackExpectedContraction(inlineSize);
218
+ return;
219
+ }
177
220
  if (this.state.isContracting) {
178
221
  return;
179
222
  }
@@ -183,102 +226,240 @@ class WorkspaceManager {
183
226
  if (!Number.isFinite(inlineSize)) {
184
227
  return;
185
228
  }
186
- if (!this.config.showWorkspace ||
187
- !this.state.containerVisible ||
188
- this.state.isContracting) {
229
+ if (shouldSkipWorkspaceUpdate(this.config, this.state)) {
189
230
  this.setWorkspaceInPanel(false);
190
231
  return;
191
232
  }
192
- const workspaceMinWidth = this.getCssLengthFromProperty("--cds-aichat-workspace-min-width", WORKSPACE_MIN_WIDTH_FALLBACK);
193
- const messagesMinWidth = this.getCssLengthFromProperty("--cds-aichat-messages-min-width", MESSAGES_MIN_WIDTH_FALLBACK);
194
- const historyWidth = this.config.showHistory
195
- ? this.getCssLengthFromProperty("--cds-aichat-history-width", HISTORY_WIDTH_FALLBACK)
196
- : 0;
197
- const sideBySideMinWidth = workspaceMinWidth + messagesMinWidth + historyWidth;
233
+ // Skip during expansion
234
+ if (this.state.isExpanding) {
235
+ return;
236
+ }
237
+ const dimensions = this.getLayoutDimensions();
238
+ const sideBySideMinWidth = calculateRequiredWidth(dimensions);
198
239
  const shouldBeInPanel = inlineSize < sideBySideMinWidth;
199
240
  this.setWorkspaceInPanel(shouldBeInPanel);
200
241
  }
242
+ /**
243
+ * Get layout dimensions from cache or compute if not available.
244
+ * Uses cached CSS values for performance, falling back to live computation.
245
+ */
246
+ getLayoutDimensions() {
247
+ // Ensure cache is populated
248
+ if (!this.lastKnownCssValues.workspaceMinWidth) {
249
+ this.updateLastKnownCssValues();
250
+ }
251
+ return {
252
+ workspaceMinWidth: this.lastKnownCssValues.workspaceMinWidth ??
253
+ WORKSPACE_MIN_WIDTH_FALLBACK,
254
+ messagesMinWidth: this.lastKnownCssValues.messagesMinWidth ?? MESSAGES_MIN_WIDTH_FALLBACK,
255
+ historyWidth: this.config.showHistory
256
+ ? (this.lastKnownCssValues.historyWidth ?? HISTORY_WIDTH_FALLBACK)
257
+ : 0,
258
+ };
259
+ }
201
260
  handleShowWorkspaceEnabled() {
202
261
  // Cancel any ongoing closing animation
203
- this.clearClosingTimer();
204
- this.setState({ isContracting: false });
205
- // Show the workspace container immediately
206
- this.setShowWorkspaceContainer(true);
262
+ this.clearContractionTimers();
263
+ this.setState({ isCheckingContracting: false, isContracting: false });
207
264
  const inlineSize = this.hostElement.getBoundingClientRect().width;
208
265
  const requiredWidth = this.getRequiredMinWidth();
209
266
  // Scenario 1: Already wide enough - show immediately
210
- if (this.isWideEnough(inlineSize, requiredWidth)) {
211
- this.finalizeImmediateDisplay(inlineSize, false);
267
+ if (isWideEnough(inlineSize, requiredWidth)) {
268
+ this.initializeImmediateDisplay("container", inlineSize);
212
269
  return;
213
270
  }
214
271
  // Scenario 2: Host can't ever reach required size - go straight to panel
215
- if (!this.canHostGrow(requiredWidth)) {
216
- this.finalizeImmediateDisplay(inlineSize, true);
272
+ if (!canHostGrow(requiredWidth)) {
273
+ this.initializeImmediateDisplay("panel", inlineSize);
217
274
  return;
218
275
  }
219
276
  // Scenario 3: Expecting expansion - setup tracking
277
+ this.initializeExpansionTracking();
278
+ }
279
+ initializeImmediateDisplay(mode, inlineSize) {
280
+ const attribute = mode === "container" ? "workspace-in-container" : "workspace-in-panel";
281
+ // Pre-set workspace attribute to prevent layout flash
282
+ this.hostElement.setAttribute(attribute, "");
283
+ // Show the workspace container immediately
284
+ this.setShowWorkspaceContainer(true);
285
+ this.observeHostWidth();
286
+ this.finalizeImmediateDisplay(inlineSize, mode === "panel");
287
+ }
288
+ initializeExpansionTracking() {
289
+ // Set isCheckingExpansion BEFORE showing container AND observing
290
+ // This allows workspace to render inline but remain invisible while we check for expansion
291
+ this.setState({ isCheckingExpansion: true });
292
+ // Don't pre-set workspace mode - let expansion tracking determine it
293
+ // The workspace-checking class on shell will handle the transition state
294
+ this.setShowWorkspaceContainer(true);
295
+ this.observeHostWidth();
220
296
  this.setupExpansionTracking();
221
297
  }
222
298
  handleShowWorkspaceDisabled() {
223
- // Step 1: Immediately mark that we're closing to prevent mode switches
224
- // This must happen FIRST before any other state changes
225
- this.setState({ isContracting: true });
226
- // Step 2: Lock in the current panel state to prevent it from changing
227
- // Store the current value so it doesn't change during closing
228
- const wasInPanel = this.state.inPanel;
229
- // Step 3: Immediately hide the workspace content (opacity goes to 0 instantly)
299
+ // Step 1: Clear any ongoing expansion tracking first
300
+ this.clearExpansionTimers();
301
+ this.setState({ isCheckingExpansion: false, isExpanding: false });
302
+ // Step 2: Immediately hide the workspace content (opacity goes to 0 instantly)
230
303
  this.setWorkspaceContentVisible(false);
231
- // Step 4: Restore the panel state in case setWorkspaceContentVisible triggered a change
232
- if (this.state.inPanel !== wasInPanel) {
233
- this.setWorkspaceInPanel(wasInPanel);
304
+ // Step 3: Check if we need to track contraction
305
+ const currentWidth = this.hostElement.getBoundingClientRect().width;
306
+ const requiredWidth = this.getRequiredMinWidth();
307
+ // If host is currently wide enough for inline workspace, expect contraction
308
+ if (isWideEnough(currentWidth, requiredWidth) &&
309
+ canHostGrow(requiredWidth)) {
310
+ this.setupContractionTracking();
311
+ }
312
+ else {
313
+ // No contraction expected, go straight to closing
314
+ this.initializeImmediateClosing();
234
315
  }
235
- // Step 5: Clear any ongoing expansion tracking
236
- this.clearExpansionTimers();
237
- this.setState({ isExpanding: false });
238
- // Step 6: Disconnect host resize observer to prevent interference during closing
239
- this.hostResizeObserver?.disconnect();
240
- this.hostResizeObserver = undefined;
241
- // Step 7: Poll to check if host has finished contracting
242
- // Keep the workspace container visible (but empty) during this time
243
- // to maintain its width and prevent messages area from expanding
244
- this.clearClosingTimer();
245
- this.closingLastInlineSize = this.hostElement.getBoundingClientRect().width;
246
- this.startClosingPolling();
247
316
  }
248
317
  startExpansionPolling() {
249
- const initialWidth = this.expansionLastInlineSize;
250
- // Check every 100ms if the host has stopped resizing
318
+ const initialWidth = this.expansionLastInlineSize ?? 0;
319
+ const hasSetContainerMode = { value: false };
251
320
  this.expansionCheckInterval = window.setInterval(() => {
252
- const currentWidth = this.hostElement.getBoundingClientRect().width;
253
- // If width hasn't changed from last check, the transition is complete
254
- if (currentWidth === this.expansionLastInlineSize) {
255
- // If we never saw any movement from initial width, go to panel mode
256
- const sawMovement = currentWidth !== initialWidth;
257
- this.finishWorkspaceExpansion(sawMovement);
258
- }
259
- else {
260
- this.expansionLastInlineSize = currentWidth;
261
- }
262
- }, 100);
321
+ this.checkExpansionProgress(initialWidth, hasSetContainerMode);
322
+ }, EXPANSION_POLL_INTERVAL_MS);
263
323
  }
264
- startClosingPolling() {
265
- // Check every 100ms if the host has stopped resizing
266
- this.closingCheckInterval = window.setInterval(() => {
324
+ /**
325
+ * Check expansion progress and determine if transition is complete.
326
+ */
327
+ checkExpansionProgress(initialWidth, hasSetContainerMode) {
328
+ const currentWidth = this.hostElement.getBoundingClientRect().width;
329
+ if (currentWidth === this.expansionLastInlineSize) {
330
+ // Width stabilized - expansion complete
331
+ const sawMovement = hasSignificantWidthChange(currentWidth, initialWidth, EXPANSION_THRESHOLD_PX);
332
+ this.finishWorkspaceExpansion(sawMovement);
333
+ }
334
+ else {
335
+ // Width still changing - track ongoing expansion
336
+ this.handleOngoingExpansion(currentWidth, initialWidth, hasSetContainerMode);
337
+ }
338
+ }
339
+ /**
340
+ * Handle ongoing width changes during expansion.
341
+ */
342
+ handleOngoingExpansion(currentWidth, initialWidth, hasSetContainerMode) {
343
+ // Set workspace-in-container mode on first detected meaningful movement
344
+ if (!hasSetContainerMode.value &&
345
+ hasSignificantWidthChange(currentWidth, initialWidth, EXPANSION_THRESHOLD_PX)) {
346
+ // Transition from checking to confirmed expanding
347
+ this.setState({ isCheckingExpansion: false, isExpanding: true });
348
+ this.setWorkspaceInContainerMode();
349
+ hasSetContainerMode.value = true;
350
+ }
351
+ this.expansionLastInlineSize = currentWidth;
352
+ }
353
+ /**
354
+ * Set workspace to container mode by updating DOM attributes.
355
+ */
356
+ setWorkspaceInContainerMode() {
357
+ this.hostElement.removeAttribute("workspace-in-panel");
358
+ this.hostElement.setAttribute("workspace-in-container", "");
359
+ }
360
+ setupContractionTracking() {
361
+ // Set isCheckingContracting BEFORE clearing attributes
362
+ // This allows workspace to stay inline while we check for contraction
363
+ this.setState({ isCheckingContracting: true });
364
+ // Store initial width for comparison
365
+ this.contractionInitialInlineSize =
366
+ this.hostElement.getBoundingClientRect().width;
367
+ this.contractionLastInlineSize = this.contractionInitialInlineSize;
368
+ // Keep observing host width to detect contraction
369
+ // Don't disconnect the observer - we need it to track contraction
370
+ this.startContractionPolling();
371
+ }
372
+ initializeImmediateClosing() {
373
+ // Mark as contracting and proceed directly to closing
374
+ this.setState({ isContracting: true });
375
+ // Disconnect host resize observer
376
+ this.hostResizeObserver?.disconnect();
377
+ this.hostResizeObserver = undefined;
378
+ // Immediately finish closing (will clear attributes)
379
+ this.finishWorkspaceClosing();
380
+ }
381
+ startContractionPolling() {
382
+ const initialWidth = this.contractionInitialInlineSize ?? 0;
383
+ const hasSetContractingMode = { value: false };
384
+ this.contractionCheckInterval = window.setInterval(() => {
385
+ this.checkContractionProgress(initialWidth, hasSetContractingMode);
386
+ }, EXPANSION_POLL_INTERVAL_MS);
387
+ }
388
+ checkContractionProgress(initialWidth, hasSetContractingMode) {
389
+ const currentWidth = this.hostElement.getBoundingClientRect().width;
390
+ if (currentWidth === this.contractionLastInlineSize) {
391
+ // Width stabilized - contraction complete
392
+ const sawMovement = hasSignificantWidthChange(currentWidth, initialWidth, EXPANSION_THRESHOLD_PX);
393
+ this.finishWorkspaceContraction(sawMovement);
394
+ }
395
+ else {
396
+ // Width still changing - track ongoing contraction
397
+ this.handleOngoingContraction(currentWidth, initialWidth, hasSetContractingMode);
398
+ }
399
+ }
400
+ handleOngoingContraction(currentWidth, initialWidth, hasSetContractingMode) {
401
+ // Set contracting mode on first detected meaningful shrinkage
402
+ if (!hasSetContractingMode.value &&
403
+ hasSignificantWidthChange(currentWidth, initialWidth, EXPANSION_THRESHOLD_PX) &&
404
+ currentWidth < initialWidth) {
405
+ // Transition from checking to confirmed contracting
406
+ // DON'T clear workspace attributes yet - keep them to maintain layout
407
+ this.setState({ isCheckingContracting: false, isContracting: true });
408
+ hasSetContractingMode.value = true;
409
+ }
410
+ this.contractionLastInlineSize = currentWidth;
411
+ }
412
+ finishWorkspaceContraction(sawMovement) {
413
+ // Clear checking-contracting flag and timers
414
+ this.setState({ isCheckingContracting: false });
415
+ this.clearContractionTimers();
416
+ if (!sawMovement) {
417
+ // No contraction happened, safe to close immediately
418
+ this.setState({ isContracting: true });
419
+ this.hostResizeObserver?.disconnect();
420
+ this.hostResizeObserver = undefined;
421
+ // Clear attributes and finish closing
422
+ this.finishWorkspaceClosing();
423
+ }
424
+ else {
425
+ // Contraction happened, need to wait for host to finish contracting
426
+ // before removing workspace from DOM
427
+ this.setState({ isContracting: true });
428
+ // DON'T clear workspace attributes yet - keep them to maintain layout
429
+ // They will be cleared in finishWorkspaceClosing()
430
+ // Disconnect host resize observer to prevent interference
431
+ this.hostResizeObserver?.disconnect();
432
+ this.hostResizeObserver = undefined;
433
+ // Start polling to detect when host has finished contracting
434
+ this.contractionLastInlineSize =
435
+ this.hostElement.getBoundingClientRect().width;
436
+ this.startFinalContractionPolling();
437
+ }
438
+ }
439
+ startFinalContractionPolling() {
440
+ this.contractionCheckInterval = window.setInterval(() => {
267
441
  const currentWidth = this.hostElement.getBoundingClientRect().width;
268
- // If width hasn't changed, the transition is complete
269
- if (currentWidth === this.closingLastInlineSize) {
442
+ // If width hasn't changed, the host has finished contracting
443
+ if (currentWidth === this.contractionLastInlineSize) {
270
444
  this.finishWorkspaceClosing();
271
445
  }
272
446
  else {
273
- this.closingLastInlineSize = currentWidth;
447
+ this.contractionLastInlineSize = currentWidth;
274
448
  }
275
- }, 100);
449
+ }, EXPANSION_POLL_INTERVAL_MS);
450
+ }
451
+ trackExpectedContraction(inlineSize) {
452
+ if (!Number.isFinite(inlineSize)) {
453
+ return;
454
+ }
455
+ // Update the last known size - the polling interval will detect when it stops changing
456
+ this.contractionLastInlineSize = inlineSize;
276
457
  }
277
458
  finishWorkspaceExpansion(sawMovement) {
278
459
  const inlineSize = this.expansionLastInlineSize ??
279
460
  this.hostElement.getBoundingClientRect().width;
280
- // Clear the expansion flag and timers first
281
- this.setState({ isExpanding: false });
461
+ // Clear both checking and expanding flags and timers
462
+ this.setState({ isCheckingExpansion: false, isExpanding: false });
282
463
  this.clearExpansionTimers();
283
464
  // Determine the correct panel state BEFORE showing content
284
465
  if (!sawMovement) {
@@ -291,15 +472,27 @@ class WorkspaceManager {
291
472
  this.setWorkspaceContentVisible(true);
292
473
  }
293
474
  finishWorkspaceClosing() {
294
- // Now hide the workspace container (removes from DOM)
475
+ // IMPORTANT: Remove workspace container from DOM FIRST while attributes are still present
476
+ // This prevents input-and-messages from expanding while workspace is still in DOM
295
477
  this.setShowWorkspaceContainer(false);
296
- // Reset workspace state
297
- this.setWorkspaceInPanel(false);
298
- this.setState({ isContracting: false });
478
+ // Now clear attributes AFTER container is removed from DOM
479
+ this.hostElement.removeAttribute("workspace-in-panel");
480
+ this.hostElement.removeAttribute("workspace-in-container");
481
+ // Reset workspace state to original values
482
+ this.setState({
483
+ inPanel: false,
484
+ contentVisible: true,
485
+ containerVisible: false,
486
+ isCheckingExpansion: false,
487
+ isExpanding: false,
488
+ isCheckingContracting: false,
489
+ isContracting: false,
490
+ });
299
491
  // Clear the timers
300
- this.clearClosingTimer();
492
+ this.clearContractionTimers();
301
493
  // Reset tracking
302
- this.closingLastInlineSize = undefined;
494
+ this.contractionLastInlineSize = undefined;
495
+ this.contractionInitialInlineSize = undefined;
303
496
  }
304
497
  trackExpectedExpansion(inlineSize) {
305
498
  if (!Number.isFinite(inlineSize)) {
@@ -315,18 +508,56 @@ class WorkspaceManager {
315
508
  }
316
509
  this.expansionLastInlineSize = undefined;
317
510
  }
318
- clearClosingTimer() {
319
- if (this.closingCheckInterval) {
320
- clearInterval(this.closingCheckInterval);
321
- this.closingCheckInterval = undefined;
511
+ clearContractionTimers() {
512
+ if (this.contractionCheckInterval) {
513
+ clearInterval(this.contractionCheckInterval);
514
+ this.contractionCheckInterval = undefined;
322
515
  }
516
+ this.contractionLastInlineSize = undefined;
517
+ this.contractionInitialInlineSize = undefined;
518
+ }
519
+ /**
520
+ * Updates workspace DOM attributes to match the panel state.
521
+ * Maintains inverse relationship between panel and container attributes.
522
+ *
523
+ * @param inPanel - True for panel mode, false for container mode
524
+ */
525
+ updateWorkspaceAttributes(inPanel) {
526
+ this.hostElement.toggleAttribute("workspace-in-panel", inPanel);
527
+ // workspace-in-container is the inverse of workspace-in-panel
528
+ this.hostElement.toggleAttribute("workspace-in-container", !inPanel);
323
529
  }
530
+ /**
531
+ * Updates the workspace panel state and corresponding DOM attributes.
532
+ *
533
+ * Synchronizes internal state with DOM attributes that control whether
534
+ * the workspace is displayed as an overlay panel or inline container.
535
+ *
536
+ * Ignores calls during expansion/contraction transitions. Only updates if
537
+ * state or attributes need changes. Maintains inverse relationship between
538
+ * panel and container attributes.
539
+ *
540
+ * @param inPanel - True to display workspace as overlay panel, false for inline
541
+ */
324
542
  setWorkspaceInPanel(inPanel) {
325
- if (this.state.inPanel === inPanel) {
543
+ // Early exit during transitions
544
+ if (this.state.isExpanding ||
545
+ this.state.isCheckingContracting ||
546
+ this.state.isContracting) {
326
547
  return;
327
548
  }
328
- this.setState({ inPanel });
329
- this.hostElement.toggleAttribute("workspace-in-panel", inPanel);
549
+ const stateChanged = this.state.inPanel !== inPanel;
550
+ const attributesCorrect = areWorkspaceAttributesCorrect(this.hostElement, inPanel);
551
+ // Nothing to do if state and attributes are already correct
552
+ if (!stateChanged && attributesCorrect) {
553
+ return;
554
+ }
555
+ // Update state if needed
556
+ if (stateChanged) {
557
+ this.setState({ inPanel });
558
+ }
559
+ // Update attributes
560
+ this.updateWorkspaceAttributes(inPanel);
330
561
  this.requestHostUpdate();
331
562
  }
332
563
  setWorkspaceContentVisible(visible) {
@@ -348,6 +579,8 @@ class WorkspaceManager {
348
579
  this.updateShellClasses();
349
580
  }
350
581
  updateShellClasses() {
582
+ this.shellRoot.classList.toggle("workspace-checking", this.state.isCheckingExpansion);
583
+ this.shellRoot.classList.toggle("workspace-checking-closing", this.state.isCheckingContracting);
351
584
  this.shellRoot.classList.toggle("workspace-closing", this.state.isContracting);
352
585
  this.shellRoot.classList.toggle("workspace-opening", this.state.isExpanding);
353
586
  }
@@ -358,19 +591,13 @@ class WorkspaceManager {
358
591
  }
359
592
  }
360
593
  getRequiredMinWidth() {
361
- const workspaceMinWidth = this.getCssLengthFromProperty("--cds-aichat-workspace-min-width", WORKSPACE_MIN_WIDTH_FALLBACK);
362
- const messagesMinWidth = this.getCssLengthFromProperty("--cds-aichat-messages-min-width", MESSAGES_MIN_WIDTH_FALLBACK);
594
+ const workspaceMinWidth = getCssLengthFromProperty(this.hostElement, "--cds-aichat-workspace-min-width", WORKSPACE_MIN_WIDTH_FALLBACK);
595
+ const messagesMinWidth = getCssLengthFromProperty(this.hostElement, "--cds-aichat-messages-min-width", MESSAGES_MIN_WIDTH_FALLBACK);
363
596
  const historyWidth = this.config.showHistory
364
- ? this.getCssLengthFromProperty("--cds-aichat-history-width", HISTORY_WIDTH_FALLBACK)
597
+ ? getCssLengthFromProperty(this.hostElement, "--cds-aichat-history-width", HISTORY_WIDTH_FALLBACK)
365
598
  : 0;
366
599
  return workspaceMinWidth + messagesMinWidth + historyWidth;
367
600
  }
368
- isWideEnough(inlineSize, requiredWidth) {
369
- return typeof window === "undefined" || inlineSize >= requiredWidth;
370
- }
371
- canHostGrow(requiredWidth) {
372
- return typeof window !== "undefined" && window.innerWidth >= requiredWidth;
373
- }
374
601
  finalizeImmediateDisplay(inlineSize, usePanel) {
375
602
  this.setWorkspaceContentVisible(true);
376
603
  this.setState({ isExpanding: false });
@@ -383,30 +610,15 @@ class WorkspaceManager {
383
610
  }
384
611
  }
385
612
  setupExpansionTracking() {
386
- this.setState({ isExpanding: true });
613
+ // isExpanding is already set in handleShowWorkspaceEnabled
387
614
  this.clearExpansionTimers();
388
- this.setWorkspaceInPanel(false);
615
+ // Don't set workspace-in-container early - wait to see if expansion actually happens
616
+ // This prevents the flash of container mode CSS when opening directly to panel mode
389
617
  this.setWorkspaceContentVisible(false);
390
618
  this.expansionLastInlineSize =
391
619
  this.hostElement.getBoundingClientRect().width;
392
620
  this.startExpansionPolling();
393
621
  }
394
- getInlineSizeFromEntry(entry) {
395
- const borderBoxSize = Array.isArray(entry.borderBoxSize)
396
- ? entry.borderBoxSize[0]
397
- : entry.borderBoxSize;
398
- return borderBoxSize?.inlineSize ?? entry.contentRect.width;
399
- }
400
- getCssLengthFromProperty(propertyName, fallback) {
401
- const value = getComputedStyle(this.hostElement)
402
- .getPropertyValue(propertyName)
403
- .trim();
404
- if (!value) {
405
- return fallback;
406
- }
407
- const parsed = Number.parseFloat(value);
408
- return Number.isNaN(parsed) ? fallback : parsed;
409
- }
410
622
  /**
411
623
  * Observe CSS custom properties that affect workspace layout.
412
624
  * When these properties change, recalculate workspace positioning.
@@ -431,18 +643,18 @@ class WorkspaceManager {
431
643
  */
432
644
  updateLastKnownCssValues() {
433
645
  this.lastKnownCssValues = {
434
- workspaceMinWidth: this.getCssLengthFromProperty("--cds-aichat-workspace-min-width", WORKSPACE_MIN_WIDTH_FALLBACK),
435
- messagesMinWidth: this.getCssLengthFromProperty("--cds-aichat-messages-min-width", MESSAGES_MIN_WIDTH_FALLBACK),
436
- historyWidth: this.getCssLengthFromProperty("--cds-aichat-history-width", HISTORY_WIDTH_FALLBACK),
646
+ workspaceMinWidth: getCssLengthFromProperty(this.hostElement, "--cds-aichat-workspace-min-width", WORKSPACE_MIN_WIDTH_FALLBACK),
647
+ messagesMinWidth: getCssLengthFromProperty(this.hostElement, "--cds-aichat-messages-min-width", MESSAGES_MIN_WIDTH_FALLBACK),
648
+ historyWidth: getCssLengthFromProperty(this.hostElement, "--cds-aichat-history-width", HISTORY_WIDTH_FALLBACK),
437
649
  };
438
650
  }
439
651
  /**
440
652
  * Check if any relevant CSS properties have changed and trigger recalculation.
441
653
  */
442
654
  checkCssPropertyChanges() {
443
- const workspaceMinWidth = this.getCssLengthFromProperty("--cds-aichat-workspace-min-width", WORKSPACE_MIN_WIDTH_FALLBACK);
444
- const messagesMinWidth = this.getCssLengthFromProperty("--cds-aichat-messages-min-width", MESSAGES_MIN_WIDTH_FALLBACK);
445
- const historyWidth = this.getCssLengthFromProperty("--cds-aichat-history-width", HISTORY_WIDTH_FALLBACK);
655
+ const workspaceMinWidth = getCssLengthFromProperty(this.hostElement, "--cds-aichat-workspace-min-width", WORKSPACE_MIN_WIDTH_FALLBACK);
656
+ const messagesMinWidth = getCssLengthFromProperty(this.hostElement, "--cds-aichat-messages-min-width", MESSAGES_MIN_WIDTH_FALLBACK);
657
+ const historyWidth = getCssLengthFromProperty(this.hostElement, "--cds-aichat-history-width", HISTORY_WIDTH_FALLBACK);
446
658
  const hasChanged = workspaceMinWidth !== this.lastKnownCssValues.workspaceMinWidth ||
447
659
  messagesMinWidth !== this.lastKnownCssValues.messagesMinWidth ||
448
660
  historyWidth !== this.lastKnownCssValues.historyWidth;
@@ -452,6 +664,7 @@ class WorkspaceManager {
452
664
  if (this.config.showWorkspace &&
453
665
  this.state.containerVisible &&
454
666
  !this.state.isExpanding &&
667
+ !this.state.isCheckingContracting &&
455
668
  !this.state.isContracting) {
456
669
  const currentWidth = this.hostElement.getBoundingClientRect().width;
457
670
  this.updateWorkspaceInPanelState(currentWidth);