@design.estate/dees-catalog 10.0.0 → 11.1.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.
- package/dist_bundle/bundle.js +914 -780
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.demo.js +24 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.js +13 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +62 -48
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +97 -57
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +541 -666
- package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +19 -1
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +45 -6
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +184 -27
- package/dist_ts_web/elements/00group-harness/interfaces.d.ts +59 -20
- package/dist_ts_web/services/DeesServiceLibLoader.d.ts +7 -0
- package/dist_ts_web/services/DeesServiceLibLoader.js +45 -22
- package/package.json +1 -2
- package/readme.md +37 -17
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.demo.ts +23 -0
- package/ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.ts +12 -0
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +107 -55
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +622 -756
- package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +18 -0
- package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +196 -24
- package/ts_web/elements/00group-harness/interfaces.ts +63 -19
- package/ts_web/services/DeesServiceLibLoader.ts +150 -102
- package/.smartconfig.json +0 -86
|
@@ -131,7 +131,21 @@ export class DeesHarnessToolCard extends DeesElement {
|
|
|
131
131
|
:host {
|
|
132
132
|
display: block;
|
|
133
133
|
container-type: inline-size;
|
|
134
|
+
margin-inline: auto;
|
|
135
|
+
max-width: 100%;
|
|
134
136
|
font-family: var(--dees-font-family);
|
|
137
|
+
transition: max-width var(--dees-transition-default) var(--dees-ease-standard);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* A collapsed card may be held to a narrower measure than its row. The host — not the inner
|
|
142
|
+
* surface — carries the constraint so the card's own inline-size container keeps measuring
|
|
143
|
+
* what is actually rendered. The width comes from the context the card sits in: a transcript
|
|
144
|
+
* row that holds this card alone sets it, a multi-card grid cell leaves it unset, and a
|
|
145
|
+
* direct host falls back to the full row.
|
|
146
|
+
*/
|
|
147
|
+
:host([data-collapsed]) {
|
|
148
|
+
max-width: var(--dees-harness-collapsed-tool-width, 100%);
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
.harness-card.subtaskPreviewCard {
|
|
@@ -1006,6 +1020,10 @@ export class DeesHarnessToolCard extends DeesElement {
|
|
|
1006
1020
|
|
|
1007
1021
|
public updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
1008
1022
|
super.updated(changedProperties);
|
|
1023
|
+
// Mirrors the rendered disclosure state onto the host so the width rule can follow it. Set
|
|
1024
|
+
// during the update that produced the render, so a card that mounts collapsed reaches its
|
|
1025
|
+
// first style resolution already constrained and never animates on mount.
|
|
1026
|
+
this.toggleAttribute('data-collapsed', !this.isOpen);
|
|
1009
1027
|
if (!this.isConnected) return;
|
|
1010
1028
|
this.terminalScrollHydrationFrame ??= requestAnimationFrame(() => {
|
|
1011
1029
|
this.terminalScrollHydrationFrame = null;
|
|
@@ -29,13 +29,46 @@ export interface ITerminalResizeDetail {
|
|
|
29
29
|
cols: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Detail of `terminal-error`: xterm could not be loaded, or the live terminal
|
|
34
|
+
* refused output. The element stays usable and keeps buffering.
|
|
35
|
+
*/
|
|
36
|
+
export interface ITerminalErrorDetail {
|
|
37
|
+
error: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Upper bound on output buffered while no terminal is live, in bytes.
|
|
42
|
+
* `Uint8Array` chunks count their `byteLength`, strings their UTF-16 memory
|
|
43
|
+
* (two bytes per code unit). Beyond the bound the oldest chunks are dropped,
|
|
44
|
+
* and the flush then emits RIS before the survivors so a truncated prefix is
|
|
45
|
+
* never rendered as if it were contiguous output.
|
|
46
|
+
*/
|
|
47
|
+
export const maxPendingWriteBytes = 8 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* RIS (ESC c), a hard reset carried *inside* the write stream: xterm parses it
|
|
51
|
+
* in order and routes it to the very same `Terminal.reset()` (InputHandler's
|
|
52
|
+
* ESC-c handler calls `fullReset()`, which fires `onRequestReset`, which
|
|
53
|
+
* `Terminal` handles with `reset()`), whereas calling `reset()` directly is
|
|
54
|
+
* documented as out of band with the pending write queue.
|
|
55
|
+
*/
|
|
56
|
+
const RIS = '\x1bc';
|
|
57
|
+
|
|
58
|
+
/** Memory cost of one buffered chunk, in bytes. */
|
|
59
|
+
const chunkByteLength = (chunkArg: string | Uint8Array): number =>
|
|
60
|
+
typeof chunkArg === 'string' ? chunkArg.length * 2 : chunkArg.byteLength;
|
|
61
|
+
|
|
32
62
|
/**
|
|
33
63
|
* Transport-agnostic xterm.js surface: the host pipes remote PTY output into
|
|
34
64
|
* `write()` and receives keystrokes via `terminal-input` — no process,
|
|
35
65
|
* runtime, or socket coupling. `terminal-resize` reports the grid size after
|
|
36
|
-
* every refit so the host can negotiate a PTY size.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
66
|
+
* every refit so the host can negotiate a PTY size. Output that arrives while
|
|
67
|
+
* no terminal is live is buffered up to `maxPendingWriteBytes` and replayed in
|
|
68
|
+
* order on the next connection, so a replay may start before the element is
|
|
69
|
+
* ready and survives being detached. xterm loads through DeesServiceLibLoader;
|
|
70
|
+
* CSP-locked hosts can pre-register bundled modules with
|
|
71
|
+
* `provideXtermModules()` to avoid the CDN.
|
|
39
72
|
*/
|
|
40
73
|
@customElement('dees-terminal-view')
|
|
41
74
|
export class DeesTerminalView extends DeesElement {
|
|
@@ -54,9 +87,37 @@ export class DeesTerminalView extends DeesElement {
|
|
|
54
87
|
private resizeObserver: ResizeObserver | undefined;
|
|
55
88
|
private inputDisposable: { dispose(): void } | undefined;
|
|
56
89
|
private lastReportedSize = '';
|
|
57
|
-
// Output that arrives before xterm
|
|
90
|
+
// Output that arrives before xterm is live is replayed in order, bounded by
|
|
91
|
+
// maxPendingWriteBytes and retained across a disconnect.
|
|
58
92
|
private pendingWrites: Array<string | Uint8Array> = [];
|
|
93
|
+
private pendingWriteBytes = 0;
|
|
94
|
+
private pendingWritesDropped = false;
|
|
59
95
|
private setupPromise: Promise<void> | undefined;
|
|
96
|
+
private readyPending = true;
|
|
97
|
+
private readyResolve: () => void = () => undefined;
|
|
98
|
+
private readyReject: (reasonArg: unknown) => void = () => undefined;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolves once the terminal of the current connection is live; a fresh
|
|
102
|
+
* promise is created for every connection. It rejects with the load error
|
|
103
|
+
* when xterm cannot be loaded (`terminal-error` carries the same error), and
|
|
104
|
+
* when the element is disconnected before the terminal came up.
|
|
105
|
+
*/
|
|
106
|
+
public ready: Promise<void>;
|
|
107
|
+
|
|
108
|
+
constructor() {
|
|
109
|
+
super();
|
|
110
|
+
this.ready = this.createReadyPromise();
|
|
111
|
+
// The house lifecycle: startup runs on every connection with an abort
|
|
112
|
+
// signal, garbage on every disconnection, so a reattached element is a
|
|
113
|
+
// live surface again instead of a permanently dead one.
|
|
114
|
+
this.registerStartupFunction(async (signalArg) => {
|
|
115
|
+
await this.ensureTerminal(signalArg);
|
|
116
|
+
});
|
|
117
|
+
this.registerGarbageFunction(() => {
|
|
118
|
+
this.teardownTerminal();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
60
121
|
|
|
61
122
|
public static styles = [
|
|
62
123
|
themeDefaultStyles,
|
|
@@ -209,43 +270,133 @@ export class DeesTerminalView extends DeesElement {
|
|
|
209
270
|
return html`<div class="terminalHost"></div>`;
|
|
210
271
|
}
|
|
211
272
|
|
|
212
|
-
public
|
|
213
|
-
|
|
214
|
-
|
|
273
|
+
public connectedCallback(): Promise<void> {
|
|
274
|
+
// A consumer awaiting readiness after reattaching must not be handed the
|
|
275
|
+
// settled promise of the previous connection.
|
|
276
|
+
if (!this.readyPending) this.ready = this.createReadyPromise();
|
|
277
|
+
return super.connectedCallback();
|
|
215
278
|
}
|
|
216
279
|
|
|
217
280
|
/** Feed remote output into the terminal; safe to call before setup finishes. */
|
|
218
281
|
public write(dataArg: string | Uint8Array): void {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
282
|
+
// While detached the terminal is about to be disposed, so output belongs in
|
|
283
|
+
// the buffer rather than in a surface nobody will see again.
|
|
284
|
+
if (this.terminal && this.isConnected) {
|
|
285
|
+
this.writeToTerminal(this.terminal, dataArg);
|
|
286
|
+
return;
|
|
223
287
|
}
|
|
288
|
+
this.bufferWrite(dataArg);
|
|
224
289
|
}
|
|
225
290
|
|
|
226
291
|
/** Reset the screen and scrollback, e.g. before a full replay. */
|
|
227
292
|
public clear(): void {
|
|
228
|
-
|
|
229
|
-
this.
|
|
293
|
+
const terminal = this.terminal;
|
|
294
|
+
if (terminal && this.isConnected) {
|
|
295
|
+
this.writeToTerminal(terminal, RIS);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Without a terminal the reset is expressed as a buffered sentinel, so the
|
|
299
|
+
// flush of the next connection still performs it in the right order.
|
|
300
|
+
this.pendingWrites = [RIS];
|
|
301
|
+
this.pendingWriteBytes = chunkByteLength(RIS);
|
|
302
|
+
this.pendingWritesDropped = false;
|
|
230
303
|
}
|
|
231
304
|
|
|
232
305
|
public focus(): void {
|
|
233
306
|
this.terminal?.focus();
|
|
234
307
|
}
|
|
235
308
|
|
|
236
|
-
/** Current grid size, or undefined while
|
|
309
|
+
/** Current grid size, or undefined while no terminal is live. */
|
|
237
310
|
public get currentSize(): ITerminalResizeDetail | undefined {
|
|
238
311
|
if (!this.terminal) return undefined;
|
|
239
312
|
return { rows: this.terminal.rows, cols: this.terminal.cols };
|
|
240
313
|
}
|
|
241
314
|
|
|
242
|
-
private
|
|
315
|
+
private createReadyPromise(): Promise<void> {
|
|
316
|
+
this.readyPending = true;
|
|
317
|
+
const promise = new Promise<void>((resolveArg, rejectArg) => {
|
|
318
|
+
this.readyResolve = () => {
|
|
319
|
+
this.readyPending = false;
|
|
320
|
+
resolveArg();
|
|
321
|
+
};
|
|
322
|
+
this.readyReject = (reasonArg) => {
|
|
323
|
+
this.readyPending = false;
|
|
324
|
+
rejectArg(reasonArg);
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
// A readiness promise nobody awaits must never surface as an unhandled
|
|
328
|
+
// rejection; consumers that do await it still observe the reason.
|
|
329
|
+
promise.catch(() => undefined);
|
|
330
|
+
return promise;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private bufferWrite(chunkArg: string | Uint8Array): void {
|
|
334
|
+
this.pendingWrites.push(chunkArg);
|
|
335
|
+
this.pendingWriteBytes += chunkByteLength(chunkArg);
|
|
336
|
+
while (this.pendingWriteBytes > maxPendingWriteBytes && this.pendingWrites.length > 1) {
|
|
337
|
+
const droppedChunk = this.pendingWrites.shift()!;
|
|
338
|
+
this.pendingWriteBytes -= chunkByteLength(droppedChunk);
|
|
339
|
+
this.pendingWritesDropped = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private flushPendingWrites(terminalArg: Terminal): void {
|
|
344
|
+
const pending = this.pendingWrites;
|
|
345
|
+
const dropped = this.pendingWritesDropped;
|
|
346
|
+
this.pendingWrites = [];
|
|
347
|
+
this.pendingWriteBytes = 0;
|
|
348
|
+
this.pendingWritesDropped = false;
|
|
349
|
+
if (dropped && !this.writeToTerminal(terminalArg, RIS)) return;
|
|
350
|
+
for (const chunk of pending) {
|
|
351
|
+
if (!this.writeToTerminal(terminalArg, chunk)) return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** xterm throws at its own flow-control watermark; that has to reach the host. */
|
|
356
|
+
private writeToTerminal(terminalArg: Terminal, chunkArg: string | Uint8Array): boolean {
|
|
357
|
+
try {
|
|
358
|
+
terminalArg.write(chunkArg);
|
|
359
|
+
return true;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
this.reportError(error);
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private reportError(errorArg: unknown): void {
|
|
367
|
+
if (this.readyPending) this.readyReject(errorArg);
|
|
368
|
+
const detail: ITerminalErrorDetail = { error: errorArg };
|
|
369
|
+
this.dispatchEvent(
|
|
370
|
+
new CustomEvent('terminal-error', { detail, bubbles: true, composed: true }),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private ensureTerminal(signalArg: AbortSignal): Promise<void> {
|
|
375
|
+
if (this.terminal) return Promise.resolve();
|
|
376
|
+
if (!this.setupPromise) this.setupPromise = this.setupTerminal(signalArg);
|
|
377
|
+
return this.setupPromise;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private async setupTerminal(signalArg: AbortSignal): Promise<void> {
|
|
381
|
+
try {
|
|
382
|
+
await this.createTerminal(signalArg);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.reportError(error);
|
|
385
|
+
} finally {
|
|
386
|
+
// Nothing came up: release the guard so the next connection retries.
|
|
387
|
+
if (!this.terminal) this.setupPromise = undefined;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private async createTerminal(signalArg: AbortSignal): Promise<void> {
|
|
392
|
+
await this.updateComplete;
|
|
243
393
|
const libLoader = DeesServiceLibLoader.getInstance();
|
|
244
394
|
const [xterm, fit] = await Promise.all([
|
|
245
395
|
libLoader.loadXterm(),
|
|
246
396
|
libLoader.loadXtermFitAddon(),
|
|
247
397
|
]);
|
|
248
|
-
|
|
398
|
+
// The load was awaited: the element may have been detached meanwhile.
|
|
399
|
+
if (signalArg.aborted || !this.isConnected) return;
|
|
249
400
|
const host = this.shadowRoot?.querySelector<HTMLElement>('.terminalHost');
|
|
250
401
|
if (!host) return;
|
|
251
402
|
const computed = getComputedStyle(this);
|
|
@@ -265,9 +416,21 @@ export class DeesTerminalView extends DeesElement {
|
|
|
265
416
|
cursor: themeColor('--dees-color-accent-primary', '#e6e6e6'),
|
|
266
417
|
},
|
|
267
418
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
419
|
+
let fitAddon: FitAddon;
|
|
420
|
+
try {
|
|
421
|
+
fitAddon = new fit.FitAddon();
|
|
422
|
+
terminal.loadAddon(fitAddon);
|
|
423
|
+
terminal.open(host);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
// The element does not own this instance yet, so teardown cannot reach
|
|
426
|
+
// it; a half-opened terminal would otherwise leak with its DOM attached.
|
|
427
|
+
try {
|
|
428
|
+
terminal.dispose();
|
|
429
|
+
} catch {
|
|
430
|
+
// Disposing a half-opened terminal must not mask the real failure.
|
|
431
|
+
}
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
271
434
|
this.terminal = terminal;
|
|
272
435
|
this.fitAddon = fitAddon;
|
|
273
436
|
this.inputDisposable = terminal.onData((dataArg) => {
|
|
@@ -277,11 +440,11 @@ export class DeesTerminalView extends DeesElement {
|
|
|
277
440
|
new CustomEvent('terminal-input', { detail, bubbles: true, composed: true }),
|
|
278
441
|
);
|
|
279
442
|
});
|
|
280
|
-
|
|
281
|
-
this.pendingWrites = [];
|
|
443
|
+
this.flushPendingWrites(terminal);
|
|
282
444
|
this.refit();
|
|
283
445
|
this.resizeObserver = new ResizeObserver(() => this.refit());
|
|
284
446
|
this.resizeObserver.observe(host);
|
|
447
|
+
this.readyResolve();
|
|
285
448
|
}
|
|
286
449
|
|
|
287
450
|
private refit(): void {
|
|
@@ -303,8 +466,7 @@ export class DeesTerminalView extends DeesElement {
|
|
|
303
466
|
);
|
|
304
467
|
}
|
|
305
468
|
|
|
306
|
-
|
|
307
|
-
await super.disconnectedCallback();
|
|
469
|
+
private teardownTerminal(): void {
|
|
308
470
|
this.resizeObserver?.disconnect();
|
|
309
471
|
this.resizeObserver = undefined;
|
|
310
472
|
this.inputDisposable?.dispose();
|
|
@@ -312,6 +474,16 @@ export class DeesTerminalView extends DeesElement {
|
|
|
312
474
|
this.terminal?.dispose();
|
|
313
475
|
this.terminal = undefined;
|
|
314
476
|
this.fitAddon = undefined;
|
|
315
|
-
|
|
477
|
+
// Instance state of the finished connection: a reattached element has to
|
|
478
|
+
// re-announce its grid and may retry a load that failed.
|
|
479
|
+
this.lastReportedSize = '';
|
|
480
|
+
this.setupPromise = undefined;
|
|
481
|
+
// Buffered output deliberately survives, so a replay that arrived while the
|
|
482
|
+
// element was detached still reaches the next connection.
|
|
483
|
+
if (this.readyPending) {
|
|
484
|
+
this.readyReject(
|
|
485
|
+
new Error('dees-terminal-view was disconnected before the terminal became ready'),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
316
488
|
}
|
|
317
489
|
}
|
|
@@ -355,12 +355,31 @@ export interface IHarnessSessionMeta {
|
|
|
355
355
|
statusLabel?: string;
|
|
356
356
|
/** Harness running this session, e.g. Codex, OpenCode or FlexHarness. */
|
|
357
357
|
harness?: IHarnessAgentIdentity;
|
|
358
|
+
/** Host-owned project or workspace this session belongs to, shown on the identity line. */
|
|
359
|
+
projectLabel?: string;
|
|
358
360
|
/** Pulse the normal/working indicator. Defaults to state === 'working'; never pulses attention/error/finished. */
|
|
359
361
|
working?: boolean;
|
|
360
362
|
/** Set false for pinned, non-sortable entries such as terminals. Defaults to true. */
|
|
361
363
|
draggable?: boolean;
|
|
362
364
|
}
|
|
363
365
|
|
|
366
|
+
/** Discriminates the two kinds of rows the session list orders as peers. */
|
|
367
|
+
export type THarnessSessionListItemKind = 'session' | 'resource';
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Stable reference to one ordered row. Session ids and resource ids are
|
|
371
|
+
* separate namespaces, so ordering always carries the kind alongside the id.
|
|
372
|
+
*/
|
|
373
|
+
export interface IHarnessSessionListItemRef {
|
|
374
|
+
kind: THarnessSessionListItemKind;
|
|
375
|
+
id: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Detail of `harness-session-select` and `harness-session-open`. */
|
|
379
|
+
export interface IHarnessSessionEventDetail {
|
|
380
|
+
session: IHarnessSessionMeta;
|
|
381
|
+
}
|
|
382
|
+
|
|
364
383
|
/** Host-owned metadata for a resource that can be associated with a harness session. */
|
|
365
384
|
export interface IHarnessResourceMeta<TState extends string = string> {
|
|
366
385
|
/** Stable identity used by selection and association records. */
|
|
@@ -373,43 +392,55 @@ export interface IHarnessResourceMeta<TState extends string = string> {
|
|
|
373
392
|
preview?: string;
|
|
374
393
|
createdAt?: number;
|
|
375
394
|
updatedAt?: number;
|
|
395
|
+
/** Host-owned project or workspace this resource belongs to, shown on the metadata line. */
|
|
396
|
+
projectLabel?: string;
|
|
376
397
|
/** Optional host-defined display state. */
|
|
377
398
|
state?: TState;
|
|
399
|
+
/** Set false for pinned, non-sortable entries. Defaults to true. */
|
|
400
|
+
draggable?: boolean;
|
|
378
401
|
}
|
|
379
402
|
|
|
380
|
-
/**
|
|
403
|
+
/**
|
|
404
|
+
* A controlled zero-or-one association for one resource. The target is any
|
|
405
|
+
* other list item — a session or another resource such as a terminal.
|
|
406
|
+
*/
|
|
381
407
|
export interface IHarnessResourceAssociation {
|
|
382
408
|
resourceId: string;
|
|
383
|
-
|
|
409
|
+
target: IHarnessSessionListItemRef;
|
|
384
410
|
}
|
|
385
411
|
|
|
386
412
|
export interface IHarnessResourceEventDetail<TState extends string = string> {
|
|
387
413
|
resource: IHarnessResourceMeta<TState>;
|
|
388
|
-
|
|
414
|
+
/** The controlled association target, or null when the resource is unattached. */
|
|
415
|
+
target: IHarnessSessionListItemRef | null;
|
|
389
416
|
}
|
|
390
417
|
|
|
391
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Association is an explicit host action, never a drop: dragging a resource
|
|
420
|
+
* only moves it inside the ordered list.
|
|
421
|
+
*/
|
|
422
|
+
export type THarnessResourceAssociationInteractionSource = 'action' | 'keyboard';
|
|
392
423
|
|
|
393
424
|
export type THarnessResourceAssociationRequestDetail<TState extends string = string> =
|
|
394
425
|
| {
|
|
395
426
|
intent: 'attach';
|
|
396
427
|
resource: IHarnessResourceMeta<TState>;
|
|
397
|
-
|
|
398
|
-
|
|
428
|
+
fromTarget: null;
|
|
429
|
+
toTarget: IHarnessSessionListItemRef;
|
|
399
430
|
source: THarnessResourceAssociationInteractionSource;
|
|
400
431
|
}
|
|
401
432
|
| {
|
|
402
433
|
intent: 'detach';
|
|
403
434
|
resource: IHarnessResourceMeta<TState>;
|
|
404
|
-
|
|
405
|
-
|
|
435
|
+
fromTarget: IHarnessSessionListItemRef;
|
|
436
|
+
toTarget: null;
|
|
406
437
|
source: THarnessResourceAssociationInteractionSource;
|
|
407
438
|
}
|
|
408
439
|
| {
|
|
409
440
|
intent: 'reassign';
|
|
410
441
|
resource: IHarnessResourceMeta<TState>;
|
|
411
|
-
|
|
412
|
-
|
|
442
|
+
fromTarget: IHarnessSessionListItemRef;
|
|
443
|
+
toTarget: IHarnessSessionListItemRef;
|
|
413
444
|
source: THarnessResourceAssociationInteractionSource;
|
|
414
445
|
};
|
|
415
446
|
|
|
@@ -423,7 +454,7 @@ export interface IHarnessResourceContextDetail<TState extends string = string>
|
|
|
423
454
|
source: THarnessResourceContextSource;
|
|
424
455
|
originalEvent: MouseEvent | KeyboardEvent;
|
|
425
456
|
/** Routes a host menu choice through the component's validated controlled request event. */
|
|
426
|
-
requestAssociation: (
|
|
457
|
+
requestAssociation: (toTarget: IHarnessSessionListItemRef | null) => boolean;
|
|
427
458
|
/** Closes the external-menu disclosure state and restores focus to its invoking control. */
|
|
428
459
|
close: () => void;
|
|
429
460
|
}
|
|
@@ -438,22 +469,35 @@ export interface IHarnessSessionContextDetail {
|
|
|
438
469
|
originalEvent: MouseEvent;
|
|
439
470
|
}
|
|
440
471
|
|
|
441
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* A user-defined group of list items; ordering of `itemIds` is the display
|
|
474
|
+
* order. Sessions and resources may be members in any interleaving.
|
|
475
|
+
*/
|
|
442
476
|
export interface IHarnessSessionGroup {
|
|
443
477
|
id: string;
|
|
444
478
|
name: string;
|
|
445
|
-
|
|
479
|
+
itemIds: IHarnessSessionListItemRef[];
|
|
446
480
|
}
|
|
447
481
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
482
|
+
export type THarnessItemMoveSource = 'pointer' | 'keyboard';
|
|
483
|
+
|
|
484
|
+
/** Detail of `harness-item-move`: one item landed in a new ordered position. */
|
|
485
|
+
export interface IHarnessItemMoveDetail {
|
|
486
|
+
item: IHarnessSessionListItemRef;
|
|
487
|
+
/** Group the item came from, or null for the ungrouped section. */
|
|
488
|
+
fromGroupId: string | null;
|
|
451
489
|
/** Target group, or null for the ungrouped section. */
|
|
490
|
+
toGroupId: string | null;
|
|
491
|
+
/** Stable target anchor in the dragged-item-excluded order; null means append. */
|
|
492
|
+
beforeItem: IHarnessSessionListItemRef | null;
|
|
493
|
+
source: THarnessItemMoveSource;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The open insertion slot a drag currently targets. */
|
|
497
|
+
export interface IDropSlot {
|
|
452
498
|
groupId: string | null;
|
|
453
|
-
/**
|
|
499
|
+
/** Index within the section's sortable, dragged-item-excluded order. */
|
|
454
500
|
index: number;
|
|
455
|
-
/** Stable target anchor in the dragged-card-excluded order; absent means append. */
|
|
456
|
-
beforeSessionId?: string;
|
|
457
501
|
}
|
|
458
502
|
|
|
459
503
|
/** Detail of `harness-group-context`, fired on right-click of a group header. */
|