@design.estate/dees-catalog 6.3.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.
package/readme.md CHANGED
@@ -1201,7 +1201,7 @@ Code display component with syntax highlighting and line numbers.
1201
1201
  ></dees-dataview-codebox>
1202
1202
  ```
1203
1203
 
1204
- Diff rendering includes word-level intraline change highlighting, unchanged-context folding with expandable "N unchanged lines" rows, `+added −removed` stats in the footer, and a "Copy Unified Diff" context-menu action. The copy button yields the after-state text. The dependency-free engine (`computeLineDiff`, `parseUnifiedDiff`, `foldContextRows`, `buildSplitRows`, `diffStats`, `toUnifiedDiff`) is exported for programmatic use.
1204
+ Diff rendering includes word-level intraline change highlighting, unchanged-context folding with expandable "N unchanged lines" rows, `+added −removed` stats in the footer, and a "Copy Unified Diff" context-menu action. The app bar switches between inline and split layouts and emits the bubbling, composed `diff-view-change` event with `IDiffViewChangeDetail` (`{ diffView: TDiffView }`). The exported `TDiffView` type is `'inline' | 'split'`. Split mode keeps a fixed center divider and gives each side an independent horizontal scroll position while the containing surface owns their shared vertical movement. Set `showDiffLineNumbers` to `false` when snippet inputs do not carry trustworthy source positions. The copy button yields the after-state text. The dependency-free engine (`computeLineDiff`, `parseUnifiedDiff`, `foldContextRows`, `buildSplitRows`, `diffStats`, `toUnifiedDiff`) is exported for programmatic use.
1205
1205
 
1206
1206
  #### `DeesDataviewStatusobject`
1207
1207
  Status display component for complex objects with nested status indicators.
@@ -1821,7 +1821,7 @@ One message: role accent, markdown (assistant default, `message.markdown` overri
1821
1821
 
1822
1822
  #### `DeesHarnessToolCard`
1823
1823
 
1824
- Tool-call card: shared chrome (icon, label, subtitle, status pill, duration, MCP badges) with a per-kind body. Kind resolution goes through `DeesHarnessToolRegistry` (see below); bodies for the seeded kinds — terminal, file-read, file-write (renders a before/after diff via `dees-dataview-codebox` when the input carries previous content), file-delete, dir-list, browser (inline screenshots), http, json, search, subtask, todo, mcp, unknown — live in `renderers.ts`. Overflowing subtitles use a 40px right-edge fade and reveal the full line on hover plus once after a two-second initial delay. Expand/collapse always uses a smooth disclosure transition. Initially closed bodies mount lazily on first expansion, then retain the same DOM and become inert plus `aria-hidden` while closed. Long outputs truncate at 6,000 chars with a "Show all" expander. Terminal and error cards open by default. Terminal calls may provide `exitCode` separately when `output` is one combined process stream; the renderer retains support for `{ stdout, stderr, exitCode }` output objects.
1824
+ Tool-call card: shared chrome (icon, label, subtitle, status pill, duration, MCP badges) with a per-kind body. Kind resolution goes through `DeesHarnessToolRegistry` (see below); bodies for the seeded kinds — terminal, file-read, file-write (renders a before/after diff via `dees-dataview-codebox` when the input carries previous content), file-delete, dir-list, browser (inline screenshots), http, json, search, subtask, todo, mcp, unknown — live in `renderers.ts`. File-write cards retain the user's inline/split choice while the same call updates. Numeric patch hunks preserve their source line numbers; patches and replacement snippets without coordinates hide gutters rather than presenting fabricated positions. Overflowing subtitles use a 40px right-edge fade and reveal the full line on hover plus once after a two-second initial delay. Expand/collapse always uses a smooth disclosure transition. Initially closed bodies mount lazily on first expansion, then retain the same DOM and become inert plus `aria-hidden` while closed. Long outputs truncate at 6,000 chars with a "Show all" expander. Terminal and error cards open by default. Terminal output uses a black inner log surface and follows the live tail after reaching the card-height cap. Scrolling upward detaches the tail; leaving the card returns to the bottom after five seconds, while re-entering cancels that pending return. Terminal calls may provide `exitCode` separately when `output` is one combined process stream; the renderer retains support for `{ stdout, stderr, exitCode }` output objects.
1825
1825
 
1826
1826
  Subtask calls may carry `subtask: IHarnessSubtaskStream`. While a projected child is busy, its Agent Brief is the leading item inside the same independently scrolling child transcript as its messages, permissions, questions, and status; no extra pinned or boxed brief sits above it. Dynamic 40px edge fades appear at the top only when content exists above and at the bottom only when content exists below, while wheel/touch scrolling remains available without visible scrollbars. Loading and unavailable projections reserve the same stable space. Once the child becomes idle or errored, the live transcript is removed from the card body and the disclosure cleanly renders Agent Brief followed by a scrollbar-free scrolling Result with the same position-aware edge fades; the full transcript remains available through the final bottom-positioned Open subagent chat action. `subtask.sessionId` is authoritative for drill-in when a projection exists; calls without a projection continue to use the existing `childSessionId` field. Authoritatively busy previews open by default and every expanded preview is bounded by `--dees-harness-active-subtask-height` (default `320px`), including its completion transition. The user's local expand/collapse choice wins. Completed subtask cards stay in their subtask-only chronological grid and untouched cards collapse individually without a grid-level summary. Hidden retained previews pause auto-follow observation without unmounting their DOM and catch up to the newest child content when reopened. Set `allowSubtaskStreams` false on a direct tool-card consumer to suppress the preview. The host remains responsible for hydration, transfer limits, authorization, gap recovery, and updating the shared objects; `truncated` projections visibly disclose omitted earlier messages.
1827
1827
 
@@ -1854,6 +1854,8 @@ DeesHarnessToolRegistry.default.register({
1854
1854
  });
1855
1855
  ```
1856
1856
 
1857
+ Custom tool renderers receive `IHarnessToolRenderHelpers`. Its `diffBlock(before, after, options)` helper accepts `language`, `filename`, and `view`, plus `unifiedDiff` for a positioned patch and `showLineNumbers` to suppress untrustworthy gutters.
1858
+
1857
1859
  `server__tool` names resolve automatically to the MCP kind with server + annotation badges (`readOnlyHint` → success, `destructiveHint` → error, `openWorldHint` → warning). Per-element isolation: assign a forked registry to `.toolRegistry`.
1858
1860
 
1859
1861
  Descriptors may set `layout: 'compact' | 'full-row' | 'subtask'` to control transcript grid packing; omitted custom descriptors default to `compact`. The built-in terminal and file-write descriptors are `full-row`, while the built-in subtask descriptor uses the isolated `subtask` lane.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '6.3.0',
6
+ version: '6.4.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
  }
@@ -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')) {
@@ -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) => {
@@ -34,6 +34,8 @@ interface IApplyPatchFile {
34
34
  path: string;
35
35
  before: string;
36
36
  after: string;
37
+ unifiedDiff?: string;
38
+ lineNumbersKnown: boolean;
37
39
  }
38
40
 
39
41
  /**
@@ -43,14 +45,27 @@ interface IApplyPatchFile {
43
45
  */
44
46
  export const parseApplyPatch = (patchText: string): IApplyPatchFile[] => {
45
47
  const files: IApplyPatchFile[] = [];
46
- let current: { op: IApplyPatchFile['op']; path: string; before: string[]; after: string[] } | null = null;
48
+ let current: {
49
+ op: IApplyPatchFile['op'];
50
+ path: string;
51
+ before: string[];
52
+ after: string[];
53
+ patch: string[];
54
+ sawHunk: boolean;
55
+ lineNumbersKnown: boolean;
56
+ } | null = null;
47
57
  const flush = (): void => {
48
58
  if (!current) return;
59
+ const unifiedDiff = current.op === 'update' && current.sawHunk && current.lineNumbersKnown
60
+ ? [`--- a/${current.path}`, `+++ b/${current.path}`, ...current.patch].join('\n')
61
+ : undefined;
49
62
  files.push({
50
63
  op: current.op,
51
64
  path: current.path,
52
65
  before: current.before.join('\n'),
53
66
  after: current.after.join('\n'),
67
+ unifiedDiff,
68
+ lineNumbersKnown: Boolean(unifiedDiff),
54
69
  });
55
70
  current = null;
56
71
  };
@@ -63,15 +78,31 @@ export const parseApplyPatch = (patchText: string): IApplyPatchFile[] => {
63
78
  path: header[2].trim(),
64
79
  before: [],
65
80
  after: [],
81
+ patch: [],
82
+ sawHunk: false,
83
+ lineNumbersKnown: true,
66
84
  };
67
85
  continue;
68
86
  }
69
- if (/^\*\*\* (Begin|End) Patch/.test(line) || /^\*\*\* Move to: /.test(line)) {
87
+ if (
88
+ /^\*\*\* (Begin|End) Patch/.test(line)
89
+ || line === '*** End of File'
90
+ || /^\*\*\* Move to: /.test(line)
91
+ ) {
70
92
  if (/End Patch/.test(line)) flush();
71
93
  continue;
72
94
  }
73
95
  if (!current) continue;
74
- if (line.startsWith('@@')) continue;
96
+ if (line.startsWith('@@')) {
97
+ current.sawHunk = true;
98
+ if (/^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(line)) {
99
+ current.patch.push(line);
100
+ } else {
101
+ current.lineNumbersKnown = false;
102
+ }
103
+ continue;
104
+ }
105
+ current.patch.push(line);
75
106
  if (line.startsWith('+')) {
76
107
  current.after.push(line.slice(1));
77
108
  } else if (line.startsWith('-')) {
@@ -154,6 +185,8 @@ const renderFileWrite = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelp
154
185
  language: patchLanguage,
155
186
  filename: patchFilename,
156
187
  view: 'split',
188
+ unifiedDiff: file.unifiedDiff,
189
+ showLineNumbers: file.lineNumbersKnown,
157
190
  });
158
191
  }
159
192
  if (file.op === 'add') {
@@ -180,7 +213,13 @@ const renderFileWrite = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelp
180
213
  const filename = path.split('/').pop() || undefined;
181
214
  const language = harnessLanguageForPath(path);
182
215
  if (previousContent) {
183
- return helpers.diffBlock(previousContent, newContent, { language, filename, view: 'split' });
216
+ const isStringReplacement = input.oldString !== undefined || input.old_string !== undefined;
217
+ return helpers.diffBlock(previousContent, newContent, {
218
+ language,
219
+ filename,
220
+ view: 'split',
221
+ showLineNumbers: !isStringReplacement,
222
+ });
184
223
  }
185
224
  if (newContent) {
186
225
  return helpers.codeBlock(newContent, language, filename);
@@ -11,7 +11,13 @@ export interface IHarnessToolRenderHelpers {
11
11
  /** highlighted code block (dees-dataview-codebox) with optional filename */
12
12
  codeBlock(code: string, language?: string, filename?: string): TemplateResult;
13
13
  /** before/after diff (dees-dataview-codebox diff mode) */
14
- diffBlock(before: string, after: string, options?: { language?: string; filename?: string; view?: 'inline' | 'split' }): TemplateResult;
14
+ diffBlock(before: string, after: string, options?: {
15
+ language?: string;
16
+ filename?: string;
17
+ view?: 'inline' | 'split';
18
+ unifiedDiff?: string;
19
+ showLineNumbers?: boolean;
20
+ }): TemplateResult;
15
21
  /** plain monospace block with truncation + expand */
16
22
  monoBlock(text: string, options?: { maxChars?: number; tone?: 'default' | 'error' }): TemplateResult;
17
23
  /** two-column key/value summary */