@design.estate/dees-catalog 9.8.0 → 11.0.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 (67) hide show
  1. package/dist_bundle/bundle.js +5626 -5343
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_bundle/third-party-notices.md +783 -10
  4. package/dist_ts_web/00_commitinfo_data.js +2 -2
  5. package/dist_ts_web/elements/00actionerror.d.ts +32 -0
  6. package/dist_ts_web/elements/00actionerror.js +45 -0
  7. package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.d.ts +16 -0
  8. package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.js +40 -3
  9. package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +7 -0
  10. package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +17 -17
  11. package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.d.ts +14 -3
  12. package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.js +23 -4
  13. package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.js +77 -5
  14. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +62 -48
  15. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +89 -51
  16. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +528 -662
  17. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +45 -6
  18. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +184 -27
  19. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +55 -20
  20. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +23 -3
  21. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +71 -11
  22. package/dist_ts_web/elements/00group-input/index.d.ts +0 -1
  23. package/dist_ts_web/elements/00group-input/index.js +1 -2
  24. package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.d.ts +7 -0
  25. package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.js +13 -3
  26. package/dist_ts_web/elements/index.d.ts +1 -2
  27. package/dist_ts_web/elements/index.js +2 -4
  28. package/dist_ts_web/services/DeesServiceLibLoader.d.ts +7 -0
  29. package/dist_ts_web/services/DeesServiceLibLoader.js +45 -22
  30. package/package.json +3 -4
  31. package/readme.md +105 -18
  32. package/scripts/check-third-party-notices.mjs +261 -0
  33. package/third-party-notices.md +783 -10
  34. package/ts_web/00_commitinfo_data.ts +1 -1
  35. package/ts_web/elements/00actionerror.ts +66 -0
  36. package/ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.ts +38 -2
  37. package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +18 -16
  38. package/ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.ts +22 -3
  39. package/ts_web/elements/00group-feedback/dees-hint/dees-hint.ts +71 -3
  40. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +99 -49
  41. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +607 -752
  42. package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +196 -24
  43. package/ts_web/elements/00group-harness/interfaces.ts +59 -19
  44. package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +73 -13
  45. package/ts_web/elements/00group-input/index.ts +0 -1
  46. package/ts_web/elements/00group-overlay/dees-modal/dees-modal.ts +13 -2
  47. package/ts_web/elements/index.ts +1 -4
  48. package/ts_web/services/DeesServiceLibLoader.ts +150 -102
  49. package/.smartconfig.json +0 -86
  50. package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.d.ts +0 -1
  51. package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.js +0 -2
  52. package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.d.ts +0 -1
  53. package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.js +0 -2
  54. package/dist_ts_web/elements/dees-search/dees-search.d.ts +0 -1
  55. package/dist_ts_web/elements/dees-search/dees-search.js +0 -2
  56. package/dist_ts_web/elements/dees-search/index.d.ts +0 -1
  57. package/dist_ts_web/elements/dees-search/index.js +0 -2
  58. package/dist_ts_web/elements/dees-tooltip/dees-tooltip.d.ts +0 -1
  59. package/dist_ts_web/elements/dees-tooltip/dees-tooltip.js +0 -2
  60. package/dist_ts_web/elements/dees-tooltip/index.d.ts +0 -1
  61. package/dist_ts_web/elements/dees-tooltip/index.js +0 -2
  62. package/ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.ts +0 -0
  63. package/ts_web/elements/00group-input/dees-input-searchselect/index.ts +0 -1
  64. package/ts_web/elements/dees-search/dees-search.ts +0 -0
  65. package/ts_web/elements/dees-search/index.ts +0 -1
  66. package/ts_web/elements/dees-tooltip/dees-tooltip.ts +0 -1
  67. package/ts_web/elements/dees-tooltip/index.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "9.8.0",
3
+ "version": "11.0.0",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
@@ -47,10 +47,10 @@
47
47
  "@design.estate/dees-wcctools": "^3.9.4",
48
48
  "@git.zone/tsbuild": "^4.4.3",
49
49
  "@git.zone/tsbundle": "^2.13.0",
50
- "@git.zone/tstest": "^6.0.0",
50
+ "@git.zone/tstest": "^6.1.0",
51
51
  "@git.zone/tswatch": "^3.3.5",
52
52
  "@push.rocks/projectinfo": "^5.1.0",
53
- "@types/node": "^26.4.1"
53
+ "@types/node": "^26.5.0"
54
54
  },
55
55
  "files": [
56
56
  "ts/**/*",
@@ -62,7 +62,6 @@
62
62
  "assets/**/*",
63
63
  "scripts/**/*",
64
64
  "cli.js",
65
- ".smartconfig.json",
66
65
  "readme.md",
67
66
  "third-party-notices.md"
68
67
  ],
package/readme.md CHANGED
@@ -229,7 +229,7 @@ task guide and examples deliberately; the generated table does not replace them.
229
229
  | `dees-storage-keys` | dataview | — | [Source](ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-keys.ts) |
230
230
  | `dees-storage-preview` | dataview | — | [Source](ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-preview.ts) |
231
231
  | `dees-table` | dataview | [DeesTable](#deestable) | [Source](ts_web/elements/00group-dataview/dees-table/dees-table.ts) |
232
- | `dees-terminal-view` | harness | — | [Source](ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts) |
232
+ | `dees-terminal-view` | harness | [DeesTerminalView](#deesterminalview) | [Source](ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts) |
233
233
  | `dees-theme` | utility | [DeesTheme](#deestheme) | [Source](ts_web/elements/00group-utility/dees-theme/dees-theme.ts) |
234
234
  | `dees-thumbnail-audio` | media | [DeesThumbnailAudio](#deesthumbnailaudio) | [Source](ts_web/elements/00group-media/dees-thumbnail-audio/component.ts) |
235
235
  | `dees-thumbnail-folder` | media | [DeesThumbnailFolder](#deesthumbnailfolder) | [Source](ts_web/elements/00group-media/dees-thumbnail-folder/component.ts) |
@@ -257,6 +257,28 @@ task guide and examples deliberately; the generated table does not replace them.
257
257
 
258
258
  ## Component API
259
259
 
260
+ ### Failed actions
261
+
262
+ Components that run an application callback — `dees-modal`'s `menuOptions[].action`
263
+ and `dees-table`'s `dataActions[].actionFunc` — await it. A rejected callback is
264
+ never dropped: the component dispatches the cancelable `dees-action-error` event
265
+ (bubbling and composed) and, unless a listener cancels it, shows the error through
266
+ `dees-toast`. Successful callbacks behave exactly as before and dispatch nothing.
267
+
268
+ ```typescript
269
+ element.addEventListener('dees-action-error', (event: CustomEvent<IActionErrorDetail>) => {
270
+ const { error, action, source } = event.detail;
271
+ // source: 'modal-menu' | 'table-header' | 'table-footer' | 'table-row'
272
+ // | 'table-rowmenu' | 'table-contextmenu' | 'table-doubleclick'
273
+ event.preventDefault(); // render your own error surface instead of the toast
274
+ reportToCrashService(error);
275
+ });
276
+ ```
277
+
278
+ `IActionErrorDetail`, `actionErrorEventName`, `actionErrorMessage`,
279
+ `reportActionError` and `runAction` are exported from the package root, so a
280
+ component outside the catalog can announce failures the same way.
281
+
260
282
  ### Core UI Components
261
283
 
262
284
  #### `DeesButton`
@@ -446,14 +468,21 @@ Consistent heading component with level and styling options.
446
468
  ```
447
469
 
448
470
  #### `DeesHint`
449
- Hint/tooltip component for providing contextual help.
471
+ An inline advisory note next to the thing it explains. Use `text` for a plain
472
+ sentence and the default slot for markup; both render when both are set. The
473
+ `type` chooses the icon and the accent color. A hint with neither text nor
474
+ slotted content renders nothing and reserves no space, so a conditional note
475
+ needs no wrapper element.
450
476
 
451
477
  ```typescript
452
478
  <dees-hint
453
- text="This field is required"
454
- type="info" // Options: info, warning, error, success
455
- position="top" // Options: top, bottom, left, right
479
+ text="Changing the URL breaks existing links."
480
+ type="info" // Options: info, success, warning, error (default: info)
456
481
  ></dees-hint>
482
+
483
+ <dees-hint type="warning">
484
+ Keys older than 90 days <strong>stop working</strong>.
485
+ </dees-hint>
457
486
  ```
458
487
 
459
488
  #### `DeesPanel`
@@ -567,6 +596,7 @@ Text input field with validation, info tooltips, description text, and context m
567
596
  <dees-input-text
568
597
  key="email" // Unique identifier for form data
569
598
  label="Email" // Input label
599
+ type="email" // Native input type: text (default), number, date, email, url, tel, password
570
600
  value="initial@value.com" // Initial value
571
601
  required // Makes the field required
572
602
  disabled // Disables the input
@@ -585,6 +615,15 @@ Text input field with validation, info tooltips, description text, and context m
585
615
  ></dees-input-text>
586
616
  ```
587
617
 
618
+ - `type` maps to the native input type and picks the matching keyboard hint; an
619
+ explicit `inputmode` on the element still wins. `value` is a string for every
620
+ type except `number`, where it is a `number`, or `null` while the field is
621
+ empty — so a form collects a number rather than a numeric string. A value bound
622
+ as the wrong shape is converted to the one the type promises.
623
+ - `type="password"` masks the value. `isPasswordBool` masks it as well and adds
624
+ the reveal button that `showPasswordBool` toggles.
625
+ - `validationFunction` always receives the value as text, whatever the type.
626
+
588
627
  > 💡 **All input components** share these common properties from `DeesInputBase`: `key`, `label`, `required`, `disabled`, `infoText`, `description`, `layoutMode`, `labelPosition`.
589
628
 
590
629
  #### `DeesInputTextarea`
@@ -1505,6 +1544,10 @@ appui.setBottomBarVisible(false);
1505
1544
  Content-panel navigation with native tab buttons, separate close controls and
1506
1545
  arrow/Home/End focus movement. Enter/Space select; Delete closes a closeable tab.
1507
1546
  `IMenuItem.key` remains the identity; optional `label` supplies display text.
1547
+ The selection follows that key, so a host that binds a literal `.tabs=${[...]}`
1548
+ keeps its open tab across rerenders: descriptors with the same key are adopted
1549
+ without re-running the tab's `action`. Only a key that is no longer present falls
1550
+ back to the first tab. Descriptors without a key fall back to their `label`.
1508
1551
  Use multitoggle for a view mode within the current panel.
1509
1552
  Both horizontal and vertical layouts use an inset selection surface.
1510
1553
  `showTabIndicator=false` hides that surface while retaining selected text and ARIA
@@ -1564,6 +1607,7 @@ html`<dees-table
1564
1607
  - Action `type` accepts `header`, `inRow`, `contextmenu`, `footer`, `doubleClick`, `preview`, and `keyCombination`. Use `actionRelevancyCheckFunc` for row-specific availability. Desktop rows show up to three relevant primary actions plus overflow; below 700px of table width, one Actions menu contains the row actions. Coarse pointers receive 44px targets independently of width. The opaque pinned column shows an edge shadow only while it covers scrolling columns.
1565
1608
  - `fixedHeight` uses internal scrolling with sticky headers and `--table-max-height`; the default uses a floating header while the page scrolls. `virtualized` limits rendered rows and retains spacer geometry.
1566
1609
  - Set a column's `editor` to `text`, `number`, `checkbox`, `dropdown`, `date`, or `tags`; `editable: true` defaults to text. Use `editorOptions`, `format`, `parse`, and `validate` for value handling. Double-click edits a cell; Enter/Tab commit and move, Escape cancels. `cellEdit` reports `{ row, key, oldValue, newValue }`; `cellEditError` reports validation failures.
1610
+ - Every `actionFunc` is awaited. A rejected action dispatches the cancelable `dees-action-error` event (see [Failed actions](#failed-actions)) instead of disappearing.
1567
1611
  - File drop onto rows and `highlightUpdates: 'flash'` remain available. Flashing respects reduced motion and stable row identity.
1568
1612
 
1569
1613
  #### `DeesDataviewCodebox`
@@ -2154,6 +2198,9 @@ runs before closing. Rejected operations leave the dialog open with their error,
2154
2198
  and repeated submission and dismissal are blocked while confirmation is pending.
2155
2199
  The modal emits `modal-close` when removed.
2156
2200
 
2201
+ Every `menuOptions` action is awaited. A rejected action keeps the dialog open and
2202
+ reports the failure through `dees-action-error` (see [Failed actions](#failed-actions)).
2203
+
2157
2204
  The modal gallery includes a working settings draft: values survive navigation,
2158
2205
  Save returns the draft, and Cancel discards it. The example lives in
2159
2206
  `ts_web/elements/00group-overlay/dees-modal/dees-modal.demo.ts`.
@@ -2704,6 +2751,14 @@ Idle/busy/error line with animated dots and `aria-live="polite"`. Renders nothin
2704
2751
 
2705
2752
  Token usage chip (`in · out · total`, `compact` shows the total only); cache read/write detail in the tooltip.
2706
2753
 
2754
+ #### `DeesTerminalView`
2755
+
2756
+ Transport-agnostic xterm.js surface. The host pipes remote PTY output in with `write(data)` and receives keystrokes as `terminal-input` (`{ data }`); there is no process, runtime or socket coupling. `terminal-resize` (`{ rows, cols }`) reports the grid after every refit so the host can negotiate a PTY size, and `currentSize` reads it back (`undefined` while no terminal is live). `fontSize` sets the grid font, `disabled` stops `terminal-input` from being emitted, and `focus()` focuses the grid. xterm is loaded through `DeesServiceLibLoader`; CSP-locked or offline hosts call `provideXtermModules()` first to supply their own bundled copy instead of the CDN.
2757
+
2758
+ `write()` is safe before the terminal exists and while the element is detached: output is buffered and replayed in order on the next connection, bounded by the exported `maxPendingWriteBytes` (8 MiB — `Uint8Array` chunks count their `byteLength`, strings their UTF-16 memory). Past that bound the oldest chunks are dropped and the replay emits RIS first, so a truncated prefix is never rendered as if it were contiguous. `clear()` resets screen and scrollback through the write stream (RIS, `ESC c`) rather than around it, so it is ordered against output already queued in xterm — write, `clear()`, write leaves only the later output on screen.
2759
+
2760
+ `ready` resolves once the terminal of the current connection is live; a fresh promise is created per connection, so a reattached element can be awaited again. Every connection builds a terminal and every disconnection disposes it, so an element moved between containers, or returned to, is a live surface rather than a dead one; buffered output survives the move. A failed xterm load is not an unhandled rejection: it dispatches the bubbling, composed `terminal-error` (`ITerminalErrorDetail { error }`), rejects `ready` with the same error, leaves the element writable, and is retried on the next connection. xterm's own flow-control refusal is reported the same way. Sizing comes from the host element — give it a bounded height, as the demo does.
2761
+
2707
2762
  #### `DeesHarnessSessionSidebar`
2708
2763
 
2709
2764
  Generic session-detail panel that composes Tasks, Tools & Subagents, Tokens, Session Scratchpad, and Session Intelligence sections. Headings stay fixed in a single-open accordion; Session Intelligence splits its expanded body into independently scrolling history and a docked question composer. Set `.toolMessages` to top-level tool messages from the currently retained transcript window and `.toolRegistry` for matching labels/icons; active subagents use authoritative child status, and removed messages disappear without a second history cache. The host also supplies `IHarnessSessionMetrics`, `IHarnessScratchpad`, and `IHarnessIntelligenceExchange[]`; the component never loads or persists data itself. Context percentage and its progress bar render only when both `currentContextTokens` and a positive `maxContextTokens` are present.
@@ -2722,13 +2777,25 @@ Each card exposes an Actions button. Actions, right-click, and ContextMenu/Shift
2722
2777
 
2723
2778
  Attention and error rows retain a colored edge even when selected. Group headings count both states, including when collapsed. `working` controls the small activity pulse on normal/working rows, defaulting to `state === 'working'`; attention, error and finished indicators remain stationary. Reduced-motion preferences disable the pulse. State changes preserve row height, selection, expansion and host-provided order.
2724
2779
 
2725
- With `enableGrouping`, sortable cards can move within and between groups. Drag from a card's focusable grip, or use its ArrowUp, ArrowDown, Home, and End keys. Pointer dragging moves a full-card preview, opens a matching drop slot, and animates surrounding cards; `harness-session-move` includes both the compatibility `index` and a stable optional `beforeSessionId` anchor. Set `draggable: false` for pinned entries such as terminals. Pass `ungroupedSessionIds` to control the order outside groups; omitted or unknown entries fall back to recency order.
2780
+ **Sessions and resources are peers in one ordered list.** Every sortable row — conversation or resource — carries a focusable grip: drag it anywhere the other kind can go, including between sessions and into or out of groups, or reorder it with ArrowUp, ArrowDown, Home, and End. Pointer dragging moves a preview the size of the dragged row, opens a matching drop slot, and animates surrounding rows. Reordering does not require `enableGrouping`, which now only adds group chrome; an active search query renders one flat, non-sortable, recency-ordered section and cancels a drag in flight.
2781
+
2782
+ Every move emits `harness-item-move` with `IHarnessItemMoveDetail`: `{ item, fromGroupId, toGroupId, beforeItem, source }`, where `item` and `beforeItem` are `IHarnessSessionListItemRef` values (`{ kind: 'session' | 'resource'; id }`) and a null `beforeItem` means append. Groups hold `itemIds: IHarnessSessionListItemRef[]`, and `ungroupedItemIds` controls the order outside groups; omitted or unknown entries of either kind fall back to `updatedAt ?? createdAt` recency. Set `draggable: false` on a session or a resource for pinned entries such as terminals; pinned entries sort first in the ungrouped section and stay outside the sortable order.
2726
2783
 
2727
2784
  The compact **New group** button sits beside search whenever `enableGrouping` is true, including while filtering, and emits `harness-group-create-request`. The host owns creation; the list keeps its current search until the host chooses to clear it. The demo clears search after adding the group. Both search and the group action use 44px targets for coarse pointers.
2728
2785
 
2729
2786
  The Sessions demo pairs the list with a selected-session workspace. Its controls step through starting, working, feedback, approval, completion and failure without timers or backend operations. Question and permission responses update the selected session; declining approval pauses it. The narrow layout switches between the preserved list and detail panes with an **All sessions** control that restores row focus.
2730
2787
 
2731
- Generic host resources use the exported `IHarnessResourceMeta` and `IHarnessResourceAssociation` contracts. Set `.resources`, `.resourceAssociations`, `.selectedResourceId`, and `.eligibleSessionIdsByResourceId`. Unattached resources render in a labelled **Resources** list; attached resources render as compact persistent child rows inside their exact session article and stay visible while session details are collapsed. Resource metadata is presentation-only: the list never mounts a terminal, browser, file viewer, or other runtime.
2788
+ Generic host resources use the exported `IHarnessResourceMeta` and `IHarnessResourceAssociation` contracts. Set `.resources`, `.resourceAssociations`, `.selectedResourceId`, and `.eligibleTargetsByResourceId`. Resource rows render as top-level rows in the ordered list, never as children of a session. Resource metadata is presentation-only: the list never mounts a terminal, browser, file viewer, or other runtime.
2789
+
2790
+ **Attaching is a separate explicit action, not a drop.** An association is an access grant: it shows as a decorative marker on the resource row (a link icon plus the target's title, resolved from `sessions` or `resources` regardless of search or collapse) and never moves the row. Dropping is purely positional and never attaches. The association stays single-valued — a resource has at most one target — and the target is any other item, so a resource may be attached to a session **or** to another resource such as a terminal:
2791
+
2792
+ ```typescript
2793
+ list.resourceAssociations = [
2794
+ { resourceId: 'file:notes', target: { kind: 'resource', id: 'terminal:build' } },
2795
+ ];
2796
+ ```
2797
+
2798
+ Association records are projected only when the resource exists, the target exists, the target is not the resource itself, and no earlier record claimed that resource. `eligibleTargetsByResourceId` restricts attach/reassign targets for host menus: a missing entry leaves the resource unrestricted, an empty array means no eligible target. Which resource kinds may host an attachment is the host's rule, not the component's.
2732
2799
 
2733
2800
  ```typescript
2734
2801
  import type {
@@ -2743,17 +2810,17 @@ list.resources = [
2743
2810
  { id: 'file:notes', kind: 'file', title: 'Release notes', preview: '6.7.0 draft' },
2744
2811
  ];
2745
2812
  list.resourceAssociations = [
2746
- { resourceId: 'shell:build', sessionId: 'session-1' },
2813
+ { resourceId: 'shell:build', target: { kind: 'session', id: 'session-1' } },
2747
2814
  ];
2748
- list.eligibleSessionIdsByResourceId = {
2749
- 'shell:build': ['session-1', 'session-2'],
2750
- 'file:notes': ['session-2'],
2815
+ list.eligibleTargetsByResourceId = {
2816
+ 'shell:build': [{ kind: 'session', id: 'session-1' }, { kind: 'session', id: 'session-2' }],
2817
+ 'file:notes': [{ kind: 'session', id: 'session-2' }, { kind: 'resource', id: 'shell:build' }],
2751
2818
  };
2752
2819
 
2753
2820
  list.addEventListener('harness-resource-association-request', async (event) => {
2754
2821
  const resourceEvent = event as CustomEvent<THarnessResourceAssociationRequestDetail>;
2755
- const { resource, toSessionId } = resourceEvent.detail;
2756
- await persistAssociation(resource.id, toSessionId);
2822
+ const { resource, toTarget } = resourceEvent.detail;
2823
+ await persistAssociation(resource.id, toTarget);
2757
2824
  list.resourceAssociations = await loadAssociations();
2758
2825
  });
2759
2826
 
@@ -2762,20 +2829,20 @@ list.addEventListener('harness-resource-context', (event) => {
2762
2829
  openHostMenu({
2763
2830
  x: resourceEvent.detail.clientX,
2764
2831
  y: resourceEvent.detail.clientY,
2765
- attach: (sessionId) => resourceEvent.detail.requestAssociation(sessionId),
2832
+ attach: (target) => resourceEvent.detail.requestAssociation(target),
2766
2833
  detach: () => resourceEvent.detail.requestAssociation(null),
2767
2834
  onClose: resourceEvent.detail.close,
2768
2835
  });
2769
2836
  });
2770
2837
  ```
2771
2838
 
2772
- `harness-resource-select` and `harness-resource-open` carry `{ resource, sessionId }`. `harness-resource-context` adds viewport coordinates, the pointer/Actions/keyboard source, the original event, `requestAssociation(toSessionId)`, and `close()` for focus restoration. The visible Actions button has menu disclosure semantics; native `ContextMenu` and `Shift+F10` invoke the same host-owned path. The component does not require `DeesContextmenu` and does not own menu choices.
2839
+ `harness-resource-select` and `harness-resource-open` carry `{ resource, target }`, where `target` is the controlled association ref or null. `harness-resource-context` adds viewport coordinates, the pointer/Actions/keyboard source, the original event, `requestAssociation(toTarget)`, and `close()` for focus restoration. The visible Actions button has menu disclosure semantics; native `ContextMenu` and `Shift+F10` invoke the same host-owned path. The component does not require `DeesContextmenu` and does not own menu choices.
2773
2840
 
2774
- All component-originated association request paths converge on the public `requestResourceAssociation(resourceId, toSessionId, source?)` method. A valid change emits one bubbling/composed `harness-resource-association-request` discriminated by `intent: 'attach' | 'detach' | 'reassign'` and carrying `resource`, `fromSessionId`, `toSessionId`, and `source: 'drop' | 'action' | 'keyboard'`. The method returns `false` and emits nothing for unknown resources/sessions, ineligible targets, detach from an unattached resource, or the current target. It never mutates resources, associations, selection, session groups, or session order.
2841
+ All component-originated association request paths converge on the public `requestResourceAssociation(resourceId, toTarget, source?)` method. A valid change emits one bubbling/composed `harness-resource-association-request` discriminated by `intent: 'attach' | 'detach' | 'reassign'` and carrying `resource`, `fromTarget`, `toTarget`, and `source: 'action' | 'keyboard'`. The method returns `false` and emits nothing for unknown resources, unknown or ineligible targets, a target that is the resource itself, detach from an unattached resource, or the current target. It never mutates resources, associations, selection, groups, or order.
2775
2842
 
2776
- Association projection is deterministic and non-destructive: resource records without a non-empty ID, kind, or title, unknown resource/session references, and later duplicate records for a resource are ignored; the first valid association wins. Duplicate resource metadata IDs likewise use the first resource. Missing eligibility entries mean no eligible attach/reassign target. Eligibility does not hide an existing authoritative association, which remains visible and detachable. Requested changes are announced separately from completion; completion is announced only after the controlled association input reflects the requested target.
2843
+ Association projection is deterministic and non-destructive: resource records without a non-empty ID, kind, or title, unknown resource/target references, self-targets, and later duplicate records for a resource are ignored; the first valid association wins. Duplicate resource metadata IDs likewise use the first resource. Eligibility does not hide an existing authoritative association, which remains visible and detachable. Requested changes are announced separately from completion; completion is announced only after the controlled association input reflects the requested target.
2777
2844
 
2778
- Search covers resource ID, kind, title, and preview. A matching attached resource reveals its parent session and only matching children; a matching session reveals all its resources; matching unattached resources remain in **Resources**. Resources render with zero sessions. Resource grip dragging edge-scrolls the list and targets only eligible whole session cards, including hits on nested card content, while never creating session reorder placeholders or emitting `harness-session-move`. Use the Actions/context path for keyboard and mobile association workflows; coarse-pointer grip and Actions targets are at least 44px.
2845
+ Search covers resource ID, kind, title, and preview, and matches sessions and resources as peers — a matching session no longer reveals its attached resources, and a matching resource no longer reveals its target. Resources render with zero sessions. Resource dragging edge-scrolls the list exactly like session dragging and emits only `harness-item-move`. Use the Actions/context path for keyboard and mobile association workflows; coarse-pointer grip and Actions targets are at least 44px.
2779
2846
 
2780
2847
  ---
2781
2848
 
@@ -2949,9 +3016,19 @@ and selecting a kit with `dees-input-multitoggle`. Its Review bag action places
2949
3016
  The library exports unified interfaces for consistent API patterns:
2950
3017
 
2951
3018
  ```typescript
3019
+ // Detail of the cancelable `dees-action-error` event (see Failed actions)
3020
+ interface IActionErrorDetail<TAction = unknown> {
3021
+ error: unknown; // whatever the action callback rejected with
3022
+ action: TAction; // the action descriptor whose callback rejected
3023
+ source: string; // where it ran, e.g. 'modal-menu' or 'table-row'
3024
+ }
3025
+
2952
3026
  // Base menu item interface (used by tabs, menus, etc.)
3027
+ // `key` is the stable identity: tab selection follows it across rerenders.
3028
+ // Give every tab a unique `key`: when it is absent the `label` is used, so two keyless tabs with the same label are the same tab.
2953
3029
  interface IMenuItem {
2954
3030
  key: string;
3031
+ label?: string; // display text; falls back to `key`
2955
3032
  iconName?: string;
2956
3033
  action: () => void;
2957
3034
  badge?: string | number;
@@ -3045,6 +3122,16 @@ module worker under the Apache License 2.0. The attribution and complete license
3045
3122
  provided in [third-party-notices.md](./third-party-notices.md) and copied into
3046
3123
  `dist_bundle/` for standalone bundle distributions.
3047
3124
 
3125
+ [third-party-notices.md](./third-party-notices.md) lists every library the package
3126
+ embeds in `dist_bundle/bundle.js` and every library it loads into the host page at
3127
+ runtime (ECharts, highlight.js, Lightweight Charts, Monaco, xterm). That list is
3128
+ generated from the built artifact and the installed packages — a dependency added,
3129
+ removed, or re-pinned without regenerating fails `pnpm test`:
3130
+
3131
+ ```sh
3132
+ node scripts/check-third-party-notices.mjs --write
3133
+ ```
3134
+
3048
3135
  ## License and Legal Information
3049
3136
 
3050
3137
  This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Third-party notices for everything this package ships or loads.
4
+ *
5
+ * The embedded list is derived from dist_bundle itself: esbuild keeps a
6
+ * `// node_modules/...` comment per embedded module, so the set of embedded
7
+ * packages is read off the built artifact instead of a hand-kept list that
8
+ * silently rots when a dependency is added, removed, or made lazy. License
9
+ * metadata is read from the installed packages, never transcribed here.
10
+ *
11
+ * Usage:
12
+ * node scripts/check-third-party-notices.mjs # verify (also covered by tests)
13
+ * node scripts/check-third-party-notices.mjs --write # regenerate the managed section
14
+ */
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ export const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ export const noticesPath = path.join(projectRoot, 'third-party-notices.md');
21
+ export const bundleDirectory = path.join(projectRoot, 'dist_bundle');
22
+ export const startMarker = '<!-- bundled-notices:start -->';
23
+ export const endMarker = '<!-- bundled-notices:end -->';
24
+
25
+ /**
26
+ * Libraries the catalog does not embed but pins and loads into the host page at
27
+ * runtime (the CDN modules behind DeesServiceLibLoader and dees-workspace-monaco).
28
+ * Nothing in the built artifact carries their code, so they are named here; their
29
+ * license metadata still comes from the installed package where one exists.
30
+ * Mirrors ts_web/services/versions.ts.
31
+ */
32
+ export const runtimeLoadedLibraries = [
33
+ { name: 'echarts' },
34
+ { name: 'highlight.js' },
35
+ { name: 'lightweight-charts' },
36
+ { name: 'monaco-editor' },
37
+ { name: 'xterm' },
38
+ { name: 'xterm-addon-fit' },
39
+ // Loaded by URL only; the catalog does not depend on it for types.
40
+ { name: 'xterm-addon-search', version: '0.13.0', license: 'MIT' },
41
+ ];
42
+
43
+ const listJsFiles = (directoryArg) => {
44
+ const files = [];
45
+ for (const entry of fs.readdirSync(directoryArg, { withFileTypes: true })) {
46
+ const entryPath = path.join(directoryArg, entry.name);
47
+ if (entry.isDirectory()) files.push(...listJsFiles(entryPath));
48
+ else if (entry.name.endsWith('.js')) files.push(entryPath);
49
+ }
50
+ return files;
51
+ };
52
+
53
+ /** The package directory a bundled module path belongs to. */
54
+ const packageDirectoryOf = (modulePathArg) => {
55
+ const marker = '/node_modules/';
56
+ const index = modulePathArg.lastIndexOf(marker);
57
+ if (index < 0) return null;
58
+ const rest = modulePathArg.slice(index + marker.length).split('/');
59
+ const name = rest[0].startsWith('@') ? `${rest[0]}/${rest[1]}` : rest[0];
60
+ if (!name || name.startsWith('.')) return null;
61
+ return modulePathArg.slice(0, index + marker.length) + name;
62
+ };
63
+
64
+ const readIfPresent = (directoryArg, matcherArg) => {
65
+ const entry = fs.readdirSync(directoryArg).find(matcherArg);
66
+ if (!entry) return null;
67
+ const text = fs.readFileSync(path.join(directoryArg, entry), 'utf8').replace(/\r\n/g, '\n').trim();
68
+ return text || null;
69
+ };
70
+
71
+ /** The license file that matches the elected license id, else the generic one. */
72
+ const readLicenseText = (packageDirectoryArg, licenseIdArg) => {
73
+ const entries = fs.readdirSync(packageDirectoryArg);
74
+ const exact = entries.find((entry) => entry.toLowerCase() === `license.${licenseIdArg.toLowerCase()}`);
75
+ const generic = entries.find((entry) => /^(licen[cs]e|copying)(\.(md|txt))?$/i.test(entry));
76
+ const chosen = exact ?? generic;
77
+ if (!chosen) return null;
78
+ const text = fs.readFileSync(path.join(packageDirectoryArg, chosen), 'utf8').replace(/\r\n/g, '\n').trim();
79
+ // A stub that only points at a sibling file is not the license text.
80
+ return text.length < 200 ? null : text;
81
+ };
82
+
83
+ /**
84
+ * A copyright notice, not prose that happens to contain the word: license bodies
85
+ * wrap sentences onto lines starting with a lowercase "copyright", and the Apache
86
+ * appendix carries a bracketed placeholder.
87
+ */
88
+ const isCopyrightLine = (lineArg) => /^\s*Copyright\b/.test(lineArg)
89
+ && !/\[yyyy\]|\[name of copyright owner\]/i.test(lineArg);
90
+
91
+ const copyrightLinesOf = (...textsArg) => [...new Set(
92
+ textsArg.filter(Boolean).flatMap((text) => text.split('\n'))
93
+ .filter(isCopyrightLine)
94
+ .map((line) => line.replace(/^\s*[-*#>]*\s*/, '').trim())
95
+ .filter((line) => line.length > 4)
96
+ )];
97
+
98
+ /** A dual license is consumed under its first option; that election is recorded. */
99
+ const electedLicense = (rawArg) => {
100
+ const raw = typeof rawArg === 'string' ? rawArg : rawArg?.type ?? 'UNKNOWN';
101
+ const options = raw.split(/\s+(?:or|OR)\s+|\//).map((option) => option.replace(/[()]/g, '').trim()).filter(Boolean);
102
+ return { elected: options[0] || 'UNKNOWN', raw, options };
103
+ };
104
+
105
+ const describePackage = (packageDirectoryArg, distributionArg) => {
106
+ const manifest = JSON.parse(fs.readFileSync(path.join(packageDirectoryArg, 'package.json'), 'utf8'));
107
+ const { elected, raw, options } = electedLicense(manifest.license ?? manifest.licenses?.[0]?.type);
108
+ const licenseText = readLicenseText(packageDirectoryArg, elected);
109
+ // Apache-2.0 section 4(d): a NOTICE file travels with the distribution.
110
+ const noticeText = readIfPresent(packageDirectoryArg, (entry) => /^notice(\.(md|txt))?$/i.test(entry));
111
+ return {
112
+ name: manifest.name,
113
+ version: manifest.version,
114
+ license: elected,
115
+ licenseRaw: raw,
116
+ dualLicensed: options.length > 1,
117
+ licenseText,
118
+ noticeText,
119
+ distribution: distributionArg,
120
+ copyrights: copyrightLinesOf(licenseText, noticeText),
121
+ };
122
+ };
123
+
124
+ const sortPackages = (entriesArg) => [...entriesArg].sort((a, b) =>
125
+ a.name.localeCompare(b.name, 'en') || a.version.localeCompare(b.version, 'en'));
126
+
127
+ /** Every npm package whose code is embedded in the built bundle. */
128
+ export const collectBundledPackages = () => {
129
+ if (!fs.existsSync(bundleDirectory)) {
130
+ throw new Error('dist_bundle is missing. Run pnpm build before checking third-party notices.');
131
+ }
132
+ const directories = new Set();
133
+ for (const file of listJsFiles(bundleDirectory)) {
134
+ const content = fs.readFileSync(file, 'utf8');
135
+ for (const match of content.matchAll(/^\/\/ (node_modules\/\S+)/gm)) {
136
+ const directory = packageDirectoryOf(match[1]);
137
+ if (directory) directories.add(directory);
138
+ }
139
+ }
140
+ const packages = new Map();
141
+ for (const directory of directories) {
142
+ const absolute = path.join(projectRoot, directory);
143
+ if (!fs.existsSync(path.join(absolute, 'package.json'))) {
144
+ throw new Error(`Bundled package without a manifest: ${directory}`);
145
+ }
146
+ const entry = describePackage(absolute, 'bundled');
147
+ packages.set(`${entry.name}@${entry.version}`, entry);
148
+ }
149
+ return sortPackages(packages.values());
150
+ };
151
+
152
+ /** The pinned libraries the catalog loads into the host page at runtime. */
153
+ export const collectRuntimePackages = () => runtimeLoadedLibraries.map((library) => {
154
+ const directory = path.join(projectRoot, 'node_modules', library.name);
155
+ if (fs.existsSync(path.join(directory, 'package.json'))) {
156
+ return describePackage(directory, 'runtime');
157
+ }
158
+ if (!library.version || !library.license) {
159
+ throw new Error(`Runtime library ${library.name} is not installed and declares no version or license`);
160
+ }
161
+ return {
162
+ name: library.name, version: library.version, license: library.license, licenseRaw: library.license,
163
+ dualLicensed: false, licenseText: null, noticeText: null, distribution: 'runtime', copyrights: [],
164
+ };
165
+ });
166
+
167
+ export const collectPackages = () => [...collectBundledPackages(), ...collectRuntimePackages()];
168
+
169
+ /**
170
+ * Identical license wording is printed once, with every copyright notice it
171
+ * covers. Two texts are the same terms when their operative part matches; the
172
+ * Apache appendix is instructions for applying the license, not a term, and
173
+ * packages ship it filled in, blank, or not at all. The longest text in a group
174
+ * is printed verbatim, so nothing is summarized away.
175
+ */
176
+ const groupByLicenseText = (packagesArg) => {
177
+ const groups = new Map();
178
+ for (const entry of packagesArg) {
179
+ const normalized = (entry.licenseText ?? '')
180
+ .split('\n').filter((line) => !isCopyrightLine(line)).join('\n')
181
+ .replace(/\s+/g, ' ').trim();
182
+ const terminator = normalized.indexOf('END OF TERMS AND CONDITIONS');
183
+ const key = `${entry.license} ${terminator < 0 ? normalized : normalized.slice(0, terminator)}`;
184
+ if (!groups.has(key)) groups.set(key, { license: entry.license, text: entry.licenseText, packages: [] });
185
+ const group = groups.get(key);
186
+ group.packages.push(entry);
187
+ if ((entry.licenseText?.length ?? 0) > (group.text?.length ?? 0)) group.text = entry.licenseText;
188
+ }
189
+ return [...groups.values()].sort((a, b) =>
190
+ a.license.localeCompare(b.license, 'en') || a.packages[0].name.localeCompare(b.packages[0].name, 'en'));
191
+ };
192
+
193
+ const listEntry = (entryArg) => `- ${entryArg.name} ${entryArg.version} — `
194
+ + (entryArg.dualLicensed ? `${entryArg.licenseRaw}, used here under ${entryArg.license}` : entryArg.license);
195
+
196
+ export const renderManagedSection = (packagesArg) => {
197
+ const bundled = packagesArg.filter((entry) => entry.distribution === 'bundled');
198
+ const runtime = packagesArg.filter((entry) => entry.distribution === 'runtime');
199
+ const lines = [startMarker, '', '## Third-party libraries', ''];
200
+ lines.push('This list is generated from the built artifact and the installed packages by');
201
+ lines.push('`scripts/check-third-party-notices.mjs`. License terms follow below, with identical');
202
+ lines.push('wording reproduced once and every copyright notice it covers listed with it.', '');
203
+ lines.push('### Embedded in the bundle', '');
204
+ lines.push('`dist_bundle/bundle.js` and its chunks contain the code of these packages.', '');
205
+ for (const entry of bundled) lines.push(listEntry(entry));
206
+ lines.push('', '### Loaded at runtime', '');
207
+ lines.push('These are not embedded. The catalog pins their versions and loads them into the host');
208
+ lines.push('page on demand, so their notices are reproduced here as well.', '');
209
+ for (const entry of runtime) lines.push(listEntry(entry));
210
+ lines.push('', '## License terms', '');
211
+ for (const group of groupByLicenseText(packagesArg)) {
212
+ const names = group.packages.map((entry) => `${entry.name} ${entry.version}`).join(', ');
213
+ const copyrights = [...new Set(group.packages.flatMap((entry) => entry.copyrights))].sort();
214
+ const notices = [...new Set(group.packages.filter((entry) => entry.noticeText).map((entry) => entry.noticeText))];
215
+ lines.push(`### ${group.license} — ${group.packages.length === 1 ? group.packages[0].name : `${group.packages.length} packages`}`, '');
216
+ lines.push(`Applies to: ${names}.`, '');
217
+ if (copyrights.length) {
218
+ lines.push('Copyright notices:', '');
219
+ for (const copyright of copyrights) lines.push(`- ${copyright}`);
220
+ lines.push('');
221
+ }
222
+ for (const notice of notices) lines.push('Attribution notice:', '', '```text', notice, '```', '');
223
+ if (group.text) {
224
+ // Verbatim, as published by the package: a license text is not ours to edit.
225
+ lines.push('```text', group.text, '```', '');
226
+ } else {
227
+ lines.push(`These packages publish no license file; they are distributed under ${group.license}.`, '');
228
+ }
229
+ }
230
+ lines.push(endMarker);
231
+ return lines.join('\n');
232
+ };
233
+
234
+ export const readNotices = () => fs.readFileSync(noticesPath, 'utf8');
235
+
236
+ export const renderNotices = (currentArg, packagesArg) => {
237
+ const first = currentArg.indexOf(startMarker);
238
+ const last = currentArg.indexOf(endMarker);
239
+ if (first < 0 || last < first) {
240
+ throw new Error('third-party-notices.md is missing the bundled-notices markers');
241
+ }
242
+ return currentArg.slice(0, first) + renderManagedSection(packagesArg) + currentArg.slice(last + endMarker.length);
243
+ };
244
+
245
+ const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
246
+ if (isCli) {
247
+ const packages = collectPackages();
248
+ const current = readNotices();
249
+ const generated = renderNotices(current, packages);
250
+ const counts = `${packages.filter((entry) => entry.distribution === 'bundled').length} bundled`
251
+ + ` and ${packages.filter((entry) => entry.distribution === 'runtime').length} runtime libraries`;
252
+ if (process.argv.includes('--write')) {
253
+ fs.writeFileSync(noticesPath, generated);
254
+ console.log(`[third-party-notices] wrote ${counts}`);
255
+ } else if (current !== generated) {
256
+ console.error('[third-party-notices] FAIL — the notices are stale. Run node scripts/check-third-party-notices.mjs --write');
257
+ process.exitCode = 1;
258
+ } else {
259
+ console.log(`[third-party-notices] OK — ${counts} listed`);
260
+ }
261
+ }