@design.estate/dees-catalog 6.9.1 → 6.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "6.9.1",
3
+ "version": "6.10.0",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
package/readme.md CHANGED
@@ -1815,15 +1815,17 @@ chat.applyDelta({
1815
1815
  });
1816
1816
  ```
1817
1817
 
1818
- Key props: `messages`, `permissions`, `questions`, `status`, `usage`, `todos`/`showTodosPanel`, `sessionMetrics`, `scratchpad`/`scratchpadBusy`/`scratchpadError`, `sessionIntelligenceEnabled`/`intelligenceExchanges`/`intelligenceBusy`/`intelligenceError`, `showSessionSidebar`, `heading`, `subheading`, `showToolbar`, `busy`, `queuedCount`, `disabled`, `suggestions`, `account`/`accountOptions`, `model`/`modelOptions`, `reasoningEffort`/`effortOptions`, `mode`/`modeOptions`, `toolRegistry`. `suggestions` is forwarded unchanged to the controlled composer. Account options use human-readable `label` values and opaque stable `value` identifiers. The status line renders inside the transcript column (it never pushes between the session panel and the composer), and both the toolbar and the docked composer cast a subtle shadow over the scrolling messages.
1818
+ Key props: `messages`, `permissions`, `questions`, `status`, `transcriptKey`, `hasEarlierMessages`/`loadingEarlier`, `usage`, `todos`/`showTodosPanel`, `sessionMetrics`, `scratchpad`/`scratchpadBusy`/`scratchpadError`, `sessionIntelligenceEnabled`/`intelligenceExchanges`/`intelligenceBusy`/`intelligenceError`, `showSessionSidebar`, `heading`, `subheading`, `showToolbar`, `busy`, `queuedCount`, `disabled`, `suggestions`, `account`/`accountOptions`, `model`/`modelOptions`, `reasoningEffort`/`effortOptions`, `mode`/`modeOptions`, `toolRegistry`. `suggestions` is forwarded unchanged to the controlled composer. Account options use human-readable `label` values and opaque stable `value` identifiers. The status line renders inside the transcript column (it never pushes between the session panel and the composer), and both the toolbar and the docked composer cast a subtle shadow over the scrolling messages.
1819
1819
 
1820
1820
  A **session side panel** shows Tasks, Tools & Subagents, Tokens, Session Scratchpad, and Session Intelligence next to the transcript. Its sections form a single-open accordion: opening one collapses the others, clicking the open heading collapses all, and each expanded section owns its scrolling while every heading remains visible. Collapsed sections retain their region wrapper but omit the content subtree until expansion; sidebar-owned scratchpad and intelligence drafts survive that lazy remount. Tools & Subagents separates active calls from terminal history and derives both directly from top-level tool messages still present in the host's retained transcript window; evicted calls are not cached. Long activity labels and descriptions use a 40px right-edge fade instead of ellipses, reveal the full line on hover, and run through once after the two-second delay when their section has measurable width. Session Intelligence keeps its exchange history scrollable above a docked, chat-style question composer. Set `.todos` explicitly, or leave it empty and the chat derives tasks from the newest todo tool call in `messages`; `showTodosPanel={false}` hides only that section. Unknown token metrics remain absent. The toolbar controls the session panel; host-owned navigation controls belong outside the chat component. In narrow containers the session panel becomes an accessible overlay; when the toolbar is disabled, an inline Session details control keeps it reachable. `showSessionSidebar={false}` disables the complete panel. Methods: `applyDelta()`, `focusComposer()`, `clearComposer()`, `scrollToBottom()`. Events (also from children, all composed): `harness-send`, `harness-steer`, `harness-abort`, `harness-input`, `harness-attachments-change`, `harness-attachment-error`, `harness-account-change`, `harness-model-change`, `harness-mode-change`, `harness-permission-response`, `harness-question-response`, `harness-subtask-open`, `harness-scratchpad-save`, `harness-session-intelligence-ask`.
1821
1821
 
1822
+ History paging adds the composed child event `harness-load-earlier`.
1823
+
1822
1824
  #### `DeesHarnessMessageList`
1823
1825
 
1824
- Scrollable streaming-safe log. Up to 40 canonical entries (`messages` + `permissions` + `questions`) keep keyed full-timeline DOM; larger inputs activate a measured variable-height window with 800px overscan and return to full rendering when they shrink. Rows outside that overscanned window are unmounted, except focused rows, both endpoints of an active selection, and fullscreen-owned cards, which remain mounted until the interaction ends; threshold-mode changes are likewise deferred while protected DOM is active. `applyDelta()` targets a mounted message or mutates its offscreen backing model. Markdown output, disclosures, reasoning expansion, question drafts, permission choices, diff layout, terminal scroll state, and live nested-transcript state restore from retained data after an actual eviction. Hosts may pass a stable mutable `IHarnessTranscriptViewState` through `viewState`; it must contain `messages`, `permissions`, and `questions` maps. The list creates entries for current canonical IDs, deletes records for removed IDs, and session-keys and prunes live nested-subtask records. Without that prop, equivalent state remains list-local.
1826
+ Scrollable streaming-safe log. Up to 40 canonical entries (`messages` + `permissions` + `questions`) keep keyed full-timeline DOM; larger inputs activate a measured variable-height window with 800px overscan and return to full rendering when they shrink. Rows outside that overscanned window are unmounted, except focused rows, both endpoints of an active selection, and fullscreen-owned cards, which remain mounted until the interaction ends; threshold-mode changes are likewise deferred while protected DOM is active. `applyDelta()` targets a mounted message or mutates its offscreen backing model. Markdown output, disclosures, reasoning expansion, question drafts, permission choices, diff layout, terminal scroll state, and live nested-transcript state restore from retained data after an actual eviction. Hosts may pass a stable mutable `IHarnessTranscriptViewState` through `viewState`; it must contain `messages`, `permissions`, and `questions` maps. The list creates entries for current canonical IDs, deletes records for removed IDs, and session-keys and prunes live nested-subtask records. Without that prop, equivalent state remains list-local. Set `transcriptKey` to the stable identity of the displayed transcript so reused components reset scroll-local state between chats. When `hasEarlierMessages` is true, an intentional upward gesture reaching the top emits one bubbling, composed `harness-load-earlier` event. Set `loadingEarlier` while handling it; the next `true` to `false` transition rearms the event. Initial rendering, anchor restoration, and other component-owned scrolling never emit it.
1825
1827
 
1826
- Producers with authoritative source chronology can set `IHarnessMessage.order` to `{ messageIndex, partIndex }`; explicitly ordered messages retain that relative order while timestamps continue to place legacy messages and ancillary cards. Consecutive tool calls use one generic responsive-grid pipeline at their chronological position, partitioned by descriptor layout: compact tools share rows with compact tools, subagents share rows only with subagents, and terminal plus file-write cards span a whole row. Consecutive same-layout runs are always capped at four cards per group row, even when their tool kinds differ. Columns respond to transcript content width rather than the viewport, and status changes never regroup cards. Untouched projected subtask cards collapse individually on completion with the normal disclosure animation; other cards retain their descriptor or user-selected disclosure state. Auto-follow responds to streamed growth but does not repeatedly repin while a disclosure shrinks. Permission requests, questions, and nested-stream limits behave as before. Props: `messages`, `permissions`/`permissionsBusy`, `questions`, `status`, `viewState`, `autoFollow`, `hideScrollbar`, `fadeScrollEdges`, `showRoles`, `showTimestamps`, `allowSubtaskStreams`, `emptyTitle`/`emptyText` (+ `slot="empty"`). Native browser find and selection cover the mounted window; hosts that require complete-history search or copy should operate on their canonical `messages`, `permissions`, and `questions` models.
1828
+ Producers with authoritative source chronology can set `IHarnessMessage.order` to `{ messageIndex, partIndex }`; explicitly ordered messages retain that relative order while timestamps continue to place legacy messages and ancillary cards. Consecutive tool calls use one generic responsive-grid pipeline at their chronological position, partitioned by descriptor layout: compact tools share rows with compact tools, subagents share rows only with subagents, and terminal plus file-write cards span a whole row. Consecutive same-layout runs are always capped at four cards per group row, even when their tool kinds differ. Columns respond to transcript content width rather than the viewport, and status changes never regroup cards. Untouched projected subtask cards collapse individually on completion with the normal disclosure animation; other cards retain their descriptor or user-selected disclosure state. Auto-follow responds to streamed growth but does not repeatedly repin while a disclosure shrinks. Permission requests, questions, and nested-stream limits behave as before. Props: `messages`, `permissions`/`permissionsBusy`, `questions`, `status`, `viewState`, `transcriptKey`, `hasEarlierMessages`, `loadingEarlier`, `autoFollow`, `hideScrollbar`, `fadeScrollEdges`, `showRoles`, `showTimestamps`, `allowSubtaskStreams`, `emptyTitle`/`emptyText` (+ `slot="empty"`). Native browser find and selection cover the mounted window; hosts that require complete-history search or copy should operate on their canonical `messages`, `permissions`, and `questions` models.
1827
1829
 
1828
1830
  #### `DeesHarnessMessage`
1829
1831
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '6.9.1',
6
+ version: '6.10.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
  }
@@ -72,6 +72,15 @@ export class DeesHarnessChat extends DeesElement {
72
72
  @property({ attribute: false })
73
73
  accessor status: IHarnessStatus = { type: 'idle' };
74
74
 
75
+ @property()
76
+ accessor transcriptKey: string = '';
77
+
78
+ @property({ type: Boolean })
79
+ accessor hasEarlierMessages: boolean = false;
80
+
81
+ @property({ type: Boolean })
82
+ accessor loadingEarlier: boolean = false;
83
+
75
84
  @property({ attribute: false })
76
85
  accessor usage: IHarnessUsage = {};
77
86
 
@@ -575,6 +584,9 @@ export class DeesHarnessChat extends DeesElement {
575
584
  .permissionsBusy=${this.disabled}
576
585
  .questions=${this.questions}
577
586
  .status=${this.status}
587
+ .transcriptKey=${this.transcriptKey}
588
+ .hasEarlierMessages=${this.hasEarlierMessages}
589
+ .loadingEarlier=${this.loadingEarlier}
578
590
  .showRoles=${this.showRoles}
579
591
  .showTimestamps=${this.showTimestamps}
580
592
  .toolRegistry=${this.toolRegistry}
@@ -40,6 +40,8 @@ declare global {
40
40
  }
41
41
 
42
42
  const pinThresholdPx = 48;
43
+ const loadEarlierThresholdPx = 24;
44
+ const scrollIntentSettlementMs = 250;
43
45
  const virtualizationThreshold = 40;
44
46
  const virtualOverscanPx = 800;
45
47
  const virtualGapPx = 8;
@@ -112,6 +114,16 @@ export class DeesHarnessMessageList extends DeesElement {
112
114
  @property({ type: Boolean })
113
115
  accessor autoFollow: boolean = true;
114
116
 
117
+ /** Stable host-owned identity for one transcript and its scroll context. */
118
+ @property()
119
+ accessor transcriptKey: string = '';
120
+
121
+ @property({ type: Boolean })
122
+ accessor hasEarlierMessages: boolean = false;
123
+
124
+ @property({ type: Boolean })
125
+ accessor loadingEarlier: boolean = false;
126
+
115
127
  @property({ type: Boolean })
116
128
  accessor hideScrollbar: boolean = false;
117
129
 
@@ -200,6 +212,16 @@ export class DeesHarnessMessageList extends DeesElement {
200
212
  private virtualizationDisablePending = false;
201
213
  private virtualizationActivationFrame: number | null = null;
202
214
  private selectionChangeListening = false;
215
+ private loadEarlierLatched = false;
216
+ private loadingEarlierObserved = false;
217
+ private earlierScrollIntent = false;
218
+ private scrollbarGestureActive = false;
219
+ private touchScrollY: number | undefined;
220
+ private scrollIntentTimer: ReturnType<typeof setTimeout> | undefined;
221
+ private programmaticScrollTop: number | undefined;
222
+ private programmaticScrollGeneration = 0;
223
+ private programmaticScrollFrame: number | null = null;
224
+ private programmaticScrollSecondFrame: number | null = null;
203
225
 
204
226
  public static styles = [
205
227
  themeDefaultStyles,
@@ -492,6 +514,20 @@ export class DeesHarnessMessageList extends DeesElement {
492
514
 
493
515
  public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
494
516
  super.willUpdate(changedProperties);
517
+ if (changedProperties.has('transcriptKey')) this.resetTranscriptContext();
518
+ if (changedProperties.has('hasEarlierMessages') && !this.hasEarlierMessages) {
519
+ this.loadEarlierLatched = false;
520
+ this.loadingEarlierObserved = false;
521
+ this.clearEarlierScrollIntent();
522
+ }
523
+ if (changedProperties.has('loadingEarlier')) {
524
+ const wasLoadingEarlier = changedProperties.get('loadingEarlier') === true;
525
+ if (this.loadingEarlier) this.loadingEarlierObserved = true;
526
+ else if (wasLoadingEarlier && this.loadingEarlierObserved) {
527
+ this.loadEarlierLatched = false;
528
+ this.loadingEarlierObserved = false;
529
+ }
530
+ }
495
531
  if (changedProperties.has('viewState') && this.viewState) {
496
532
  this.clearTranscriptViewState(this.localViewState);
497
533
  }
@@ -503,6 +539,7 @@ export class DeesHarnessMessageList extends DeesElement {
503
539
  || changedProperties.has('questions')
504
540
  || changedProperties.has('toolRegistry')
505
541
  || changedProperties.has('viewState')
542
+ || changedProperties.has('transcriptKey')
506
543
  || registryChanged
507
544
  ) {
508
545
  this.rebuildTimeline();
@@ -766,8 +803,12 @@ export class DeesHarnessMessageList extends DeesElement {
766
803
  <div
767
804
  class=${`scroll ${this.hideScrollbar ? 'hideScrollbar' : ''} ${this.scrollFadeTop ? 'fadeTop' : ''} ${this.scrollFadeBottom ? 'fadeBottom' : ''}`}
768
805
  @scroll=${this.handleScroll}
806
+ @scrollend=${this.handleScrollEnd}
769
807
  @wheel=${this.handleUserScrollIntent}
808
+ @touchstart=${this.handleTouchStart}
770
809
  @touchmove=${this.handleUserScrollIntent}
810
+ @touchend=${this.handleTouchEnd}
811
+ @touchcancel=${this.handleTouchCancel}
771
812
  @pointerdown=${this.handlePointerDown}
772
813
  @pointerup=${this.handlePointerUp}
773
814
  @pointercancel=${this.handlePointerCancel}
@@ -1042,8 +1083,7 @@ export class DeesHarnessMessageList extends DeesElement {
1042
1083
  if (!this.autoFollow || this.userUnpinned) return;
1043
1084
  const container = this.scrollContainer;
1044
1085
  if (!container) return;
1045
- container.scrollTop = container.scrollHeight;
1046
- this.lastObservedScrollTop = container.scrollTop;
1086
+ this.setProgrammaticScrollTop(container, container.scrollHeight);
1047
1087
  };
1048
1088
 
1049
1089
  private followAfterContentChange(): void {
@@ -1068,26 +1108,213 @@ export class DeesHarnessMessageList extends DeesElement {
1068
1108
  }
1069
1109
 
1070
1110
  private handleUserScrollIntent = (event: Event): void => {
1071
- if (event.composedPath().some(
1072
- (candidate) => candidate instanceof DeesHarnessMessageList && candidate !== this,
1073
- )) return;
1111
+ if (this.eventComesFromNestedMessageList(event)) return;
1074
1112
  if (event.type === 'wheel') {
1075
1113
  this.cancelFullAnchorRestore();
1076
- if ((event as WheelEvent).deltaY >= 0) return;
1114
+ const deltaY = (event as WheelEvent).deltaY;
1115
+ if (deltaY < 0) {
1116
+ this.armEarlierScrollIntent();
1117
+ this.requestEarlierIfAtTop();
1118
+ this.markUserUnpinned();
1119
+ } else if (deltaY > 0) {
1120
+ this.clearEarlierScrollIntent();
1121
+ }
1122
+ return;
1077
1123
  }
1078
- if (
1079
- event.type === 'keydown'
1080
- && !['ArrowUp', 'PageUp', 'Home'].includes((event as KeyboardEvent).key)
1081
- ) {
1082
- if (['ArrowDown', 'PageDown', 'End', ' '].includes((event as KeyboardEvent).key)) {
1124
+ if (event.type === 'keydown') {
1125
+ if (this.keyboardEventOriginIsInteractive(event as KeyboardEvent)) return;
1126
+ const key = (event as KeyboardEvent).key;
1127
+ if (['ArrowUp', 'PageUp', 'Home'].includes(key)) {
1128
+ this.cancelFullAnchorRestore();
1129
+ this.armEarlierScrollIntent();
1130
+ this.requestEarlierIfAtTop();
1131
+ this.markUserUnpinned();
1132
+ } else if (['ArrowDown', 'PageDown', 'End', ' '].includes(key)) {
1133
+ this.clearEarlierScrollIntent();
1083
1134
  this.cancelFullAnchorRestore();
1084
1135
  }
1085
1136
  return;
1086
1137
  }
1087
- if (event.type === 'pointerdown' && event.target !== this.scrollContainer) return;
1088
- this.cancelFullAnchorRestore();
1138
+ if (event.type === 'touchmove') {
1139
+ const currentY = (event as TouchEvent).touches[0]?.clientY;
1140
+ const previousY = this.touchScrollY;
1141
+ this.touchScrollY = currentY;
1142
+ if (currentY === undefined || previousY === undefined) return;
1143
+ if (currentY > previousY + 0.5) {
1144
+ this.armEarlierScrollIntent();
1145
+ this.requestEarlierIfAtTop();
1146
+ } else if (currentY < previousY - 0.5) {
1147
+ this.clearEarlierScrollIntent();
1148
+ } else {
1149
+ return;
1150
+ }
1151
+ this.cancelFullAnchorRestore();
1152
+ this.markUserUnpinned();
1153
+ return;
1154
+ }
1155
+ if (event.type === 'pointerdown') {
1156
+ if ((event as PointerEvent).button !== 0) return;
1157
+ if (event.target !== this.scrollContainer) return;
1158
+ this.scrollbarGestureActive = true;
1159
+ this.cancelFullAnchorRestore();
1160
+ this.markUserUnpinned();
1161
+ }
1162
+ };
1163
+
1164
+ private markUserUnpinned(): void {
1089
1165
  this.userUnpinned = true;
1090
1166
  this.measurementAnchor = undefined;
1167
+ }
1168
+
1169
+ private eventComesFromNestedMessageList(eventArg: Event): boolean {
1170
+ return eventArg.composedPath().some(
1171
+ (candidate) => candidate instanceof DeesHarnessMessageList && candidate !== this,
1172
+ );
1173
+ }
1174
+
1175
+ private keyboardEventOriginIsInteractive(eventArg: KeyboardEvent): boolean {
1176
+ return eventArg.composedPath().some((candidate) => (
1177
+ candidate instanceof HTMLElement
1178
+ && candidate !== this
1179
+ && (
1180
+ candidate.isContentEditable
1181
+ || candidate.matches('input, textarea, select, button, [contenteditable]')
1182
+ )
1183
+ ));
1184
+ }
1185
+
1186
+ private armEarlierScrollIntent(): void {
1187
+ this.earlierScrollIntent = true;
1188
+ this.scheduleScrollIntentSettlement();
1189
+ }
1190
+
1191
+ private scheduleScrollIntentSettlement(): void {
1192
+ if (this.scrollIntentTimer) clearTimeout(this.scrollIntentTimer);
1193
+ this.scrollIntentTimer = setTimeout(() => {
1194
+ this.scrollIntentTimer = undefined;
1195
+ this.earlierScrollIntent = false;
1196
+ }, scrollIntentSettlementMs);
1197
+ }
1198
+
1199
+ private clearEarlierScrollIntent(): void {
1200
+ if (this.scrollIntentTimer) clearTimeout(this.scrollIntentTimer);
1201
+ this.scrollIntentTimer = undefined;
1202
+ this.earlierScrollIntent = false;
1203
+ }
1204
+
1205
+ private requestEarlierIfAtTop(): void {
1206
+ const container = this.scrollContainer;
1207
+ if (
1208
+ !container
1209
+ || container.scrollTop > loadEarlierThresholdPx
1210
+ || !this.hasEarlierMessages
1211
+ || this.loadingEarlier
1212
+ || this.loadEarlierLatched
1213
+ ) return;
1214
+ this.loadEarlierLatched = true;
1215
+ this.clearEarlierScrollIntent();
1216
+ this.dispatchEvent(new CustomEvent('harness-load-earlier', {
1217
+ bubbles: true,
1218
+ composed: true,
1219
+ }));
1220
+ }
1221
+
1222
+ private resetLoadEarlierState(): void {
1223
+ this.loadEarlierLatched = false;
1224
+ this.loadingEarlierObserved = false;
1225
+ this.scrollbarGestureActive = false;
1226
+ this.touchScrollY = undefined;
1227
+ this.clearEarlierScrollIntent();
1228
+ }
1229
+
1230
+ private setProgrammaticScrollTop(containerArg: HTMLElement, topArg: number): void {
1231
+ this.clearProgrammaticScrollMarker();
1232
+ const generation = ++this.programmaticScrollGeneration;
1233
+ containerArg.scrollTop = topArg;
1234
+ this.programmaticScrollTop = containerArg.scrollTop;
1235
+ this.lastObservedScrollTop = containerArg.scrollTop;
1236
+ this.programmaticScrollFrame = requestAnimationFrame(() => {
1237
+ this.programmaticScrollFrame = null;
1238
+ if (generation !== this.programmaticScrollGeneration) return;
1239
+ this.programmaticScrollSecondFrame = requestAnimationFrame(() => {
1240
+ this.programmaticScrollSecondFrame = null;
1241
+ if (generation === this.programmaticScrollGeneration) {
1242
+ this.programmaticScrollTop = undefined;
1243
+ }
1244
+ });
1245
+ });
1246
+ }
1247
+
1248
+ private consumeProgrammaticScroll(topArg: number): boolean {
1249
+ if (this.programmaticScrollTop === undefined) return false;
1250
+ const matches = Math.abs(this.programmaticScrollTop - topArg) <= 1;
1251
+ this.clearProgrammaticScrollMarker();
1252
+ return matches;
1253
+ }
1254
+
1255
+ private clearProgrammaticScrollMarker(): void {
1256
+ this.programmaticScrollGeneration += 1;
1257
+ if (this.programmaticScrollFrame !== null) cancelAnimationFrame(this.programmaticScrollFrame);
1258
+ if (this.programmaticScrollSecondFrame !== null) cancelAnimationFrame(this.programmaticScrollSecondFrame);
1259
+ this.programmaticScrollFrame = null;
1260
+ this.programmaticScrollSecondFrame = null;
1261
+ this.programmaticScrollTop = undefined;
1262
+ }
1263
+
1264
+ private resetTranscriptContext(): void {
1265
+ this.cancelFullAnchorRestore();
1266
+ if (this.followFrame !== null) cancelAnimationFrame(this.followFrame);
1267
+ if (this.followSecondFrame !== null) cancelAnimationFrame(this.followSecondFrame);
1268
+ if (this.virtualMeasureFrame !== null) cancelAnimationFrame(this.virtualMeasureFrame);
1269
+ if (this.virtualRangeFrame !== null) cancelAnimationFrame(this.virtualRangeFrame);
1270
+ if (this.virtualizationActivationFrame !== null) cancelAnimationFrame(this.virtualizationActivationFrame);
1271
+ this.followFrame = null;
1272
+ this.followSecondFrame = null;
1273
+ this.virtualMeasureFrame = null;
1274
+ this.virtualRangeFrame = null;
1275
+ this.virtualizationActivationFrame = null;
1276
+ this.pendingVirtualAnchor = undefined;
1277
+ this.pendingFullAnchor = undefined;
1278
+ this.measurementAnchor = undefined;
1279
+ this.pendingVirtualRepin = false;
1280
+ this.virtualizationEnabled = false;
1281
+ this.virtualizationEnablePending = false;
1282
+ this.virtualizationDisablePending = false;
1283
+ this.virtualStartIndex = 0;
1284
+ this.virtualEndIndex = 0;
1285
+ this.retainedVirtualKeys.clear();
1286
+ this.explicitRetainedVirtualKey = undefined;
1287
+ this.measuredVirtualHeights.clear();
1288
+ this.virtualOffsets = [];
1289
+ this.virtualTotalHeight = 0;
1290
+ this.toolGroupIdByMessageId.clear();
1291
+ this.nextToolGroupId = 1;
1292
+ this.timelineRegistryRevision = -1;
1293
+ this.knownContentKeys.clear();
1294
+ this.currentNewContentKeys.clear();
1295
+ this.clearTranscriptViewState(this.localViewState);
1296
+ this.clearSelectionGesture();
1297
+ this.disconnectSelectionChangeListener();
1298
+ this.userUnpinned = false;
1299
+ this.lastObservedScrollTop = 0;
1300
+ this.showJumpChip = false;
1301
+ this.resetLoadEarlierState();
1302
+ this.clearProgrammaticScrollMarker();
1303
+ }
1304
+
1305
+ private readonly handleTouchStart = (eventArg: TouchEvent): void => {
1306
+ if (this.eventComesFromNestedMessageList(eventArg)) return;
1307
+ this.touchScrollY = eventArg.touches[0]?.clientY;
1308
+ };
1309
+
1310
+ private readonly handleTouchEnd = (): void => {
1311
+ this.touchScrollY = undefined;
1312
+ if (this.earlierScrollIntent) this.scheduleScrollIntentSettlement();
1313
+ };
1314
+
1315
+ private readonly handleTouchCancel = (): void => {
1316
+ this.touchScrollY = undefined;
1317
+ this.clearEarlierScrollIntent();
1091
1318
  };
1092
1319
 
1093
1320
  public scrollToBottom(force = false): void {
@@ -1099,8 +1326,7 @@ export class DeesHarnessMessageList extends DeesElement {
1099
1326
  this.measurementAnchor = undefined;
1100
1327
  }
1101
1328
  if (force || this.isPinnedToBottom) {
1102
- container.scrollTop = container.scrollHeight;
1103
- this.lastObservedScrollTop = container.scrollTop;
1329
+ this.setProgrammaticScrollTop(container, container.scrollHeight);
1104
1330
  this.showJumpChip = false;
1105
1331
  }
1106
1332
  this.syncScrollEdgeFades();
@@ -1110,6 +1336,20 @@ export class DeesHarnessMessageList extends DeesElement {
1110
1336
  const container = this.scrollContainer;
1111
1337
  if (!container) return;
1112
1338
  const top = container.scrollTop;
1339
+ const previousTop = this.lastObservedScrollTop;
1340
+ const programmatic = this.consumeProgrammaticScroll(top);
1341
+ if (programmatic) {
1342
+ this.clearEarlierScrollIntent();
1343
+ } else {
1344
+ if (this.scrollbarGestureActive && top < previousTop - 0.5) {
1345
+ this.armEarlierScrollIntent();
1346
+ } else if (top > previousTop + 0.5) {
1347
+ this.clearEarlierScrollIntent();
1348
+ } else if (this.earlierScrollIntent) {
1349
+ this.scheduleScrollIntentSettlement();
1350
+ }
1351
+ if (this.earlierScrollIntent) this.requestEarlierIfAtTop();
1352
+ }
1113
1353
  this.syncScrollEdgeFades();
1114
1354
  if (this.isPinnedToBottom) {
1115
1355
  // reaching the bottom — by any means — resumes following
@@ -1123,6 +1363,10 @@ export class DeesHarnessMessageList extends DeesElement {
1123
1363
  this.schedulePendingVirtualizationActivation();
1124
1364
  };
1125
1365
 
1366
+ private readonly handleScrollEnd = (): void => {
1367
+ this.clearEarlierScrollIntent();
1368
+ };
1369
+
1126
1370
  private schedulePendingVirtualizationActivation(): void {
1127
1371
  if (
1128
1372
  (!this.virtualizationEnablePending && !this.virtualizationDisablePending)
@@ -1187,6 +1431,7 @@ export class DeesHarnessMessageList extends DeesElement {
1187
1431
  };
1188
1432
 
1189
1433
  private readonly handlePointerUp = (eventArg: PointerEvent): void => {
1434
+ this.scrollbarGestureActive = false;
1190
1435
  if (eventArg.button !== 0) return;
1191
1436
  const key = this.timelineKeyForEvent(eventArg);
1192
1437
  if (key) this.selectionGestureKeys.add(key);
@@ -1205,6 +1450,8 @@ export class DeesHarnessMessageList extends DeesElement {
1205
1450
  };
1206
1451
 
1207
1452
  private readonly handlePointerCancel = (): void => {
1453
+ this.scrollbarGestureActive = false;
1454
+ this.clearEarlierScrollIntent();
1208
1455
  this.clearSelectionGesture();
1209
1456
  this.handleProtectedStateChange();
1210
1457
  };
@@ -1228,6 +1475,13 @@ export class DeesHarnessMessageList extends DeesElement {
1228
1475
 
1229
1476
  public updated(changedProperties: Map<string, any>): void {
1230
1477
  super.updated(changedProperties);
1478
+ if (changedProperties.has('transcriptKey')) {
1479
+ const container = this.scrollContainer;
1480
+ if (container) {
1481
+ if (this.autoFollow) this.scrollToBottom(true);
1482
+ else this.setProgrammaticScrollTop(container, 0);
1483
+ }
1484
+ }
1231
1485
  if (changedProperties.has('messages') || changedProperties.has('status') || changedProperties.has('permissions') || changedProperties.has('questions')) {
1232
1486
  this.followAfterContentChange();
1233
1487
  }
@@ -1424,7 +1678,10 @@ export class DeesHarnessMessageList extends DeesElement {
1424
1678
  const index = this.timelineIndexByKey.get(anchorArg.key);
1425
1679
  if (index === undefined) return;
1426
1680
  const stack = this.shadowRoot?.querySelector<HTMLElement>('.virtualStack');
1427
- containerArg.scrollTop = (stack?.offsetTop ?? 0) + (this.virtualOffsets[index] ?? 0) + anchorArg.offset;
1681
+ this.setProgrammaticScrollTop(
1682
+ containerArg,
1683
+ (stack?.offsetTop ?? 0) + (this.virtualOffsets[index] ?? 0) + anchorArg.offset,
1684
+ );
1428
1685
  }
1429
1686
 
1430
1687
  private restoreFullAnchor(
@@ -1435,9 +1692,13 @@ export class DeesHarnessMessageList extends DeesElement {
1435
1692
  `.stack > [data-timeline-key="${CSS.escape(anchorArg.key)}"]`,
1436
1693
  );
1437
1694
  if (!element) return;
1438
- containerArg.scrollTop += element.getBoundingClientRect().top
1439
- - containerArg.getBoundingClientRect().top
1440
- + anchorArg.offset;
1695
+ this.setProgrammaticScrollTop(
1696
+ containerArg,
1697
+ containerArg.scrollTop
1698
+ + element.getBoundingClientRect().top
1699
+ - containerArg.getBoundingClientRect().top
1700
+ + anchorArg.offset,
1701
+ );
1441
1702
  }
1442
1703
 
1443
1704
  private scheduleFullAnchorRestore(
@@ -1774,6 +2035,8 @@ export class DeesHarnessMessageList extends DeesElement {
1774
2035
  this.explicitRetainedVirtualKey = undefined;
1775
2036
  this.retainedVirtualKeys.clear();
1776
2037
  this.clearSelectionGesture();
2038
+ this.resetLoadEarlierState();
2039
+ this.clearProgrammaticScrollMarker();
1777
2040
  this.entryAnimationsEnabled = true;
1778
2041
  this.disconnectContentResizeObserver();
1779
2042
  this.disconnectVirtualObservers();