@design.estate/dees-catalog 6.2.0 → 6.4.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 +301 -67
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.d.ts +9 -2
  4. package/dist_ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js +133 -30
  5. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.d.ts +3 -1
  6. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +24 -2
  7. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.d.ts +6 -4
  8. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.js +7 -1
  9. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.js +67 -6
  10. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.d.ts +10 -0
  11. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +77 -9
  12. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/renderers.d.ts +2 -0
  13. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/renderers.js +31 -4
  14. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +8 -0
  15. package/dist_ts_web/elements/00group-harness/toolregistry.d.ts +2 -0
  16. package/dist_ts_web/elements/00group-harness/toolregistry.js +1 -1
  17. package/package.json +1 -1
  18. package/readme.hints.md +2 -1
  19. package/readme.md +13 -6
  20. package/ts_web/00_commitinfo_data.ts +1 -1
  21. package/ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.ts +135 -31
  22. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +13 -0
  23. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.ts +6 -0
  24. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts +64 -5
  25. package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +74 -7
  26. package/ts_web/elements/00group-harness/dees-harness-tool-card/renderers.ts +43 -4
  27. package/ts_web/elements/00group-harness/interfaces.ts +10 -0
  28. package/ts_web/elements/00group-harness/toolregistry.ts +7 -1
@@ -40,6 +40,12 @@ declare global {
40
40
  }
41
41
  }
42
42
 
43
+ export type TDiffView = 'inline' | 'split';
44
+
45
+ export interface IDiffViewChangeDetail {
46
+ diffView: TDiffView;
47
+ }
48
+
43
49
  @customElement('dees-dataview-codebox')
44
50
  export class DeesDataviewCodebox extends DeesElement {
45
51
  public static demo = demoFunc;
@@ -69,7 +75,11 @@ export class DeesDataviewCodebox extends DeesElement {
69
75
 
70
76
  /** '' = regular code view; 'inline' = unified rows; 'split' = side by side */
71
77
  @property({ type: String })
72
- accessor diffView: '' | 'inline' | 'split' = '';
78
+ accessor diffView: '' | TDiffView = '';
79
+
80
+ /** Hide gutters when a diff source does not provide trustworthy positions. */
81
+ @property({ type: Boolean })
82
+ accessor showDiffLineNumbers: boolean = true;
73
83
 
74
84
  @state()
75
85
  accessor highlightedCode: string = '';
@@ -123,9 +133,40 @@ export class DeesDataviewCodebox extends DeesElement {
123
133
  .appbar .actions {
124
134
  grid-column: 3;
125
135
  display: flex;
136
+ align-items: center;
137
+ gap: var(--dees-spacing-sm);
126
138
  justify-content: flex-end;
127
139
  }
128
140
 
141
+ .viewToggle {
142
+ display: flex;
143
+ padding: 2px;
144
+ border: 1px solid var(--dees-color-border-subtle);
145
+ border-radius: var(--dees-radius-md);
146
+ background: var(--dees-color-bg-tertiary);
147
+ }
148
+
149
+ .viewButton {
150
+ padding: 3px 7px;
151
+ border: 0;
152
+ border-radius: calc(var(--dees-radius-md) - 2px);
153
+ background: transparent;
154
+ color: var(--dees-color-text-muted);
155
+ cursor: pointer;
156
+ font: inherit;
157
+ font-size: var(--dees-font-caption2-size);
158
+ line-height: 1.2;
159
+ }
160
+
161
+ .viewButton:hover {
162
+ color: var(--dees-color-text-primary);
163
+ }
164
+
165
+ .viewButton.active {
166
+ background: var(--dees-color-active);
167
+ color: var(--dees-color-text-primary);
168
+ }
169
+
129
170
  .copyButton {
130
171
  display: flex;
131
172
  align-items: center;
@@ -247,8 +288,37 @@ export class DeesDataviewCodebox extends DeesElement {
247
288
  grid-template-columns: minmax(3ch, auto) minmax(3ch, auto) 2ch minmax(0, 1fr);
248
289
  }
249
290
 
250
- .diffGrid.split {
251
- grid-template-columns: minmax(3ch, auto) 2ch minmax(24ch, 1fr) minmax(3ch, auto) 2ch minmax(24ch, 1fr);
291
+ .diffGrid.inline.withoutLineNumbers {
292
+ grid-template-columns: 2ch minmax(0, 1fr);
293
+ }
294
+
295
+ .diffSplit {
296
+ display: grid;
297
+ grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
298
+ align-items: start;
299
+ min-width: 0;
300
+ padding: var(--dees-spacing-sm) 0;
301
+ }
302
+
303
+ .diffSplitDivider {
304
+ align-self: stretch;
305
+ background: var(--dees-color-border-default);
306
+ }
307
+
308
+ .diffSplitPane {
309
+ min-width: 0;
310
+ overflow-x: auto;
311
+ overflow-y: hidden;
312
+ scrollbar-width: thin;
313
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
314
+ }
315
+
316
+ .diffGrid.splitSide {
317
+ grid-template-columns: minmax(3ch, auto) 2ch minmax(24ch, 1fr);
318
+ }
319
+
320
+ .diffGrid.splitSide.withoutLineNumbers {
321
+ grid-template-columns: 2ch minmax(24ch, 1fr);
252
322
  }
253
323
 
254
324
  .diffGrid > div {
@@ -336,10 +406,6 @@ export class DeesDataviewCodebox extends DeesElement {
336
406
  color: var(--dees-color-link-hover);
337
407
  }
338
408
 
339
- .diffSplitSep {
340
- border-left: 1px solid var(--dees-color-border-subtle);
341
- }
342
-
343
409
  .diffStat {
344
410
  font-weight: 600;
345
411
  }
@@ -382,6 +448,24 @@ export class DeesDataviewCodebox extends DeesElement {
382
448
  <div slot="header" class="appbar">
383
449
  <div class="fileName">${this.filename}</div>
384
450
  <div class="actions">
451
+ ${this.isDiffActive
452
+ ? html`
453
+ <div class="viewToggle" role="group" aria-label="Diff layout">
454
+ <button
455
+ class="viewButton ${this.effectiveDiffView === 'inline' ? 'active' : ''}"
456
+ type="button"
457
+ aria-pressed=${String(this.effectiveDiffView === 'inline')}
458
+ @click=${() => this.setDiffView('inline')}
459
+ >Inline</button>
460
+ <button
461
+ class="viewButton ${this.effectiveDiffView === 'split' ? 'active' : ''}"
462
+ type="button"
463
+ aria-pressed=${String(this.effectiveDiffView === 'split')}
464
+ @click=${() => this.setDiffView('split')}
465
+ >Split</button>
466
+ </div>
467
+ `
468
+ : ''}
385
469
  <div
386
470
  class="copyButton ${this.copied ? 'copied' : ''}"
387
471
  title="Copy code"
@@ -431,18 +515,14 @@ export class DeesDataviewCodebox extends DeesElement {
431
515
 
432
516
  private renderDiff(): TemplateResult {
433
517
  const displayRows = this.displayRowsWithFolds();
434
- return html`
435
- <div class="diffScroll">
436
- ${this.effectiveDiffView === 'split'
437
- ? this.renderSplitGrid(displayRows)
438
- : this.renderInlineGrid(displayRows)}
439
- </div>
440
- `;
518
+ return this.effectiveDiffView === 'split'
519
+ ? this.renderSplitGrid(displayRows)
520
+ : html`<div class="diffScroll">${this.renderInlineGrid(displayRows)}</div>`;
441
521
  }
442
522
 
443
523
  private renderInlineGrid(displayRows: TDiffDisplayRow[]): TemplateResult {
444
524
  return html`
445
- <div class="diffGrid inline">
525
+ <div class="diffGrid inline ${this.showDiffLineNumbers ? '' : 'withoutLineNumbers'}">
446
526
  ${displayRows.map((displayRow) => {
447
527
  if (displayRow.kind === 'fold') {
448
528
  return this.renderFoldRow(displayRow.rows.length, displayRow.ordinal ?? 0);
@@ -452,8 +532,12 @@ export class DeesDataviewCodebox extends DeesElement {
452
532
  }
453
533
  const rowClass = displayRow.kind === 'add' ? 'row-add' : displayRow.kind === 'remove' ? 'row-remove' : '';
454
534
  return html`
455
- <div class="diffNum ${rowClass}">${displayRow.oldLine ?? ''}</div>
456
- <div class="diffNum ${rowClass}">${displayRow.newLine ?? ''}</div>
535
+ ${this.showDiffLineNumbers
536
+ ? html`
537
+ <div class="diffNum ${rowClass}">${displayRow.oldLine ?? ''}</div>
538
+ <div class="diffNum ${rowClass}">${displayRow.newLine ?? ''}</div>
539
+ `
540
+ : ''}
457
541
  <div class="diffSign ${rowClass}">${displayRow.kind === 'add' ? '+' : displayRow.kind === 'remove' ? '−' : ''}</div>
458
542
  <div class="diffText ${rowClass}">${this.renderDiffLine(displayRow.text, displayRow.segments, displayRow.kind)}</div>
459
543
  `;
@@ -463,18 +547,17 @@ export class DeesDataviewCodebox extends DeesElement {
463
547
  }
464
548
 
465
549
  private renderSplitGrid(displayRows: TDiffDisplayRow[]): TemplateResult {
466
- // pair each contiguous non-fold run; folds render as full-width rows
467
- const rendered: TemplateResult[] = [];
550
+ const leftRendered: TemplateResult[] = [];
551
+ const rightRendered: TemplateResult[] = [];
468
552
  let run: IDiffRow[] = [];
469
553
  const flushRun = () => {
470
554
  for (const splitRow of buildSplitRows(run)) {
471
555
  if (splitRow.kind === 'hunk') {
472
- rendered.push(html`<div class="diffHunk">${splitRow.text}</div>`);
556
+ leftRendered.push(html`<div class="diffHunk">${splitRow.text}</div>`);
557
+ rightRendered.push(html`<div class="diffHunk">${splitRow.text}</div>`);
473
558
  } else {
474
- rendered.push(html`
475
- ${this.renderSplitCell(splitRow.left, 'left')}
476
- ${this.renderSplitCell(splitRow.right, 'right')}
477
- `);
559
+ leftRendered.push(this.renderSplitCell(splitRow.left));
560
+ rightRendered.push(this.renderSplitCell(splitRow.right));
478
561
  }
479
562
  }
480
563
  run = [];
@@ -482,27 +565,38 @@ export class DeesDataviewCodebox extends DeesElement {
482
565
  for (const displayRow of displayRows) {
483
566
  if (displayRow.kind === 'fold') {
484
567
  flushRun();
485
- rendered.push(this.renderFoldRow(displayRow.rows.length, displayRow.ordinal ?? 0));
568
+ leftRendered.push(this.renderFoldRow(displayRow.rows.length, displayRow.ordinal ?? 0));
569
+ rightRendered.push(this.renderFoldRow(displayRow.rows.length, displayRow.ordinal ?? 0));
486
570
  } else {
487
571
  run.push(displayRow);
488
572
  }
489
573
  }
490
574
  flushRun();
491
- return html`<div class="diffGrid split">${rendered}</div>`;
575
+ const lineNumberClass = this.showDiffLineNumbers ? '' : 'withoutLineNumbers';
576
+ return html`
577
+ <div class="diffSplit">
578
+ <div class="diffSplitPane" data-side="left" aria-label="Before">
579
+ <div class="diffGrid splitSide ${lineNumberClass}">${leftRendered}</div>
580
+ </div>
581
+ <div class="diffSplitDivider" aria-hidden="true"></div>
582
+ <div class="diffSplitPane" data-side="right" aria-label="After">
583
+ <div class="diffGrid splitSide ${lineNumberClass}">${rightRendered}</div>
584
+ </div>
585
+ </div>
586
+ `;
492
587
  }
493
588
 
494
- private renderSplitCell(cell: ISplitCell | undefined, side: 'left' | 'right'): TemplateResult {
495
- const separatorClass = side === 'right' ? 'diffSplitSep' : '';
589
+ private renderSplitCell(cell: ISplitCell | undefined): TemplateResult {
496
590
  if (!cell) {
497
591
  return html`
498
- <div class="diffNum row-filler ${separatorClass}"></div>
592
+ ${this.showDiffLineNumbers ? html`<div class="diffNum row-filler"></div>` : ''}
499
593
  <div class="diffSign row-filler"></div>
500
594
  <div class="diffText row-filler"></div>
501
595
  `;
502
596
  }
503
597
  const rowClass = cell.kind === 'add' ? 'row-add' : cell.kind === 'remove' ? 'row-remove' : '';
504
598
  return html`
505
- <div class="diffNum ${rowClass} ${separatorClass}">${cell.line || ''}</div>
599
+ ${this.showDiffLineNumbers ? html`<div class="diffNum ${rowClass}">${cell.line || ''}</div>` : ''}
506
600
  <div class="diffSign ${rowClass}">${cell.kind === 'add' ? '+' : cell.kind === 'remove' ? '−' : ''}</div>
507
601
  <div class="diffText ${rowClass}">${this.renderDiffLine(cell.text, cell.segments, cell.kind)}</div>
508
602
  `;
@@ -552,10 +646,20 @@ export class DeesDataviewCodebox extends DeesElement {
552
646
  return Boolean(this.unifiedDiff || this.codeBefore || this.diffView);
553
647
  }
554
648
 
555
- public get effectiveDiffView(): 'inline' | 'split' {
649
+ public get effectiveDiffView(): TDiffView {
556
650
  return this.diffView === 'split' ? 'split' : 'inline';
557
651
  }
558
652
 
653
+ private setDiffView(diffViewArg: TDiffView): void {
654
+ if (this.effectiveDiffView === diffViewArg) return;
655
+ this.diffView = diffViewArg;
656
+ this.dispatchEvent(new CustomEvent<IDiffViewChangeDetail>('diff-view-change', {
657
+ detail: { diffView: diffViewArg },
658
+ bubbles: true,
659
+ composed: true,
660
+ }));
661
+ }
662
+
559
663
  public willUpdate(changedProperties: Map<string, any>) {
560
664
  super.willUpdate(changedProperties);
561
665
  if (changedProperties.has('codeToDisplay')) {
@@ -11,7 +11,9 @@ import {
11
11
  } from '@design.estate/dees-element';
12
12
  import { themeDefaultStyles } from '../../00theme.js';
13
13
  import type {
14
+ IHarnessAccountChangeDetail,
14
15
  IHarnessAttachment,
16
+ IHarnessComposerOption,
15
17
  IHarnessMessage,
16
18
  IHarnessPermissionRequest,
17
19
  IHarnessQuestionRequest,
@@ -148,6 +150,12 @@ export class DeesHarnessChat extends DeesElement {
148
150
  @property({ attribute: false })
149
151
  accessor attachments: IHarnessAttachment[] = [];
150
152
 
153
+ @property()
154
+ accessor account: string = '';
155
+
156
+ @property({ attribute: false })
157
+ accessor accountOptions: IHarnessComposerOption[] = [];
158
+
151
159
  @property()
152
160
  accessor model: string = '';
153
161
 
@@ -641,6 +649,8 @@ export class DeesHarnessChat extends DeesElement {
641
649
  .disabled=${this.disabled}
642
650
  .busy=${this.busy || this.status.type === 'busy'}
643
651
  .queuedCount=${this.queuedCount}
652
+ .account=${this.account}
653
+ .accountOptions=${this.accountOptions}
644
654
  .model=${this.model}
645
655
  .modelOptions=${this.modelOptions}
646
656
  .reasoningEffort=${this.reasoningEffort}
@@ -653,6 +663,9 @@ export class DeesHarnessChat extends DeesElement {
653
663
  @harness-attachments-change=${(event: CustomEvent<{ attachments: IHarnessAttachment[] }>) => {
654
664
  this.attachments = event.detail?.attachments ?? [];
655
665
  }}
666
+ @harness-account-change=${(event: CustomEvent<IHarnessAccountChangeDetail>) => {
667
+ if (event.detail?.account) this.account = event.detail.account;
668
+ }}
656
669
  @harness-model-change=${(event: CustomEvent<{ model?: string; reasoningEffort?: string }>) => {
657
670
  if (event.detail?.model) this.model = event.detail.model;
658
671
  if (event.detail?.reasoningEffort) this.reasoningEffort = event.detail.reasoningEffort;
@@ -29,15 +29,21 @@ export const demoFunc = () => html`
29
29
  @harness-input=${logEvent}
30
30
  @harness-attachments-change=${logEvent}
31
31
  @harness-attachment-error=${logEvent}
32
+ @harness-account-change=${logEvent}
32
33
  @harness-model-change=${logEvent}
33
34
  @harness-mode-change=${logEvent}
34
35
  >
35
36
  <div class="label">default with mode switcher + model controls (events log to console)</div>
36
37
  <dees-harness-composer
38
+ account="account-1"
37
39
  model="gpt-5.5"
38
40
  reasoningEffort="high"
39
41
  mode="Planning"
40
42
  .modeOptions=${['Planning', 'Building']}
43
+ .accountOptions=${[
44
+ { label: 'Personal · alex@example.com', value: 'account-1' },
45
+ { label: 'Team · alex@example.com', value: 'account-2' },
46
+ ]}
41
47
  .modelOptions=${['gpt-5.5', 'gpt-5', 'o4-mini']}
42
48
  .effortOptions=${['xhigh', 'high', 'medium', 'low']}
43
49
  ></dees-harness-composer>
@@ -9,7 +9,11 @@ import {
9
9
  cssManager,
10
10
  } from '@design.estate/dees-element';
11
11
  import { themeDefaultStyles } from '../../00theme.js';
12
- import type { IHarnessAttachment, IHarnessComposerSendDetail } from '../interfaces.js';
12
+ import type {
13
+ IHarnessAttachment,
14
+ IHarnessComposerOption,
15
+ IHarnessComposerSendDetail,
16
+ } from '../interfaces.js';
13
17
  import {
14
18
  harnessFileToBase64,
15
19
  harnessFormatBytes,
@@ -33,16 +37,19 @@ type TDropdownOption = { option: string; key: string };
33
37
  const toDropdownOptions = (values: string[]): TDropdownOption[] =>
34
38
  values.map((value) => ({ option: value, key: value }));
35
39
 
40
+ const toLabeledDropdownOptions = (values: IHarnessComposerOption[]): TDropdownOption[] =>
41
+ values.map((value) => ({ option: value.label, key: value.value }));
42
+
36
43
  /**
37
44
  * Prompt composer: autogrow textarea, attachments (picker / paste /
38
- * drag-drop with per-file and total size limits), optional model and
39
- * reasoning-effort dropdowns. Cmd/Ctrl+Enter sends. While busy, Stop is an
45
+ * drag-drop with per-file and total size limits), optional account, model,
46
+ * and reasoning-effort dropdowns. Cmd/Ctrl+Enter sends. While busy, Stop is an
40
47
  * additional button and Send stays available for queueing; an empty prompt
41
48
  * with queued messages offers "Steer now".
42
49
  *
43
50
  * Controlled component: `value` and `attachments` are owned by the host;
44
51
  * the composer emits `harness-input`, `harness-attachments-change`,
45
- * `harness-attachment-error`, `harness-model-change`, `harness-send`,
52
+ * `harness-attachment-error`, `harness-account-change`, `harness-model-change`, `harness-send`,
46
53
  * `harness-steer`, and `harness-abort` (all bubbling + composed).
47
54
  */
48
55
  @customElement('dees-harness-composer')
@@ -90,6 +97,12 @@ export class DeesHarnessComposer extends DeesElement {
90
97
  @property({ type: Number })
91
98
  accessor maxTotalAttachmentBytes: number = harnessMaxTotalAttachmentBytes;
92
99
 
100
+ @property()
101
+ accessor account: string = '';
102
+
103
+ @property({ attribute: false })
104
+ accessor accountOptions: IHarnessComposerOption[] = [];
105
+
93
106
  @property()
94
107
  accessor model: string = '';
95
108
 
@@ -122,6 +135,7 @@ export class DeesHarnessComposer extends DeesElement {
122
135
  :host {
123
136
  display: block;
124
137
  font-family: var(--dees-font-family);
138
+ container-type: inline-size;
125
139
  }
126
140
 
127
141
  .composer {
@@ -296,6 +310,12 @@ export class DeesHarnessComposer extends DeesElement {
296
310
  margin: 0;
297
311
  }
298
312
 
313
+ .accountSelect {
314
+ flex: 0 1 180px;
315
+ width: 180px;
316
+ min-width: 120px;
317
+ }
318
+
299
319
  /* Model identifiers are provider-qualified ("anthropic/claude-opus-5"),
300
320
  so the control grows into the free space instead of truncating,
301
321
  while still shrinking on narrow hosts. */
@@ -313,6 +333,25 @@ export class DeesHarnessComposer extends DeesElement {
313
333
  .spacer {
314
334
  flex: 1;
315
335
  }
336
+
337
+ @container (max-width: 760px) {
338
+ .controlsRow {
339
+ flex-wrap: wrap;
340
+ }
341
+
342
+ .modelControls {
343
+ order: 3;
344
+ flex: 1 0 100%;
345
+ flex-wrap: wrap;
346
+ }
347
+
348
+ .accountSelect,
349
+ .modelSelect {
350
+ flex: 1 1 140px;
351
+ width: auto;
352
+ min-width: 0;
353
+ }
354
+ }
316
355
  `,
317
356
  ];
318
357
 
@@ -386,9 +425,28 @@ export class DeesHarnessComposer extends DeesElement {
386
425
  </div>
387
426
  `
388
427
  : ''}
389
- ${this.showModelControls && (this.modelOptions.length || this.effortOptions.length)
428
+ ${this.showModelControls && (
429
+ this.accountOptions.length || this.modelOptions.length || this.effortOptions.length
430
+ )
390
431
  ? html`
391
432
  <div class="modelControls">
433
+ ${this.accountOptions.length
434
+ ? html`
435
+ <dees-input-dropdown
436
+ class="accountSelect"
437
+ .options=${toLabeledDropdownOptions(this.accountOptions)}
438
+ .selectedOption=${this.account
439
+ ? toLabeledDropdownOptions(this.accountOptions)
440
+ .find((option) => option.key === this.account) ?? null
441
+ : null}
442
+ .disabled=${this.disabled}
443
+ @selectedOption=${(event: CustomEvent<TDropdownOption>) => {
444
+ this.account = event.detail?.key ?? this.account;
445
+ this.emit('harness-account-change', { account: this.account });
446
+ }}
447
+ ></dees-input-dropdown>
448
+ `
449
+ : ''}
392
450
  ${this.modelOptions.length
393
451
  ? html`
394
452
  <dees-input-dropdown
@@ -589,6 +647,7 @@ export class DeesHarnessComposer extends DeesElement {
589
647
  const detail: IHarnessComposerSendDetail = {
590
648
  text: this.value,
591
649
  attachments: this.attachments,
650
+ account: this.account || undefined,
592
651
  model: this.model || undefined,
593
652
  reasoningEffort: this.reasoningEffort || undefined,
594
653
  };
@@ -31,6 +31,10 @@ import {
31
31
  harnessTruncate,
32
32
  } from '../harness.helpers.js';
33
33
  import { DeesHarnessMessageList } from '../dees-harness-message-list/dees-harness-message-list.js';
34
+ import type {
35
+ IDiffViewChangeDetail,
36
+ TDiffView,
37
+ } from '../../00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js';
34
38
  import '../dees-harness-content-blocks/dees-harness-content-blocks.js';
35
39
  import '../dees-harness-overflow-text.js';
36
40
  import '../../00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js';
@@ -91,12 +95,18 @@ export class DeesHarnessToolCard extends DeesElement {
91
95
  @state()
92
96
  accessor outcomeScanTone: 'success' | 'error' | null = null;
93
97
 
98
+ @state()
99
+ accessor diffViewOverride: TDiffView | null = null;
100
+
94
101
  private bodyResizeObserver: ResizeObserver | null = null;
95
102
  private observedBodyInner: Element | null = null;
96
103
  private fullscreenPortal: HTMLElementTagNameMap['dees-harness-tool-fullscreen'] | null = null;
97
104
  private disclosureFrame: number | null = null;
98
105
  private subtaskResultElement: HTMLElement | null = null;
99
106
  private subtaskResultResizeObserver: ResizeObserver | null = null;
107
+ private terminalTailFollowing = true;
108
+ private terminalTailReturnTimeout: ReturnType<typeof setTimeout> | null = null;
109
+ private terminalTailReturnDelayMs = 5000;
100
110
 
101
111
  public static styles = [
102
112
  themeDefaultStyles,
@@ -496,7 +506,7 @@ export class DeesHarnessToolCard extends DeesElement {
496
506
  border: 1px solid var(--dees-color-border-subtle);
497
507
  border-radius: var(--dees-radius-md);
498
508
  corner-shape: var(--dees-corner-shape);
499
- background: var(--dees-color-code-block-bg, #0c0c0e);
509
+ background: #0c0c0e;
500
510
  overflow: hidden;
501
511
  font-family: var(--dees-font-family-mono);
502
512
  font-size: var(--dees-font-control-size-sm, 12px);
@@ -506,7 +516,7 @@ export class DeesHarnessToolCard extends DeesElement {
506
516
  .harness-terminal-command {
507
517
  padding: var(--dees-spacing-xs) var(--dees-spacing-md);
508
518
  border-bottom: 1px solid var(--dees-color-border-subtle);
509
- color: var(--dees-color-text-primary);
519
+ color: #f5f5f7;
510
520
  font-weight: 600;
511
521
  white-space: pre-wrap;
512
522
  overflow-wrap: anywhere;
@@ -521,7 +531,7 @@ export class DeesHarnessToolCard extends DeesElement {
521
531
  margin: 0;
522
532
  padding: var(--dees-spacing-xs) var(--dees-spacing-md) var(--dees-spacing-sm);
523
533
  max-height: 100%;
524
- color: var(--dees-color-text-secondary);
534
+ color: #c7c7cc;
525
535
  white-space: pre-wrap;
526
536
  overflow-wrap: anywhere;
527
537
  }
@@ -535,7 +545,7 @@ export class DeesHarnessToolCard extends DeesElement {
535
545
  align-items: center;
536
546
  gap: var(--dees-spacing-xs);
537
547
  padding: var(--dees-spacing-xs) var(--dees-spacing-md) var(--dees-spacing-sm);
538
- color: var(--dees-color-text-muted);
548
+ color: #8e8e93;
539
549
  }
540
550
 
541
551
  .harness-terminal-running .cursor {
@@ -691,6 +701,8 @@ export class DeesHarnessToolCard extends DeesElement {
691
701
  const duration = harnessFormatDuration(call.startedAt, call.finishedAt);
692
702
  return html`
693
703
  <div class="harness-card status-${effectiveStatus} ${descriptor.destructive ? 'destructive' : ''} ${this.hasSubtaskPreview ? 'subtaskPreviewCard' : ''} ${this.hasSubtaskPreview && open ? 'subtaskPreviewOpen' : ''} ${this.returnFlash ? 'returnFlash' : ''} ${this.outcomeScanTone ? `outcomeScan outcomeScan-${this.outcomeScanTone}` : ''}"
704
+ @mouseenter=${this.cancelTerminalTailReturn}
705
+ @mouseleave=${this.scheduleTerminalTailReturn}
694
706
  @animationend=${(event: AnimationEvent) => {
695
707
  if (event.animationName === 'harness-card-return-flash') {
696
708
  this.endReturnFlash();
@@ -735,6 +747,7 @@ export class DeesHarnessToolCard extends DeesElement {
735
747
  <div
736
748
  class="bodyClipFrame"
737
749
  style=${this.uncapped || this.activeSubtaskPreview ? '' : `max-height: ${this.bodyCapPx}px;`}
750
+ @scroll=${this.handleBodyScroll}
738
751
  >
739
752
  <div class=${`body bodyInner ${this.hasLiveSubtaskPreview ? 'liveSubtaskBody' : ''}`}>
740
753
  ${this.renderBody(call)}
@@ -849,7 +862,13 @@ export class DeesHarnessToolCard extends DeesElement {
849
862
  public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
850
863
  super.willUpdate(changedProperties);
851
864
  const previousCall = changedProperties.get('call') as IHarnessToolCall | undefined;
852
- if (!previousCall || previousCall.id !== this.call.id) return;
865
+ if (!previousCall) return;
866
+ if (previousCall.id !== this.call.id) {
867
+ this.diffViewOverride = null;
868
+ this.terminalTailFollowing = true;
869
+ this.cancelTerminalTailReturn();
870
+ return;
871
+ }
853
872
  const previousStatus = harnessEffectiveToolStatus(previousCall);
854
873
  const currentStatus = harnessEffectiveToolStatus(this.call);
855
874
  const wasActive = previousStatus === 'pending' || previousStatus === 'running';
@@ -880,7 +899,15 @@ export class DeesHarnessToolCard extends DeesElement {
880
899
  return;
881
900
  }
882
901
  const bodyInner = this.shadowRoot?.querySelector('.bodyInner') ?? null;
883
- if (bodyInner === this.observedBodyInner) return;
902
+ if (bodyInner === this.observedBodyInner) {
903
+ if (
904
+ (changedProperties.has('call') || changedProperties.has('bodyCapPx'))
905
+ && this.terminalTailFollowing
906
+ ) {
907
+ this.scrollTerminalToTail();
908
+ }
909
+ return;
910
+ }
884
911
  this.bodyResizeObserver?.disconnect();
885
912
  this.observedBodyInner = bodyInner;
886
913
  if (!bodyInner) {
@@ -892,10 +919,46 @@ export class DeesHarnessToolCard extends DeesElement {
892
919
  if (!inner) return;
893
920
  // small slack so a few px never trigger the cap chrome
894
921
  this.bodyOverflowing = inner.scrollHeight > this.bodyCapPx + 24;
922
+ if (this.terminalTailFollowing) this.scrollTerminalToTail();
895
923
  });
896
924
  this.bodyResizeObserver.observe(bodyInner);
925
+ if (this.terminalTailFollowing) this.scrollTerminalToTail();
897
926
  }
898
927
 
928
+ private readonly handleBodyScroll = (eventArg: Event): void => {
929
+ if (this.descriptor.kind !== 'terminal' || this.uncapped) return;
930
+ const frame = eventArg.currentTarget as HTMLElement;
931
+ this.terminalTailFollowing = frame.scrollHeight - frame.scrollTop - frame.clientHeight <= 2;
932
+ if (this.terminalTailFollowing) this.cancelTerminalTailReturn();
933
+ };
934
+
935
+ private scrollTerminalToTail(): void {
936
+ if (this.descriptor.kind !== 'terminal' || this.uncapped || !this.isOpen) return;
937
+ const frame = this.shadowRoot?.querySelector<HTMLElement>('.bodyClipFrame');
938
+ if (!frame) return;
939
+ frame.scrollTop = frame.scrollHeight;
940
+ }
941
+
942
+ private readonly scheduleTerminalTailReturn = (): void => {
943
+ if (this.descriptor.kind !== 'terminal' || this.terminalTailFollowing || this.uncapped) return;
944
+ this.cancelTerminalTailReturn();
945
+ this.terminalTailReturnTimeout = setTimeout(() => {
946
+ this.terminalTailReturnTimeout = null;
947
+ this.terminalTailFollowing = true;
948
+ this.scrollTerminalToTail();
949
+ }, this.terminalTailReturnDelayMs);
950
+ };
951
+
952
+ private readonly cancelTerminalTailReturn = (): void => {
953
+ if (!this.terminalTailReturnTimeout) return;
954
+ clearTimeout(this.terminalTailReturnTimeout);
955
+ this.terminalTailReturnTimeout = null;
956
+ };
957
+
958
+ private readonly handleDiffViewChange = (eventArg: CustomEvent<IDiffViewChangeDetail>): void => {
959
+ this.diffViewOverride = eventArg.detail.diffView;
960
+ };
961
+
899
962
  private readonly syncSubtaskResultScroll = (): void => {
900
963
  const result = this.subtaskResultElement;
901
964
  if (!result) return;
@@ -943,6 +1006,7 @@ export class DeesHarnessToolCard extends DeesElement {
943
1006
  clearTimeout(this.outcomeScanFallbackTimeout);
944
1007
  this.outcomeScanFallbackTimeout = null;
945
1008
  }
1009
+ this.cancelTerminalTailReturn();
946
1010
  this.fullscreenPortal?.remove();
947
1011
  this.fullscreenPortal = null;
948
1012
  this.bodyResizeObserver?.disconnect();
@@ -1076,9 +1140,12 @@ export class DeesHarnessToolCard extends DeesElement {
1076
1140
  <dees-dataview-codebox
1077
1141
  .progLang=${options?.language ?? 'plaintext'}
1078
1142
  .filename=${options?.filename ?? ''}
1079
- .diffView=${options?.view ?? 'inline'}
1143
+ .diffView=${this.diffViewOverride ?? options?.view ?? 'inline'}
1144
+ .unifiedDiff=${options?.unifiedDiff ?? ''}
1145
+ .showDiffLineNumbers=${options?.showLineNumbers ?? true}
1080
1146
  .codeBefore=${before}
1081
1147
  .codeToDisplay=${after}
1148
+ @diff-view-change=${this.handleDiffViewChange}
1082
1149
  ></dees-dataview-codebox>
1083
1150
  `,
1084
1151
  monoBlock: (text, options) => {