@design.estate/dees-catalog 6.7.1 → 6.8.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 (30) hide show
  1. package/dist_bundle/bundle.js +1025 -99
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-harness/dees-harness-message/dees-harness-message.d.ts +2 -1
  4. package/dist_ts_web/elements/00group-harness/dees-harness-message/dees-harness-message.js +32 -3
  5. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.d.ts +92 -6
  6. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.js +921 -49
  7. package/dist_ts_web/elements/00group-harness/dees-harness-permission-card/dees-harness-permission-card.d.ts +5 -1
  8. package/dist_ts_web/elements/00group-harness/dees-harness-permission-card/dees-harness-permission-card.js +42 -7
  9. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.d.ts +6 -1
  10. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.js +53 -14
  11. package/dist_ts_web/elements/00group-harness/dees-harness-reasoning/dees-harness-reasoning.d.ts +2 -1
  12. package/dist_ts_web/elements/00group-harness/dees-harness-reasoning/dees-harness-reasoning.js +13 -3
  13. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.js +9 -3
  14. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.d.ts +6 -1
  15. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +124 -6
  16. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +30 -0
  17. package/dist_ts_web/elements/00group-harness/toolregistry.d.ts +2 -0
  18. package/dist_ts_web/elements/00group-harness/toolregistry.js +7 -1
  19. package/package.json +1 -1
  20. package/readme.md +4 -2
  21. package/ts_web/00_commitinfo_data.ts +1 -1
  22. package/ts_web/elements/00group-harness/dees-harness-message/dees-harness-message.ts +28 -1
  23. package/ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.ts +943 -51
  24. package/ts_web/elements/00group-harness/dees-harness-permission-card/dees-harness-permission-card.ts +37 -8
  25. package/ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.ts +40 -12
  26. package/ts_web/elements/00group-harness/dees-harness-reasoning/dees-harness-reasoning.ts +6 -2
  27. package/ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts +8 -2
  28. package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +108 -4
  29. package/ts_web/elements/00group-harness/interfaces.ts +34 -0
  30. package/ts_web/elements/00group-harness/toolregistry.ts +7 -0
@@ -10,7 +10,11 @@ import {
10
10
  cssManager,
11
11
  } from '@design.estate/dees-element';
12
12
  import { themeDefaultStyles } from '../../00theme.js';
13
- import type { IHarnessPermissionRequest, THarnessPermissionResponse } from '../interfaces.js';
13
+ import type {
14
+ IHarnessPermissionRequest,
15
+ IHarnessPermissionViewState,
16
+ THarnessPermissionResponse,
17
+ } from '../interfaces.js';
14
18
  import { harnessStringifyValue } from '../harness.helpers.js';
15
19
  import '../../00group-button/dees-button/dees-button.js';
16
20
  import '../../00group-feedback/dees-badge/dees-badge.js';
@@ -45,6 +49,12 @@ export class DeesHarnessPermissionCard extends DeesElement {
45
49
  @property({ type: Boolean })
46
50
  accessor busy: boolean = false;
47
51
 
52
+ @property({ attribute: false })
53
+ accessor viewState: IHarnessPermissionViewState | undefined = undefined;
54
+
55
+ @property({ type: Boolean })
56
+ accessor suppressEntry: boolean = false;
57
+
48
58
  @state()
49
59
  accessor remember: boolean = false;
50
60
 
@@ -53,7 +63,11 @@ export class DeesHarnessPermissionCard extends DeesElement {
53
63
  accessor answered: THarnessPermissionResponse | undefined = undefined;
54
64
 
55
65
  public get effectiveResponse(): THarnessPermissionResponse | undefined {
56
- return this.request.response ?? this.answered;
66
+ return this.request.response ?? this.answered ?? this.viewState?.answered;
67
+ }
68
+
69
+ private get effectiveRemember(): boolean {
70
+ return this.remember || this.viewState?.remember === true;
57
71
  }
58
72
 
59
73
  public static styles = [
@@ -82,6 +96,10 @@ export class DeesHarnessPermissionCard extends DeesElement {
82
96
  }
83
97
  }
84
98
 
99
+ :host(.no-entry) {
100
+ animation: none;
101
+ }
102
+
85
103
  @media (prefers-reduced-motion: reduce) {
86
104
  :host {
87
105
  animation: none;
@@ -183,7 +201,7 @@ export class DeesHarnessPermissionCard extends DeesElement {
183
201
  : ''}
184
202
  <div class="actions">
185
203
  ${this.effectiveResponse
186
- ? html`<span class="answeredNote">Answered: ${this.effectiveResponse}${this.remember ? ' (remembered)' : ''}</span>`
204
+ ? html`<span class="answeredNote">Answered: ${this.effectiveResponse}${this.effectiveRemember ? ' (remembered)' : ''}</span>`
187
205
  : html`
188
206
  <dees-button type="accent" size="sm" ?disabled=${disabled} @clicked=${() => this.respond('once')}>
189
207
  Allow Once
@@ -197,11 +215,12 @@ export class DeesHarnessPermissionCard extends DeesElement {
197
215
  <label class="rememberLabel">
198
216
  <input
199
217
  type="checkbox"
200
- .checked=${this.remember}
218
+ .checked=${this.effectiveRemember}
201
219
  ?disabled=${disabled}
202
- @change=${(event: Event) => {
203
- this.remember = (event.currentTarget as HTMLInputElement).checked;
204
- }}
220
+ @change=${(event: Event) => {
221
+ this.remember = (event.currentTarget as HTMLInputElement).checked;
222
+ if (this.viewState) this.viewState.remember = this.remember;
223
+ }}
205
224
  />
206
225
  remember for this session
207
226
  </label>
@@ -213,13 +232,18 @@ export class DeesHarnessPermissionCard extends DeesElement {
213
232
 
214
233
  private respond(response: THarnessPermissionResponse): void {
215
234
  if (this.busy || this.effectiveResponse !== undefined) return;
235
+ const remember = this.effectiveRemember || response === 'always';
216
236
  this.answered = response;
237
+ if (this.viewState) {
238
+ this.viewState.answered = response;
239
+ this.viewState.remember = remember;
240
+ }
217
241
  this.dispatchEvent(
218
242
  new CustomEvent('harness-permission-response', {
219
243
  detail: {
220
244
  requestId: this.request.id,
221
245
  response,
222
- remember: this.remember || response === 'always',
246
+ remember,
223
247
  request: this.request,
224
248
  },
225
249
  bubbles: true,
@@ -227,4 +251,9 @@ export class DeesHarnessPermissionCard extends DeesElement {
227
251
  }),
228
252
  );
229
253
  }
254
+
255
+ public firstUpdated(changedPropertiesArg: Map<string, unknown>): void {
256
+ super.firstUpdated(changedPropertiesArg);
257
+ if (this.suppressEntry) this.classList.add('no-entry');
258
+ }
230
259
  }
@@ -10,7 +10,7 @@ import {
10
10
  cssManager,
11
11
  } from '@design.estate/dees-element';
12
12
  import { themeDefaultStyles } from '../../00theme.js';
13
- import type { IHarnessQuestionRequest } from '../interfaces.js';
13
+ import type { IHarnessQuestionRequest, IHarnessQuestionViewState } from '../interfaces.js';
14
14
  import '../../00group-button/dees-button/dees-button.js';
15
15
  import '../../00group-utility/dees-icon/dees-icon.js';
16
16
 
@@ -38,6 +38,12 @@ export class DeesHarnessQuestionCard extends DeesElement {
38
38
  @property({ type: Boolean })
39
39
  accessor busy: boolean = false;
40
40
 
41
+ @property({ attribute: false })
42
+ accessor viewState: IHarnessQuestionViewState | undefined = undefined;
43
+
44
+ @property({ type: Boolean })
45
+ accessor suppressEntry: boolean = false;
46
+
41
47
  /** local optimistic answer; request.response (host echo) takes precedence */
42
48
  @state()
43
49
  accessor answered: string[] | undefined = undefined;
@@ -49,7 +55,17 @@ export class DeesHarnessQuestionCard extends DeesElement {
49
55
  accessor customText: string = '';
50
56
 
51
57
  public get effectiveResponse(): string[] | undefined {
52
- return this.request.response ?? this.answered;
58
+ return this.request.response ?? this.answered ?? this.viewState?.answered;
59
+ }
60
+
61
+ private get effectiveSelected(): ReadonlySet<string> {
62
+ return this.selected.size > 0
63
+ ? this.selected
64
+ : new Set(this.viewState?.selected ?? []);
65
+ }
66
+
67
+ private get effectiveCustomText(): string {
68
+ return this.customText || this.viewState?.customText || '';
53
69
  }
54
70
 
55
71
  public static styles = [
@@ -72,6 +88,10 @@ export class DeesHarnessQuestionCard extends DeesElement {
72
88
  }
73
89
  }
74
90
 
91
+ :host(.no-entry) {
92
+ animation: none;
93
+ }
94
+
75
95
  @media (prefers-reduced-motion: reduce) {
76
96
  :host {
77
97
  animation: none;
@@ -230,7 +250,7 @@ export class DeesHarnessQuestionCard extends DeesElement {
230
250
  <div class="options" role=${request.multiSelect ? 'group' : 'radiogroup'}>
231
251
  ${request.options.map((option) => html`
232
252
  <button
233
- class="option ${this.selected.has(option.label) ? 'selected' : ''}"
253
+ class="option ${this.effectiveSelected.has(option.label) ? 'selected' : ''}"
234
254
  type="button"
235
255
  ?disabled=${disabled}
236
256
  @click=${() => this.handleOptionClick(option.label)}
@@ -249,23 +269,24 @@ export class DeesHarnessQuestionCard extends DeesElement {
249
269
  <div class="customRow">
250
270
  <input
251
271
  placeholder="Answer in your own words…"
252
- .value=${this.customText}
272
+ .value=${this.effectiveCustomText}
253
273
  ?disabled=${disabled}
254
274
  @input=${(event: Event) => {
255
275
  this.customText = (event.currentTarget as HTMLInputElement).value;
276
+ if (this.viewState) this.viewState.customText = this.customText;
256
277
  }}
257
278
  @keydown=${(event: KeyboardEvent) => {
258
- if (event.key === 'Enter' && this.customText.trim()) {
259
- this.respond([...this.selected, this.customText.trim()]);
279
+ if (event.key === 'Enter' && this.effectiveCustomText.trim()) {
280
+ this.respond([...this.effectiveSelected, this.effectiveCustomText.trim()]);
260
281
  }
261
282
  }}
262
283
  />
263
284
  <dees-button
264
285
  size="sm"
265
- ?disabled=${disabled || (!this.customText.trim() && !this.selected.size)}
286
+ ?disabled=${disabled || (!this.effectiveCustomText.trim() && !this.effectiveSelected.size)}
266
287
  @clicked=${() => {
267
- const answers = [...this.selected];
268
- if (this.customText.trim()) answers.push(this.customText.trim());
288
+ const answers = [...this.effectiveSelected];
289
+ if (this.effectiveCustomText.trim()) answers.push(this.effectiveCustomText.trim());
269
290
  this.respond(answers);
270
291
  }}
271
292
  >Send Answer</dees-button>
@@ -278,8 +299,8 @@ export class DeesHarnessQuestionCard extends DeesElement {
278
299
  <dees-button
279
300
  type="accent"
280
301
  size="sm"
281
- ?disabled=${disabled || !this.selected.size}
282
- @clicked=${() => this.respond([...this.selected])}
302
+ ?disabled=${disabled || !this.effectiveSelected.size}
303
+ @clicked=${() => this.respond([...this.effectiveSelected])}
283
304
  >Confirm</dees-button>
284
305
  </div>
285
306
  `
@@ -292,10 +313,11 @@ export class DeesHarnessQuestionCard extends DeesElement {
292
313
  private handleOptionClick(label: string): void {
293
314
  if (this.busy || this.effectiveResponse !== undefined) return;
294
315
  if (this.request.multiSelect) {
295
- const next = new Set(this.selected);
316
+ const next = new Set(this.effectiveSelected);
296
317
  if (next.has(label)) next.delete(label);
297
318
  else next.add(label);
298
319
  this.selected = next;
320
+ if (this.viewState) this.viewState.selected = [...next];
299
321
  return;
300
322
  }
301
323
  this.respond([label]);
@@ -304,6 +326,7 @@ export class DeesHarnessQuestionCard extends DeesElement {
304
326
  private respond(answers: string[]): void {
305
327
  if (this.busy || this.effectiveResponse !== undefined || !answers.length) return;
306
328
  this.answered = answers;
329
+ if (this.viewState) this.viewState.answered = answers;
307
330
  this.dispatchEvent(
308
331
  new CustomEvent('harness-question-response', {
309
332
  detail: {
@@ -316,4 +339,9 @@ export class DeesHarnessQuestionCard extends DeesElement {
316
339
  }),
317
340
  );
318
341
  }
342
+
343
+ public firstUpdated(changedPropertiesArg: Map<string, unknown>): void {
344
+ super.firstUpdated(changedPropertiesArg);
345
+ if (this.suppressEntry) this.classList.add('no-entry');
346
+ }
319
347
  }
@@ -10,7 +10,7 @@ import {
10
10
  cssManager,
11
11
  } from '@design.estate/dees-element';
12
12
  import { themeDefaultStyles } from '../../00theme.js';
13
- import type { IHarnessReasoningPart } from '../interfaces.js';
13
+ import type { IHarnessMessageViewState, IHarnessReasoningPart } from '../interfaces.js';
14
14
  import { harnessFormatDuration } from '../harness.helpers.js';
15
15
  import '../../00group-utility/dees-icon/dees-icon.js';
16
16
 
@@ -37,6 +37,9 @@ export class DeesHarnessReasoning extends DeesElement {
37
37
  @property({ attribute: false })
38
38
  accessor reasoningParts: IHarnessReasoningPart[] = [];
39
39
 
40
+ @property({ attribute: false })
41
+ accessor viewState: IHarnessMessageViewState | undefined = undefined;
42
+
40
43
  @state()
41
44
  accessor expanded: boolean | undefined = undefined;
42
45
 
@@ -178,7 +181,7 @@ export class DeesHarnessReasoning extends DeesElement {
178
181
  }
179
182
 
180
183
  public get isOpen(): boolean {
181
- return this.expanded ?? this.isStreaming;
184
+ return this.expanded ?? this.viewState?.reasoningExpanded ?? this.isStreaming;
182
185
  }
183
186
 
184
187
  /** All parts joined into the one full trace the expanded body shows. */
@@ -224,6 +227,7 @@ export class DeesHarnessReasoning extends DeesElement {
224
227
  aria-expanded=${this.isOpen ? 'true' : 'false'}
225
228
  @click=${() => {
226
229
  this.expanded = !this.isOpen;
230
+ if (this.viewState) this.viewState.reasoningExpanded = this.expanded;
227
231
  }}
228
232
  >
229
233
  <span class="chevron ${this.isOpen ? 'open' : ''}">
@@ -694,8 +694,14 @@ export class DeesHarnessSessionSidebar extends DeesElement {
694
694
  <span class="sectionHeadingText">${labelArg}</span>
695
695
  ${summaryArg ? html`<span class="sectionSummary">${summaryArg}</span>` : ''}
696
696
  </button>
697
- <div id=${contentId} class="sectionContent" ?hidden=${!expanded}>
698
- ${contentArg}
697
+ <div
698
+ id=${contentId}
699
+ class="sectionContent"
700
+ role="region"
701
+ aria-hidden=${String(!expanded)}
702
+ ?hidden=${!expanded}
703
+ >
704
+ ${expanded ? contentArg : ''}
699
705
  </div>
700
706
  </section>
701
707
  `;
@@ -11,6 +11,7 @@ import {
11
11
  } from '@design.estate/dees-element';
12
12
  import { themeDefaultStyles } from '../../00theme.js';
13
13
  import type {
14
+ IHarnessMessageViewState,
14
15
  IHarnessToolCall,
15
16
  THarnessContentBlock,
16
17
  THarnessMessageStreamDelta,
@@ -69,6 +70,9 @@ export class DeesHarnessToolCard extends DeesElement {
69
70
  @property({ type: Boolean })
70
71
  accessor allowSubtaskStreams: boolean = true;
71
72
 
73
+ @property({ attribute: false })
74
+ accessor viewState: IHarnessMessageViewState | undefined = undefined;
75
+
72
76
  @state()
73
77
  accessor expanded: boolean | undefined = undefined;
74
78
 
@@ -102,9 +106,12 @@ export class DeesHarnessToolCard extends DeesElement {
102
106
  private observedBodyInner: Element | null = null;
103
107
  private fullscreenPortal: HTMLElementTagNameMap['dees-harness-tool-fullscreen'] | null = null;
104
108
  private disclosureFrame: number | null = null;
109
+ private returnScrollFrame: number | null = null;
105
110
  private subtaskResultElement: HTMLElement | null = null;
106
111
  private subtaskResultResizeObserver: ResizeObserver | null = null;
107
112
  private terminalTailFollowing = true;
113
+ private terminalScrollHydrated = false;
114
+ private terminalScrollHydrationFrame: number | null = null;
108
115
  private terminalTailReturnTimeout: ReturnType<typeof setTimeout> | null = null;
109
116
  private terminalTailReturnDelayMs = 5000;
110
117
 
@@ -669,6 +676,7 @@ export class DeesHarnessToolCard extends DeesElement {
669
676
 
670
677
  public get isOpen(): boolean {
671
678
  if (this.expanded !== undefined) return this.expanded;
679
+ if (this.viewState?.toolExpanded !== undefined) return this.viewState.toolExpanded;
672
680
  const descriptor = this.descriptor;
673
681
  if (descriptor.kind === 'subtask' && this.call.subtask) {
674
682
  return this.allowSubtaskStreams
@@ -778,12 +786,14 @@ export class DeesHarnessToolCard extends DeesElement {
778
786
  if (this.disclosureFrame !== null) cancelAnimationFrame(this.disclosureFrame);
779
787
  this.disclosureFrame = null;
780
788
  this.expanded = false;
789
+ if (this.viewState) this.viewState.toolExpanded = false;
781
790
  return;
782
791
  }
783
792
  if (this.bodyMounted) {
784
793
  if (this.disclosureFrame !== null) cancelAnimationFrame(this.disclosureFrame);
785
794
  this.disclosureFrame = null;
786
795
  this.expanded = true;
796
+ if (this.viewState) this.viewState.toolExpanded = true;
787
797
  return;
788
798
  }
789
799
  this.bodyMounted = true;
@@ -795,12 +805,18 @@ export class DeesHarnessToolCard extends DeesElement {
795
805
  if (!this.isConnected || this.expanded !== false) return;
796
806
  this.shadowRoot?.querySelector<HTMLElement>('.bodyDisclosure')?.getBoundingClientRect();
797
807
  this.expanded = true;
808
+ if (this.viewState) this.viewState.toolExpanded = true;
798
809
  });
799
810
  });
800
811
  };
801
812
 
802
813
  private readonly openFullscreen = (): void => {
803
814
  if (this.fullscreenPortal || this.hasSubtaskPreview) return;
815
+ this.dispatchEvent(new CustomEvent('harness-card-retention', {
816
+ detail: { active: true },
817
+ bubbles: true,
818
+ composed: true,
819
+ }));
804
820
  // Appended to document.body: content-visibility containment in the
805
821
  // transcript would trap a position: fixed layer inside this card.
806
822
  const portal = document.createElement('dees-harness-tool-fullscreen');
@@ -830,7 +846,10 @@ export class DeesHarnessToolCard extends DeesElement {
830
846
  // reduced-motion skips the animation, so animationend never fires there
831
847
  if (this.flashFallbackTimeout) clearTimeout(this.flashFallbackTimeout);
832
848
  this.flashFallbackTimeout = setTimeout(() => this.endReturnFlash(), 900);
833
- requestAnimationFrame(() => {
849
+ if (this.returnScrollFrame !== null) cancelAnimationFrame(this.returnScrollFrame);
850
+ this.returnScrollFrame = requestAnimationFrame(() => {
851
+ this.returnScrollFrame = null;
852
+ if (!this.isConnected) return;
834
853
  this.scrollIntoView({ block: 'center', behavior: 'smooth' });
835
854
  });
836
855
  };
@@ -842,6 +861,11 @@ export class DeesHarnessToolCard extends DeesElement {
842
861
  }
843
862
  if (!this.returnFlash) return;
844
863
  this.returnFlash = false;
864
+ this.dispatchEvent(new CustomEvent('harness-card-retention', {
865
+ detail: { active: false },
866
+ bubbles: true,
867
+ composed: true,
868
+ }));
845
869
  this.dispatchEvent(new CustomEvent('harness-card-flash', {
846
870
  detail: { active: false },
847
871
  bubbles: true,
@@ -865,11 +889,44 @@ export class DeesHarnessToolCard extends DeesElement {
865
889
 
866
890
  public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
867
891
  super.willUpdate(changedProperties);
892
+ if (changedProperties.has('viewState') || changedProperties.has('call')) {
893
+ this.terminalScrollHydrated = false;
894
+ if (this.terminalScrollHydrationFrame !== null) {
895
+ cancelAnimationFrame(this.terminalScrollHydrationFrame);
896
+ this.terminalScrollHydrationFrame = null;
897
+ }
898
+ if (this.expanded === undefined && this.viewState?.toolExpanded !== undefined) {
899
+ this.expanded = this.viewState.toolExpanded;
900
+ }
901
+ if (!this.outputExpanded && this.viewState?.toolOutputExpanded === true) this.outputExpanded = true;
902
+ if (this.diffViewOverride === null && this.viewState?.toolDiffView !== undefined) {
903
+ this.diffViewOverride = this.viewState.toolDiffView;
904
+ }
905
+ if (this.viewState?.toolTerminalTailFollowing !== undefined) {
906
+ this.terminalTailFollowing = this.viewState.toolTerminalTailFollowing;
907
+ }
908
+ if (this.viewState) {
909
+ const subtask = this.call.subtask;
910
+ const ownsLiveTranscript = Boolean(
911
+ this.hasLiveSubtaskPreview && subtask?.previewState === 'ready',
912
+ );
913
+ if (!subtask || !ownsLiveTranscript) {
914
+ delete this.viewState.subtaskTranscript;
915
+ delete this.viewState.subtaskTranscriptSessionId;
916
+ } else if (
917
+ this.viewState.subtaskTranscriptSessionId !== subtask.sessionId
918
+ || !this.viewState.subtaskTranscript
919
+ ) {
920
+ this.viewState.subtaskTranscriptSessionId = subtask.sessionId;
921
+ this.viewState.subtaskTranscript = { messages: {}, permissions: {}, questions: {} };
922
+ }
923
+ }
924
+ }
868
925
  const previousCall = changedProperties.get('call') as IHarnessToolCall | undefined;
869
926
  if (!previousCall) return;
870
927
  if (previousCall.id !== this.call.id) {
871
- this.diffViewOverride = null;
872
- this.terminalTailFollowing = true;
928
+ this.diffViewOverride = this.viewState?.toolDiffView ?? null;
929
+ this.terminalTailFollowing = this.viewState?.toolTerminalTailFollowing ?? true;
873
930
  this.cancelTerminalTailReturn();
874
931
  return;
875
932
  }
@@ -891,6 +948,10 @@ export class DeesHarnessToolCard extends DeesElement {
891
948
  public updated(changedProperties: Map<PropertyKey, unknown>): void {
892
949
  super.updated(changedProperties);
893
950
  if (!this.isConnected) return;
951
+ this.terminalScrollHydrationFrame ??= requestAnimationFrame(() => {
952
+ this.terminalScrollHydrationFrame = null;
953
+ if (this.isConnected) this.terminalScrollHydrated = true;
954
+ });
894
955
  if (this.isOpen && !this.bodyMounted) this.bodyMounted = true;
895
956
  if (changedProperties.has('call') && this.fullscreenPortal) {
896
957
  this.fullscreenPortal.call = this.enrichedCall;
@@ -927,12 +988,21 @@ export class DeesHarnessToolCard extends DeesElement {
927
988
  });
928
989
  this.bodyResizeObserver.observe(bodyInner);
929
990
  if (this.terminalTailFollowing) this.scrollTerminalToTail();
991
+ else if (this.viewState?.toolTerminalScrollTop !== undefined) {
992
+ const frame = this.shadowRoot?.querySelector<HTMLElement>('.bodyClipFrame');
993
+ if (frame) this.setTerminalScrollTop(frame, this.viewState.toolTerminalScrollTop);
994
+ }
930
995
  }
931
996
 
932
997
  private readonly handleBodyScroll = (eventArg: Event): void => {
933
998
  if (this.descriptor.kind !== 'terminal' || this.uncapped) return;
999
+ if (!this.terminalScrollHydrated) return;
934
1000
  const frame = eventArg.currentTarget as HTMLElement;
935
1001
  this.terminalTailFollowing = frame.scrollHeight - frame.scrollTop - frame.clientHeight <= 2;
1002
+ if (this.viewState) {
1003
+ this.viewState.toolTerminalTailFollowing = this.terminalTailFollowing;
1004
+ this.viewState.toolTerminalScrollTop = frame.scrollTop;
1005
+ }
936
1006
  if (this.terminalTailFollowing) this.cancelTerminalTailReturn();
937
1007
  };
938
1008
 
@@ -940,7 +1010,16 @@ export class DeesHarnessToolCard extends DeesElement {
940
1010
  if (this.descriptor.kind !== 'terminal' || this.uncapped || !this.isOpen) return;
941
1011
  const frame = this.shadowRoot?.querySelector<HTMLElement>('.bodyClipFrame');
942
1012
  if (!frame) return;
943
- frame.scrollTop = frame.scrollHeight;
1013
+ if (this.viewState?.toolTerminalTailFollowing === false) {
1014
+ this.terminalTailFollowing = false;
1015
+ this.setTerminalScrollTop(frame, this.viewState.toolTerminalScrollTop ?? frame.scrollTop);
1016
+ return;
1017
+ }
1018
+ this.setTerminalScrollTop(frame, frame.scrollHeight);
1019
+ }
1020
+
1021
+ private setTerminalScrollTop(frameArg: HTMLElement, scrollTopArg: number): void {
1022
+ frameArg.scrollTop = scrollTopArg;
944
1023
  }
945
1024
 
946
1025
  private readonly scheduleTerminalTailReturn = (): void => {
@@ -949,7 +1028,12 @@ export class DeesHarnessToolCard extends DeesElement {
949
1028
  this.terminalTailReturnTimeout = setTimeout(() => {
950
1029
  this.terminalTailReturnTimeout = null;
951
1030
  this.terminalTailFollowing = true;
1031
+ if (this.viewState) this.viewState.toolTerminalTailFollowing = true;
952
1032
  this.scrollTerminalToTail();
1033
+ if (this.viewState) {
1034
+ const frame = this.shadowRoot?.querySelector<HTMLElement>('.bodyClipFrame');
1035
+ if (frame) this.viewState.toolTerminalScrollTop = frame.scrollTop;
1036
+ }
953
1037
  }, this.terminalTailReturnDelayMs);
954
1038
  };
955
1039
 
@@ -961,6 +1045,7 @@ export class DeesHarnessToolCard extends DeesElement {
961
1045
 
962
1046
  private readonly handleDiffViewChange = (eventArg: CustomEvent<IDiffViewChangeDetail>): void => {
963
1047
  this.diffViewOverride = eventArg.detail.diffView;
1048
+ if (this.viewState) this.viewState.toolDiffView = this.diffViewOverride;
964
1049
  };
965
1050
 
966
1051
  private readonly syncSubtaskResultScroll = (): void => {
@@ -1002,6 +1087,15 @@ export class DeesHarnessToolCard extends DeesElement {
1002
1087
  cancelAnimationFrame(this.disclosureFrame);
1003
1088
  this.disclosureFrame = null;
1004
1089
  }
1090
+ if (this.returnScrollFrame !== null) {
1091
+ cancelAnimationFrame(this.returnScrollFrame);
1092
+ this.returnScrollFrame = null;
1093
+ }
1094
+ if (this.terminalScrollHydrationFrame !== null) {
1095
+ cancelAnimationFrame(this.terminalScrollHydrationFrame);
1096
+ this.terminalScrollHydrationFrame = null;
1097
+ }
1098
+ this.terminalScrollHydrated = false;
1005
1099
  if (this.flashFallbackTimeout) {
1006
1100
  clearTimeout(this.flashFallbackTimeout);
1007
1101
  this.flashFallbackTimeout = null;
@@ -1011,8 +1105,16 @@ export class DeesHarnessToolCard extends DeesElement {
1011
1105
  this.outcomeScanFallbackTimeout = null;
1012
1106
  }
1013
1107
  this.cancelTerminalTailReturn();
1108
+ if (this.fullscreenPortal || this.returnFlash) {
1109
+ this.dispatchEvent(new CustomEvent('harness-card-retention', {
1110
+ detail: { active: false },
1111
+ bubbles: true,
1112
+ composed: true,
1113
+ }));
1114
+ }
1014
1115
  this.fullscreenPortal?.remove();
1015
1116
  this.fullscreenPortal = null;
1117
+ this.returnFlash = false;
1016
1118
  this.bodyResizeObserver?.disconnect();
1017
1119
  this.bodyResizeObserver = null;
1018
1120
  this.observedBodyInner = null;
@@ -1068,6 +1170,7 @@ export class DeesHarnessToolCard extends DeesElement {
1068
1170
  .questions=${subtask.questions ?? []}
1069
1171
  .status=${subtask.status}
1070
1172
  .toolRegistry=${this.toolRegistry}
1173
+ .viewState=${this.viewState?.subtaskTranscript}
1071
1174
  .allowSubtaskStreams=${false}
1072
1175
  .autoFollow=${this.isOpen}
1073
1176
  .hideScrollbar=${true}
@@ -1164,6 +1267,7 @@ export class DeesHarnessToolCard extends DeesElement {
1164
1267
  type="button"
1165
1268
  @click=${() => {
1166
1269
  this.outputExpanded = true;
1270
+ if (this.viewState) this.viewState.toolOutputExpanded = true;
1167
1271
  }}
1168
1272
  >
1169
1273
  Show all
@@ -49,6 +49,40 @@ export interface IHarnessTranscriptOrder {
49
49
  partIndex: number;
50
50
  }
51
51
 
52
+ /** View state retained by a virtualized transcript while a message is unmounted. */
53
+ export interface IHarnessMessageViewState {
54
+ reasoningExpanded?: boolean;
55
+ markdownText?: string;
56
+ markdownHtml?: string;
57
+ toolExpanded?: boolean;
58
+ toolOutputExpanded?: boolean;
59
+ toolDiffView?: 'inline' | 'split' | null;
60
+ toolTerminalTailFollowing?: boolean;
61
+ toolTerminalScrollTop?: number;
62
+ subtaskTranscriptSessionId?: string;
63
+ subtaskTranscript?: IHarnessTranscriptViewState;
64
+ }
65
+
66
+ /** Optimistic permission state retained while a virtualized card is unmounted. */
67
+ export interface IHarnessPermissionViewState {
68
+ remember?: boolean;
69
+ answered?: THarnessPermissionResponse;
70
+ }
71
+
72
+ /** In-progress question state retained while a virtualized card is unmounted. */
73
+ export interface IHarnessQuestionViewState {
74
+ answered?: string[];
75
+ selected?: string[];
76
+ customText?: string;
77
+ }
78
+
79
+ /** Complete interaction state for rows that a virtual transcript may evict. */
80
+ export interface IHarnessTranscriptViewState {
81
+ messages: Record<string, IHarnessMessageViewState>;
82
+ permissions: Record<string, IHarnessPermissionViewState>;
83
+ questions: Record<string, IHarnessQuestionViewState>;
84
+ }
85
+
52
86
  /** Session-level token facts. Unknown metrics stay undefined. */
53
87
  export interface IHarnessSessionMetrics {
54
88
  /** Cumulative token usage across the complete session. */
@@ -239,6 +239,11 @@ export class DeesHarnessToolRegistry {
239
239
  private descriptors: IHarnessToolDescriptor[] = [];
240
240
  private byName = new Map<string, IHarnessToolDescriptor>();
241
241
  private byKind = new Map<string, IHarnessToolDescriptor>();
242
+ private registryRevision = 0;
243
+
244
+ public get revision(): number {
245
+ return this.registryRevision;
246
+ }
242
247
 
243
248
  constructor(seedDescriptors: IHarnessToolDescriptor[] = []) {
244
249
  for (const descriptor of seedDescriptors) {
@@ -260,6 +265,7 @@ export class DeesHarnessToolRegistry {
260
265
  for (const name of descriptor.names ?? []) {
261
266
  this.byName.set(name.toLowerCase(), descriptor);
262
267
  }
268
+ this.registryRevision += 1;
263
269
  return this;
264
270
  }
265
271
 
@@ -271,6 +277,7 @@ export class DeesHarnessToolRegistry {
271
277
  for (const [name, mapped] of this.byName) {
272
278
  if (mapped === existing) this.byName.delete(name);
273
279
  }
280
+ this.registryRevision += 1;
274
281
  return this;
275
282
  }
276
283