@design.estate/dees-catalog 6.5.0 → 6.7.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.
@@ -280,6 +280,62 @@ export interface IHarnessSessionMeta {
280
280
  /** Set false for pinned, non-sortable entries such as terminals. Defaults to true. */
281
281
  draggable?: boolean;
282
282
  }
283
+ /** Host-owned metadata for a resource that can be associated with a harness session. */
284
+ export interface IHarnessResourceMeta<TState extends string = string> {
285
+ /** Stable identity used by selection and association records. */
286
+ id: string;
287
+ /** Open resource kind supplied by the host, for example `terminal`, `file`, or `browser`. */
288
+ kind: string;
289
+ title: string;
290
+ /** Optional dees-icon name (e.g. `lucide:FileText`). */
291
+ icon?: string;
292
+ preview?: string;
293
+ createdAt?: number;
294
+ updatedAt?: number;
295
+ /** Optional host-defined display state. */
296
+ state?: TState;
297
+ }
298
+ /** A controlled zero-or-one session association for one resource. */
299
+ export interface IHarnessResourceAssociation {
300
+ resourceId: string;
301
+ sessionId: string;
302
+ }
303
+ export interface IHarnessResourceEventDetail<TState extends string = string> {
304
+ resource: IHarnessResourceMeta<TState>;
305
+ sessionId: string | null;
306
+ }
307
+ export type THarnessResourceAssociationInteractionSource = 'drop' | 'action' | 'keyboard';
308
+ export type THarnessResourceAssociationRequestDetail<TState extends string = string> = {
309
+ intent: 'attach';
310
+ resource: IHarnessResourceMeta<TState>;
311
+ fromSessionId: null;
312
+ toSessionId: string;
313
+ source: THarnessResourceAssociationInteractionSource;
314
+ } | {
315
+ intent: 'detach';
316
+ resource: IHarnessResourceMeta<TState>;
317
+ fromSessionId: string;
318
+ toSessionId: null;
319
+ source: THarnessResourceAssociationInteractionSource;
320
+ } | {
321
+ intent: 'reassign';
322
+ resource: IHarnessResourceMeta<TState>;
323
+ fromSessionId: string;
324
+ toSessionId: string;
325
+ source: THarnessResourceAssociationInteractionSource;
326
+ };
327
+ export type THarnessResourceContextSource = 'pointer' | 'action' | 'keyboard';
328
+ /** Detail of `harness-resource-context`; the host owns the actual menu. */
329
+ export interface IHarnessResourceContextDetail<TState extends string = string> extends IHarnessResourceEventDetail<TState> {
330
+ clientX: number;
331
+ clientY: number;
332
+ source: THarnessResourceContextSource;
333
+ originalEvent: MouseEvent | KeyboardEvent;
334
+ /** Routes a host menu choice through the component's validated controlled request event. */
335
+ requestAssociation: (toSessionId: string | null) => boolean;
336
+ /** Closes the external-menu disclosure state and restores focus to its invoking control. */
337
+ close: () => void;
338
+ }
283
339
  /** Detail of `harness-session-context`, fired on right-click of a session item. */
284
340
  export interface IHarnessSessionContextDetail {
285
341
  session: IHarnessSessionMeta;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "6.5.0",
3
+ "version": "6.7.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",
package/readme.hints.md CHANGED
@@ -5,6 +5,9 @@
5
5
 
6
6
  ## Agent Chat Architecture
7
7
 
8
+ - `DeesHarnessSessionList` resource associations are controlled projections. Resources and associations remain host-owned; component-originated attach, detach, and reassign requests use `requestResourceAssociation()` and reconcile only when fresh `resourceAssociations` arrive.
9
+ - Resource association normalization accepts the first valid record per known resource and ignores incomplete metadata, unknown references, or later duplicate records without mutating inputs. A missing `eligibleSessionIdsByResourceId` entry means no attach/reassign target; existing authoritative associations still render and can detach.
10
+ - Resource pointer dragging is a separate gesture from session grip reordering. It targets an eligible whole session article, never creates a session drop slot, and never emits `harness-session-move`.
8
11
  - `DeesHarnessMessageList` owns transcript ordering and responsive tool grouping. Consecutive tools use one generic keyed-grid pipeline partitioned by descriptor layout: compact with compact, subtask with subtask, and full-row tools spanning every column. Never move cards between active and historical owners when status changes.
9
12
  - The transcript scroll box is the named inline-size container. Tool columns must respond to its content width so opening the session panel naturally reduces the column count.
10
13
  - Group identity is retained through front-window eviction while any card remains. Untouched projected subtasks collapse individually on completion without adding a second grid-level disclosure; other tools keep descriptor or user disclosure state.
package/readme.md CHANGED
@@ -1882,7 +1882,7 @@ A question the agent asks mid-run: option buttons with descriptions (single-sele
1882
1882
 
1883
1883
  #### `DeesHarnessComposer`
1884
1884
 
1885
- Prompt input: autogrow textarea, attachment picker/paste/drag-drop with per-file (10 MB) and total (30 MB) limits, image/text/binary classification, optional account, model, and reasoning-effort dropdowns, Cmd/Ctrl+Enter to send. Controlled component — the host owns `value`, `attachments`, `account`, and the other selections. Set `accountOptions` to `{ label, value }[]` so the dropdown displays readable labels while events retain opaque stable account values. Account changes emit `harness-account-change` with `{ account }`; `harness-send` includes optional `account` alongside `model` and `reasoningEffort`.
1885
+ Prompt input: autogrow textarea, attachment picker/paste/drag-drop with configurable `maxAttachmentCount`, `maxAttachmentBytes` (10 MB default), and `maxTotalAttachmentBytes` (30 MB default), image/text/binary classification, optional account, model, and reasoning-effort dropdowns, Cmd/Ctrl+Enter to send. Attachment count is unlimited unless the host sets it. `dees-harness-chat` forwards all three limits to its composer. Controlled component — the host owns `value`, `attachments`, `account`, and the other selections. Set `accountOptions` to `{ label, value }[]` so the dropdown displays readable labels while events retain opaque stable account values. Account changes emit `harness-account-change` with `{ account }`; `harness-send` includes optional `account` alongside `model` and `reasoningEffort`.
1886
1886
 
1887
1887
  While `busy`, Stop renders as an additional button and Send stays available so hosts can queue messages. With an empty prompt and `queuedCount > 0`, the primary action becomes **Steer now** (`harness-steer` event) to force the agent to continue with the queued messages immediately.
1888
1888
 
@@ -1912,6 +1912,55 @@ Searchable, card-based conversation column for sidebars or a `DeesModal` (`harne
1912
1912
 
1913
1913
  `IHarnessSessionMeta.state` accepts `normal`, `working`, `finished`, `attention`, or `error`; `working` remains an independent activity signal and does not change card expansion. 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.
1914
1914
 
1915
+ 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.
1916
+
1917
+ ```typescript
1918
+ import type {
1919
+ IHarnessResourceContextDetail,
1920
+ THarnessResourceAssociationRequestDetail,
1921
+ } from '@design.estate/dees-catalog';
1922
+
1923
+ const list = document.querySelector('dees-harness-session-list')!;
1924
+
1925
+ list.resources = [
1926
+ { id: 'shell:build', kind: 'terminal', title: 'Build shell', icon: 'lucide:Terminal' },
1927
+ { id: 'file:notes', kind: 'file', title: 'Release notes', preview: '6.7.0 draft' },
1928
+ ];
1929
+ list.resourceAssociations = [
1930
+ { resourceId: 'shell:build', sessionId: 'session-1' },
1931
+ ];
1932
+ list.eligibleSessionIdsByResourceId = {
1933
+ 'shell:build': ['session-1', 'session-2'],
1934
+ 'file:notes': ['session-2'],
1935
+ };
1936
+
1937
+ list.addEventListener('harness-resource-association-request', async (event) => {
1938
+ const resourceEvent = event as CustomEvent<THarnessResourceAssociationRequestDetail>;
1939
+ const { resource, toSessionId } = resourceEvent.detail;
1940
+ await persistAssociation(resource.id, toSessionId);
1941
+ list.resourceAssociations = await loadAssociations();
1942
+ });
1943
+
1944
+ list.addEventListener('harness-resource-context', (event) => {
1945
+ const resourceEvent = event as CustomEvent<IHarnessResourceContextDetail>;
1946
+ openHostMenu({
1947
+ x: resourceEvent.detail.clientX,
1948
+ y: resourceEvent.detail.clientY,
1949
+ attach: (sessionId) => resourceEvent.detail.requestAssociation(sessionId),
1950
+ detach: () => resourceEvent.detail.requestAssociation(null),
1951
+ onClose: resourceEvent.detail.close,
1952
+ });
1953
+ });
1954
+ ```
1955
+
1956
+ `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.
1957
+
1958
+ 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.
1959
+
1960
+ 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.
1961
+
1962
+ 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.
1963
+
1915
1964
  ---
1916
1965
 
1917
1966
  ### Pre-built Templates
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '6.5.0',
6
+ version: '6.7.0',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -25,7 +25,11 @@ import type {
25
25
  IHarnessUsage,
26
26
  THarnessStreamDelta,
27
27
  } from '../interfaces.js';
28
- import { harnessParseTodos } from '../harness.helpers.js';
28
+ import {
29
+ harnessMaxAttachmentBytes,
30
+ harnessMaxTotalAttachmentBytes,
31
+ harnessParseTodos,
32
+ } from '../harness.helpers.js';
29
33
  import { DeesHarnessToolRegistry } from '../toolregistry.js';
30
34
  import { DeesHarnessMessageList } from '../dees-harness-message-list/dees-harness-message-list.js';
31
35
  import { DeesHarnessComposer } from '../dees-harness-composer/dees-harness-composer.js';
@@ -150,6 +154,15 @@ export class DeesHarnessChat extends DeesElement {
150
154
  @property({ attribute: false })
151
155
  accessor attachments: IHarnessAttachment[] = [];
152
156
 
157
+ @property({ type: Number })
158
+ accessor maxAttachmentCount: number = Number.POSITIVE_INFINITY;
159
+
160
+ @property({ type: Number })
161
+ accessor maxAttachmentBytes: number = harnessMaxAttachmentBytes;
162
+
163
+ @property({ type: Number })
164
+ accessor maxTotalAttachmentBytes: number = harnessMaxTotalAttachmentBytes;
165
+
153
166
  @property()
154
167
  accessor account: string = '';
155
168
 
@@ -646,6 +659,9 @@ export class DeesHarnessChat extends DeesElement {
646
659
  docked
647
660
  .value=${this.composerValue}
648
661
  .attachments=${this.attachments}
662
+ .maxAttachmentCount=${this.maxAttachmentCount}
663
+ .maxAttachmentBytes=${this.maxAttachmentBytes}
664
+ .maxTotalAttachmentBytes=${this.maxTotalAttachmentBytes}
649
665
  .disabled=${this.disabled}
650
666
  .busy=${this.busy || this.status.type === 'busy'}
651
667
  .queuedCount=${this.queuedCount}
@@ -97,6 +97,9 @@ export class DeesHarnessComposer extends DeesElement {
97
97
  @property({ type: Number })
98
98
  accessor maxTotalAttachmentBytes: number = harnessMaxTotalAttachmentBytes;
99
99
 
100
+ @property({ type: Number })
101
+ accessor maxAttachmentCount: number = Number.POSITIVE_INFINITY;
102
+
100
103
  @property()
101
104
  accessor account: string = '';
102
105
 
@@ -598,12 +601,26 @@ export class DeesHarnessComposer extends DeesElement {
598
601
  void this.addFiles(files);
599
602
  };
600
603
 
601
- public async addFiles(fileList: FileList | File[] | null): Promise<void> {
604
+ private attachmentAdditionTail = Promise.resolve();
605
+
606
+ public addFiles(fileList: FileList | File[] | null): Promise<void> {
602
607
  const files = Array.from(fileList ?? []);
603
- if (!files.length) return;
608
+ if (!files.length) return Promise.resolve();
609
+ const addition = this.attachmentAdditionTail.then(() => this.addFilesSequential(files));
610
+ this.attachmentAdditionTail = addition.catch(() => undefined);
611
+ return addition;
612
+ }
613
+
614
+ private async addFilesSequential(files: File[]): Promise<void> {
604
615
  const added: IHarnessAttachment[] = [];
605
616
  let totalBytes = this.attachments.reduce((sum, attachment) => sum + attachment.size, 0);
606
617
  for (const file of files) {
618
+ if (this.attachments.length + added.length >= this.maxAttachmentCount) {
619
+ this.emit('harness-attachment-error', {
620
+ message: `A maximum of ${this.maxAttachmentCount} attachments is allowed.`,
621
+ });
622
+ break;
623
+ }
607
624
  const mediaType = harnessMediaTypeForFile(file);
608
625
  if (file.size > this.maxAttachmentBytes) {
609
626
  this.emit('harness-attachment-error', {
@@ -1,8 +1,13 @@
1
1
  import { html } from '@design.estate/dees-element';
2
2
  import type {
3
+ IHarnessResourceAssociation,
4
+ IHarnessResourceContextDetail,
5
+ IHarnessResourceEventDetail,
6
+ IHarnessResourceMeta,
3
7
  IHarnessSessionGroup,
4
8
  IHarnessSessionMeta,
5
9
  IHarnessSessionMoveDetail,
10
+ THarnessResourceAssociationRequestDetail,
6
11
  } from '../interfaces.js';
7
12
 
8
13
  const demoSessions: IHarnessSessionMeta[] = Array.from({ length: 24 }, (_, index) => ({
@@ -28,8 +33,48 @@ const demoGroups: IHarnessSessionGroup[] = [
28
33
  { id: 'review', name: 'Review', sessionIds: ['session-3', 'session-4'] },
29
34
  ];
30
35
 
36
+ const demoResources: IHarnessResourceMeta[] = [
37
+ {
38
+ id: 'terminal:build',
39
+ kind: 'terminal',
40
+ title: 'Build shell',
41
+ icon: 'lucide:Terminal',
42
+ preview: 'pnpm run build',
43
+ updatedAt: Date.now() - 90_000,
44
+ state: 'ready',
45
+ },
46
+ {
47
+ id: 'file:release-notes',
48
+ kind: 'file',
49
+ title: 'Release notes',
50
+ icon: 'lucide:FileText',
51
+ preview: '6.7.0 controlled resource draft',
52
+ updatedAt: Date.now() - 180_000,
53
+ },
54
+ {
55
+ id: 'browser:preview',
56
+ kind: 'browser',
57
+ title: 'Documentation preview',
58
+ icon: 'lucide:Globe',
59
+ preview: 'localhost:3000',
60
+ updatedAt: Date.now() - 240_000,
61
+ },
62
+ ];
63
+
64
+ const demoResourceAssociations: IHarnessResourceAssociation[] = [
65
+ { resourceId: 'terminal:build', sessionId: 'session-0' },
66
+ { resourceId: 'file:release-notes', sessionId: 'session-0' },
67
+ ];
68
+
69
+ const demoResourceEligibleSessionIds = Object.fromEntries(
70
+ demoResources.map((resource) => [resource.id, ['session-0', 'session-1', 'session-2']]),
71
+ );
72
+
31
73
  type TDemoSessionList = HTMLElement & {
32
74
  groups: IHarnessSessionGroup[];
75
+ resources: IHarnessResourceMeta[];
76
+ resourceAssociations: IHarnessResourceAssociation[];
77
+ selectedResourceId: string;
33
78
  selectedSessionId: string;
34
79
  ungroupedSessionIds: string[];
35
80
  };
@@ -57,6 +102,72 @@ const handleSessionMove = (event: CustomEvent<IHarnessSessionMoveDetail>): void
57
102
  list.ungroupedSessionIds = ungroupedSessionIds;
58
103
  };
59
104
 
105
+ const handleResourceAssociation = (
106
+ event: CustomEvent<THarnessResourceAssociationRequestDetail>,
107
+ ): void => {
108
+ const list = event.currentTarget as TDemoSessionList;
109
+ const next = list.resourceAssociations.filter(
110
+ (association) => association.resourceId !== event.detail.resource.id,
111
+ );
112
+ if (event.detail.toSessionId) {
113
+ next.push({
114
+ resourceId: event.detail.resource.id,
115
+ sessionId: event.detail.toSessionId,
116
+ });
117
+ }
118
+ list.resourceAssociations = next;
119
+ };
120
+
121
+ const handleResourceContext = async (
122
+ event: CustomEvent<IHarnessResourceContextDetail>,
123
+ ): Promise<void> => {
124
+ const list = event.currentTarget as TDemoSessionList;
125
+ const { DeesContextmenu } = await import('../../00group-overlay/dees-contextmenu/dees-contextmenu.js');
126
+ const currentSessionId = event.detail.sessionId;
127
+ const eligibleSessionIds = ['session-0', 'session-1', 'session-2'];
128
+ const menu = await DeesContextmenu.openContextMenuWithOptions(
129
+ new MouseEvent('contextmenu', {
130
+ clientX: event.detail.clientX,
131
+ clientY: event.detail.clientY,
132
+ }),
133
+ [
134
+ ...eligibleSessionIds
135
+ .filter((sessionId) => sessionId !== currentSessionId)
136
+ .map((sessionId) => ({
137
+ name: `${currentSessionId ? 'Reassign' : 'Attach'} to ${
138
+ demoSessions.find((session) => session.id === sessionId)?.title
139
+ }`,
140
+ iconName: 'lucide:Link',
141
+ action: async () => {
142
+ event.detail.requestAssociation(sessionId);
143
+ event.detail.close();
144
+ },
145
+ })),
146
+ ...(currentSessionId
147
+ ? [{
148
+ name: 'Detach',
149
+ iconName: 'lucide:Unlink',
150
+ action: async () => {
151
+ event.detail.requestAssociation(null);
152
+ event.detail.close();
153
+ },
154
+ }]
155
+ : []),
156
+ ],
157
+ );
158
+ if (!menu) {
159
+ event.detail.close();
160
+ } else {
161
+ const observer = new MutationObserver(() => {
162
+ if (menu.isConnected) return;
163
+ observer.disconnect();
164
+ event.detail.close();
165
+ });
166
+ observer.observe(document.body, { childList: true });
167
+ }
168
+ list.selectedResourceId = event.detail.resource.id;
169
+ };
170
+
60
171
  const openModal = async () => {
61
172
  const { DeesModal } = await import('../../00group-overlay/dees-modal/dees-modal.js');
62
173
  const content = document.createElement('dees-harness-session-list');
@@ -95,15 +206,26 @@ export const demoFunc = () => html`
95
206
  </style>
96
207
  <div class="sessionDemo">
97
208
  <dees-button @clicked=${openModal}>Open in a DeesModal</dees-button>
98
- <div class="demoHint">Drag the grip to reorder conversations or move them between groups.</div>
209
+ <div class="demoHint">
210
+ Use a conversation grip to reorder sessions. Use a resource grip to associate it with a
211
+ session, or open Actions with click, ContextMenu, or Shift+F10 for attach, reassign, and detach.
212
+ </div>
99
213
  <dees-harness-session-list
100
214
  .sessions=${demoSessions}
101
215
  .groups=${demoGroups}
102
216
  .ungroupedSessionIds=${demoSessions.slice(5).map((session) => session.id)}
217
+ .resources=${demoResources}
218
+ .resourceAssociations=${demoResourceAssociations}
219
+ .eligibleSessionIdsByResourceId=${demoResourceEligibleSessionIds}
103
220
  .enableGrouping=${true}
104
221
  selectedSessionId="session-2"
105
222
  @harness-session-select=${handleSessionSelect}
106
223
  @harness-session-move=${handleSessionMove}
224
+ @harness-resource-select=${(event: CustomEvent<IHarnessResourceEventDetail>) => {
225
+ (event.currentTarget as TDemoSessionList).selectedResourceId = event.detail.resource.id;
226
+ }}
227
+ @harness-resource-context=${handleResourceContext}
228
+ @harness-resource-association-request=${handleResourceAssociation}
107
229
  ></dees-harness-session-list>
108
230
  </div>
109
231
  `;