@design.estate/dees-catalog 12.0.0 → 12.2.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 (28) hide show
  1. package/dist_bundle/bundle.js +1712 -1223
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +3 -3
  4. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.js +3 -107
  5. package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.d.ts +70 -0
  6. package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.demo.d.ts +1 -0
  7. package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.demo.js +78 -0
  8. package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.js +261 -0
  9. package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/index.d.ts +1 -0
  10. package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/index.js +2 -0
  11. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +101 -7
  12. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.demo.js +38 -7
  13. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +252 -22
  14. package/dist_ts_web/elements/00group-harness/harness.styles.d.ts +10 -0
  15. package/dist_ts_web/elements/00group-harness/harness.styles.js +116 -1
  16. package/dist_ts_web/elements/00group-harness/index.d.ts +1 -0
  17. package/dist_ts_web/elements/00group-harness/index.js +2 -1
  18. package/package.json +3 -3
  19. package/readme.md +49 -1
  20. package/ts_web/00_commitinfo_data.ts +3 -3
  21. package/ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts +2 -106
  22. package/ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.demo.ts +87 -0
  23. package/ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.ts +245 -0
  24. package/ts_web/elements/00group-harness/dees-harness-sidebar/index.ts +1 -0
  25. package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.demo.ts +48 -12
  26. package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +283 -22
  27. package/ts_web/elements/00group-harness/harness.styles.ts +116 -0
  28. package/ts_web/elements/00group-harness/index.ts +1 -0
@@ -9,6 +9,7 @@ import {
9
9
  } from '@design.estate/dees-element';
10
10
  import type { Terminal } from 'xterm';
11
11
  import type { FitAddon } from 'xterm-addon-fit';
12
+ import * as plugins from '../../00plugins.js';
12
13
  import { themeDefaultStyles } from '../../00theme.js';
13
14
  import { DeesServiceLibLoader } from '../../../services/index.js';
14
15
 
@@ -23,7 +24,10 @@ export interface ITerminalInputDetail {
23
24
  data: string;
24
25
  }
25
26
 
26
- /** Detail of `terminal-resize`, fired after the terminal refits its container. */
27
+ /**
28
+ * A terminal grid size. It is the detail of `terminal-resize`, fired after the
29
+ * terminal refits its container, and the size `restore()` paints a snapshot at.
30
+ */
27
31
  export interface ITerminalResizeDetail {
28
32
  rows: number;
29
33
  cols: number;
@@ -59,6 +63,49 @@ const RIS = '\x1bc';
59
63
  const chunkByteLength = (chunkArg: string | Uint8Array): number =>
60
64
  typeof chunkArg === 'string' ? chunkArg.length * 2 : chunkArg.byteLength;
61
65
 
66
+ /** Identity of a grid size, as compared when deciding whether to report a refit. */
67
+ const gridKey = (sizeArg: ITerminalResizeDetail): string => `${sizeArg.rows}x${sizeArg.cols}`;
68
+
69
+ /**
70
+ * Grid bounds `restore()` accepts. xterm silently widens anything narrower than
71
+ * two columns (a wide character needs two cells), which would paint a snapshot
72
+ * at a size other than the one it was serialized at; a PTY window size is a
73
+ * 16-bit field, so no real snapshot is larger than the upper bound.
74
+ */
75
+ const minRestoreCols = 2;
76
+ const minRestoreRows = 1;
77
+ const maxRestoreGridSize = 0xffff;
78
+
79
+ const isGridDimension = (valueArg: number, minArg: number): boolean =>
80
+ Number.isInteger(valueArg) && valueArg >= minArg && valueArg <= maxRestoreGridSize;
81
+
82
+ /**
83
+ * One `restore()` call: the snapshot, the grid it was serialized at, and the
84
+ * deferred whose promise is handed back to the caller. Its rejections are
85
+ * lifecycle outcomes, not programming errors, so, as with `ready`, an
86
+ * unawaited promise never surfaces as an unhandled rejection; a caller that
87
+ * awaits it still observes the reason.
88
+ */
89
+ class TerminalRestore {
90
+ public readonly deferred = plugins.smartpromise.defer<void>();
91
+
92
+ constructor(
93
+ public readonly data: string | Uint8Array,
94
+ public readonly size: ITerminalResizeDetail,
95
+ ) {
96
+ this.deferred.promise.catch(() => undefined);
97
+ }
98
+ }
99
+
100
+ /** Everything buffered while no terminal is live, in replay order. */
101
+ interface IPendingOutput {
102
+ /** The base of the writes; it resets everything that was buffered before it. */
103
+ restore: TerminalRestore | undefined;
104
+ writes: Array<string | Uint8Array>;
105
+ /** Whether older output was dropped, so the replay has to reset before the writes. */
106
+ dropped: boolean;
107
+ }
108
+
62
109
  /**
63
110
  * Transport-agnostic xterm.js surface: the host pipes remote PTY output into
64
111
  * `write()` and receives keystrokes via `terminal-input` — no process,
@@ -66,9 +113,10 @@ const chunkByteLength = (chunkArg: string | Uint8Array): number =>
66
113
  * every refit so the host can negotiate a PTY size. Output that arrives while
67
114
  * no terminal is live is buffered up to `maxPendingWriteBytes` and replayed in
68
115
  * 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.
116
+ * ready and survives being detached. `restore()` paints a serialized snapshot
117
+ * invisibly at the grid it was taken at, then fits it to the container. xterm
118
+ * loads through DeesServiceLibLoader; CSP-locked hosts can pre-register
119
+ * bundled modules with `provideXtermModules()` to avoid the CDN.
72
120
  */
73
121
  @customElement('dees-terminal-view')
74
122
  export class DeesTerminalView extends DeesElement {
@@ -88,11 +136,24 @@ export class DeesTerminalView extends DeesElement {
88
136
  private inputDisposable: { dispose(): void } | undefined;
89
137
  private lastReportedSize = '';
90
138
  // Output that arrives before xterm is live is replayed in order, bounded by
91
- // maxPendingWriteBytes and retained across a disconnect.
139
+ // maxPendingWriteBytes and retained across a disconnect. A buffered restore
140
+ // resets everything before it, so there is at most one and it is the base
141
+ // of the chunks buffered after it; its bytes count towards the bound.
142
+ private pendingRestore: TerminalRestore | undefined;
92
143
  private pendingWrites: Array<string | Uint8Array> = [];
93
144
  private pendingWriteBytes = 0;
94
145
  private pendingWritesDropped = false;
95
146
  private setupPromise: Promise<void> | undefined;
147
+ /**
148
+ * The restore handed to the live terminal whose snapshot is not parsed yet. While it is set the
149
+ * grid belongs to the snapshot: the terminal stays invisible and nothing refits it.
150
+ */
151
+ private activeRestore: TerminalRestore | undefined;
152
+ /**
153
+ * Set only by an explicit `focus()` that arrived before the terminal existed. Nothing else sets
154
+ * it, so a re-render, a write, or a resize can never move the caret into this element.
155
+ */
156
+ private focusWhenReady = false;
96
157
  private readyPending = true;
97
158
  private readyResolve: () => void = () => undefined;
98
159
  private readyReject: (reasonArg: unknown) => void = () => undefined;
@@ -136,6 +197,14 @@ export class DeesTerminalView extends DeesElement {
136
197
  box-sizing: border-box;
137
198
  }
138
199
 
200
+ /*
201
+ * A restore paints invisibly. The grid keeps its layout, so fitting still measures it, and
202
+ * it stays focusable: visibility: hidden would blur the input surface and refuse focus().
203
+ */
204
+ .terminalHost.restoring {
205
+ opacity: 0;
206
+ }
207
+
139
208
  /* xterm.js styles (shadow DOM needs them locally) */
140
209
  .xterm {
141
210
  font-feature-settings: 'liga' 0;
@@ -288,7 +357,10 @@ export class DeesTerminalView extends DeesElement {
288
357
  this.bufferWrite(dataArg);
289
358
  }
290
359
 
291
- /** Reset the screen and scrollback, e.g. before a full replay. */
360
+ /**
361
+ * Reset the screen and scrollback, e.g. before a full replay. Before the terminal exists it also
362
+ * replaces a buffered `restore()`, whose promise then resolves as superseded.
363
+ */
292
364
  public clear(): void {
293
365
  const terminal = this.terminal;
294
366
  if (terminal && this.isConnected) {
@@ -297,16 +369,104 @@ export class DeesTerminalView extends DeesElement {
297
369
  }
298
370
  // Without a terminal the reset is expressed as a buffered sentinel, so the
299
371
  // 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;
372
+ this.discardPendingOutput();
373
+ this.bufferWrite(RIS);
374
+ }
375
+
376
+ /**
377
+ * Replace the terminal state with a serialized snapshot, e.g. from xterm's serialize addon, and
378
+ * show it only once it is complete. A snapshot is exact only in a grid of the size it was
379
+ * serialized at, so the terminal is fully reset (screen, scrollback, modes, alternate buffer),
380
+ * set to `sizeArg`, fed `dataArg` while invisible, then fitted to its container and revealed.
381
+ * `terminal-resize` fires at the end when the fitted grid differs from `sizeArg`, which is the
382
+ * host's cue to resize the PTY so the application redraws at the new size. Until then
383
+ * `currentSize` reports `sizeArg`, so the closing event, or this promise, is what a host
384
+ * negotiates a PTY size on.
385
+ *
386
+ * The restore is part of the output stream: writes issued after it are applied after the
387
+ * snapshot, in order, at the fitted size. Like `write()` it is safe before the terminal exists
388
+ * and while the element is detached; a buffered restore replaces everything buffered before
389
+ * it. The terminal keeps its focus and keeps forwarding input while the restore runs.
390
+ *
391
+ * The promise resolves once the snapshot is on screen, and also, without revealing anything,
392
+ * when a newer `restore()` supersedes this one or `clear()` replaces it while it is buffered.
393
+ * It rejects, without ever surfacing as an unhandled rejection, when the element is
394
+ * disconnected while the snapshot is being parsed (the terminal is disposed with it, as with
395
+ * any live output), when output buffered after it pushes it out of `maxPendingWriteBytes`, and
396
+ * when xterm refuses the data (`terminal-error` carries the same error). Buffered output is
397
+ * dropped oldest first and the restore is the oldest entry, so a buffered snapshot that alone
398
+ * exceeds the bound is dropped by the next buffered write of any size. An invalid size is a
399
+ * programming error: the promise rejects with a `RangeError` and nothing changes; unlike the
400
+ * reasons above this one is not pre-handled, so a `restore()` nobody observes reports it as an
401
+ * unhandled rejection.
402
+ *
403
+ * There is a fourth outcome: a restore that is still buffered when the element is disconnected
404
+ * stays pending, because the snapshot survives the move like buffered output and is applied on
405
+ * the next connection. A host that discards the element instead of reattaching it must not await
406
+ * that promise.
407
+ *
408
+ * @param dataArg The serialized terminal state.
409
+ * @param sizeArg The grid the snapshot was serialized at: integer `cols` in [2, 65535] and
410
+ * `rows` in [1, 65535].
411
+ */
412
+ public restore(dataArg: string | Uint8Array, sizeArg: ITerminalResizeDetail): Promise<void> {
413
+ const { cols, rows } = sizeArg;
414
+ if (!isGridDimension(cols, minRestoreCols) || !isGridDimension(rows, minRestoreRows)) {
415
+ return Promise.reject(
416
+ new RangeError(
417
+ `restore() needs integer cols in [${minRestoreCols}, ${maxRestoreGridSize}] and rows in ` +
418
+ `[${minRestoreRows}, ${maxRestoreGridSize}], got ${cols}x${rows}`,
419
+ ),
420
+ );
421
+ }
422
+ const restore = new TerminalRestore(dataArg, { cols, rows });
423
+ const terminal = this.terminal;
424
+ if (terminal && this.isConnected) {
425
+ this.startRestore(terminal, restore);
426
+ } else {
427
+ this.discardPendingOutput();
428
+ this.pendingRestore = restore;
429
+ this.pendingWriteBytes = chunkByteLength(dataArg);
430
+ }
431
+ return restore.deferred.promise;
432
+ }
433
+
434
+ /**
435
+ * Moves the caret into the terminal's input surface, so the next keystroke goes to the pty
436
+ * rather than to whatever the page focused last. This is the call a host makes when the user
437
+ * selects a terminal in a list.
438
+ *
439
+ * The terminal is created asynchronously — the xterm modules are loaded on first connection —
440
+ * so a call that arrives before it exists is remembered and applied once it is ready, exactly
441
+ * once. The same applies to an element that is focused while detached and then reattached: the
442
+ * pending request is dropped on disconnect, because by then the user's intent has expired, and
443
+ * a fresh `focus()` after the move is what a host should issue.
444
+ *
445
+ * Only this method arms that behaviour; re-renders, buffered output and background updates
446
+ * never steal focus.
447
+ */
448
+ public focus(optionsArg?: FocusOptions): void {
449
+ const terminal = this.terminal;
450
+ if (terminal && this.isConnected) {
451
+ this.focusWhenReady = false;
452
+ terminal.focus();
453
+ return;
454
+ }
455
+ // Preventing scroll is a hint for the eventual focus, not a reason to skip it.
456
+ void optionsArg;
457
+ this.focusWhenReady = true;
303
458
  }
304
459
 
305
- public focus(): void {
306
- this.terminal?.focus();
460
+ /** True while an explicit `focus()` is waiting for the terminal to come up. */
461
+ public get focusPending(): boolean {
462
+ return this.focusWhenReady;
307
463
  }
308
464
 
309
- /** Current grid size, or undefined while no terminal is live. */
465
+ /**
466
+ * Current grid size, or undefined while no terminal is live. While a snapshot is being parsed
467
+ * this is the grid the snapshot was serialized at; the closing `terminal-resize`, or the promise
468
+ * `restore()` returned, is the grid to negotiate a PTY size on.
469
+ */
310
470
  public get currentSize(): ITerminalResizeDetail | undefined {
311
471
  if (!this.terminal) return undefined;
312
472
  return { rows: this.terminal.rows, cols: this.terminal.cols };
@@ -333,25 +493,105 @@ export class DeesTerminalView extends DeesElement {
333
493
  private bufferWrite(chunkArg: string | Uint8Array): void {
334
494
  this.pendingWrites.push(chunkArg);
335
495
  this.pendingWriteBytes += chunkByteLength(chunkArg);
336
- while (this.pendingWriteBytes > maxPendingWriteBytes && this.pendingWrites.length > 1) {
337
- const droppedChunk = this.pendingWrites.shift()!;
338
- this.pendingWriteBytes -= chunkByteLength(droppedChunk);
496
+ // Oldest first, and never the newest entry: a buffered restore is older
497
+ // than every chunk, so it goes before any of them.
498
+ while (
499
+ this.pendingWriteBytes > maxPendingWriteBytes &&
500
+ this.pendingWrites.length > (this.pendingRestore ? 0 : 1)
501
+ ) {
502
+ const droppedRestore = this.pendingRestore;
503
+ if (droppedRestore) {
504
+ this.pendingRestore = undefined;
505
+ this.pendingWriteBytes -= chunkByteLength(droppedRestore.data);
506
+ droppedRestore.deferred.reject(
507
+ new Error(
508
+ 'dees-terminal-view dropped a buffered restore: the output buffered after it exceeded maxPendingWriteBytes',
509
+ ),
510
+ );
511
+ } else {
512
+ this.pendingWriteBytes -= chunkByteLength(this.pendingWrites.shift()!);
513
+ }
339
514
  this.pendingWritesDropped = true;
340
515
  }
341
516
  }
342
517
 
343
- private flushPendingWrites(terminalArg: Terminal): void {
344
- const pending = this.pendingWrites;
345
- const dropped = this.pendingWritesDropped;
518
+ /** Hands the buffered output over and leaves the buffer empty. */
519
+ private takePendingOutput(): IPendingOutput {
520
+ const output: IPendingOutput = {
521
+ restore: this.pendingRestore,
522
+ writes: this.pendingWrites,
523
+ dropped: this.pendingWritesDropped,
524
+ };
525
+ this.pendingRestore = undefined;
346
526
  this.pendingWrites = [];
347
527
  this.pendingWriteBytes = 0;
348
528
  this.pendingWritesDropped = false;
529
+ return output;
530
+ }
531
+
532
+ /** Empties the buffer ahead of a reset; a buffered restore is superseded by it. */
533
+ private discardPendingOutput(): void {
534
+ this.takePendingOutput().restore?.deferred.resolve();
535
+ }
536
+
537
+ private flushPendingWrites(terminalArg: Terminal): void {
538
+ const { restore, writes, dropped } = this.takePendingOutput();
539
+ if (restore && !this.startRestore(terminalArg, restore)) return;
349
540
  if (dropped && !this.writeToTerminal(terminalArg, RIS)) return;
350
- for (const chunk of pending) {
541
+ for (const chunk of writes) {
351
542
  if (!this.writeToTerminal(terminalArg, chunk)) return;
352
543
  }
353
544
  }
354
545
 
546
+ /**
547
+ * Queues a restore into xterm's write stream. Resetting and resizing happen in the callback of
548
+ * the reset, so they take effect exactly at the restore's position in the stream, and the
549
+ * restore ends in the callback of its snapshot, before xterm parses the next chunk.
550
+ */
551
+ private startRestore(terminalArg: Terminal, restoreArg: TerminalRestore): boolean {
552
+ this.activeRestore?.deferred.resolve();
553
+ this.activeRestore = restoreArg;
554
+ this.terminalHost?.classList.add('restoring');
555
+ try {
556
+ terminalArg.write(RIS, () => this.applyRestoreSize(terminalArg, restoreArg));
557
+ terminalArg.write(restoreArg.data, () => this.finishRestore(restoreArg));
558
+ return true;
559
+ } catch (error) {
560
+ // xterm refused the snapshot at its flow-control watermark. A reset it did accept is parsed
561
+ // later, finds the restore inactive and leaves the grid alone.
562
+ this.endRestore();
563
+ this.reportError(error);
564
+ restoreArg.deferred.reject(error);
565
+ return false;
566
+ }
567
+ }
568
+
569
+ private applyRestoreSize(terminalArg: Terminal, restoreArg: TerminalRestore): void {
570
+ if (this.activeRestore !== restoreArg) return;
571
+ terminalArg.resize(restoreArg.size.cols, restoreArg.size.rows);
572
+ // The host serialized the snapshot at this grid, so it is the size the host already knows:
573
+ // the closing refit reports only a difference from it.
574
+ this.lastReportedSize = gridKey(restoreArg.size);
575
+ }
576
+
577
+ private finishRestore(restoreArg: TerminalRestore): void {
578
+ // A superseded restore was settled when the newer one started.
579
+ if (this.activeRestore !== restoreArg) return;
580
+ this.endRestore();
581
+ restoreArg.deferred.resolve();
582
+ }
583
+
584
+ /**
585
+ * Hands the grid back to the container: one refit, then the terminal is revealed. The refit
586
+ * dispatches `terminal-resize` synchronously, and a restore its listener starts keeps the
587
+ * terminal hidden.
588
+ */
589
+ private endRestore(): void {
590
+ this.activeRestore = undefined;
591
+ this.refit();
592
+ if (!this.activeRestore) this.terminalHost?.classList.remove('restoring');
593
+ }
594
+
355
595
  /** xterm throws at its own flow-control watermark; that has to reach the host. */
356
596
  private writeToTerminal(terminalArg: Terminal, chunkArg: string | Uint8Array): boolean {
357
597
  try {
@@ -371,6 +611,10 @@ export class DeesTerminalView extends DeesElement {
371
611
  );
372
612
  }
373
613
 
614
+ private get terminalHost(): HTMLElement | null {
615
+ return this.shadowRoot?.querySelector<HTMLElement>('.terminalHost') ?? null;
616
+ }
617
+
374
618
  private ensureTerminal(signalArg: AbortSignal): Promise<void> {
375
619
  if (this.terminal) return Promise.resolve();
376
620
  if (!this.setupPromise) this.setupPromise = this.setupTerminal(signalArg);
@@ -397,7 +641,7 @@ export class DeesTerminalView extends DeesElement {
397
641
  ]);
398
642
  // The load was awaited: the element may have been detached meanwhile.
399
643
  if (signalArg.aborted || !this.isConnected) return;
400
- const host = this.shadowRoot?.querySelector<HTMLElement>('.terminalHost');
644
+ const host = this.terminalHost;
401
645
  if (!host) return;
402
646
  const computed = getComputedStyle(this);
403
647
  const themeColor = (nameArg: string, fallbackArg: string): string => {
@@ -444,17 +688,23 @@ export class DeesTerminalView extends DeesElement {
444
688
  this.refit();
445
689
  this.resizeObserver = new ResizeObserver(() => this.refit());
446
690
  this.resizeObserver.observe(host);
691
+ // An explicit focus() that arrived before the terminal existed is honoured now, once.
692
+ if (this.focusWhenReady) {
693
+ this.focusWhenReady = false;
694
+ terminal.focus();
695
+ }
447
696
  this.readyResolve();
448
697
  }
449
698
 
450
699
  private refit(): void {
451
- if (!this.terminal || !this.fitAddon) return;
700
+ // A restore owns the grid until its snapshot is parsed and refits once when it ends.
701
+ if (!this.terminal || !this.fitAddon || this.activeRestore) return;
452
702
  try {
453
703
  this.fitAddon.fit();
454
704
  } catch {
455
705
  return;
456
706
  }
457
- const size = `${this.terminal.rows}x${this.terminal.cols}`;
707
+ const size = gridKey(this.terminal);
458
708
  if (size === this.lastReportedSize) return;
459
709
  this.lastReportedSize = size;
460
710
  const detail: ITerminalResizeDetail = {
@@ -471,6 +721,11 @@ export class DeesTerminalView extends DeesElement {
471
721
  this.resizeObserver = undefined;
472
722
  this.inputDisposable?.dispose();
473
723
  this.inputDisposable = undefined;
724
+ // A disposed terminal never parses the rest of its queue, so a restore in flight would
725
+ // otherwise stay pending forever; the snapshot is lost with the terminal, like live output.
726
+ const interruptedRestore = this.activeRestore;
727
+ this.activeRestore = undefined;
728
+ this.terminalHost?.classList.remove('restoring');
474
729
  this.terminal?.dispose();
475
730
  this.terminal = undefined;
476
731
  this.fitAddon = undefined;
@@ -478,6 +733,9 @@ export class DeesTerminalView extends DeesElement {
478
733
  // re-announce its grid and may retry a load that failed.
479
734
  this.lastReportedSize = '';
480
735
  this.setupPromise = undefined;
736
+ // A focus request belongs to the connection it was made in; a reattached element must not
737
+ // grab the caret from wherever the user moved it meanwhile.
738
+ this.focusWhenReady = false;
481
739
  // Buffered output deliberately survives, so a replay that arrived while the
482
740
  // element was detached still reaches the next connection.
483
741
  if (this.readyPending) {
@@ -485,5 +743,8 @@ export class DeesTerminalView extends DeesElement {
485
743
  new Error('dees-terminal-view was disconnected before the terminal became ready'),
486
744
  );
487
745
  }
746
+ interruptedRestore?.deferred.reject(
747
+ new Error('dees-terminal-view was disconnected before the restore completed'),
748
+ );
488
749
  }
489
750
  }
@@ -110,3 +110,119 @@ export const harnessMonoStyles: CSSResult = css`
110
110
  text-decoration: underline;
111
111
  }
112
112
  `;
113
+
114
+ /**
115
+ * Panel chrome of the harness sidebars: a full-height column of collapsible
116
+ * sections, each a heading button (chevron, optional icon, label, right-aligned
117
+ * summary) above the scrolling body of the expanded section. Shared by
118
+ * `dees-harness-sidebar` and `dees-harness-session-sidebar` so a host-composed
119
+ * panel and the session panel are the same surface. It expects the
120
+ * `.sidebar` > `.section` > `.sectionHeading` + `.sectionContent` structure and
121
+ * styles the element itself, so it belongs to those two elements only.
122
+ */
123
+ export const harnessSidebarStyles: CSSResult = css`
124
+ :host {
125
+ display: flex;
126
+ flex-direction: column;
127
+ min-width: 0;
128
+ min-height: 0;
129
+ height: 100%;
130
+ font-family: var(--dees-font-family);
131
+ color: var(--dees-color-text-primary);
132
+ }
133
+
134
+ .sidebar {
135
+ display: flex;
136
+ flex-direction: column;
137
+ min-height: 0;
138
+ height: 100%;
139
+ overflow: hidden;
140
+ }
141
+
142
+ .section {
143
+ display: flex;
144
+ flex: 0 0 auto;
145
+ flex-direction: column;
146
+ min-height: 0;
147
+ overflow: hidden;
148
+ border-bottom: 1px solid var(--dees-color-border-subtle);
149
+ }
150
+
151
+ .section.expanded {
152
+ flex: 1 1 0;
153
+ }
154
+
155
+ .section:last-child {
156
+ border-bottom: 0;
157
+ }
158
+
159
+ .sectionHeading {
160
+ display: flex;
161
+ flex: 0 0 auto;
162
+ align-items: center;
163
+ gap: var(--dees-spacing-xs);
164
+ width: 100%;
165
+ min-height: 36px;
166
+ margin: 0;
167
+ padding: 6px 12px 6px 8px;
168
+ border: 0;
169
+ background: var(--dees-color-bg-canvas);
170
+ color: var(--dees-color-text-primary);
171
+ font-family: inherit;
172
+ font-size: var(--dees-font-control-size-sm, 12px);
173
+ font-weight: 600;
174
+ text-align: left;
175
+ cursor: pointer;
176
+ }
177
+
178
+ .sectionHeading:hover {
179
+ background: var(--dees-color-badge-default-bg);
180
+ }
181
+
182
+ .sectionHeading:focus-visible {
183
+ position: relative;
184
+ z-index: 1;
185
+ outline: 2px solid var(--dees-color-accent-primary);
186
+ outline-offset: -2px;
187
+ }
188
+
189
+ .sectionHeading dees-icon {
190
+ flex: 0 0 auto;
191
+ color: var(--dees-color-text-secondary);
192
+ }
193
+
194
+ .sectionHeadingText {
195
+ min-width: 0;
196
+ flex-shrink: 0;
197
+ overflow: hidden;
198
+ text-overflow: ellipsis;
199
+ white-space: nowrap;
200
+ }
201
+
202
+ .sectionSummary {
203
+ flex: 0 1 auto;
204
+ min-width: 0;
205
+ overflow: hidden;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
208
+ margin-left: auto;
209
+ color: var(--dees-color-text-muted);
210
+ font-size: 11px;
211
+ font-variant-numeric: tabular-nums;
212
+ font-weight: 500;
213
+ }
214
+
215
+ .sectionContent {
216
+ flex: 1;
217
+ min-height: 0;
218
+ overflow-x: hidden;
219
+ overflow-y: auto;
220
+ padding: var(--dees-spacing-sm) var(--dees-spacing-md);
221
+ scrollbar-width: thin;
222
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
223
+ }
224
+
225
+ .sectionContent[hidden] {
226
+ display: none;
227
+ }
228
+ `;
@@ -20,6 +20,7 @@ export * from './dees-harness-message-list/index.js';
20
20
  export * from './dees-harness-reasoning/index.js';
21
21
  export * from './dees-harness-session-list/index.js';
22
22
  export * from './dees-harness-session-sidebar/index.js';
23
+ export * from './dees-harness-sidebar/index.js';
23
24
  export * from './dees-terminal-view/index.js';
24
25
  export * from './dees-harness-status/index.js';
25
26
  export * from './dees-harness-todos/index.js';