@servicenow/aiux 22.42.1 → 22.42.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicenow/aiux-commons",
3
- "version": "3.9.1",
3
+ "version": "3.9.3",
4
4
  "description": "AIUX Common Utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -313318,6 +313318,17 @@ async function buildPages({
313318
313318
  }
313319
313319
  }
313320
313320
  const lifecycleBundle = applicationFile ? entryMap["application"] : null;
313321
+ let lifecycleRequiredTranslations = [];
313322
+ if (applicationFile) {
313323
+ const { requiredTranslations: appTranslations } = analyzePageSource(applicationFile);
313324
+ lifecycleRequiredTranslations = [...appTranslations];
313325
+ if (lifecycleBundle) {
313326
+ lifecycleRequiredTranslations = mergeRequiredTranslations([
313327
+ lifecycleRequiredTranslations,
313328
+ collectEntryOwnTranslations(lifecycleBundle, chunksByFileName)
313329
+ ]);
313330
+ }
313331
+ }
313321
313332
  const serverDir = path_browser_shim_default.join(distDir, "server");
313322
313333
  if (IS_KARUNA_META_PACKAGE_MODE && fs_browser_shim_default.existsSync(serverDir)) {
313323
313334
  const codemod = rewriteDir(serverDir, fs_browser_shim_default);
@@ -313541,13 +313552,27 @@ async function buildPages({
313541
313552
  ...Object.keys(chunkRequiredTranslations).length > 0 ? { chunkRequiredTranslations } : {},
313542
313553
  ...appDefaultLayout ? { applicationLayout: appDefaultLayout } : {},
313543
313554
  ...lifecycleBundle ? { lifecycleBundle } : {},
313555
+ ...lifecycleRequiredTranslations.length > 0 ? { lifecycleRequiredTranslations } : {},
313544
313556
  ...extLifecycleBundles.length > 0 ? {
313545
- extensionLifecycleBundles: extLifecycleBundles.map((e) => ({
313546
- scope: e.scope,
313547
- host: e.host,
313548
- ...e.experienceSysId ? { experienceSysId: e.experienceSysId } : {},
313549
- bundle: e.bundle
313550
- }))
313557
+ extensionLifecycleBundles: extLifecycleBundles.map((e) => {
313558
+ const { requiredTranslations: extTranslations } = analyzePageSource(
313559
+ e.file
313560
+ );
313561
+ let extRequiredTranslations = [...extTranslations];
313562
+ if (e.bundle) {
313563
+ extRequiredTranslations = mergeRequiredTranslations([
313564
+ extRequiredTranslations,
313565
+ collectEntryOwnTranslations(e.bundle, chunksByFileName)
313566
+ ]);
313567
+ }
313568
+ return {
313569
+ scope: e.scope,
313570
+ host: e.host,
313571
+ ...e.experienceSysId ? { experienceSysId: e.experienceSysId } : {},
313572
+ bundle: e.bundle,
313573
+ ...extRequiredTranslations.length > 0 ? { requiredTranslations: extRequiredTranslations } : {}
313574
+ };
313575
+ })
313551
313576
  } : {},
313552
313577
  ...requiresCheckManifestPath ? { requiresCheck: requiresCheckManifestPath } : {},
313553
313578
  ...appRequirements || appFileRequirements ? {
@@ -315655,7 +315680,10 @@ async function buildLifecycleRecord({ distDir, manifest, shared }) {
315655
315680
  required_plugins: "[]",
315656
315681
  required_user_preferences: "[]",
315657
315682
  category: "internal",
315658
- purpose: "other"
315683
+ purpose: "other",
315684
+ required_translations: JSON.stringify(
315685
+ manifest.lifecycleRequiredTranslations || []
315686
+ )
315659
315687
  }
315660
315688
  });
315661
315689
  const chunks = extractChunkImports(component2);
@@ -315676,7 +315704,7 @@ async function buildLifecycleRecord({ distDir, manifest, shared }) {
315676
315704
  return { record, lifecycleBundle };
315677
315705
  }
315678
315706
  async function buildExtensionLifecycleRecord({ distDir, entry, shared }) {
315679
- const { bundle, scope, host, experienceSysId } = entry;
315707
+ const { bundle, scope, host, experienceSysId, requiredTranslations } = entry;
315680
315708
  if (!bundle) return { record: null };
315681
315709
  const { component: component2, sourcemap } = readBundleAndSourcemap(distDir, bundle);
315682
315710
  const keySuffix = experienceSysId || `${scope}-${host}`;
@@ -315700,7 +315728,8 @@ async function buildExtensionLifecycleRecord({ distDir, entry, shared }) {
315700
315728
  required_plugins: "[]",
315701
315729
  required_user_preferences: "[]",
315702
315730
  category: "internal",
315703
- purpose: "other"
315731
+ purpose: "other",
315732
+ required_translations: JSON.stringify(requiredTranslations || [])
315704
315733
  }
315705
315734
  });
315706
315735
  const chunks = extractChunkImports(component2);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicenow/aiux-sdk",
3
- "version": "4.10.3",
3
+ "version": "4.10.5",
4
4
  "description": "Metadata discovery and transformation pipeline for AIUX widgets",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,7 +59,10 @@ export async function buildLifecycleRecord({distDir, manifest, shared}) {
59
59
  required_plugins: '[]',
60
60
  required_user_preferences: '[]',
61
61
  category: 'internal',
62
- purpose: 'other'
62
+ purpose: 'other',
63
+ required_translations: JSON.stringify(
64
+ manifest.lifecycleRequiredTranslations || []
65
+ )
63
66
  }
64
67
  });
65
68
 
@@ -91,7 +94,7 @@ export async function buildLifecycleRecord({distDir, manifest, shared}) {
91
94
  * @returns {{record: object|null}}
92
95
  */
93
96
  export async function buildExtensionLifecycleRecord({distDir, entry, shared}) {
94
- const {bundle, scope, host, experienceSysId} = entry;
97
+ const {bundle, scope, host, experienceSysId, requiredTranslations} = entry;
95
98
  if (!bundle) return {record: null};
96
99
 
97
100
  const {component, sourcemap} = readBundleAndSourcemap(distDir, bundle);
@@ -120,7 +123,8 @@ export async function buildExtensionLifecycleRecord({distDir, entry, shared}) {
120
123
  required_plugins: '[]',
121
124
  required_user_preferences: '[]',
122
125
  category: 'internal',
123
- purpose: 'other'
126
+ purpose: 'other',
127
+ required_translations: JSON.stringify(requiredTranslations || [])
124
128
  }
125
129
  });
126
130
 
@@ -3113,6 +3113,22 @@ async function buildPages({
3113
3113
 
3114
3114
  const lifecycleBundle = applicationFile ? entryMap['application'] : null;
3115
3115
 
3116
+ // Extract required translations from application.js
3117
+ let lifecycleRequiredTranslations = [];
3118
+ if (applicationFile) {
3119
+ const {requiredTranslations: appTranslations} =
3120
+ analyzePageSource(applicationFile);
3121
+ lifecycleRequiredTranslations = [...appTranslations];
3122
+
3123
+ // Recover strings from components imported by only the lifecycle bundle
3124
+ if (lifecycleBundle) {
3125
+ lifecycleRequiredTranslations = mergeRequiredTranslations([
3126
+ lifecycleRequiredTranslations,
3127
+ collectEntryOwnTranslations(lifecycleBundle, chunksByFileName)
3128
+ ]);
3129
+ }
3130
+ }
3131
+
3116
3132
  // Karuna meta-package codemod: rewrite bare-specifier imports of
3117
3133
  // @servicenow/aiux-* in the compiled SSR output to @servicenow/karuna
3118
3134
  // subpath imports. This lets Node's standard ESM resolver locate
@@ -3478,14 +3494,38 @@ async function buildPages({
3478
3494
  : {}),
3479
3495
  ...(appDefaultLayout ? {applicationLayout: appDefaultLayout} : {}),
3480
3496
  ...(lifecycleBundle ? {lifecycleBundle} : {}),
3497
+ ...(lifecycleRequiredTranslations.length > 0
3498
+ ? {lifecycleRequiredTranslations}
3499
+ : {}),
3481
3500
  ...(extLifecycleBundles.length > 0
3482
3501
  ? {
3483
- extensionLifecycleBundles: extLifecycleBundles.map(e => ({
3484
- scope: e.scope,
3485
- host: e.host,
3486
- ...(e.experienceSysId ? {experienceSysId: e.experienceSysId} : {}),
3487
- bundle: e.bundle
3488
- }))
3502
+ extensionLifecycleBundles: extLifecycleBundles.map(e => {
3503
+ // Extract translations from each extension lifecycle file
3504
+ const {requiredTranslations: extTranslations} = analyzePageSource(
3505
+ e.file
3506
+ );
3507
+ let extRequiredTranslations = [...extTranslations];
3508
+
3509
+ // Recover strings from components imported by only this extension lifecycle bundle
3510
+ if (e.bundle) {
3511
+ extRequiredTranslations = mergeRequiredTranslations([
3512
+ extRequiredTranslations,
3513
+ collectEntryOwnTranslations(e.bundle, chunksByFileName)
3514
+ ]);
3515
+ }
3516
+
3517
+ return {
3518
+ scope: e.scope,
3519
+ host: e.host,
3520
+ ...(e.experienceSysId
3521
+ ? {experienceSysId: e.experienceSysId}
3522
+ : {}),
3523
+ bundle: e.bundle,
3524
+ ...(extRequiredTranslations.length > 0
3525
+ ? {requiredTranslations: extRequiredTranslations}
3526
+ : {})
3527
+ };
3528
+ })
3489
3529
  }
3490
3530
  : {}),
3491
3531
  ...(requiresCheckManifestPath
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicenow/aiux-devtools",
3
- "version": "1.12.1",
3
+ "version": "1.12.2",
4
4
  "description": "Runtime devtools for AIUX Lit-based widgets — inspect widget trees, properties, lifecycle timing, events, console output, and network requests",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -166,7 +166,11 @@ export async function openPictureInPictureWindow() {
166
166
  try {
167
167
  pipWin = await window.documentPictureInPicture.requestWindow({
168
168
  width: 1200,
169
- height: 700
169
+ height: 700,
170
+ // "back to tab" and close fire the identical pagehide event, so we
171
+ // can't treat them differently — hide it, leaving one unambiguous
172
+ // close; the in-panel dock dropdown is what redocks instead.
173
+ disallowReturnToOpener: true
170
174
  });
171
175
  } catch (err) {
172
176
  // Rejected (no transient activation, blocked by Permissions Policy, or
@@ -234,6 +238,11 @@ export async function openPictureInPictureWindow() {
234
238
  return null;
235
239
  }
236
240
 
241
+ // Remember that PiP is the last-used surface, so an inspect-widget request
242
+ // (or re-open) after the PiP window is closed restores PiP rather than
243
+ // falling back to the docked panel.
244
+ preferences.set({detached: true});
245
+
237
246
  // Suppress the in-page overlay while the PiP window owns the panel.
238
247
  overlayManager.setDetachedActive(true);
239
248
  overlayManager.hide();
@@ -246,14 +255,26 @@ export async function openPictureInPictureWindow() {
246
255
  }
247
256
 
248
257
  /**
249
- * Restore the in-page panel after the PiP window closes. The host panel never
250
- * left the page, so we just clear detached state and re-show the overlay.
258
+ * Handle the PiP window closing. Closes devtools by default; re-shows the
259
+ * in-page panel only when overlayManager.markRedockPending() was set first
260
+ * (the dock dropdown's position picks) — see the comment below.
251
261
  */
252
262
  function _redockFromPip() {
253
263
  _pipWindowRef = null;
254
264
  _pipPageHideHandler = null;
255
265
  detachedHostApi.uninstall();
256
266
  overlayManager.setDetachedActive(false);
267
+
268
+ // Default: any popup close fully closes devtools. Exception: picking a
269
+ // dock position marks redockPending before calling window.close(), so
270
+ // the panel returns docked instead of staying hidden.
271
+ //
272
+ // Closing the PiP window (no redock) leaves the `detached` preference set,
273
+ // so a subsequent inspect-widget request reopens in PiP. Only an explicit
274
+ // redock to a dock position switches the remembered surface back to docked.
275
+ if (!overlayManager.consumeRedockPending()) return;
276
+
277
+ preferences.set({detached: false});
257
278
  overlayManager.show();
258
279
  // Safety net in case the overlay state callback was lost.
259
280
  if (_panelElement) {
@@ -288,6 +309,11 @@ function _destroy() {
288
309
  if (!_pipWindowRef.closed) _pipWindowRef.close();
289
310
  _pipWindowRef = null;
290
311
  overlayManager.setDetachedActive(false);
312
+ // The pagehide listener above is what normally consumes redockPending
313
+ // (set by picking a dock position inside PiP, just before this teardown
314
+ // raced ahead of it) — clear it here too, or it would wrongly redock a
315
+ // later, unrelated PiP session's close.
316
+ overlayManager.consumeRedockPending();
291
317
  }
292
318
  overlayManager.hide();
293
319
  // Unmount panel first — triggers disconnectedCallback so panels
@@ -331,6 +357,25 @@ function _destroy() {
331
357
  shared.initialized = false;
332
358
  }
333
359
 
360
+ /**
361
+ * Open devtools for an inspect-widget request on the last-used surface.
362
+ *
363
+ * If devtools was last shown in a Picture-in-Picture window (the `detached`
364
+ * preference) and the browser supports PiP, reopen in PiP; otherwise show the
365
+ * docked panel. Must be called from a user-gesture call stack for the PiP
366
+ * branch to satisfy Document PiP's transient-activation requirement.
367
+ *
368
+ * @param {string} widgetId
369
+ */
370
+ function _openInspectSurface(widgetId) {
371
+ sessionManager.setActiveWidget(widgetId);
372
+ if (preferences.get().detached && 'documentPictureInPicture' in window) {
373
+ openPictureInPictureWindow();
374
+ } else {
375
+ overlayManager.show();
376
+ }
377
+ }
378
+
334
379
  /** @type {HTMLElement|null} Active context menu element */
335
380
  let _contextMenuEl = null;
336
381
 
@@ -407,8 +452,7 @@ function _showContextMenu(x, y, widgetId) {
407
452
  });
408
453
  item.addEventListener('click', () => {
409
454
  _dismissContextMenu();
410
- sessionManager.setActiveWidget(widgetId);
411
- overlayManager.show();
455
+ _openInspectSurface(widgetId);
412
456
  });
413
457
 
414
458
  menu.appendChild(item);
@@ -633,6 +677,12 @@ export {registerAdapter, getAdapterFor};
633
677
  export {toggleDevtools};
634
678
  export {openPictureInPictureWindow as openPip};
635
679
 
680
+ /**
681
+ * Test-only internals. Not part of the public API.
682
+ * @see CLAUDE.md — "Reach internals through the `_testOnly` export".
683
+ */
684
+ export const _testOnly = {openInspectSurface: _openInspectSurface};
685
+
636
686
  // Auto-initialize when loaded in the browser.
637
687
  // The full bundle is an ES module loaded by the bootstrap script.
638
688
  if (typeof window !== 'undefined' && !getShared().initialized) {
@@ -106,6 +106,7 @@ export const EXTENDED_EVENT_TYPES = Object.freeze(
106
106
  * @property {'dark'|'light'} theme - UI theme preference
107
107
  * @property {'bottom'|'top'|'left'|'right'} dockPosition - Devtools panel dock position
108
108
  * @property {boolean} panelVisible - Whether the devtools panel was open
109
+ * @property {boolean} detached - Whether devtools was last shown in a Picture-in-Picture window (so inspect/re-open restores that surface)
109
110
  * @property {boolean} timelineRecording - @deprecated Unified into `recording`. Kept for backward compat.
110
111
  * @property {string|null} activeTab - Last selected primary tab
111
112
  * @property {string|null} activeSubTab - Last selected inspector sub-tab
@@ -122,6 +123,7 @@ const DEFAULTS = {
122
123
  dockPosition: 'bottom',
123
124
  timelineRecording: false, // deprecated — unified into `recording`
124
125
  panelVisible: false,
126
+ detached: false,
125
127
  activeTab: null,
126
128
  activeSubTab: null
127
129
  };
@@ -1106,8 +1106,10 @@ export class DevtoolsPanel extends LitElement {
1106
1106
  }
1107
1107
 
1108
1108
  /**
1109
- * Dock back from detached mode — close the popup/PiP window.
1110
- * The main page's close-detection (or pagehide handler) re-shows the overlay.
1109
+ * Close the popup/PiP window. By itself this fully closes devtools (the
1110
+ * host's pagehide handler in index.js) — callers that want to redock
1111
+ * instead must call overlayManager.markRedockPending() first (see the
1112
+ * dock-position click handler in _renderDockBtn).
1111
1113
  */
1112
1114
  _onDockBack() {
1113
1115
  window.close();
@@ -1424,6 +1426,15 @@ export class DevtoolsPanel extends LitElement {
1424
1426
  right: svg`<rect x="9.5" y="1.5" width="5" height="11" rx="0.75" fill="currentColor" opacity="0.85"/>`
1425
1427
  };
1426
1428
 
1429
+ /**
1430
+ * Pip icon — Chrome's "undock into separate window" glyph: two overlapping
1431
+ * windows. The front window is a full outline; the back window is drawn as
1432
+ * an open path (top/right/partial-bottom edges only) so it tucks behind the
1433
+ * front without needing a background-colored mask — keeping it correct on
1434
+ * hover and across light/dark themes.
1435
+ */
1436
+ static _pipIcon = svg`<path d="M5.8,4.2 L5.8,3 Q5.8,1.8 7,1.8 L13,1.8 Q14.2,1.8 14.2,3 L14.2,8.6 Q14.2,9.8 13,9.8 L10.2,9.8" stroke="currentColor" stroke-width="1.2" fill="none" stroke-linejoin="round" stroke-linecap="round"/><rect x="1.8" y="4.2" width="8.4" height="8" rx="1.3" stroke="currentColor" stroke-width="1.2" fill="none"/>`;
1437
+
1427
1438
  /** Close the dock overflow menu and remove outside-click listener. */
1428
1439
  _closeDockMenu() {
1429
1440
  if (!this._dockMenuOpen) return;
@@ -1467,42 +1478,85 @@ export class DevtoolsPanel extends LitElement {
1467
1478
 
1468
1479
  _renderDockBtn(position, activeDock) {
1469
1480
  const labels = {bottom: 'bottom', top: 'top', left: 'left', right: 'right'};
1481
+ const isPip = position === 'pip';
1470
1482
  return html`
1471
1483
  <button
1472
1484
  class="dock-btn ${activeDock === position ? 'active' : ''}"
1473
1485
  @click=${() => {
1474
- overlayManager.setDockPosition(position);
1486
+ if (isPip) {
1487
+ // Already in PiP when this is the current-state indicator.
1488
+ if (this.mode !== 'detached') this._onPopOut();
1489
+ } else {
1490
+ overlayManager.setDockPosition(position);
1491
+ // Redock (not full close) when picking a position from PiP.
1492
+ if (this.mode === 'detached') {
1493
+ overlayManager.markRedockPending();
1494
+ this._onDockBack();
1495
+ }
1496
+ }
1475
1497
  this._closeDockMenu();
1476
1498
  }}
1477
- title="Dock to ${labels[position]}"
1478
- aria-label="Dock to ${labels[position]}"
1479
- role="radio"
1480
- aria-checked=${activeDock === position ? 'true' : 'false'}
1499
+ title=${isPip
1500
+ ? this.mode === 'detached'
1501
+ ? 'Currently in Picture-in-Picture'
1502
+ : 'Undock into separate window'
1503
+ : `Dock to ${labels[position]}`}
1504
+ aria-label=${isPip
1505
+ ? this.mode === 'detached'
1506
+ ? 'Currently in Picture-in-Picture'
1507
+ : 'Open devtools in a Picture-in-Picture window'
1508
+ : `Dock to ${labels[position]}`}
1509
+ role=${isPip ? 'menuitem' : 'radio'}
1510
+ aria-checked=${isPip
1511
+ ? nothing
1512
+ : activeDock === position
1513
+ ? 'true'
1514
+ : 'false'}
1481
1515
  >
1482
1516
  <svg viewBox="0 0 16 14" fill="none">
1483
- <rect
1484
- x="1"
1485
- y="1"
1486
- width="14"
1487
- height="12"
1488
- rx="1.5"
1489
- stroke="currentColor"
1490
- stroke-width="1.2"
1491
- />
1492
- ${DevtoolsPanel._dockFills[position]}
1517
+ ${isPip
1518
+ ? DevtoolsPanel._pipIcon
1519
+ : svg`
1520
+ <rect
1521
+ x="1"
1522
+ y="1"
1523
+ width="14"
1524
+ height="12"
1525
+ rx="1.5"
1526
+ stroke="currentColor"
1527
+ stroke-width="1.2"
1528
+ />
1529
+ ${DevtoolsPanel._dockFills[position]}
1530
+ `}
1493
1531
  </svg>
1494
1532
  </button>
1495
1533
  `;
1496
1534
  }
1497
1535
 
1498
1536
  /**
1499
- * Render dock toggle — always compact: active icon + dropdown.
1537
+ * Render dock toggle — active icon + dropdown. Pip is a non-radio option
1538
+ * in the same dropdown. While detached, the indicator shows Pip (not the
1539
+ * stale dock position) and the dropdown offers the dock positions instead.
1540
+ * Menu order is fixed: undock (PiP), left, bottom, right, top.
1500
1541
  */
1501
1542
  _renderDockToggle(dock) {
1502
- const others = ['bottom', 'top', 'left', 'right'].filter(p => p !== dock);
1543
+ const isDetached = this.mode === 'detached';
1544
+ const current = isDetached ? 'pip' : dock;
1545
+ // Canonical dropdown order: undock (PiP) first, then left/bottom/right,
1546
+ // with top last. Pip is only offered when supported and not already
1547
+ // detached; the current surface is shown as the indicator, not repeated
1548
+ // in the menu.
1549
+ const showPip = this._pipSupported && !isDetached;
1550
+ const others = ['pip', 'left', 'bottom', 'right', 'top'].filter(
1551
+ p => p !== current && (p !== 'pip' || showPip)
1552
+ );
1503
1553
  return html`
1504
- <div class="dock-toggle" role="radiogroup" aria-label="Dock position">
1505
- ${this._renderDockBtn(dock, dock)}
1554
+ <div
1555
+ class="dock-toggle"
1556
+ role=${isDetached ? nothing : 'radiogroup'}
1557
+ aria-label="Dock position"
1558
+ >
1559
+ ${this._renderDockBtn(current, current)}
1506
1560
  <div class="dock-more">
1507
1561
  <button
1508
1562
  class="dock-btn"
@@ -1521,7 +1575,7 @@ export class DevtoolsPanel extends LitElement {
1521
1575
  ${this._dockMenuOpen
1522
1576
  ? html`
1523
1577
  <div class="dock-menu">
1524
- ${others.map(p => this._renderDockBtn(p, dock))}
1578
+ ${others.map(p => this._renderDockBtn(p, current))}
1525
1579
  </div>
1526
1580
  `
1527
1581
  : nothing}
@@ -1606,6 +1660,10 @@ export class DevtoolsPanel extends LitElement {
1606
1660
  Export
1607
1661
  </button>
1608
1662
 
1663
+ <span class="toolbar-separator"></span>
1664
+
1665
+ ${this._renderDockToggle(dock)}
1666
+
1609
1667
  <button
1610
1668
  class="theme-btn"
1611
1669
  @click=${this._onToggleTheme}
@@ -1618,15 +1676,6 @@ export class DevtoolsPanel extends LitElement {
1618
1676
  >
1619
1677
  ${this.theme === 'dark' ? '🌙' : '☀️'}
1620
1678
  </button>
1621
-
1622
- <button
1623
- class="toolbar-btn"
1624
- @click=${this._onDockBack}
1625
- title="Dock back into page"
1626
- aria-label="Dock back into page"
1627
- >
1628
- ${svg`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="12" height="12" rx="1.5"/><path d="M8 5v6M5 8h6"/></svg>`}
1629
- </button>
1630
1679
  </div>
1631
1680
  <div class="inspector-layout">
1632
1681
  ${this._primaryTab === 'inspector'
@@ -1738,18 +1787,6 @@ export class DevtoolsPanel extends LitElement {
1738
1787
  <span class="tab-spacer"></span>
1739
1788
  <span class="toolbar-separator"></span>
1740
1789
 
1741
- ${this._pipSupported
1742
- ? html`
1743
- <button
1744
- class="toolbar-btn"
1745
- @click=${this._onPopOut}
1746
- title="Open in a Picture-in-Picture window"
1747
- aria-label="Open devtools in a Picture-in-Picture window"
1748
- >
1749
- ${svg`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 2h5v5"/><path d="M14 2L7 9"/><path d="M12 9v4.5a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5H7"/></svg>`}
1750
- </button>
1751
- `
1752
- : nothing}
1753
1790
  <button
1754
1791
  class="record-btn ${this._globalRecording ? 'recording' : ''}"
1755
1792
  @click=${this._onToggleGlobalRecording}
@@ -15,7 +15,13 @@ import * as preferences from '../../store/preferences.js';
15
15
 
16
16
  const _o = getShared().overlay;
17
17
 
18
- // Restore persisted state from preferences
18
+ /** @type {Set<(state: object) => void>} */
19
+ const _stateCallbacks = _o.stateCallbacks;
20
+
21
+ // Restore persisted state. Uses show() rather than a raw `_o.visible = true`
22
+ // — this module can be re-evaluated mid-session (e.g. by a bundler), and a
23
+ // raw write would silently clobber an in-progress PiP session. Must run
24
+ // after _stateCallbacks above: show() -> _notifyState() reads it.
19
25
  try {
20
26
  const saved = preferences.get();
21
27
  if (
@@ -25,7 +31,7 @@ try {
25
31
  _o.dockPosition = saved.dockPosition;
26
32
  }
27
33
  if (saved.panelVisible) {
28
- _o.visible = true;
34
+ show();
29
35
  }
30
36
  } catch {
31
37
  /* preferences may not be available yet */
@@ -37,9 +43,6 @@ const MIN_SIZE = 150;
37
43
  /** @type {number} Max panel size (fraction of viewport) */
38
44
  const MAX_SIZE_FRACTION = 0.7;
39
45
 
40
- /** @type {Set<(state: object) => void>} */
41
- const _stateCallbacks = _o.stateCallbacks;
42
-
43
46
  function _notifyState() {
44
47
  const state = getState();
45
48
  // Snapshot the Set — a callback may unsubscribe (delete from the Set)
@@ -71,7 +74,10 @@ export function getState() {
71
74
  export function toggle() {
72
75
  if (_o.detachedActive) return;
73
76
  _o.visible = !_o.visible;
74
- preferences.set({panelVisible: _o.visible});
77
+ // Interacting with the docked panel makes it the last-used surface, so
78
+ // clear the detached (Picture-in-Picture) preference — a later inspect
79
+ // should reopen docked, not in PiP.
80
+ preferences.set({panelVisible: _o.visible, detached: false});
75
81
  _notifyState();
76
82
  }
77
83
 
@@ -82,7 +88,9 @@ export function show() {
82
88
  if (_o.detachedActive) return;
83
89
  if (!_o.visible) {
84
90
  _o.visible = true;
85
- preferences.set({panelVisible: true});
91
+ // Showing the docked panel makes it the last-used surface — clear the
92
+ // detached (PiP) preference so a later inspect reopens docked.
93
+ preferences.set({panelVisible: true, detached: false});
86
94
  _notifyState();
87
95
  }
88
96
  }
@@ -170,6 +178,25 @@ export function onStateChange(callback) {
170
178
  return () => _stateCallbacks.delete(callback);
171
179
  }
172
180
 
181
+ /**
182
+ * Mark that the PiP popup is closing to redock at a picked dock position,
183
+ * not to close devtools outright. Consumed once by `index.js`'s pagehide
184
+ * handler, which otherwise treats any popup close as closing devtools.
185
+ */
186
+ export function markRedockPending() {
187
+ _o.redockPending = true;
188
+ }
189
+
190
+ /**
191
+ * Read and clear the redock-pending flag.
192
+ * @returns {boolean} Whether the popup close should redock vs. fully close.
193
+ */
194
+ export function consumeRedockPending() {
195
+ const pending = !!_o.redockPending;
196
+ _o.redockPending = false;
197
+ return pending;
198
+ }
199
+
173
200
  /**
174
201
  * Reset state. Used for teardown/testing.
175
202
  */
@@ -179,5 +206,6 @@ export function reset() {
179
206
  _o.panelWidth = 420;
180
207
  _o.dockPosition = 'bottom';
181
208
  _o.detachedActive = false;
209
+ _o.redockPending = false;
182
210
  _stateCallbacks.clear();
183
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicenow/aiux",
3
- "version": "22.42.1",
3
+ "version": "22.42.3",
4
4
  "description": "Public dev server for AINPX applications — SSR, HMR, and AIEL/chat support",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicenow/aiux-public-dev-server",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Single-process, main-thread local dev server for AIUX apps. Self-contained: no proprietary server internals. Local dev tool, not the production runtime.",
5
5
  "type": "module",
6
6
  "main": "./index.js",