agl 22.0.4 → 24.0.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 (44) hide show
  1. package/changelog.md +21 -0
  2. package/dist_serve/bundle.js +2418 -2408
  3. package/dist_serve/bundle.js.map +1 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/classes.authstore.js +2 -2
  6. package/dist_ts/classes.browserresourcehost.d.ts +1 -0
  7. package/dist_ts/classes.browserresourcehost.js +7 -1
  8. package/dist_ts/classes.controller.d.ts +37 -5
  9. package/dist_ts/classes.controller.js +1491 -249
  10. package/dist_ts/classes.flexservice.d.ts +5 -0
  11. package/dist_ts/classes.flexservice.js +131 -4
  12. package/dist_ts/classes.opencodeclient.d.ts +2 -0
  13. package/dist_ts/classes.opencodeclient.js +23 -1
  14. package/dist_ts/classes.validation.d.ts +9 -1
  15. package/dist_ts/classes.validation.js +62 -7
  16. package/dist_ts/interfaces.flexipc.d.ts +18 -1
  17. package/dist_ts/interfaces.flexipc.js +42 -8
  18. package/dist_ts_interfaces/browsertransport.d.ts +72 -0
  19. package/dist_ts_interfaces/browsertransport.js +47 -0
  20. package/dist_ts_interfaces/index.d.ts +1 -0
  21. package/dist_ts_interfaces/index.js +2 -1
  22. package/dist_ts_interfaces/interfaces.d.ts +42 -7
  23. package/dist_ts_interfaces/interfaces.js +5 -3
  24. package/dist_ts_interfaces/requests.d.ts +20 -21
  25. package/package.json +7 -7
  26. package/readme.md +12 -2
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.authstore.ts +1 -1
  29. package/ts/classes.browserresourcehost.ts +7 -0
  30. package/ts/classes.controller.ts +1960 -305
  31. package/ts/classes.flexservice.ts +132 -2
  32. package/ts/classes.opencodeclient.ts +19 -0
  33. package/ts/classes.validation.ts +77 -6
  34. package/ts/interfaces.flexipc.ts +65 -7
  35. package/ts_interfaces/browsertransport.ts +126 -0
  36. package/ts_interfaces/index.ts +1 -0
  37. package/ts_interfaces/interfaces.ts +46 -11
  38. package/ts_interfaces/requests.ts +23 -30
  39. package/ts_web/00_commitinfo_data.ts +1 -1
  40. package/ts_web/classes.browserviewtransport.ts +532 -0
  41. package/ts_web/classes.controllersocketclient.ts +119 -29
  42. package/ts_web/elements.harnesscontrollerapp.ts +1727 -287
  43. package/ts_web/index.ts +1 -0
  44. package/ts_web/plugins.ts +2 -0
@@ -1,10 +1,8 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import * as interfaces from '../ts_interfaces/index.js';
3
3
  import { commitinfo } from './00_commitinfo_data.js';
4
- import {
5
- ControllerSocketClient,
6
- type TControllerBrowserViewEventMessage,
7
- } from './classes.controllersocketclient.js';
4
+ import { ControllerSocketClient } from './classes.controllersocketclient.js';
5
+ import { ControllerBrowserViewTransportClient } from './classes.browserviewtransport.js';
8
6
  import {
9
7
  SessionDraftSync,
10
8
  type ISessionDraftSyncState,
@@ -128,6 +126,33 @@ interface IDetailLoadOwnership {
128
126
  messageStreamEpoch: number;
129
127
  }
130
128
 
129
+ interface ILiveMessageBaseline<
130
+ TUpdate extends interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate,
131
+ > {
132
+ update: TUpdate;
133
+ textUtf8Bytes: number;
134
+ bytes: number;
135
+ }
136
+
137
+ interface ICanonicalReasoningTarget {
138
+ message: plugins.deesCatalog.IHarnessMessage;
139
+ part: plugins.deesCatalog.IHarnessReasoningPart;
140
+ }
141
+
142
+ interface ICanonicalChatProjection {
143
+ key: string;
144
+ sourceDetail: IControllerSessionRenderDetail | undefined;
145
+ messages: plugins.deesCatalog.IHarnessMessage[];
146
+ messagesById: Map<string, plugins.deesCatalog.IHarnessMessage>;
147
+ messagesByOrder: Map<string, plugins.deesCatalog.IHarnessMessage>;
148
+ reasoningById: Map<string, ICanonicalReasoningTarget>;
149
+ }
150
+
151
+ interface ILiveMessageDeltaBlock {
152
+ sessionId: TBrowserSessionId;
153
+ streamEpoch: number;
154
+ }
155
+
131
156
  interface ISlashCatalogOwnership {
132
157
  key: string;
133
158
  projectId: string;
@@ -282,6 +307,57 @@ const controllerJsonValuesEqual = (leftArg: unknown, rightArg: unknown): boolean
282
307
  ));
283
308
  };
284
309
 
310
+ const reconcileKeyedJsonArray = <TValue>(
311
+ currentArg: readonly TValue[],
312
+ nextArg: readonly TValue[],
313
+ keyForArg: (valueArg: TValue, indexArg: number) => string,
314
+ ): TValue[] => {
315
+ const currentByKey = new Map(
316
+ currentArg.map((value, index) => [keyForArg(value, index), value]),
317
+ );
318
+ const seenKeys = new Set<string>();
319
+ let unchanged = currentArg.length === nextArg.length;
320
+ const reconciled = nextArg.map((nextValue, index) => {
321
+ const key = keyForArg(nextValue, index);
322
+ const currentValue = seenKeys.has(key) ? undefined : currentByKey.get(key);
323
+ seenKeys.add(key);
324
+ const value = currentValue !== undefined && controllerJsonValuesEqual(currentValue, nextValue)
325
+ ? currentValue
326
+ : nextValue;
327
+ unchanged &&= value === currentArg[index];
328
+ return value;
329
+ });
330
+ return unchanged ? currentArg as TValue[] : reconciled;
331
+ };
332
+
333
+ const browserTextEncoder = new TextEncoder();
334
+
335
+ const controllerTextAfterAppendUtf8Bytes = (
336
+ textArg: string,
337
+ textUtf8BytesArg: number,
338
+ deltaArg: string,
339
+ ): number | undefined => {
340
+ const previousCodeUnit = textArg.charCodeAt(textArg.length - 1);
341
+ const nextCodeUnit = deltaArg.charCodeAt(0);
342
+ // Separately encoded surrogate halves cost six bytes; the joined pair costs four.
343
+ const boundaryCorrection = textArg.length > 0
344
+ && deltaArg.length > 0
345
+ && previousCodeUnit >= 0xD800
346
+ && previousCodeUnit <= 0xDBFF
347
+ && nextCodeUnit >= 0xDC00
348
+ && nextCodeUnit <= 0xDFFF
349
+ ? -2
350
+ : 0;
351
+ const result = textUtf8BytesArg
352
+ + browserTextEncoder.encode(deltaArg).byteLength
353
+ + boundaryCorrection;
354
+ return Number.isSafeInteger(result) && result >= 0 ? result : undefined;
355
+ };
356
+
357
+ const transcriptOrderKey = (orderArg: interfaces.IControllerTranscriptOrder): string => (
358
+ `${orderArg.messageIndex}\0${orderArg.partIndex}`
359
+ );
360
+
285
361
  const controllerNormalizeBrowserAddress = (addressArg: string): string => {
286
362
  const address = addressArg.trim();
287
363
  if (!address) throw new Error('Enter a browser address.');
@@ -381,6 +457,7 @@ const layoutRequestTimeoutMs = 10_000;
381
457
  const providerLoginPollTimeoutMs = 15 * 60 * 1000;
382
458
  const providerRefreshPollTimeoutMs = 5 * 60 * 1000;
383
459
  const providerOpenCodeSwitchTimeoutMs = 6 * 60 * 1000;
460
+ const browserRendererLifecycleTimeoutMs = 1_000;
384
461
  const maximumSlashSuggestions = 12;
385
462
  const maximumWorkspaceNoticeCharacters = 500;
386
463
 
@@ -543,6 +620,25 @@ const errorMessage = (errorArg: unknown): string => {
543
620
  return String(errorArg || 'The operation failed.');
544
621
  };
545
622
 
623
+ const waitForBrowserRendererLifecycle = async (
624
+ operationArg: Promise<void>,
625
+ actionArg: string,
626
+ ): Promise<void> => {
627
+ let timeout: ReturnType<typeof setTimeout> | undefined;
628
+ try {
629
+ await Promise.race([
630
+ operationArg,
631
+ new Promise<never>((_resolve, reject) => {
632
+ timeout = setTimeout(() => {
633
+ reject(new Error(`Browser renderer ${actionArg} timed out.`));
634
+ }, browserRendererLifecycleTimeoutMs);
635
+ }),
636
+ ]);
637
+ } finally {
638
+ if (timeout) clearTimeout(timeout);
639
+ }
640
+ };
641
+
546
642
  const formatPercentage = (valueArg: number | null | undefined): string => (
547
643
  typeof valueArg === 'number' && Number.isFinite(valueArg)
548
644
  ? `${Math.round(valueArg)}%`
@@ -628,6 +724,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
628
724
  private highCpuStartedAt: number | undefined;
629
725
  private lastCpuSampleAt: number | undefined;
630
726
  private refreshPending = false;
727
+ private refreshRelatedPending = false;
631
728
  private projectsRequestId = 0;
632
729
  private modelCatalogRequestId = 0;
633
730
  private layoutRequestId = 0;
@@ -637,14 +734,27 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
637
734
  private browserNavigationRequestId = 0;
638
735
  private browserViewOpenAbortController: AbortController | undefined;
639
736
  private browserNavigationAbortController: AbortController | undefined;
737
+ private browserViewRecoveryAbortController: AbortController | undefined;
640
738
  private browserViewId = '';
641
739
  private browserViewConnectionGeneration = -1;
642
- private browserViewPendingClose: { viewId: string; connectionGeneration: number } | undefined;
740
+ private browserViewStreamGeneration = -1;
741
+ private browserViewActivated = false;
742
+ private browserViewPendingClose: {
743
+ viewId: string;
744
+ streamGeneration: number;
745
+ connectionGeneration: number;
746
+ attempts: number;
747
+ } | undefined;
748
+ private browserViewCloseTask: Promise<void> | undefined;
749
+ private browserViewCloseRetryTimer: ReturnType<typeof setTimeout> | undefined;
643
750
  private browserViewResourceId = '';
644
751
  private browserViewAttachmentRevision = -1;
645
- private browserViewState: interfaces.IControllerBrowserViewState | undefined;
646
752
  private readonly browserViewListeners = new Set<plugins.TLiveBrowserEventListener>();
753
+ private browserViewTransport?: ControllerBrowserViewTransportClient;
754
+ private browserViewTransportRecoveryTask?: Promise<void>;
647
755
  private browserRenderer?: plugins.LiveBrowserCanvasRenderer;
756
+ private browserAddressDraft = '';
757
+ private browserAddressEditing = false;
648
758
  private suggestRequestId = 0;
649
759
  private suggestDebounce: ReturnType<typeof setTimeout> | undefined;
650
760
  private controllerStatus: interfaces.IControllerStatus | undefined;
@@ -757,13 +867,30 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
757
867
  private readonly liveToolOverlays = new Map<string, ILiveToolOverlay>();
758
868
  private readonly liveReasoningUpdates = new Map<string, interfaces.IControllerReasoningUpdate>();
759
869
  private readonly liveTextUpdates = new Map<string, interfaces.IControllerTextUpdate>();
870
+ private readonly liveReasoningBaselines = new Map<
871
+ string,
872
+ ILiveMessageBaseline<interfaces.IControllerReasoningUpdate>
873
+ >();
874
+ private readonly liveTextBaselines = new Map<
875
+ string,
876
+ ILiveMessageBaseline<interfaces.IControllerTextUpdate>
877
+ >();
878
+ private readonly liveReasoningTextUtf8Bytes = new Map<string, number>();
879
+ private readonly liveTextTextUtf8Bytes = new Map<string, number>();
880
+ private readonly liveReasoningStoredBytes = new Map<string, number>();
881
+ private readonly liveTextStoredBytes = new Map<string, number>();
760
882
  private readonly liveToolHydrationFloors = new Map<string, ILiveToolHydrationFloor>();
761
883
  private readonly liveMessageHydrationFloors = new Map<string, ILiveMessageHydrationFloor>();
884
+ private readonly liveMessageDeltaBlocks = new Map<string, ILiveMessageDeltaBlock>();
762
885
  private readonly liveHistoryBarriers = new Map<string, number>();
763
886
  private liveToolOverlayBytes = 0;
764
887
  private liveMessageOverlayBytes = 0;
765
888
  private readonly liveToolStreamEpochs = new Map<TBrowserSessionHarnessId, number>();
766
889
  private readonly liveMessageStreamEpochs = new Map<TBrowserSessionHarnessId, number>();
890
+ private canonicalChatProjection: ICanonicalChatProjection | undefined;
891
+ private canonicalChatProjectionDirty = false;
892
+ private canonicalTimelineRefreshPending = false;
893
+ private readonly canonicalMessageRefreshIds = new Set<string>();
767
894
  private readonly subtaskPreviewEntries = new Map<string, ISubtaskPreviewEntry>();
768
895
  private subtaskPreviewGeneration = 0;
769
896
  private subtaskPreviewHydrationQueue: string[] = [];
@@ -841,8 +968,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
841
968
  @plugins.deesElement.state()
842
969
  accessor staleBundleVersion = '';
843
970
 
844
- @plugins.deesElement.state()
845
- accessor systemMetrics: interfaces.IControllerSystemMetrics | undefined;
971
+ public systemMetrics: interfaces.IControllerSystemMetrics | undefined;
846
972
 
847
973
  @plugins.deesElement.state()
848
974
  accessor highCpuWarning = false;
@@ -919,6 +1045,9 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
919
1045
  @plugins.deesElement.state()
920
1046
  accessor browserViewLoading = false;
921
1047
 
1048
+ @plugins.deesElement.state()
1049
+ accessor browserViewState: interfaces.IControllerBrowserViewState | undefined;
1050
+
922
1051
  @plugins.deesElement.state()
923
1052
  accessor selectedTerminalId: TBrowserTerminalId | undefined;
924
1053
 
@@ -940,7 +1069,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
940
1069
  onStatus: (statusArg) => this.handleConnectionStatus(statusArg),
941
1070
  onControllerEvent: (eventArg) => this.handleControllerEvent(eventArg),
942
1071
  onTerminalOutput: (outputArg) => this.handleTerminalOutput(outputArg),
943
- onBrowserViewEvent: (eventArg) => this.handleBrowserViewEvent(eventArg),
944
1072
  onTransportClose: (closeCodeArg, reasonArg) => {
945
1073
  this.recordTransportClose(closeCodeArg, reasonArg);
946
1074
  console.error(`Controller transport closed (code ${closeCodeArg}${reasonArg ? `: ${reasonArg}` : ''}).`);
@@ -949,6 +1077,13 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
949
1077
  ? 'The connection was closed because a response exceeded the transport frame limit. Reconnecting…'
950
1078
  : `The connection was closed by the transport (code ${closeCodeArg}${reasonArg ? `: ${reasonArg}` : ''}). Reconnecting…`;
951
1079
  },
1080
+ onReconnectExhausted: (attemptsArg, endpointArg) => {
1081
+ if (!this.componentConnected) return;
1082
+ this.connectionError = `Automatic reconnection stopped after ${attemptsArg} attempts to ${endpointArg}.`;
1083
+ void this.socketClient.stop().finally(() => {
1084
+ if (this.componentConnected) this.connectionStatus = 'disconnected';
1085
+ });
1086
+ },
952
1087
  });
953
1088
  this.sessionDraftSync = new SessionDraftSync({
954
1089
  transport: {
@@ -1824,8 +1959,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1824
1959
  padding: 0 var(--dees-spacing-sm);
1825
1960
  }
1826
1961
 
1827
- .brandSubtitle,
1828
- .headerActions .refreshAction {
1962
+ .brandSubtitle {
1829
1963
  display: none;
1830
1964
  }
1831
1965
 
@@ -1979,6 +2113,8 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1979
2113
 
1980
2114
  this.connectionStatus = statusArg;
1981
2115
  this.connectionGeneration += 1;
2116
+ if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer);
2117
+ this.browserViewCloseRetryTimer = undefined;
1982
2118
  if (this.browserViewPendingClose?.connectionGeneration !== this.connectionGeneration) {
1983
2119
  this.browserViewPendingClose = undefined;
1984
2120
  }
@@ -1997,6 +2133,8 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1997
2133
 
1998
2134
  private invalidateConnectionState(): void {
1999
2135
  this.connectionGeneration += 1;
2136
+ if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer);
2137
+ this.browserViewCloseRetryTimer = undefined;
2000
2138
  if (this.browserViewPendingClose?.connectionGeneration !== this.connectionGeneration) {
2001
2139
  this.browserViewPendingClose = undefined;
2002
2140
  }
@@ -2016,6 +2154,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2016
2154
  this.draftSessionDetailReadyId = undefined;
2017
2155
  this.authenticated = false;
2018
2156
  this.refreshPending = false;
2157
+ this.refreshRelatedPending = false;
2019
2158
  this.detailRefreshPending = false;
2020
2159
  this.projects = [];
2021
2160
  this.creatingProject = false;
@@ -2513,12 +2652,54 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2513
2652
 
2514
2653
  this.lastCpuSampleAt = sampledAt;
2515
2654
  this.systemMetrics = metricsArg;
2655
+ this.updateSystemMetricsDom();
2516
2656
  }
2517
2657
 
2518
2658
  private resetHighCpuWarning(): void {
2519
2659
  this.highCpuStartedAt = undefined;
2520
2660
  this.lastCpuSampleAt = undefined;
2521
2661
  this.highCpuWarning = false;
2662
+ this.updateSystemMetricsDom();
2663
+ }
2664
+
2665
+ private updateSystemMetricsDom(): void {
2666
+ const metrics = this.systemMetrics;
2667
+ const memoryPercent = metrics?.memoryUsedBytes !== null
2668
+ && metrics?.memoryUsedBytes !== undefined
2669
+ && metrics.memoryTotalBytes !== null
2670
+ && metrics.memoryTotalBytes !== undefined
2671
+ && metrics.memoryTotalBytes > 0
2672
+ ? (metrics.memoryUsedBytes / metrics.memoryTotalBytes) * 100
2673
+ : null;
2674
+ const cpu = this.shadowRoot?.querySelector<HTMLElement>('.cpuMetric');
2675
+ const memory = this.shadowRoot?.querySelector<HTMLElement>('.memoryMetric');
2676
+ const network = this.shadowRoot?.querySelector<HTMLElement>('.networkMetric');
2677
+ const cpuValue = cpu?.querySelector<HTMLElement>('.metricValue');
2678
+ const memoryValue = memory?.querySelector<HTMLElement>('.metricValue');
2679
+ const networkReceive = network?.querySelector<HTMLElement>('.networkReceive');
2680
+ const networkTransmit = network?.querySelector<HTMLElement>('.networkTransmit');
2681
+ if (cpu) {
2682
+ cpu.classList.toggle('highCpuWarning', this.highCpuWarning);
2683
+ cpu.title = this.highCpuWarning
2684
+ ? 'Whole-system CPU usage has remained above 50% for at least 10 seconds'
2685
+ : 'Whole-system CPU usage';
2686
+ }
2687
+ if (cpuValue) cpuValue.textContent = formatPercentage(metrics?.cpuUsagePercent);
2688
+ if (memory) {
2689
+ memory.title = metrics?.memoryUsedBytes !== null
2690
+ && metrics?.memoryUsedBytes !== undefined
2691
+ && metrics.memoryTotalBytes !== null
2692
+ && metrics.memoryTotalBytes !== undefined
2693
+ ? `${formatBytes(metrics.memoryUsedBytes)} used of ${formatBytes(metrics.memoryTotalBytes)}`
2694
+ : 'Memory usage unavailable';
2695
+ }
2696
+ if (memoryValue) memoryValue.textContent = formatPercentage(memoryPercent);
2697
+ if (networkReceive) {
2698
+ networkReceive.textContent = `in ${formatByteRate(metrics?.networkReceiveBytesPerSecond)}`;
2699
+ }
2700
+ if (networkTransmit) {
2701
+ networkTransmit.textContent = `out ${formatByteRate(metrics?.networkTransmitBytesPerSecond)}`;
2702
+ }
2522
2703
  }
2523
2704
 
2524
2705
  private readonly showLegalDisclosure = (): void => {
@@ -2843,12 +3024,13 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2843
3024
  }
2844
3025
  }
2845
3026
 
2846
- private refreshSessions(): Promise<void> {
3027
+ private refreshSessions(includeRelatedArg = true): Promise<void> {
2847
3028
  if (!this.authenticated || !this.socketClient.isConnected) {
2848
3029
  return Promise.resolve();
2849
3030
  }
2850
3031
 
2851
3032
  this.refreshPending = true;
3033
+ this.refreshRelatedPending ||= includeRelatedArg;
2852
3034
  if (!this.refreshInFlight) {
2853
3035
  this.refreshInFlight = this.runRefreshQueue();
2854
3036
  }
@@ -2930,8 +3112,16 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2930
3112
  if (harnessIdArg === undefined) {
2931
3113
  this.liveReasoningUpdates.clear();
2932
3114
  this.liveTextUpdates.clear();
3115
+ this.liveReasoningBaselines.clear();
3116
+ this.liveTextBaselines.clear();
3117
+ this.liveReasoningTextUtf8Bytes.clear();
3118
+ this.liveTextTextUtf8Bytes.clear();
3119
+ this.liveReasoningStoredBytes.clear();
3120
+ this.liveTextStoredBytes.clear();
2933
3121
  this.liveMessageHydrationFloors.clear();
3122
+ this.liveMessageDeltaBlocks.clear();
2934
3123
  this.liveMessageOverlayBytes = 0;
3124
+ this.canonicalChatProjectionDirty = true;
2935
3125
  return;
2936
3126
  }
2937
3127
  for (const [key, floor] of this.liveMessageHydrationFloors) {
@@ -2943,6 +3133,18 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2943
3133
  for (const [key, update] of this.liveTextUpdates) {
2944
3134
  if (update.sessionId.harnessId === harnessIdArg) this.deleteLiveTextUpdate(key);
2945
3135
  }
3136
+ for (const [key, baseline] of this.liveReasoningBaselines) {
3137
+ if (baseline.update.sessionId.harnessId === harnessIdArg) {
3138
+ this.deleteLiveReasoningBaseline(key);
3139
+ }
3140
+ }
3141
+ for (const [key, baseline] of this.liveTextBaselines) {
3142
+ if (baseline.update.sessionId.harnessId === harnessIdArg) this.deleteLiveTextBaseline(key);
3143
+ }
3144
+ for (const [key, block] of this.liveMessageDeltaBlocks) {
3145
+ if (block.sessionId.harnessId === harnessIdArg) this.liveMessageDeltaBlocks.delete(key);
3146
+ }
3147
+ this.canonicalChatProjectionDirty = true;
2946
3148
  }
2947
3149
 
2948
3150
  private clearLiveToolSession(
@@ -2967,21 +3169,38 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2967
3169
  && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
2968
3170
  ) this.deleteLiveTextUpdate(key);
2969
3171
  }
3172
+ for (const [key, baseline] of this.liveReasoningBaselines) {
3173
+ if (
3174
+ key.startsWith(`${projectIdArg}\0`)
3175
+ && controllerRuntimeIdsEqual(baseline.update.sessionId, sessionIdArg)
3176
+ ) this.deleteLiveReasoningBaseline(key);
3177
+ }
3178
+ for (const [key, baseline] of this.liveTextBaselines) {
3179
+ if (
3180
+ key.startsWith(`${projectIdArg}\0`)
3181
+ && controllerRuntimeIdsEqual(baseline.update.sessionId, sessionIdArg)
3182
+ ) this.deleteLiveTextBaseline(key);
3183
+ }
3184
+ this.canonicalChatProjectionDirty = true;
2970
3185
  // Keep both hydration floors: they continue fencing snapshots already
2971
3186
  // represented by the last authoritative detail until its replacement lands.
2972
3187
  }
2973
3188
 
2974
3189
  private liveMessageUpdateBytes(updateArg: unknown): number {
2975
- return new TextEncoder().encode(JSON.stringify(updateArg)).byteLength;
3190
+ return browserTextEncoder.encode(JSON.stringify(updateArg)).byteLength;
2976
3191
  }
2977
3192
 
2978
3193
  private deleteLiveReasoningUpdate(keyArg: string): void {
2979
3194
  const update = this.liveReasoningUpdates.get(keyArg);
2980
3195
  if (!update) return;
2981
3196
  this.liveReasoningUpdates.delete(keyArg);
3197
+ this.liveReasoningTextUtf8Bytes.delete(keyArg);
3198
+ const bytes = this.liveReasoningStoredBytes.get(keyArg)
3199
+ ?? this.liveMessageUpdateBytes(update);
3200
+ this.liveReasoningStoredBytes.delete(keyArg);
2982
3201
  this.liveMessageOverlayBytes = Math.max(
2983
3202
  0,
2984
- this.liveMessageOverlayBytes - this.liveMessageUpdateBytes(update),
3203
+ this.liveMessageOverlayBytes - bytes,
2985
3204
  );
2986
3205
  }
2987
3206
 
@@ -2989,29 +3208,61 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2989
3208
  const update = this.liveTextUpdates.get(keyArg);
2990
3209
  if (!update) return;
2991
3210
  this.liveTextUpdates.delete(keyArg);
3211
+ this.liveTextTextUtf8Bytes.delete(keyArg);
3212
+ const bytes = this.liveTextStoredBytes.get(keyArg)
3213
+ ?? this.liveMessageUpdateBytes(update);
3214
+ this.liveTextStoredBytes.delete(keyArg);
2992
3215
  this.liveMessageOverlayBytes = Math.max(
2993
3216
  0,
2994
- this.liveMessageOverlayBytes - this.liveMessageUpdateBytes(update),
3217
+ this.liveMessageOverlayBytes - bytes,
2995
3218
  );
2996
3219
  }
2997
3220
 
3221
+ private deleteLiveReasoningBaseline(keyArg: string): void {
3222
+ const baseline = this.liveReasoningBaselines.get(keyArg);
3223
+ if (!baseline) return;
3224
+ this.liveReasoningBaselines.delete(keyArg);
3225
+ this.liveMessageOverlayBytes = Math.max(0, this.liveMessageOverlayBytes - baseline.bytes);
3226
+ }
3227
+
3228
+ private deleteLiveTextBaseline(keyArg: string): void {
3229
+ const baseline = this.liveTextBaselines.get(keyArg);
3230
+ if (!baseline) return;
3231
+ this.liveTextBaselines.delete(keyArg);
3232
+ this.liveMessageOverlayBytes = Math.max(0, this.liveMessageOverlayBytes - baseline.bytes);
3233
+ }
3234
+
3235
+ private liveMessageEntryCount(): number {
3236
+ return this.liveReasoningUpdates.size
3237
+ + this.liveTextUpdates.size
3238
+ + this.liveReasoningBaselines.size
3239
+ + this.liveTextBaselines.size;
3240
+ }
3241
+
2998
3242
  private storeLiveReasoningUpdate(
2999
3243
  keyArg: string,
3000
3244
  updateArg: interfaces.IControllerReasoningUpdate,
3001
3245
  ): boolean {
3002
3246
  const bytes = this.liveMessageUpdateBytes(updateArg);
3003
3247
  const existing = this.liveReasoningUpdates.get(keyArg);
3004
- const existingBytes = existing ? this.liveMessageUpdateBytes(existing) : 0;
3248
+ const baseline = this.liveReasoningBaselines.get(keyArg);
3249
+ const existingBytes = existing
3250
+ ? this.liveReasoningStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(existing)
3251
+ : baseline?.bytes ?? 0;
3005
3252
  if (
3006
- (!existing && this.liveReasoningUpdates.size + this.liveTextUpdates.size >= maxLiveToolOverlayEntries)
3253
+ (!existing && !baseline && this.liveMessageEntryCount() >= maxLiveToolOverlayEntries)
3007
3254
  || this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes
3008
3255
  ) {
3009
- this.clearLiveMessageState();
3010
- this.refreshSessionsNow();
3011
3256
  return false;
3012
3257
  }
3013
3258
  if (existing) this.deleteLiveReasoningUpdate(keyArg);
3259
+ if (baseline) this.deleteLiveReasoningBaseline(keyArg);
3014
3260
  this.liveReasoningUpdates.set(keyArg, updateArg);
3261
+ this.liveReasoningTextUtf8Bytes.set(
3262
+ keyArg,
3263
+ browserTextEncoder.encode(updateArg.text).byteLength,
3264
+ );
3265
+ this.liveReasoningStoredBytes.set(keyArg, bytes);
3015
3266
  this.liveMessageOverlayBytes += bytes;
3016
3267
  return true;
3017
3268
  }
@@ -3022,21 +3273,168 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3022
3273
  ): boolean {
3023
3274
  const bytes = this.liveMessageUpdateBytes(updateArg);
3024
3275
  const existing = this.liveTextUpdates.get(keyArg);
3025
- const existingBytes = existing ? this.liveMessageUpdateBytes(existing) : 0;
3276
+ const baseline = this.liveTextBaselines.get(keyArg);
3277
+ const existingBytes = existing
3278
+ ? this.liveTextStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(existing)
3279
+ : baseline?.bytes ?? 0;
3026
3280
  if (
3027
- (!existing && this.liveReasoningUpdates.size + this.liveTextUpdates.size >= maxLiveToolOverlayEntries)
3281
+ (!existing && !baseline && this.liveMessageEntryCount() >= maxLiveToolOverlayEntries)
3028
3282
  || this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes
3029
3283
  ) {
3030
- this.clearLiveMessageState();
3031
- this.refreshSessionsNow();
3032
3284
  return false;
3033
3285
  }
3034
3286
  if (existing) this.deleteLiveTextUpdate(keyArg);
3287
+ if (baseline) this.deleteLiveTextBaseline(keyArg);
3288
+ this.liveTextUpdates.set(keyArg, updateArg);
3289
+ this.liveTextTextUtf8Bytes.set(keyArg, browserTextEncoder.encode(updateArg.text).byteLength);
3290
+ this.liveTextStoredBytes.set(keyArg, bytes);
3291
+ this.liveMessageOverlayBytes += bytes;
3292
+ return true;
3293
+ }
3294
+
3295
+ private retireLiveReasoningUpdateToBaseline(
3296
+ keyArg: string,
3297
+ updateArg: interfaces.IControllerReasoningUpdate,
3298
+ textArg: string,
3299
+ cursorArg: interfaces.IControllerMessageStreamCursor,
3300
+ ): void {
3301
+ const update = { ...updateArg, text: textArg, revision: cursorArg.revision };
3302
+ const bytes = this.liveMessageUpdateBytes(update);
3303
+ const existingBytes = this.liveReasoningStoredBytes.get(keyArg)
3304
+ ?? this.liveReasoningBaselines.get(keyArg)?.bytes
3305
+ ?? this.liveMessageUpdateBytes(updateArg);
3306
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) {
3307
+ this.deleteLiveReasoningUpdate(keyArg);
3308
+ this.deleteLiveReasoningBaseline(keyArg);
3309
+ return;
3310
+ }
3311
+ this.deleteLiveReasoningUpdate(keyArg);
3312
+ this.deleteLiveReasoningBaseline(keyArg);
3313
+ this.liveReasoningBaselines.set(keyArg, {
3314
+ update,
3315
+ textUtf8Bytes: browserTextEncoder.encode(textArg).byteLength,
3316
+ bytes,
3317
+ });
3318
+ this.liveMessageOverlayBytes += bytes;
3319
+ }
3320
+
3321
+ private retireLiveTextUpdateToBaseline(
3322
+ keyArg: string,
3323
+ updateArg: interfaces.IControllerTextUpdate,
3324
+ textArg: string,
3325
+ cursorArg: interfaces.IControllerMessageStreamCursor,
3326
+ ): void {
3327
+ const update = { ...updateArg, text: textArg, revision: cursorArg.revision };
3328
+ const bytes = this.liveMessageUpdateBytes(update);
3329
+ const existingBytes = this.liveTextStoredBytes.get(keyArg)
3330
+ ?? this.liveTextBaselines.get(keyArg)?.bytes
3331
+ ?? this.liveMessageUpdateBytes(updateArg);
3332
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) {
3333
+ this.deleteLiveTextUpdate(keyArg);
3334
+ this.deleteLiveTextBaseline(keyArg);
3335
+ return;
3336
+ }
3337
+ this.deleteLiveTextUpdate(keyArg);
3338
+ this.deleteLiveTextBaseline(keyArg);
3339
+ this.liveTextBaselines.set(keyArg, {
3340
+ update,
3341
+ textUtf8Bytes: browserTextEncoder.encode(textArg).byteLength,
3342
+ bytes,
3343
+ });
3344
+ this.liveMessageOverlayBytes += bytes;
3345
+ }
3346
+
3347
+ private storeLiveReasoningDeltaState(
3348
+ keyArg: string,
3349
+ updateArg: interfaces.IControllerReasoningUpdate,
3350
+ textUtf8BytesArg: number,
3351
+ deltaArg: string,
3352
+ ): boolean {
3353
+ const active = this.liveReasoningUpdates.get(keyArg);
3354
+ const baseline = this.liveReasoningBaselines.get(keyArg);
3355
+ const existingBytes = active
3356
+ ? this.liveReasoningStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(active)
3357
+ : baseline?.bytes ?? 0;
3358
+ const bytes = existingBytes + Math.max(
3359
+ browserTextEncoder.encode(deltaArg).byteLength,
3360
+ deltaArg.length * 6,
3361
+ );
3362
+ if (!active && !baseline) return false;
3363
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) return false;
3364
+ this.deleteLiveReasoningUpdate(keyArg);
3365
+ this.deleteLiveReasoningBaseline(keyArg);
3366
+ this.liveReasoningUpdates.set(keyArg, updateArg);
3367
+ this.liveReasoningTextUtf8Bytes.set(keyArg, textUtf8BytesArg);
3368
+ this.liveReasoningStoredBytes.set(keyArg, bytes);
3369
+ this.liveMessageOverlayBytes += bytes;
3370
+ return true;
3371
+ }
3372
+
3373
+ private storeLiveTextDeltaState(
3374
+ keyArg: string,
3375
+ updateArg: interfaces.IControllerTextUpdate,
3376
+ textUtf8BytesArg: number,
3377
+ deltaArg: string,
3378
+ ): boolean {
3379
+ const active = this.liveTextUpdates.get(keyArg);
3380
+ const baseline = this.liveTextBaselines.get(keyArg);
3381
+ const existingBytes = active
3382
+ ? this.liveTextStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(active)
3383
+ : baseline?.bytes ?? 0;
3384
+ const bytes = existingBytes + Math.max(
3385
+ browserTextEncoder.encode(deltaArg).byteLength,
3386
+ deltaArg.length * 6,
3387
+ );
3388
+ if (!active && !baseline) return false;
3389
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) return false;
3390
+ this.deleteLiveTextUpdate(keyArg);
3391
+ this.deleteLiveTextBaseline(keyArg);
3035
3392
  this.liveTextUpdates.set(keyArg, updateArg);
3393
+ this.liveTextTextUtf8Bytes.set(keyArg, textUtf8BytesArg);
3394
+ this.liveTextStoredBytes.set(keyArg, bytes);
3036
3395
  this.liveMessageOverlayBytes += bytes;
3037
3396
  return true;
3038
3397
  }
3039
3398
 
3399
+ private liveMessageUpdateKey(
3400
+ projectIdArg: string,
3401
+ updateArg: interfaces.IControllerReasoningUpdate
3402
+ | interfaces.IControllerTextUpdate
3403
+ | interfaces.IControllerReasoningDelta
3404
+ | interfaces.IControllerTextDelta,
3405
+ ): string {
3406
+ return `${projectIdArg}\0${controllerRuntimeIdToUiKey(updateArg.sessionId)}\0${controllerRuntimeIdToUiKey(updateArg.partId)}`;
3407
+ }
3408
+
3409
+ private blockLiveMessageDeltas(
3410
+ projectIdArg: string,
3411
+ sessionIdArg: TBrowserSessionId,
3412
+ streamEpochArg: number,
3413
+ refreshArg = true,
3414
+ ): void {
3415
+ const sessionKey = this.sessionOperationKey(projectIdArg, sessionIdArg);
3416
+ if (this.liveMessageDeltaBlocks.get(sessionKey)?.streamEpoch === streamEpochArg) return;
3417
+ this.liveMessageDeltaBlocks.set(sessionKey, {
3418
+ sessionId: sessionIdArg,
3419
+ streamEpoch: streamEpochArg,
3420
+ });
3421
+ if (
3422
+ refreshArg
3423
+ && projectIdArg === this.selectedProjectId
3424
+ && controllerRuntimeIdsEqual(sessionIdArg, this.selectedSessionId)
3425
+ ) void this.refreshSelectedSessionDetail();
3426
+ }
3427
+
3428
+ private liveMessageDeltasBlocked(
3429
+ projectIdArg: string,
3430
+ sessionIdArg: TBrowserSessionId,
3431
+ streamEpochArg: number,
3432
+ ): boolean {
3433
+ return this.liveMessageDeltaBlocks.get(
3434
+ this.sessionOperationKey(projectIdArg, sessionIdArg),
3435
+ )?.streamEpoch === streamEpochArg;
3436
+ }
3437
+
3040
3438
  private acceptLiveToolEpoch(
3041
3439
  harnessIdArg: TBrowserSessionHarnessId,
3042
3440
  epochArg: number,
@@ -3067,6 +3465,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3067
3465
  harnessIdArg: TBrowserSessionHarnessId,
3068
3466
  epochArg: number,
3069
3467
  handleBarrierArg = true,
3468
+ refreshSessionListArg = true,
3070
3469
  ): boolean {
3071
3470
  const currentEpoch = this.liveMessageStreamEpochs.get(harnessIdArg) ?? 0;
3072
3471
  if (epochArg < currentEpoch) return false;
@@ -3083,7 +3482,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3083
3482
  this.invalidateDetailEnrichment();
3084
3483
  void this.refreshSelectedSessionDetail();
3085
3484
  }
3086
- this.refreshSessionsNow();
3485
+ if (refreshSessionListArg) this.refreshSessionsNow();
3087
3486
  }
3088
3487
  }
3089
3488
  return true;
@@ -3153,6 +3552,20 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3153
3552
  updateArg: interfaces.IControllerReasoningUpdate,
3154
3553
  detailArg: IControllerSessionRenderDetail,
3155
3554
  ): boolean {
3555
+ const target = this.authoritativeReasoningTarget(updateArg, detailArg);
3556
+ return target !== undefined
3557
+ && (target.part.text === updateArg.text || target.part.text.startsWith(updateArg.text))
3558
+ && (
3559
+ updateArg.status === 'running'
3560
+ || target.part.endedAt !== undefined
3561
+ || target.message.streaming !== true
3562
+ );
3563
+ }
3564
+
3565
+ private authoritativeReasoningTarget(
3566
+ updateArg: interfaces.IControllerReasoningUpdate,
3567
+ detailArg: IControllerSessionRenderDetail,
3568
+ ): { message: interfaces.IControllerMessage; part: interfaces.IControllerReasoningPart } | undefined {
3156
3569
  const message = detailArg.messages.find((messageArg) => (
3157
3570
  messageArg.role === 'assistant'
3158
3571
  && (
@@ -3169,16 +3582,24 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3169
3582
  const part = message?.reasoning?.find((partArg) => (
3170
3583
  controllerRuntimeIdsEqual(partArg.id, updateArg.partId)
3171
3584
  ));
3172
- return part !== undefined
3173
- && (part.text === updateArg.text || part.text.startsWith(updateArg.text))
3174
- && (updateArg.status === 'running' || part.endedAt !== undefined || message?.streaming !== true);
3585
+ return message && part ? { message, part } : undefined;
3175
3586
  }
3176
3587
 
3177
3588
  private authoritativeTextUpdateCovers(
3178
3589
  updateArg: interfaces.IControllerTextUpdate,
3179
3590
  detailArg: IControllerSessionRenderDetail,
3180
3591
  ): boolean {
3181
- const message = detailArg.messages.find((messageArg) => (
3592
+ const message = this.authoritativeTextTarget(updateArg, detailArg);
3593
+ return message !== undefined
3594
+ && (message.text === updateArg.text || message.text.startsWith(updateArg.text))
3595
+ && (updateArg.status === 'running' || message.streaming !== true);
3596
+ }
3597
+
3598
+ private authoritativeTextTarget(
3599
+ updateArg: interfaces.IControllerTextUpdate,
3600
+ detailArg: IControllerSessionRenderDetail,
3601
+ ): interfaces.IControllerMessage | undefined {
3602
+ return detailArg.messages.find((messageArg) => (
3182
3603
  messageArg.role === 'assistant'
3183
3604
  && (
3184
3605
  controllerRuntimeIdsEqual(messageArg.id, updateArg.partId)
@@ -3189,9 +3610,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3189
3610
  )
3190
3611
  )
3191
3612
  ));
3192
- return message !== undefined
3193
- && (message.text === updateArg.text || message.text.startsWith(updateArg.text))
3194
- && (updateArg.status === 'running' || message.streaming !== true);
3195
3613
  }
3196
3614
 
3197
3615
  private liveToolSnapshotIsNewer(
@@ -3259,6 +3677,257 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3259
3677
  return this.authoritativeToolCallCovers(merged, toolCallArg) ? undefined : merged;
3260
3678
  }
3261
3679
 
3680
+ private ensureCanonicalChatProjection(
3681
+ detailArg: IControllerSessionRenderDetail,
3682
+ projectIdArg: string,
3683
+ sessionIdArg: TBrowserSessionId,
3684
+ ): ICanonicalChatProjection {
3685
+ const key = this.sessionOperationKey(projectIdArg, sessionIdArg);
3686
+ let projection = this.canonicalChatProjection;
3687
+ if (!projection || projection.key !== key) {
3688
+ projection = {
3689
+ key,
3690
+ sourceDetail: undefined,
3691
+ messages: [],
3692
+ messagesById: new Map(),
3693
+ messagesByOrder: new Map(),
3694
+ reasoningById: new Map(),
3695
+ };
3696
+ this.canonicalChatProjection = projection;
3697
+ this.canonicalChatProjectionDirty = true;
3698
+ }
3699
+ if (projection.sourceDetail === detailArg && !this.canonicalChatProjectionDirty) {
3700
+ return projection;
3701
+ }
3702
+
3703
+ const messages = detailArg.messages
3704
+ .filter((messageArg) => messageArg.toolCall?.name !== 'question')
3705
+ .map((messageArg) => this.toHarnessMessage(messageArg, projectIdArg, sessionIdArg));
3706
+ const visibleMessageIds = new Set(messages.map((message) => message.id));
3707
+ for (const prompt of detailArg.pendingPrompts) {
3708
+ const id = controllerRuntimeIdToUiKey(prompt.id);
3709
+ if (visibleMessageIds.has(id)) continue;
3710
+ visibleMessageIds.add(id);
3711
+ messages.push({
3712
+ id,
3713
+ role: 'user',
3714
+ text: prompt.text,
3715
+ createdAt: prompt.createdAt,
3716
+ markdown: false,
3717
+ attachments: prompt.attachments.map((attachment) => ({ ...attachment })),
3718
+ });
3719
+ }
3720
+ this.mergeLiveReasoningMessages(messages, projectIdArg, sessionIdArg);
3721
+ this.mergeLiveTextMessages(messages, projectIdArg, sessionIdArg);
3722
+ this.mergeLiveToolMessages(messages, projectIdArg, sessionIdArg);
3723
+ const structureChanged = this.canonicalHarnessStructureChanged(
3724
+ projection.messages,
3725
+ messages,
3726
+ );
3727
+ this.reconcileCanonicalHarnessMessages(projection, messages);
3728
+ this.canonicalTimelineRefreshPending ||= structureChanged;
3729
+ projection.sourceDetail = detailArg;
3730
+ this.canonicalChatProjectionDirty = false;
3731
+ return projection;
3732
+ }
3733
+
3734
+ private canonicalHarnessStructureChanged(
3735
+ currentArg: readonly plugins.deesCatalog.IHarnessMessage[],
3736
+ nextArg: readonly plugins.deesCatalog.IHarnessMessage[],
3737
+ ): boolean {
3738
+ return currentArg.length !== nextArg.length || currentArg.some((current, index) => {
3739
+ const next = nextArg[index];
3740
+ return !next
3741
+ || current.id !== next.id
3742
+ || current.role !== next.role
3743
+ || current.toolCall?.id !== next.toolCall?.id
3744
+ || current.toolCall?.name !== next.toolCall?.name
3745
+ || !this.liveMessageOrdersEqual(current.order, next.order);
3746
+ });
3747
+ }
3748
+
3749
+ private reconcileCanonicalHarnessMessages(
3750
+ projectionArg: ICanonicalChatProjection,
3751
+ nextMessagesArg: plugins.deesCatalog.IHarnessMessage[],
3752
+ ): void {
3753
+ const currentById = new Map(
3754
+ projectionArg.messages.map((message) => [message.id, message]),
3755
+ );
3756
+ const reconciled = nextMessagesArg.map((nextMessage) => {
3757
+ const current = currentById.get(nextMessage.id);
3758
+ if (!current) return nextMessage;
3759
+ if (!controllerJsonValuesEqual(current, nextMessage)) {
3760
+ this.canonicalMessageRefreshIds.add(current.id);
3761
+ }
3762
+ this.reconcileCanonicalHarnessMessage(current, nextMessage);
3763
+ return current;
3764
+ });
3765
+ projectionArg.messages.splice(0, projectionArg.messages.length, ...reconciled);
3766
+ this.rebuildCanonicalChatIndexes(projectionArg);
3767
+ }
3768
+
3769
+ private reconcileCanonicalHarnessMessage(
3770
+ targetArg: plugins.deesCatalog.IHarnessMessage,
3771
+ sourceArg: plugins.deesCatalog.IHarnessMessage,
3772
+ ): void {
3773
+ const targetReasoning = targetArg.reasoning;
3774
+ const sourceReasoning = sourceArg.reasoning;
3775
+ const targetToolCall = targetArg.toolCall;
3776
+ const sourceToolCall = sourceArg.toolCall;
3777
+ const target = targetArg as unknown as Record<string, unknown>;
3778
+ const source = sourceArg as unknown as Record<string, unknown>;
3779
+ for (const key of Object.keys(target)) {
3780
+ if (!(key in source)) delete target[key];
3781
+ }
3782
+ for (const [key, value] of Object.entries(source)) {
3783
+ if (key !== 'reasoning' && key !== 'toolCall') target[key] = value;
3784
+ }
3785
+
3786
+ if (sourceReasoning) {
3787
+ const currentById = new Map((targetReasoning ?? []).map((part) => [part.id, part]));
3788
+ const reasoning = sourceReasoning.map((sourcePart) => {
3789
+ const currentPart = currentById.get(sourcePart.id);
3790
+ if (!currentPart) return sourcePart;
3791
+ const current = currentPart as unknown as Record<string, unknown>;
3792
+ const next = sourcePart as unknown as Record<string, unknown>;
3793
+ for (const key of Object.keys(current)) {
3794
+ if (!(key in next)) delete current[key];
3795
+ }
3796
+ Object.assign(current, next);
3797
+ return currentPart;
3798
+ });
3799
+ if (targetReasoning) targetReasoning.splice(0, targetReasoning.length, ...reasoning);
3800
+ else targetArg.reasoning = reasoning;
3801
+ }
3802
+
3803
+ if (sourceToolCall) {
3804
+ if (targetToolCall?.id === sourceToolCall.id) {
3805
+ const current = targetToolCall as unknown as Record<string, unknown>;
3806
+ const next = sourceToolCall as unknown as Record<string, unknown>;
3807
+ for (const key of Object.keys(current)) {
3808
+ if (!(key in next)) delete current[key];
3809
+ }
3810
+ Object.assign(current, next);
3811
+ targetArg.toolCall = targetToolCall;
3812
+ } else {
3813
+ targetArg.toolCall = sourceToolCall;
3814
+ }
3815
+ }
3816
+ }
3817
+
3818
+ private rebuildCanonicalChatIndexes(projectionArg: ICanonicalChatProjection): void {
3819
+ projectionArg.messagesById.clear();
3820
+ projectionArg.messagesByOrder.clear();
3821
+ projectionArg.reasoningById.clear();
3822
+ for (const message of projectionArg.messages) {
3823
+ projectionArg.messagesById.set(message.id, message);
3824
+ if (message.order && !projectionArg.messagesByOrder.has(transcriptOrderKey(message.order))) {
3825
+ projectionArg.messagesByOrder.set(transcriptOrderKey(message.order), message);
3826
+ }
3827
+ for (const part of message.reasoning ?? []) {
3828
+ projectionArg.reasoningById.set(part.id, { message, part });
3829
+ }
3830
+ }
3831
+ }
3832
+
3833
+ private selectedCanonicalChatProjection(
3834
+ projectIdArg: string,
3835
+ sessionIdArg: TBrowserSessionId,
3836
+ ): ICanonicalChatProjection | undefined {
3837
+ const detail = this.sessionDetail;
3838
+ if (
3839
+ projectIdArg !== this.selectedProjectId
3840
+ || !detail
3841
+ || !controllerRuntimeIdsEqual(detail.session.id, sessionIdArg)
3842
+ || !controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg)
3843
+ ) return undefined;
3844
+ return this.ensureCanonicalChatProjection(detail, projectIdArg, sessionIdArg);
3845
+ }
3846
+
3847
+ private canonicalTextTarget(
3848
+ projectionArg: ICanonicalChatProjection,
3849
+ updateArg: interfaces.IControllerTextUpdate | interfaces.IControllerTextDelta,
3850
+ ): plugins.deesCatalog.IHarnessMessage | undefined {
3851
+ return projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.messageId))
3852
+ ?? projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.partId))
3853
+ ?? (updateArg.order
3854
+ ? projectionArg.messagesByOrder.get(transcriptOrderKey(updateArg.order))
3855
+ : undefined);
3856
+ }
3857
+
3858
+ private canonicalReasoningTarget(
3859
+ projectionArg: ICanonicalChatProjection,
3860
+ updateArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerReasoningDelta,
3861
+ ): ICanonicalReasoningTarget | undefined {
3862
+ const reasoningId = controllerRuntimeIdToUiKey(updateArg.partId);
3863
+ const partTarget = projectionArg.reasoningById.get(reasoningId);
3864
+ const message = projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.messageId))
3865
+ ?? partTarget?.message
3866
+ ?? (updateArg.order
3867
+ ? projectionArg.messagesByOrder.get(transcriptOrderKey(updateArg.order))
3868
+ : undefined);
3869
+ if (!message) return undefined;
3870
+ const part = partTarget?.message === message
3871
+ ? partTarget.part
3872
+ : message.reasoning?.find((candidate) => candidate.id === reasoningId);
3873
+ return part ? { message, part } : undefined;
3874
+ }
3875
+
3876
+ private mountedCanonicalChat(
3877
+ projectionArg: ICanonicalChatProjection,
3878
+ ): InstanceType<typeof plugins.deesCatalog.DeesHarnessChat> | undefined {
3879
+ const chat = this.shadowRoot?.querySelector('dees-harness-chat');
3880
+ return chat instanceof plugins.deesCatalog.DeesHarnessChat
3881
+ && chat.messages === projectionArg.messages
3882
+ && chat.transcriptKey === projectionArg.key
3883
+ ? chat
3884
+ : undefined;
3885
+ }
3886
+
3887
+ private applyCanonicalTextDelta(
3888
+ projectionArg: ICanonicalChatProjection,
3889
+ messageArg: plugins.deesCatalog.IHarnessMessage,
3890
+ deltaArg: string,
3891
+ ): boolean {
3892
+ const previousText = messageArg.text;
3893
+ const expectedText = `${previousText}${deltaArg}`;
3894
+ const chat = this.mountedCanonicalChat(projectionArg);
3895
+ chat?.applyDelta({ type: 'text', messageId: messageArg.id, delta: deltaArg });
3896
+ if (messageArg.text === previousText) {
3897
+ messageArg.text = expectedText;
3898
+ messageArg.streaming = true;
3899
+ }
3900
+ if (messageArg.text === expectedText) return true;
3901
+ messageArg.text = previousText;
3902
+ return false;
3903
+ }
3904
+
3905
+ private applyCanonicalReasoningDelta(
3906
+ projectionArg: ICanonicalChatProjection,
3907
+ targetArg: ICanonicalReasoningTarget,
3908
+ deltaArg: string,
3909
+ ): boolean {
3910
+ const previousText = targetArg.part.text;
3911
+ const expectedText = `${previousText}${deltaArg}`;
3912
+ const chat = this.mountedCanonicalChat(projectionArg);
3913
+ chat?.applyDelta({
3914
+ type: 'reasoning',
3915
+ messageId: targetArg.message.id,
3916
+ partId: targetArg.part.id,
3917
+ delta: deltaArg,
3918
+ });
3919
+ if (targetArg.part.text === previousText) targetArg.part.text = expectedText;
3920
+ if (targetArg.part.text === expectedText) return true;
3921
+ targetArg.part.text = previousText;
3922
+ return false;
3923
+ }
3924
+
3925
+ private requestCanonicalChatReconciliation(structureMayChangeArg = true): void {
3926
+ this.canonicalChatProjectionDirty = true;
3927
+ this.canonicalTimelineRefreshPending ||= structureMayChangeArg;
3928
+ this.requestUpdate();
3929
+ }
3930
+
3262
3931
  private applyLiveToolExecution(
3263
3932
  projectIdArg: string,
3264
3933
  executionArg: interfaces.IControllerToolExecution,
@@ -3332,24 +4001,22 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3332
4001
  }
3333
4002
  this.reconcileSubtaskPreviews(projectIdArg, executionArg.sessionId);
3334
4003
 
3335
- const chat = this.shadowRoot?.querySelector('dees-harness-chat');
4004
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, executionArg.sessionId);
4005
+ if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate();
3336
4006
  const messageId = controllerRuntimeIdToUiKey(execution.partId);
3337
4007
  const harnessToolCall = this.toHarnessLiveToolCall(execution, projectIdArg);
3338
- if (
3339
- chat instanceof plugins.deesCatalog.DeesHarnessChat
3340
- && chat.messages.some((messageArg) => messageArg.id === messageId)
3341
- ) {
3342
- const message = chat.messages.find((messageArg) => messageArg.id === messageId)!;
4008
+ const message = projection?.messagesById.get(messageId);
4009
+ if (projection && message) {
3343
4010
  message.toolCall = harnessToolCall;
3344
4011
  message.streaming = execution.status === 'pending' || execution.status === 'running';
3345
4012
  message.updatedAt = execution.finishedAt ?? execution.sourceUpdatedAt;
3346
- chat.applyDelta({
4013
+ this.mountedCanonicalChat(projection)?.applyDelta({
3347
4014
  type: 'tool-update',
3348
4015
  messageId,
3349
4016
  tool: harnessToolCall,
3350
4017
  });
3351
4018
  } else {
3352
- this.requestUpdate();
4019
+ this.requestCanonicalChatReconciliation();
3353
4020
  }
3354
4021
  }
3355
4022
 
@@ -3388,23 +4055,59 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3388
4055
  if (cursorArg.streamEpoch < (this.liveMessageStreamEpochs.get(harnessId) ?? 0)) return false;
3389
4056
  if (!this.acceptLiveMessageEpoch(harnessId, cursorArg.streamEpoch, false)) return false;
3390
4057
  const sessionKey = this.liveToolSessionKey(projectIdArg, sessionIdArg);
4058
+ const deltaBlockKey = this.sessionOperationKey(projectIdArg, sessionIdArg);
4059
+ const recoveringDeltaGap = this.liveMessageDeltaBlocks.get(deltaBlockKey)?.streamEpoch
4060
+ === cursorArg.streamEpoch;
3391
4061
  this.liveMessageHydrationFloors.set(sessionKey, { sessionId: sessionIdArg, cursor: cursorArg });
3392
4062
  for (const [key, update] of this.liveReasoningUpdates) {
4063
+ const target = this.authoritativeReasoningTarget(update, detailArg);
3393
4064
  if (
3394
4065
  key.startsWith(`${projectIdArg}\0`)
3395
4066
  && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
3396
4067
  && this.hydrationCursorCovers(cursorArg, update)
3397
- && this.authoritativeReasoningUpdateCovers(update, detailArg)
3398
- ) this.deleteLiveReasoningUpdate(key);
4068
+ && target !== undefined
4069
+ && (recoveringDeltaGap || this.authoritativeReasoningUpdateCovers(update, detailArg))
4070
+ ) this.retireLiveReasoningUpdateToBaseline(key, update, target.part.text, cursorArg);
3399
4071
  }
3400
4072
  for (const [key, update] of this.liveTextUpdates) {
4073
+ const target = this.authoritativeTextTarget(update, detailArg);
3401
4074
  if (
3402
4075
  key.startsWith(`${projectIdArg}\0`)
3403
4076
  && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
3404
4077
  && this.hydrationCursorCovers(cursorArg, update)
3405
- && this.authoritativeTextUpdateCovers(update, detailArg)
3406
- ) this.deleteLiveTextUpdate(key);
4078
+ && target !== undefined
4079
+ && (recoveringDeltaGap || this.authoritativeTextUpdateCovers(update, detailArg))
4080
+ ) this.retireLiveTextUpdateToBaseline(key, update, target.text, cursorArg);
3407
4081
  }
4082
+ for (const [key, baseline] of [...this.liveReasoningBaselines]) {
4083
+ const update = baseline.update;
4084
+ if (
4085
+ !key.startsWith(`${projectIdArg}\0`)
4086
+ || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
4087
+ || !this.hydrationCursorCovers(cursorArg, update)
4088
+ ) continue;
4089
+ const target = this.authoritativeReasoningTarget(update, detailArg);
4090
+ if (target) {
4091
+ this.retireLiveReasoningUpdateToBaseline(key, update, target.part.text, cursorArg);
4092
+ } else if (recoveringDeltaGap) {
4093
+ this.deleteLiveReasoningBaseline(key);
4094
+ }
4095
+ }
4096
+ for (const [key, baseline] of [...this.liveTextBaselines]) {
4097
+ const update = baseline.update;
4098
+ if (
4099
+ !key.startsWith(`${projectIdArg}\0`)
4100
+ || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
4101
+ || !this.hydrationCursorCovers(cursorArg, update)
4102
+ ) continue;
4103
+ const target = this.authoritativeTextTarget(update, detailArg);
4104
+ if (target) {
4105
+ this.retireLiveTextUpdateToBaseline(key, update, target.text, cursorArg);
4106
+ } else if (recoveringDeltaGap) {
4107
+ this.deleteLiveTextBaseline(key);
4108
+ }
4109
+ }
4110
+ this.liveMessageDeltaBlocks.delete(deltaBlockKey);
3408
4111
  return true;
3409
4112
  }
3410
4113
 
@@ -3448,6 +4151,30 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3448
4151
  || !controllerRuntimeIdsEqual(update.sessionId, sessionId)
3449
4152
  ) this.deleteLiveTextUpdate(key);
3450
4153
  }
4154
+ for (const [key, baseline] of this.liveReasoningBaselines) {
4155
+ if (
4156
+ !projectId
4157
+ || !isSessionRuntimeId(sessionId)
4158
+ || !key.startsWith(`${projectId}\0`)
4159
+ || !controllerRuntimeIdsEqual(baseline.update.sessionId, sessionId)
4160
+ ) this.deleteLiveReasoningBaseline(key);
4161
+ }
4162
+ for (const [key, baseline] of this.liveTextBaselines) {
4163
+ if (
4164
+ !projectId
4165
+ || !isSessionRuntimeId(sessionId)
4166
+ || !key.startsWith(`${projectId}\0`)
4167
+ || !controllerRuntimeIdsEqual(baseline.update.sessionId, sessionId)
4168
+ ) this.deleteLiveTextBaseline(key);
4169
+ }
4170
+ for (const [key, block] of this.liveMessageDeltaBlocks) {
4171
+ if (
4172
+ !projectId
4173
+ || !isSessionRuntimeId(sessionId)
4174
+ || key !== this.sessionOperationKey(projectId, sessionId)
4175
+ || !controllerRuntimeIdsEqual(block.sessionId, sessionId)
4176
+ ) this.liveMessageDeltaBlocks.delete(key);
4177
+ }
3451
4178
  }
3452
4179
 
3453
4180
  private subtaskPreviewKey(
@@ -3667,7 +4394,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3667
4394
  if (isBusy) this.queueSubtaskPreviewHydration(entry);
3668
4395
  changed = true;
3669
4396
  }
3670
- if (changed) this.requestUpdate();
4397
+ if (changed) this.requestCanonicalChatReconciliation();
3671
4398
  }
3672
4399
 
3673
4400
  private queueSubtaskPreviewHydration(entryArg: ISubtaskPreviewEntry): void {
@@ -3795,7 +4522,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3795
4522
  messages,
3796
4523
  ...((page.truncated === true || page.nextCursor !== undefined) ? { truncated: true } : {}),
3797
4524
  };
3798
- this.requestUpdate();
4525
+ this.requestCanonicalChatReconciliation();
3799
4526
  } catch {
3800
4527
  if (abortControllerArg.signal.aborted) return;
3801
4528
  if (!this.subtaskPreviewEntryIsCurrent(entryArg)) return;
@@ -3807,7 +4534,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3807
4534
  status: entryArg.stream.status,
3808
4535
  notice: subtaskPreviewUnavailableNotice,
3809
4536
  };
3810
- this.requestUpdate();
4537
+ this.requestCanonicalChatReconciliation();
3811
4538
  }
3812
4539
  }
3813
4540
 
@@ -3840,7 +4567,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3840
4567
  this.queueSubtaskPreviewHydration(entry);
3841
4568
  }, subtaskPreviewRefreshDelayMs);
3842
4569
  }
3843
- if (changed) this.requestUpdate();
4570
+ if (changed) this.requestCanonicalChatReconciliation();
3844
4571
  }
3845
4572
 
3846
4573
  private clearSubtaskPreviewState(): void {
@@ -3851,6 +4578,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3851
4578
  }
3852
4579
  this.subtaskPreviewEntries.clear();
3853
4580
  this.subtaskPreviewHydrationQueue = [];
4581
+ this.canonicalChatProjectionDirty = true;
3854
4582
  }
3855
4583
 
3856
4584
  private applySessionEventState(eventArg: interfaces.IControllerEvent): void {
@@ -4001,7 +4729,9 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4001
4729
  && this.socketClient.isConnected
4002
4730
  ) {
4003
4731
  this.refreshPending = false;
4004
- await this.performSessionRefresh();
4732
+ const includeRelated = this.refreshRelatedPending;
4733
+ this.refreshRelatedPending = false;
4734
+ await this.performSessionRefresh(includeRelated);
4005
4735
  }
4006
4736
  } finally {
4007
4737
  this.refreshInFlight = undefined;
@@ -4041,7 +4771,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4041
4771
  }
4042
4772
  }
4043
4773
 
4044
- private async performSessionRefresh(): Promise<void> {
4774
+ private async performSessionRefresh(includeRelatedArg = true): Promise<void> {
4045
4775
  const projectId = this.selectedProjectId;
4046
4776
  if (!isNonEmptyString(projectId)) {
4047
4777
  this.sessions = [];
@@ -4062,10 +4792,14 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4062
4792
  && controllerRuntimeIdsEqual(sessionArg.id, selectedSessionAtStart)
4063
4793
  && sessionArg.archivedAt !== undefined
4064
4794
  ));
4065
- if (selectedSessionAtStart) void this.refreshSelectedSessionDetail(false);
4066
- void this.loadSessionGroups();
4067
- void this.loadTerminals();
4068
- void this.loadResources();
4795
+ if (includeRelatedArg && selectedSessionAtStart) {
4796
+ void this.refreshSelectedSessionDetail(false);
4797
+ }
4798
+ if (includeRelatedArg) {
4799
+ void this.loadSessionGroups();
4800
+ void this.loadTerminals();
4801
+ void this.loadResources();
4802
+ }
4069
4803
  // The loading state is for a list we cannot show yet; background refreshes
4070
4804
  // must not flip it, or the sidebar flashes on every pushed event.
4071
4805
  const initialLoad = this.sessionsLoadedForProjectId !== projectId;
@@ -4091,11 +4825,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4091
4825
  ? response.sessions.filter(hasSessionRuntimeId)
4092
4826
  : [];
4093
4827
  this.applySessionSnapshot(projectId, nextSessions, requestEventRevision);
4094
- // Keep the array identity stable when nothing changed so the list does
4095
- // not re-render for every no-op refresh.
4096
- if (JSON.stringify(nextSessions) !== JSON.stringify(this.sessions)) {
4097
- this.sessions = nextSessions;
4098
- }
4828
+ const reconciledSessions = reconcileKeyedJsonArray(
4829
+ this.sessions,
4830
+ nextSessions,
4831
+ (session) => controllerRuntimeIdToUiKey(session.id),
4832
+ );
4833
+ if (reconciledSessions !== this.sessions) this.sessions = reconciledSessions;
4099
4834
  const selectedSessionBecameArchived = selectedSessionAtStart !== undefined
4100
4835
  && !selectedSessionWasArchived
4101
4836
  && this.sessions.some((sessionArg) => (
@@ -4171,6 +4906,107 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4171
4906
  }
4172
4907
  }
4173
4908
 
4909
+ private reconcileControllerMessages(
4910
+ currentArg: readonly interfaces.IControllerMessage[],
4911
+ nextArg: readonly interfaces.IControllerMessage[],
4912
+ ): interfaces.IControllerMessage[] {
4913
+ return reconcileKeyedJsonArray(
4914
+ currentArg,
4915
+ nextArg,
4916
+ (message) => controllerRuntimeIdToUiKey(message.id),
4917
+ );
4918
+ }
4919
+
4920
+ private reconcileControllerMessageBundles(
4921
+ currentArg: readonly interfaces.IControllerMessageBundle[],
4922
+ nextArg: readonly interfaces.IControllerMessageBundle[],
4923
+ ): interfaces.IControllerMessageBundle[] {
4924
+ const currentByKey = new Map(currentArg.map((bundle) => [
4925
+ controllerRuntimeIdToUiKey(bundle.sourceMessageId),
4926
+ bundle,
4927
+ ]));
4928
+ let unchanged = currentArg.length === nextArg.length;
4929
+ const bundles = nextArg.map((nextBundle, index) => {
4930
+ const current = currentByKey.get(controllerRuntimeIdToUiKey(nextBundle.sourceMessageId));
4931
+ if (!current) {
4932
+ unchanged = false;
4933
+ return nextBundle;
4934
+ }
4935
+ const messages = this.reconcileControllerMessages(current.messages, nextBundle.messages);
4936
+ const bundle = current.structuralDigest === nextBundle.structuralDigest
4937
+ && messages === current.messages
4938
+ && controllerRuntimeIdsEqual(current.sourceMessageId, nextBundle.sourceMessageId)
4939
+ ? current
4940
+ : { ...nextBundle, messages };
4941
+ unchanged &&= bundle === currentArg[index];
4942
+ return bundle;
4943
+ });
4944
+ return unchanged ? currentArg as interfaces.IControllerMessageBundle[] : bundles;
4945
+ }
4946
+
4947
+ private reconcileSessionRenderDetail(
4948
+ currentArg: IControllerSessionRenderDetail | undefined,
4949
+ nextArg: IControllerSessionRenderDetail,
4950
+ ): IControllerSessionRenderDetail {
4951
+ if (!currentArg || !controllerRuntimeIdsEqual(currentArg.session.id, nextArg.session.id)) {
4952
+ return nextArg;
4953
+ }
4954
+ const reuse = <TValue>(currentValueArg: TValue, nextValueArg: TValue): TValue => (
4955
+ controllerJsonValuesEqual(currentValueArg, nextValueArg) ? currentValueArg : nextValueArg
4956
+ );
4957
+ const messages = this.reconcileControllerMessages(currentArg.messages, nextArg.messages);
4958
+ const pendingPrompts = reconcileKeyedJsonArray(
4959
+ currentArg.pendingPrompts,
4960
+ nextArg.pendingPrompts,
4961
+ (prompt) => controllerRuntimeIdToUiKey(prompt.id),
4962
+ );
4963
+ const permissions = reconcileKeyedJsonArray(
4964
+ currentArg.permissions,
4965
+ nextArg.permissions,
4966
+ (permission) => controllerRuntimeIdToUiKey(permission.id),
4967
+ );
4968
+ const questions = reconcileKeyedJsonArray(
4969
+ currentArg.questions,
4970
+ nextArg.questions,
4971
+ (question) => controllerRuntimeIdToUiKey(question.id),
4972
+ );
4973
+ const todos = reconcileKeyedJsonArray(
4974
+ currentArg.todos,
4975
+ nextArg.todos,
4976
+ (todo, index) => todo.id ? controllerRuntimeIdToUiKey(todo.id) : `index:${index}`,
4977
+ );
4978
+ const intelligenceExchanges = reconcileKeyedJsonArray(
4979
+ currentArg.intelligenceExchanges,
4980
+ nextArg.intelligenceExchanges,
4981
+ (exchange) => exchange.id,
4982
+ );
4983
+ const detail: IControllerSessionRenderDetail = {
4984
+ ...nextArg,
4985
+ session: reuse(currentArg.session, nextArg.session),
4986
+ messages,
4987
+ pendingPrompts,
4988
+ permissions,
4989
+ questions,
4990
+ todos,
4991
+ scratchpad: reuse(currentArg.scratchpad, nextArg.scratchpad),
4992
+ intelligenceExchanges,
4993
+ sessionMetrics: reuse(currentArg.sessionMetrics, nextArg.sessionMetrics),
4994
+ toolStreamCursor: reuse(currentArg.toolStreamCursor, nextArg.toolStreamCursor),
4995
+ messageStreamCursor: reuse(currentArg.messageStreamCursor, nextArg.messageStreamCursor),
4996
+ ...(nextArg.modelChoice === undefined
4997
+ ? {}
4998
+ : { modelChoice: reuse(currentArg.modelChoice, nextArg.modelChoice) }),
4999
+ };
5000
+ const current = currentArg as unknown as Record<string, unknown>;
5001
+ const reconciled = detail as unknown as Record<string, unknown>;
5002
+ const currentKeys = Object.keys(current);
5003
+ const nextKeys = Object.keys(reconciled);
5004
+ return currentKeys.length === nextKeys.length
5005
+ && nextKeys.every((key) => Object.is(current[key], reconciled[key]))
5006
+ ? currentArg
5007
+ : detail;
5008
+ }
5009
+
4174
5010
  private async loadSessionDetail(
4175
5011
  sessionIdArg: TBrowserSessionId,
4176
5012
  restartEnrichmentArg = true,
@@ -4284,6 +5120,10 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4284
5120
  ));
4285
5121
  provisionalBundleKeys.clear();
4286
5122
  }
5123
+ accumulated.bundles = this.reconcileControllerMessageBundles(
5124
+ this.detailBundles,
5125
+ accumulated.bundles,
5126
+ );
4287
5127
  const { messagePage, ...coreDetail } = response;
4288
5128
  const renderDetail: IControllerSessionRenderDetail = {
4289
5129
  ...coreDetail,
@@ -4356,9 +5196,9 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4356
5196
  unavailable: this.detailHistoryLimits.unavailable,
4357
5197
  };
4358
5198
  }
4359
- if (JSON.stringify(renderDetail) !== JSON.stringify(this.sessionDetail)) {
4360
- this.sessionDetail = renderDetail;
4361
- }
5199
+ const reconciledDetail = this.reconcileSessionRenderDetail(this.sessionDetail, renderDetail);
5200
+ if (reconciledDetail !== this.sessionDetail) this.sessionDetail = reconciledDetail;
5201
+ this.ensureCanonicalChatProjection(reconciledDetail, projectId, sessionIdArg);
4362
5202
  this.reconcileSubtaskPreviews(projectId, sessionIdArg);
4363
5203
  // A draft that materialized keeps its placeholder view until this first
4364
5204
  // real detail arrives; now the actual chat takes over.
@@ -4422,6 +5262,10 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4422
5262
  this.detailHistoryOwnership = undefined;
4423
5263
  this.detailHistoryLimits = emptyDetailHistoryLimits();
4424
5264
  this.detailHistoryStatus = 'idle';
5265
+ this.canonicalChatProjection = undefined;
5266
+ this.canonicalChatProjectionDirty = false;
5267
+ this.canonicalTimelineRefreshPending = false;
5268
+ this.canonicalMessageRefreshIds.clear();
4425
5269
  }
4426
5270
 
4427
5271
  private invalidateDetailEnrichment(): void {
@@ -4551,14 +5395,19 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4551
5395
  (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId),
4552
5396
  );
4553
5397
  if (provisionalContentChanged || !sameStringArray(previousKeys, nextKeys)) {
4554
- this.detailBundles = bounded.bundles;
5398
+ this.detailBundles = this.reconcileControllerMessageBundles(
5399
+ this.detailBundles,
5400
+ bounded.bundles,
5401
+ );
4555
5402
  this.detailBundleKeys = new Set(nextKeys);
4556
5403
  const detail = this.sessionDetail;
4557
5404
  if (!detail || !controllerRuntimeIdsEqual(detail.session.id, ownership.sessionId)) return;
4558
- this.sessionDetail = {
5405
+ const nextDetail = this.reconcileSessionRenderDetail(detail, {
4559
5406
  ...detail,
4560
- messages: bounded.bundles.flatMap((bundle) => bundle.messages),
4561
- };
5407
+ messages: this.detailBundles.flatMap((bundle) => bundle.messages),
5408
+ });
5409
+ if (nextDetail !== detail) this.sessionDetail = nextDetail;
5410
+ this.ensureCanonicalChatProjection(nextDetail, ownership.projectId, ownership.sessionId);
4562
5411
  }
4563
5412
  this.detailHistoryCursor = nextCursor;
4564
5413
  if (nextCursor !== undefined) this.detailHistorySeenCursors.add(nextCursor);
@@ -4636,6 +5485,18 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4636
5485
  const resourceId = eventArg.detail?.resource?.id;
4637
5486
  const resource = this.resources.find((candidate) => candidate.id === resourceId);
4638
5487
  if (!resource || resource.lifecycle !== 'active') return;
5488
+ if (
5489
+ resource.kind === 'browser'
5490
+ && this.selectedResourceId === resource.id
5491
+ && (
5492
+ this.browserViewLoading
5493
+ || (
5494
+ this.browserViewResourceId === resource.id
5495
+ && this.browserViewAttachmentRevision === resource.attachment.revision
5496
+ && this.browserViewId !== ''
5497
+ )
5498
+ )
5499
+ ) return;
4639
5500
  this.invalidateSlashCatalog();
4640
5501
  this.selectedResourceId = resource.id;
4641
5502
  if (resource.kind === 'browser') {
@@ -4671,6 +5532,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4671
5532
  this.browserViewLoading = true;
4672
5533
  this.workspaceError = '';
4673
5534
  let openedViewId = '';
5535
+ let openedStreamGeneration = -1;
4674
5536
  try {
4675
5537
  const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewOpen>(
4676
5538
  'controller.browser.view.open',
@@ -4682,6 +5544,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4682
5544
  { maxRetries: 0, abortSignal: openAbortController.signal },
4683
5545
  );
4684
5546
  openedViewId = response.viewId;
5547
+ openedStreamGeneration = response.streamGeneration;
4685
5548
  if (
4686
5549
  generation !== this.browserViewGeneration
4687
5550
  || connectionGeneration !== this.connectionGeneration
@@ -4689,7 +5552,11 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4689
5552
  || response.resourceId !== resourceArg.id
4690
5553
  || response.attachmentRevision !== resourceArg.attachment.revision
4691
5554
  ) {
4692
- void this.requestBrowserViewClose(response.viewId, connectionGeneration);
5555
+ void this.requestBrowserViewClose(
5556
+ response.viewId,
5557
+ response.streamGeneration,
5558
+ connectionGeneration,
5559
+ );
4693
5560
  return;
4694
5561
  }
4695
5562
  if (
@@ -4698,29 +5565,49 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4698
5565
  ) this.browserViewPendingClose = undefined;
4699
5566
  this.browserViewId = response.viewId;
4700
5567
  this.browserViewConnectionGeneration = connectionGeneration;
5568
+ this.browserViewStreamGeneration = response.streamGeneration;
5569
+ this.browserViewActivated = false;
4701
5570
  this.browserViewResourceId = response.resourceId;
4702
5571
  this.browserViewAttachmentRevision = response.attachmentRevision;
4703
- this.browserViewState = response.state;
5572
+ this.applyBrowserViewState(response.state);
5573
+ const transport = this.createBrowserViewTransportClient(
5574
+ response.streamGeneration,
5575
+ response.events,
5576
+ response.operations,
5577
+ generation,
5578
+ );
5579
+ this.browserViewTransport = transport;
5580
+ const transportStart = transport.start();
4704
5581
  this.browserViewLoading = false;
4705
5582
  this.requestUpdate();
4706
- await this.updateComplete;
5583
+ await Promise.all([this.updateComplete, transportStart]);
4707
5584
  const rendererReady = await this.mountBrowserRenderer(generation);
4708
5585
  if (
4709
5586
  !rendererReady
4710
5587
  || generation !== this.browserViewGeneration
4711
5588
  || connectionGeneration !== this.connectionGeneration
4712
5589
  || response.viewId !== this.browserViewId
5590
+ || response.streamGeneration !== this.browserViewStreamGeneration
4713
5591
  ) return;
4714
5592
  await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewActivate>(
4715
5593
  'controller.browser.view.activate',
4716
- { viewId: response.viewId },
5594
+ { viewId: response.viewId, streamGeneration: response.streamGeneration },
4717
5595
  { maxRetries: 0, abortSignal: openAbortController.signal },
4718
5596
  );
5597
+ if (
5598
+ generation === this.browserViewGeneration
5599
+ && response.viewId === this.browserViewId
5600
+ && response.streamGeneration === this.browserViewStreamGeneration
5601
+ ) this.browserViewActivated = true;
4719
5602
  } catch (errorArg) {
4720
5603
  if (openedViewId && openedViewId === this.browserViewId) {
4721
5604
  await this.closeBrowserView(false);
4722
5605
  } else if (openedViewId && this.socketClient.isConnected) {
4723
- await this.requestBrowserViewClose(openedViewId, connectionGeneration);
5606
+ await this.requestBrowserViewClose(
5607
+ openedViewId,
5608
+ openedStreamGeneration,
5609
+ connectionGeneration,
5610
+ );
4724
5611
  }
4725
5612
  if (
4726
5613
  generation === this.browserViewGeneration
@@ -4743,114 +5630,275 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4743
5630
  this.browserViewOpenAbortController = undefined;
4744
5631
  this.browserNavigationAbortController?.abort(new Error('Browser view changed.'));
4745
5632
  this.browserNavigationAbortController = undefined;
5633
+ this.browserViewRecoveryAbortController?.abort(new Error('Browser view changed.'));
5634
+ this.browserViewRecoveryAbortController = undefined;
4746
5635
  const renderer = this.browserRenderer;
4747
- if (renderer) {
4748
- await renderer.stop().then(
4749
- () => {
4750
- if (this.browserRenderer === renderer) this.browserRenderer = undefined;
4751
- },
4752
- (errorArg) => {
4753
- this.workspaceError = `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`;
4754
- },
4755
- );
4756
- }
5636
+ const transport = this.browserViewTransport;
4757
5637
  const viewId = this.browserViewId;
5638
+ const viewStreamGeneration = this.browserViewStreamGeneration;
4758
5639
  const viewConnectionGeneration = this.browserViewConnectionGeneration;
4759
5640
  this.browserViewId = '';
4760
5641
  this.browserViewConnectionGeneration = -1;
5642
+ this.browserViewStreamGeneration = -1;
5643
+ this.browserViewActivated = false;
4761
5644
  this.browserViewResourceId = '';
4762
5645
  this.browserViewAttachmentRevision = -1;
4763
5646
  this.browserViewState = undefined;
5647
+ this.browserAddressDraft = '';
5648
+ this.browserAddressEditing = false;
4764
5649
  this.browserViewLoading = false;
4765
5650
  this.browserViewListeners.clear();
4766
- if (viewId && viewConnectionGeneration === this.connectionGeneration) {
4767
- await this.requestBrowserViewClose(viewId, viewConnectionGeneration);
4768
- } else {
4769
- await this.flushPendingBrowserViewClose();
5651
+ const remoteCloseTask = (
5652
+ viewId
5653
+ && viewStreamGeneration > 0
5654
+ && viewConnectionGeneration === this.connectionGeneration
5655
+ )
5656
+ ? this.requestBrowserViewClose(
5657
+ viewId,
5658
+ viewStreamGeneration,
5659
+ viewConnectionGeneration,
5660
+ )
5661
+ : this.flushPendingBrowserViewClose();
5662
+ const rendererStopTask = renderer
5663
+ ? this.stopBrowserRenderer(renderer)
5664
+ : Promise.resolve();
5665
+ const transportStopTask = transport
5666
+ ? transport.stop(new Error('Browser view changed.')).then(() => {
5667
+ if (this.browserViewTransport === transport) this.browserViewTransport = undefined;
5668
+ })
5669
+ : Promise.resolve();
5670
+ await Promise.all([remoteCloseTask, rendererStopTask, transportStopTask]);
5671
+ }
5672
+
5673
+ private async stopBrowserRenderer(rendererArg: plugins.LiveBrowserCanvasRenderer): Promise<void> {
5674
+ let stopTask: Promise<void>;
5675
+ try {
5676
+ stopTask = rendererArg.stop();
5677
+ } catch (errorArg) {
5678
+ if (this.browserRenderer === rendererArg) {
5679
+ this.workspaceError = `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`;
5680
+ }
5681
+ return;
5682
+ }
5683
+ void stopTask.then(() => {
5684
+ if (this.browserRenderer === rendererArg) this.browserRenderer = undefined;
5685
+ }, () => undefined);
5686
+ try {
5687
+ await waitForBrowserRendererLifecycle(stopTask, 'stop');
5688
+ } catch (errorArg) {
5689
+ if (this.browserRenderer === rendererArg) {
5690
+ this.workspaceError = `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`;
5691
+ }
4770
5692
  }
4771
5693
  }
4772
5694
 
4773
5695
  private async requestBrowserViewClose(
4774
5696
  viewIdArg: string,
5697
+ streamGenerationArg: number,
4775
5698
  connectionGenerationArg: number,
4776
5699
  ): Promise<void> {
4777
5700
  if (connectionGenerationArg !== this.connectionGeneration) return;
4778
- const pendingClose = { viewId: viewIdArg, connectionGeneration: connectionGenerationArg };
5701
+ const pendingClose = {
5702
+ viewId: viewIdArg,
5703
+ streamGeneration: streamGenerationArg,
5704
+ connectionGeneration: connectionGenerationArg,
5705
+ attempts: 0,
5706
+ };
4779
5707
  this.browserViewPendingClose = pendingClose;
4780
5708
  await this.flushPendingBrowserViewClose();
4781
5709
  }
4782
5710
 
4783
- private async flushPendingBrowserViewClose(): Promise<void> {
4784
- const pendingClose = this.browserViewPendingClose;
5711
+ private async flushPendingBrowserViewClose(): Promise<void> {
5712
+ const pendingClose = this.browserViewPendingClose;
5713
+ if (
5714
+ !pendingClose
5715
+ || pendingClose.connectionGeneration !== this.connectionGeneration
5716
+ || !this.socketClient.isConnected
5717
+ ) return;
5718
+ if (this.browserViewCloseTask) return this.browserViewCloseTask;
5719
+ pendingClose.attempts += 1;
5720
+ const closeTask = this.socketClient.fire<interfaces.IReq_ControllerBrowserViewClose>(
5721
+ 'controller.browser.view.close',
5722
+ {
5723
+ viewId: pendingClose.viewId,
5724
+ expectedStreamGeneration: pendingClose.streamGeneration,
5725
+ },
5726
+ { maxRetries: 0 },
5727
+ ).then(() => {
5728
+ if (this.browserViewPendingClose === pendingClose) {
5729
+ this.browserViewPendingClose = undefined;
5730
+ }
5731
+ }).catch(() => {
5732
+ if (
5733
+ this.browserViewPendingClose === pendingClose
5734
+ && pendingClose.attempts >= 3
5735
+ ) {
5736
+ this.browserViewPendingClose = undefined;
5737
+ this.workspaceError = 'The browser view close request failed after three attempts.';
5738
+ return;
5739
+ }
5740
+ if (
5741
+ this.browserViewPendingClose !== pendingClose
5742
+ || pendingClose.connectionGeneration !== this.connectionGeneration
5743
+ ) return;
5744
+ if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer);
5745
+ this.browserViewCloseRetryTimer = setTimeout(() => {
5746
+ this.browserViewCloseRetryTimer = undefined;
5747
+ void this.flushPendingBrowserViewClose();
5748
+ }, 250 * pendingClose.attempts);
5749
+ }).finally(() => {
5750
+ if (this.browserViewCloseTask === closeTask) this.browserViewCloseTask = undefined;
5751
+ if (
5752
+ this.browserViewPendingClose
5753
+ && this.browserViewPendingClose !== pendingClose
5754
+ ) void this.flushPendingBrowserViewClose();
5755
+ });
5756
+ this.browserViewCloseTask = closeTask;
5757
+ return closeTask;
5758
+ }
5759
+
5760
+ private createBrowserViewTransportClient(
5761
+ streamGenerationArg: number,
5762
+ eventsArg: plugins.typedrequestInterfaces.TVirtualStream<'receive'>,
5763
+ operationsArg: plugins.typedrequestInterfaces.TVirtualStream<'send'>,
5764
+ viewGenerationArg: number,
5765
+ ): ControllerBrowserViewTransportClient {
5766
+ return new ControllerBrowserViewTransportClient({
5767
+ streamGeneration: streamGenerationArg,
5768
+ events: eventsArg,
5769
+ operations: operationsArg,
5770
+ onEvent: (eventArg) => {
5771
+ if (
5772
+ viewGenerationArg !== this.browserViewGeneration
5773
+ || streamGenerationArg !== this.browserViewStreamGeneration
5774
+ ) return;
5775
+ this.handleBrowserViewEvent(eventArg);
5776
+ },
5777
+ onFailure: (errorArg, recoverableArg) => {
5778
+ this.handleBrowserTransportFailure(
5779
+ viewGenerationArg,
5780
+ streamGenerationArg,
5781
+ errorArg,
5782
+ recoverableArg,
5783
+ );
5784
+ },
5785
+ });
5786
+ }
5787
+
5788
+ private handleBrowserViewEvent(eventArg: interfaces.TControllerBrowserViewEvent): void {
5789
+ if (eventArg.type === 'state') this.applyBrowserViewState(eventArg.state);
5790
+ if (eventArg.type === 'error' && eventArg.error.fatal) this.workspaceError = eventArg.error.message;
5791
+ for (const listener of this.browserViewListeners) listener(eventArg);
5792
+ }
5793
+
5794
+ private applyBrowserViewState(stateArg: interfaces.IControllerBrowserViewState): boolean {
5795
+ if (this.browserViewState && stateArg.revision <= this.browserViewState.revision) return false;
5796
+ this.browserViewState = stateArg;
5797
+ if (!this.browserAddressEditing) {
5798
+ this.browserAddressDraft = stateArg.tabs.find((tabArg) => tabArg.active)?.url ?? '';
5799
+ }
5800
+ return true;
5801
+ }
5802
+
5803
+ private handleBrowserTransportFailure(
5804
+ viewGenerationArg: number,
5805
+ streamGenerationArg: number,
5806
+ errorArg: unknown,
5807
+ recoverableArg: boolean,
5808
+ ): void {
4785
5809
  if (
4786
- !pendingClose
4787
- || pendingClose.connectionGeneration !== this.connectionGeneration
4788
- || !this.socketClient.isConnected
5810
+ viewGenerationArg !== this.browserViewGeneration
5811
+ || streamGenerationArg !== this.browserViewStreamGeneration
5812
+ || !this.browserViewId
4789
5813
  ) return;
4790
- try {
4791
- await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewClose>(
4792
- 'controller.browser.view.close',
4793
- { viewId: pendingClose.viewId },
4794
- { maxRetries: 0 },
4795
- );
4796
- } catch {
5814
+ if (!recoverableArg || !this.browserViewActivated || !this.browserRenderer) {
5815
+ this.workspaceError = `Browser transport failed: ${errorMessage(errorArg)}`;
5816
+ void this.closeBrowserView();
4797
5817
  return;
4798
5818
  }
4799
- if (this.browserViewPendingClose === pendingClose) this.browserViewPendingClose = undefined;
5819
+ if (this.browserViewTransportRecoveryTask) return;
5820
+ const recovery = this.recoverBrowserViewTransport(
5821
+ viewGenerationArg,
5822
+ streamGenerationArg,
5823
+ errorArg,
5824
+ ).finally(() => {
5825
+ if (this.browserViewTransportRecoveryTask === recovery) {
5826
+ this.browserViewTransportRecoveryTask = undefined;
5827
+ }
5828
+ });
5829
+ this.browserViewTransportRecoveryTask = recovery;
5830
+ void recovery.catch(() => undefined);
4800
5831
  }
4801
5832
 
4802
- private async handleBrowserViewEvent(
4803
- messageArg: TControllerBrowserViewEventMessage,
5833
+ private async recoverBrowserViewTransport(
5834
+ viewGenerationArg: number,
5835
+ streamGenerationArg: number,
5836
+ reasonArg: unknown,
4804
5837
  ): Promise<void> {
4805
- const isCurrent = (): boolean => (
4806
- messageArg.viewId !== this.browserViewId
4807
- ? false
4808
- : messageArg.resourceId === this.browserViewResourceId
4809
- && messageArg.attachmentRevision === this.browserViewAttachmentRevision
4810
- );
4811
- if (messageArg.event.type === 'frame') {
4812
- const { dataStream, ...frame } = messageArg.event.frame;
4813
- const reject = async (reasonArg: unknown): Promise<void> => {
4814
- await dataStream.reject(reasonArg).catch(async () => {
4815
- await dataStream.abort(reasonArg).catch(() => undefined);
4816
- });
4817
- };
4818
- if (!isCurrent()) {
4819
- await reject(new Error('The browser frame belongs to a stale view.'));
4820
- return;
5838
+ const viewId = this.browserViewId;
5839
+ const connectionGeneration = this.connectionGeneration;
5840
+ const oldTransport = this.browserViewTransport;
5841
+ const renderer = this.browserRenderer;
5842
+ if (!viewId || !oldTransport || !renderer) return;
5843
+ const recoveryAbortController = new AbortController();
5844
+ this.browserViewRecoveryAbortController = recoveryAbortController;
5845
+ this.browserViewActivated = false;
5846
+ try {
5847
+ await waitForBrowserRendererLifecycle(renderer.suspend(), 'suspension');
5848
+ await oldTransport.stop(reasonArg);
5849
+ const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewRecover>(
5850
+ 'controller.browser.view.recover',
5851
+ { viewId, expectedStreamGeneration: streamGenerationArg },
5852
+ { maxRetries: 0, abortSignal: recoveryAbortController.signal },
5853
+ );
5854
+ if (
5855
+ viewGenerationArg !== this.browserViewGeneration
5856
+ || connectionGeneration !== this.connectionGeneration
5857
+ || viewId !== this.browserViewId
5858
+ || streamGenerationArg !== this.browserViewStreamGeneration
5859
+ || response.viewId !== viewId
5860
+ || response.resourceId !== this.browserViewResourceId
5861
+ || response.attachmentRevision !== this.browserViewAttachmentRevision
5862
+ || response.streamGeneration !== streamGenerationArg + 1
5863
+ ) throw new Error('The recovered browser stream generation is stale.');
5864
+ const transport = this.createBrowserViewTransportClient(
5865
+ response.streamGeneration,
5866
+ response.events,
5867
+ response.operations,
5868
+ viewGenerationArg,
5869
+ );
5870
+ this.browserViewTransport = transport;
5871
+ this.browserViewStreamGeneration = response.streamGeneration;
5872
+ this.applyBrowserViewState(response.state);
5873
+ await transport.start();
5874
+ await waitForBrowserRendererLifecycle(renderer.resume(), 'resume');
5875
+ await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewActivate>(
5876
+ 'controller.browser.view.activate',
5877
+ { viewId, streamGeneration: response.streamGeneration },
5878
+ { maxRetries: 0, abortSignal: recoveryAbortController.signal },
5879
+ );
5880
+ if (
5881
+ viewGenerationArg === this.browserViewGeneration
5882
+ && viewId === this.browserViewId
5883
+ && response.streamGeneration === this.browserViewStreamGeneration
5884
+ ) {
5885
+ this.browserViewActivated = true;
5886
+ this.workspaceError = '';
4821
5887
  }
4822
- let data: Uint8Array;
4823
- try {
4824
- const firstChunk = await dataStream.receive();
4825
- if (firstChunk === undefined) throw new Error('The browser frame stream was empty.');
4826
- if (await dataStream.receive() !== undefined) {
4827
- throw new Error('The browser frame stream contained multiple chunks.');
4828
- }
4829
- if (
4830
- dataStream.contentType !== frame.mimeType
4831
- || dataStream.integrity?.algorithm !== 'sha256'
4832
- || dataStream.integrity.byteLength !== firstChunk.byteLength
4833
- ) throw new Error('The browser frame stream metadata is invalid.');
4834
- if (!isCurrent()) {
4835
- await reject(new Error('The browser frame became stale during transfer.'));
4836
- return;
4837
- }
4838
- data = firstChunk;
4839
- await dataStream.accept();
4840
- } catch (errorArg) {
4841
- await reject(errorArg);
4842
- throw errorArg;
5888
+ } catch (errorArg) {
5889
+ if (
5890
+ viewGenerationArg === this.browserViewGeneration
5891
+ && connectionGeneration === this.connectionGeneration
5892
+ && viewId === this.browserViewId
5893
+ ) {
5894
+ this.workspaceError = `Browser transport recovery failed: ${errorMessage(errorArg)}`;
5895
+ await this.closeBrowserView();
5896
+ }
5897
+ } finally {
5898
+ if (this.browserViewRecoveryAbortController === recoveryAbortController) {
5899
+ this.browserViewRecoveryAbortController = undefined;
4843
5900
  }
4844
- if (!isCurrent()) return;
4845
- const event = { type: 'frame' as const, frame: { ...frame, data } };
4846
- for (const listener of this.browserViewListeners) listener(event);
4847
- return;
4848
5901
  }
4849
- if (!isCurrent()) return;
4850
- const event = messageArg.event;
4851
- if (event.type === 'state') this.browserViewState = event.state;
4852
- if (event.type === 'error' && event.error.fatal) this.workspaceError = event.error.message;
4853
- for (const listener of this.browserViewListeners) listener(event);
4854
5902
  }
4855
5903
 
4856
5904
  private async browserViewOperation(
@@ -4859,14 +5907,19 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4859
5907
  abortSignalArg?: AbortSignal,
4860
5908
  ): Promise<interfaces.IControllerBrowserViewState> {
4861
5909
  const viewId = this.browserViewId;
4862
- if (!viewId) throw new Error('The browser view is unavailable.');
4863
- const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewOperate>(
4864
- 'controller.browser.view.operate',
4865
- { viewId, operation: operationArg },
4866
- { maxRetries: 0, abortSignal: abortSignalArg },
4867
- );
4868
- if (publishStateArg && viewId === this.browserViewId) this.browserViewState = response.state;
4869
- return response.state;
5910
+ const transport = this.browserViewTransport;
5911
+ if (!viewId || !transport) throw new Error('The browser view is unavailable.');
5912
+ const result = await transport.operate(operationArg, abortSignalArg);
5913
+ if (
5914
+ result.status === 'succeeded'
5915
+ && result.state
5916
+ && viewId === this.browserViewId
5917
+ ) {
5918
+ if (!publishStateArg) return result.state;
5919
+ this.applyBrowserViewState(result.state);
5920
+ }
5921
+ if (!this.browserViewState) throw new Error('The browser view state is unavailable.');
5922
+ return this.browserViewState;
4870
5923
  }
4871
5924
 
4872
5925
  private async navigateBrowserAddress(formArg: HTMLFormElement): Promise<void> {
@@ -4879,6 +5932,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4879
5932
  const input = formArg.querySelector<HTMLInputElement>('input');
4880
5933
  try {
4881
5934
  const url = controllerNormalizeBrowserAddress(input?.value ?? '');
5935
+ this.browserAddressDraft = input?.value ?? '';
4882
5936
  this.workspaceError = '';
4883
5937
  const state = await this.browserViewOperation(
4884
5938
  { type: 'navigate', url },
@@ -4889,7 +5943,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4889
5943
  requestId === this.browserNavigationRequestId
4890
5944
  && viewGeneration === this.browserViewGeneration
4891
5945
  && viewId === this.browserViewId
4892
- ) this.browserViewState = state;
5946
+ ) this.applyBrowserViewState(state);
4893
5947
  } catch (errorArg) {
4894
5948
  if (
4895
5949
  requestId === this.browserNavigationRequestId
@@ -4914,6 +5968,23 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4914
5968
  if (form) void this.navigateBrowserAddress(form);
4915
5969
  };
4916
5970
 
5971
+ private readonly handleBrowserAddressFocus = (eventArg: Event): void => {
5972
+ const input = eventArg.currentTarget;
5973
+ if (!(input instanceof HTMLInputElement)) return;
5974
+ this.browserAddressEditing = true;
5975
+ this.browserAddressDraft = input.value;
5976
+ };
5977
+
5978
+ private readonly handleBrowserAddressInput = (eventArg: Event): void => {
5979
+ const input = eventArg.currentTarget;
5980
+ if (input instanceof HTMLInputElement) this.browserAddressDraft = input.value;
5981
+ };
5982
+
5983
+ private readonly handleBrowserAddressBlur = (): void => {
5984
+ this.browserAddressEditing = false;
5985
+ this.requestUpdate();
5986
+ };
5987
+
4917
5988
  private browserCanvasClient(): plugins.ILiveBrowserCanvasClient {
4918
5989
  return {
4919
5990
  getState: () => {
@@ -4925,12 +5996,13 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4925
5996
  return () => this.browserViewListeners.delete(listenerArg);
4926
5997
  },
4927
5998
  acknowledgeFrame: async (acknowledgement, options) => {
4928
- const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewOperate>(
4929
- 'controller.browser.view.operate',
4930
- { viewId: this.browserViewId, operation: { type: 'acknowledgeFrame', acknowledgement } },
4931
- { maxRetries: 0, abortSignal: options.signal },
5999
+ const transport = this.browserViewTransport;
6000
+ if (!transport) throw new Error('The browser view is unavailable.');
6001
+ const result = await transport.operate(
6002
+ { type: 'acknowledgeFrame', acknowledgement },
6003
+ options.signal,
4932
6004
  );
4933
- return { accepted: response.accepted };
6005
+ return { accepted: result.status === 'succeeded' && result.accepted === true };
4934
6006
  },
4935
6007
  setViewport: async (viewport, options) => {
4936
6008
  await this.browserViewOperation({ type: 'setViewport', viewport }, true, options.signal);
@@ -4961,14 +6033,30 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4961
6033
  focusTarget: surface,
4962
6034
  resizeTarget: surface,
4963
6035
  onError: (errorArg) => {
4964
- if (generationArg === this.browserViewGeneration) this.workspaceError = errorArg.message;
6036
+ if (generationArg !== this.browserViewGeneration) return;
6037
+ this.workspaceError = errorArg.message;
6038
+ if (
6039
+ errorArg.code === 'frame_acknowledgement_failed'
6040
+ || errorArg.code === 'frame_render_failed'
6041
+ ) {
6042
+ this.handleBrowserTransportFailure(
6043
+ generationArg,
6044
+ this.browserViewStreamGeneration,
6045
+ errorArg,
6046
+ true,
6047
+ );
6048
+ }
4965
6049
  },
4966
6050
  });
4967
6051
  this.browserRenderer = renderer;
4968
- await renderer.start();
6052
+ await this.startBrowserRenderer(renderer);
4969
6053
  return generationArg === this.browserViewGeneration && this.browserRenderer === renderer;
4970
6054
  }
4971
6055
 
6056
+ private startBrowserRenderer(rendererArg: plugins.LiveBrowserCanvasRenderer): Promise<void> {
6057
+ return waitForBrowserRendererLifecycle(rendererArg.start(), 'start');
6058
+ }
6059
+
4972
6060
  private readonly handleResourceAssociationRequest = (
4973
6061
  eventArg: CustomEvent<plugins.deesCatalog.THarnessResourceAssociationRequestDetail>,
4974
6062
  ): void => {
@@ -6707,9 +7795,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
6707
7795
  const terminals = Array.isArray(response.terminals)
6708
7796
  ? response.terminals.filter(hasTerminalRuntimeId)
6709
7797
  : [];
6710
- if (JSON.stringify(terminals) !== JSON.stringify(this.terminals)) {
6711
- this.terminals = terminals;
6712
- }
7798
+ const reconciledTerminals = reconcileKeyedJsonArray(
7799
+ this.terminals,
7800
+ terminals,
7801
+ (terminal) => controllerRuntimeIdToUiKey(terminal.id),
7802
+ );
7803
+ if (reconciledTerminals !== this.terminals) this.terminals = reconciledTerminals;
6713
7804
  this.terminalsLoadedForProjectId = projectId;
6714
7805
  if (
6715
7806
  this.selectedTerminalId !== undefined
@@ -6751,7 +7842,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
6751
7842
  ) return;
6752
7843
  this.resources = Array.isArray(response.resources) ? response.resources : [];
6753
7844
  this.resourcesLoadedForProjectId = projectId;
6754
- const viewedBrowserResource = this.resources.find((resource) => (
7845
+ const viewedBrowserResource = this.resources.find((resource): resource is interfaces.IControllerBrowserResource => (
6755
7846
  resource.id === this.browserViewResourceId && resource.kind === 'browser'
6756
7847
  ));
6757
7848
  if (
@@ -6761,7 +7852,22 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
6761
7852
  || viewedBrowserResource.lifecycle !== 'active'
6762
7853
  || viewedBrowserResource.attachment.revision !== this.browserViewAttachmentRevision
6763
7854
  )
6764
- ) void this.closeBrowserView();
7855
+ ) {
7856
+ const reopen = Boolean(
7857
+ viewedBrowserResource
7858
+ && viewedBrowserResource.lifecycle === 'active'
7859
+ && viewedBrowserResource.browserRuntimeState === 'available'
7860
+ && this.selectedResourceId === viewedBrowserResource.id,
7861
+ );
7862
+ await this.closeBrowserView();
7863
+ if (
7864
+ reopen
7865
+ && viewedBrowserResource
7866
+ && this.selectedResourceId === viewedBrowserResource.id
7867
+ && this.selectedProjectId === projectId
7868
+ && this.isCurrentConnection(generation)
7869
+ ) await this.openBrowserView(viewedBrowserResource);
7870
+ }
6765
7871
  if (
6766
7872
  this.selectedResourceId
6767
7873
  && !this.resources.some((resource) => resource.id === this.selectedResourceId)
@@ -6981,6 +8087,17 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
6981
8087
  public updated(changedPropertiesArg: Map<PropertyKey, unknown>): void {
6982
8088
  super.updated(changedPropertiesArg);
6983
8089
  this.flushComposerFocus();
8090
+ const projection = this.canonicalChatProjection;
8091
+ const chat = projection ? this.mountedCanonicalChat(projection) : undefined;
8092
+ if (this.canonicalTimelineRefreshPending) {
8093
+ this.canonicalTimelineRefreshPending = false;
8094
+ this.canonicalMessageRefreshIds.clear();
8095
+ chat?.refreshMessages();
8096
+ } else if (this.canonicalMessageRefreshIds.size > 0) {
8097
+ const messageIds = [...this.canonicalMessageRefreshIds];
8098
+ this.canonicalMessageRefreshIds.clear();
8099
+ chat?.refreshMessages(messageIds);
8100
+ }
6984
8101
  if (
6985
8102
  changedPropertiesArg.has('selectedProjectId')
6986
8103
  || changedPropertiesArg.has('selectedSessionId')
@@ -7933,6 +9050,417 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
7933
9050
  }
7934
9051
  }
7935
9052
 
9053
+ private liveMessageOrdersEqual(
9054
+ leftArg: interfaces.IControllerTranscriptOrder | undefined,
9055
+ rightArg: interfaces.IControllerTranscriptOrder | undefined,
9056
+ ): boolean {
9057
+ return leftArg === undefined
9058
+ ? rightArg === undefined
9059
+ : rightArg !== undefined
9060
+ && leftArg.messageIndex === rightArg.messageIndex
9061
+ && leftArg.partIndex === rightArg.partIndex;
9062
+ }
9063
+
9064
+ private liveMessageIdentityMatches(
9065
+ previousArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate,
9066
+ deltaArg: interfaces.IControllerReasoningDelta | interfaces.IControllerTextDelta,
9067
+ ): boolean {
9068
+ return controllerRuntimeIdsEqual(previousArg.sessionId, deltaArg.sessionId)
9069
+ && controllerRuntimeIdsEqual(previousArg.messageId, deltaArg.messageId)
9070
+ && controllerRuntimeIdsEqual(previousArg.partId, deltaArg.partId)
9071
+ && this.liveMessageOrdersEqual(previousArg.order, deltaArg.order);
9072
+ }
9073
+
9074
+ private liveMessageEventPassesBarriers(
9075
+ projectIdArg: string,
9076
+ updateArg: interfaces.IControllerReasoningUpdate
9077
+ | interfaces.IControllerTextUpdate
9078
+ | interfaces.IControllerReasoningDelta
9079
+ | interfaces.IControllerTextDelta,
9080
+ ): { accepted: boolean; epochAdvanced: boolean } {
9081
+ if (!isSessionRuntimeId(updateArg.sessionId)) {
9082
+ return { accepted: false, epochAdvanced: false };
9083
+ }
9084
+ if (
9085
+ updateArg.sourceUpdatedAt
9086
+ <= (this.liveHistoryBarriers.get(
9087
+ this.liveToolSessionKey(projectIdArg, updateArg.sessionId),
9088
+ ) ?? Number.NEGATIVE_INFINITY)
9089
+ ) return { accepted: false, epochAdvanced: false };
9090
+ const previousEpoch = this.liveMessageStreamEpochs.get(updateArg.sessionId.harnessId) ?? 0;
9091
+ const accepted = this.acceptLiveMessageEpoch(
9092
+ updateArg.sessionId.harnessId,
9093
+ updateArg.streamEpoch,
9094
+ true,
9095
+ false,
9096
+ );
9097
+ return { accepted, epochAdvanced: accepted && updateArg.streamEpoch > previousEpoch };
9098
+ }
9099
+
9100
+ private refreshAfterTerminalLiveMessage(
9101
+ projectIdArg: string,
9102
+ sessionIdArg: TBrowserSessionId,
9103
+ ): void {
9104
+ if (
9105
+ projectIdArg !== this.selectedProjectId
9106
+ || !controllerRuntimeIdsEqual(sessionIdArg, this.selectedSessionId)
9107
+ ) return;
9108
+ void this.refreshSelectedSessionDetail();
9109
+ void this.refreshSessions(false);
9110
+ }
9111
+
9112
+ private applyLiveReasoningUpdate(
9113
+ projectIdArg: string,
9114
+ updateArg: interfaces.IControllerReasoningUpdate,
9115
+ ): void {
9116
+ if (!isSessionRuntimeId(updateArg.sessionId)) return;
9117
+ const sessionId = updateArg.sessionId;
9118
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, updateArg);
9119
+ if (!barrier.accepted) return;
9120
+ if (controllerRuntimeIdsEqual(updateArg.sessionId, this.subagentModalSessionId)) {
9121
+ this.scheduleSubagentModalRefresh();
9122
+ }
9123
+ if (
9124
+ projectIdArg !== this.selectedProjectId
9125
+ || !controllerRuntimeIdsEqual(updateArg.sessionId, this.selectedSessionId)
9126
+ ) return;
9127
+ const floor = this.liveMessageHydrationFloors.get(
9128
+ this.liveToolSessionKey(projectIdArg, updateArg.sessionId),
9129
+ )?.cursor;
9130
+ if (
9131
+ floor
9132
+ && floor.streamEpoch === updateArg.streamEpoch
9133
+ && updateArg.revision <= floor.revision
9134
+ ) return;
9135
+ const key = this.liveMessageUpdateKey(projectIdArg, updateArg);
9136
+ const previous = this.liveReasoningUpdates.get(key)
9137
+ ?? this.liveReasoningBaselines.get(key)?.update;
9138
+ if (
9139
+ previous
9140
+ && previous.streamEpoch === updateArg.streamEpoch
9141
+ && updateArg.revision <= previous.revision
9142
+ ) return;
9143
+ if (!this.storeLiveReasoningUpdate(key, updateArg)) {
9144
+ this.blockLiveMessageDeltas(
9145
+ projectIdArg,
9146
+ sessionId,
9147
+ updateArg.streamEpoch,
9148
+ !barrier.epochAdvanced,
9149
+ );
9150
+ return;
9151
+ }
9152
+
9153
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
9154
+ if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate();
9155
+ const target = projection ? this.canonicalReasoningTarget(projection, updateArg) : undefined;
9156
+ if (!projection || !target) {
9157
+ this.requestCanonicalChatReconciliation();
9158
+ } else if (!this.harnessMessageIsTerminal(target.message)) {
9159
+ const previousStreaming = target.message.streaming === true;
9160
+ const previousText = target.part.text;
9161
+ let correction = !updateArg.text.startsWith(previousText);
9162
+ if (!correction && updateArg.text.length > previousText.length) {
9163
+ correction = !this.applyCanonicalReasoningDelta(
9164
+ projection,
9165
+ target,
9166
+ updateArg.text.slice(previousText.length),
9167
+ );
9168
+ }
9169
+ if (correction) target.part.text = updateArg.text;
9170
+ target.part.startedAt = updateArg.sourceUpdatedAt;
9171
+ if (updateArg.status === 'running') delete target.part.endedAt;
9172
+ target.message.streaming = updateArg.status === 'running'
9173
+ || (updateArg.sessionId.harnessId === 'opencode' && previousStreaming);
9174
+ if (
9175
+ !correction
9176
+ && updateArg.status === 'running'
9177
+ && !previousStreaming
9178
+ && updateArg.text === previousText
9179
+ ) {
9180
+ this.mountedCanonicalChat(projection)?.applyDelta({
9181
+ type: 'reasoning',
9182
+ messageId: target.message.id,
9183
+ partId: target.part.id,
9184
+ delta: '',
9185
+ });
9186
+ }
9187
+ if (updateArg.status !== 'running') {
9188
+ this.mountedCanonicalChat(projection)?.applyDelta({
9189
+ type: 'reasoning-end',
9190
+ messageId: target.message.id,
9191
+ partId: target.part.id,
9192
+ endedAt: updateArg.sourceUpdatedAt,
9193
+ });
9194
+ target.part.endedAt = updateArg.sourceUpdatedAt;
9195
+ }
9196
+ if (correction) {
9197
+ this.canonicalMessageRefreshIds.add(target.message.id);
9198
+ this.requestCanonicalChatReconciliation(false);
9199
+ }
9200
+ }
9201
+ if (updateArg.status !== 'running') {
9202
+ this.refreshAfterTerminalLiveMessage(projectIdArg, sessionId);
9203
+ }
9204
+ }
9205
+
9206
+ private applyLiveTextUpdate(
9207
+ projectIdArg: string,
9208
+ updateArg: interfaces.IControllerTextUpdate,
9209
+ ): void {
9210
+ if (!isSessionRuntimeId(updateArg.sessionId)) return;
9211
+ const sessionId = updateArg.sessionId;
9212
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, updateArg);
9213
+ if (!barrier.accepted) return;
9214
+ if (controllerRuntimeIdsEqual(updateArg.sessionId, this.subagentModalSessionId)) {
9215
+ this.scheduleSubagentModalRefresh();
9216
+ }
9217
+ if (
9218
+ projectIdArg !== this.selectedProjectId
9219
+ || !controllerRuntimeIdsEqual(updateArg.sessionId, this.selectedSessionId)
9220
+ ) return;
9221
+ const floor = this.liveMessageHydrationFloors.get(
9222
+ this.liveToolSessionKey(projectIdArg, updateArg.sessionId),
9223
+ )?.cursor;
9224
+ if (
9225
+ floor
9226
+ && floor.streamEpoch === updateArg.streamEpoch
9227
+ && updateArg.revision <= floor.revision
9228
+ ) return;
9229
+ const key = this.liveMessageUpdateKey(projectIdArg, updateArg);
9230
+ const previous = this.liveTextUpdates.get(key) ?? this.liveTextBaselines.get(key)?.update;
9231
+ if (
9232
+ previous
9233
+ && previous.streamEpoch === updateArg.streamEpoch
9234
+ && updateArg.revision <= previous.revision
9235
+ ) return;
9236
+ if (!this.storeLiveTextUpdate(key, updateArg)) {
9237
+ this.blockLiveMessageDeltas(
9238
+ projectIdArg,
9239
+ sessionId,
9240
+ updateArg.streamEpoch,
9241
+ !barrier.epochAdvanced,
9242
+ );
9243
+ return;
9244
+ }
9245
+
9246
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
9247
+ if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate();
9248
+ const target = projection ? this.canonicalTextTarget(projection, updateArg) : undefined;
9249
+ if (!projection || !target) {
9250
+ this.requestCanonicalChatReconciliation();
9251
+ } else if (!this.harnessMessageIsTerminal(target)) {
9252
+ const previousStreaming = target.streaming === true;
9253
+ const previousText = target.text;
9254
+ let correction = !updateArg.text.startsWith(previousText);
9255
+ if (!correction && updateArg.text.length > previousText.length) {
9256
+ correction = !this.applyCanonicalTextDelta(
9257
+ projection,
9258
+ target,
9259
+ updateArg.text.slice(previousText.length),
9260
+ );
9261
+ }
9262
+ if (correction) target.text = updateArg.text;
9263
+ if (updateArg.status === 'running') {
9264
+ target.streaming = true;
9265
+ if (!correction && !previousStreaming && updateArg.text === previousText) {
9266
+ this.mountedCanonicalChat(projection)?.applyDelta({
9267
+ type: 'text',
9268
+ messageId: target.id,
9269
+ delta: '',
9270
+ });
9271
+ }
9272
+ } else {
9273
+ this.mountedCanonicalChat(projection)?.applyDelta({
9274
+ type: 'message-end',
9275
+ messageId: target.id,
9276
+ });
9277
+ target.streaming = false;
9278
+ target.updatedAt = updateArg.sourceUpdatedAt;
9279
+ }
9280
+ if (correction) {
9281
+ this.canonicalMessageRefreshIds.add(target.id);
9282
+ this.requestCanonicalChatReconciliation(false);
9283
+ }
9284
+ }
9285
+ if (updateArg.status === 'completed') {
9286
+ this.refreshAfterTerminalLiveMessage(projectIdArg, sessionId);
9287
+ }
9288
+ }
9289
+
9290
+ private applyLiveReasoningDelta(
9291
+ projectIdArg: string,
9292
+ deltaArg: interfaces.IControllerReasoningDelta,
9293
+ ): void {
9294
+ if (!isSessionRuntimeId(deltaArg.sessionId)) return;
9295
+ const sessionId = deltaArg.sessionId;
9296
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, deltaArg);
9297
+ if (!barrier.accepted) return;
9298
+ if (
9299
+ projectIdArg !== this.selectedProjectId
9300
+ || !controllerRuntimeIdsEqual(deltaArg.sessionId, this.selectedSessionId)
9301
+ || this.liveMessageDeltasBlocked(projectIdArg, sessionId, deltaArg.streamEpoch)
9302
+ ) return;
9303
+ const floor = this.liveMessageHydrationFloors.get(
9304
+ this.liveToolSessionKey(projectIdArg, deltaArg.sessionId),
9305
+ )?.cursor;
9306
+ if (
9307
+ floor
9308
+ && floor.streamEpoch === deltaArg.streamEpoch
9309
+ && deltaArg.revision <= floor.revision
9310
+ ) return;
9311
+ const key = this.liveMessageUpdateKey(projectIdArg, deltaArg);
9312
+ const active = this.liveReasoningUpdates.get(key);
9313
+ const baseline = this.liveReasoningBaselines.get(key);
9314
+ const previous = active ?? baseline?.update;
9315
+ if (
9316
+ previous
9317
+ && previous.streamEpoch === deltaArg.streamEpoch
9318
+ && deltaArg.revision <= previous.revision
9319
+ ) return;
9320
+ const textUtf8Bytes = active
9321
+ ? this.liveReasoningTextUtf8Bytes.get(key)
9322
+ ?? browserTextEncoder.encode(active.text).byteLength
9323
+ : baseline?.textUtf8Bytes;
9324
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
9325
+ const target = projection ? this.canonicalReasoningTarget(projection, deltaArg) : undefined;
9326
+ const nextTextUtf8Bytes = previous && textUtf8Bytes !== undefined
9327
+ ? controllerTextAfterAppendUtf8Bytes(previous.text, textUtf8Bytes, deltaArg.delta)
9328
+ : undefined;
9329
+ if (
9330
+ !previous
9331
+ || previous.status !== 'running'
9332
+ || previous.streamEpoch !== deltaArg.streamEpoch
9333
+ || deltaArg.revision <= previous.revision
9334
+ || deltaArg.sourceUpdatedAt < previous.sourceUpdatedAt
9335
+ || !this.liveMessageIdentityMatches(previous, deltaArg)
9336
+ || textUtf8Bytes !== deltaArg.baseTextUtf8Bytes
9337
+ || nextTextUtf8Bytes !== deltaArg.textUtf8Bytes
9338
+ || !projection
9339
+ || !target
9340
+ || this.harnessMessageIsTerminal(target.message)
9341
+ || target.part.text !== previous.text
9342
+ || (deltaArg.order !== undefined
9343
+ && (!target.message.order
9344
+ || transcriptOrderKey(target.message.order) !== transcriptOrderKey(deltaArg.order)))
9345
+ ) {
9346
+ this.blockLiveMessageDeltas(
9347
+ projectIdArg,
9348
+ sessionId,
9349
+ deltaArg.streamEpoch,
9350
+ !barrier.epochAdvanced,
9351
+ );
9352
+ return;
9353
+ }
9354
+ if (!this.applyCanonicalReasoningDelta(projection, target, deltaArg.delta)) {
9355
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9356
+ return;
9357
+ }
9358
+ target.message.streaming = true;
9359
+ const update: interfaces.IControllerReasoningUpdate = {
9360
+ ...previous,
9361
+ text: `${previous.text}${deltaArg.delta}`,
9362
+ sourceUpdatedAt: deltaArg.sourceUpdatedAt,
9363
+ revision: deltaArg.revision,
9364
+ streamEpoch: deltaArg.streamEpoch,
9365
+ };
9366
+ if (!this.storeLiveReasoningDeltaState(
9367
+ key,
9368
+ update,
9369
+ deltaArg.textUtf8Bytes,
9370
+ deltaArg.delta,
9371
+ )) {
9372
+ target.part.text = previous.text;
9373
+ this.requestCanonicalChatReconciliation();
9374
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9375
+ }
9376
+ }
9377
+
9378
+ private applyLiveTextDelta(
9379
+ projectIdArg: string,
9380
+ deltaArg: interfaces.IControllerTextDelta,
9381
+ ): void {
9382
+ if (!isSessionRuntimeId(deltaArg.sessionId)) return;
9383
+ const sessionId = deltaArg.sessionId;
9384
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, deltaArg);
9385
+ if (!barrier.accepted) return;
9386
+ if (
9387
+ projectIdArg !== this.selectedProjectId
9388
+ || !controllerRuntimeIdsEqual(deltaArg.sessionId, this.selectedSessionId)
9389
+ || this.liveMessageDeltasBlocked(projectIdArg, sessionId, deltaArg.streamEpoch)
9390
+ ) return;
9391
+ const floor = this.liveMessageHydrationFloors.get(
9392
+ this.liveToolSessionKey(projectIdArg, deltaArg.sessionId),
9393
+ )?.cursor;
9394
+ if (
9395
+ floor
9396
+ && floor.streamEpoch === deltaArg.streamEpoch
9397
+ && deltaArg.revision <= floor.revision
9398
+ ) return;
9399
+ const key = this.liveMessageUpdateKey(projectIdArg, deltaArg);
9400
+ const active = this.liveTextUpdates.get(key);
9401
+ const baseline = this.liveTextBaselines.get(key);
9402
+ const previous = active ?? baseline?.update;
9403
+ if (
9404
+ previous
9405
+ && previous.streamEpoch === deltaArg.streamEpoch
9406
+ && deltaArg.revision <= previous.revision
9407
+ ) return;
9408
+ const textUtf8Bytes = active
9409
+ ? this.liveTextTextUtf8Bytes.get(key)
9410
+ ?? browserTextEncoder.encode(active.text).byteLength
9411
+ : baseline?.textUtf8Bytes;
9412
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
9413
+ const target = projection ? this.canonicalTextTarget(projection, deltaArg) : undefined;
9414
+ const nextTextUtf8Bytes = previous && textUtf8Bytes !== undefined
9415
+ ? controllerTextAfterAppendUtf8Bytes(previous.text, textUtf8Bytes, deltaArg.delta)
9416
+ : undefined;
9417
+ if (
9418
+ !previous
9419
+ || previous.status !== 'running'
9420
+ || previous.streamEpoch !== deltaArg.streamEpoch
9421
+ || deltaArg.revision <= previous.revision
9422
+ || deltaArg.sourceUpdatedAt < previous.sourceUpdatedAt
9423
+ || !this.liveMessageIdentityMatches(previous, deltaArg)
9424
+ || textUtf8Bytes !== deltaArg.baseTextUtf8Bytes
9425
+ || nextTextUtf8Bytes !== deltaArg.textUtf8Bytes
9426
+ || !projection
9427
+ || !target
9428
+ || this.harnessMessageIsTerminal(target)
9429
+ || target.text !== previous.text
9430
+ || (deltaArg.order !== undefined
9431
+ && (!target.order || transcriptOrderKey(target.order) !== transcriptOrderKey(deltaArg.order)))
9432
+ ) {
9433
+ this.blockLiveMessageDeltas(
9434
+ projectIdArg,
9435
+ sessionId,
9436
+ deltaArg.streamEpoch,
9437
+ !barrier.epochAdvanced,
9438
+ );
9439
+ return;
9440
+ }
9441
+ if (!this.applyCanonicalTextDelta(projection, target, deltaArg.delta)) {
9442
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9443
+ return;
9444
+ }
9445
+ const update: interfaces.IControllerTextUpdate = {
9446
+ ...previous,
9447
+ text: `${previous.text}${deltaArg.delta}`,
9448
+ sourceUpdatedAt: deltaArg.sourceUpdatedAt,
9449
+ revision: deltaArg.revision,
9450
+ streamEpoch: deltaArg.streamEpoch,
9451
+ };
9452
+ if (!this.storeLiveTextDeltaState(
9453
+ key,
9454
+ update,
9455
+ deltaArg.textUtf8Bytes,
9456
+ deltaArg.delta,
9457
+ )) {
9458
+ target.text = previous.text;
9459
+ this.requestCanonicalChatReconciliation();
9460
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9461
+ }
9462
+ }
9463
+
7936
9464
  private async handleControllerEvent(eventArg: interfaces.IControllerEvent): Promise<void> {
7937
9465
  if (!this.authenticated) {
7938
9466
  return;
@@ -8038,79 +9566,27 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
8038
9566
  return;
8039
9567
  }
8040
9568
  if (eventArg.type === 'session.reasoning.updated' && eventArg.reasoningUpdate) {
8041
- const update = eventArg.reasoningUpdate;
8042
9569
  if (typeof eventArg.projectId !== 'string') return;
8043
- if (!isSessionRuntimeId(update.sessionId)) return;
8044
- if (
8045
- update.sourceUpdatedAt
8046
- <= (this.liveHistoryBarriers.get(
8047
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8048
- ) ?? Number.NEGATIVE_INFINITY)
8049
- ) return;
8050
- if (!this.acceptLiveMessageEpoch(update.sessionId.harnessId, update.streamEpoch)) return;
8051
- if (controllerRuntimeIdsEqual(update.sessionId, this.subagentModalSessionId)) {
8052
- this.scheduleSubagentModalRefresh();
8053
- }
8054
- if (
8055
- eventArg.projectId !== this.selectedProjectId
8056
- || !controllerRuntimeIdsEqual(update.sessionId, this.selectedSessionId)
8057
- ) return;
8058
- const floor = this.liveMessageHydrationFloors.get(
8059
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8060
- )?.cursor;
8061
- if (
8062
- floor
8063
- && floor.streamEpoch === update.streamEpoch
8064
- && update.revision <= floor.revision
8065
- ) return;
8066
- const key = `${eventArg.projectId ?? ''}\0${controllerRuntimeIdToUiKey(update.sessionId)}\0${controllerRuntimeIdToUiKey(update.partId)}`;
8067
- const previous = this.liveReasoningUpdates.get(key);
8068
- if (
8069
- !previous
8070
- || update.streamEpoch > previous.streamEpoch
8071
- || (update.streamEpoch === previous.streamEpoch && update.revision > previous.revision)
8072
- ) {
8073
- if (this.storeLiveReasoningUpdate(key, update)) this.requestUpdate();
8074
- }
8075
- if (update.status !== 'running') this.refreshSessionsNow();
9570
+ if (!isSessionRuntimeId(eventArg.reasoningUpdate.sessionId)) return;
9571
+ this.applyLiveReasoningUpdate(eventArg.projectId, eventArg.reasoningUpdate);
9572
+ return;
9573
+ }
9574
+ if (eventArg.type === 'session.reasoning.delta' && eventArg.reasoningDelta) {
9575
+ if (typeof eventArg.projectId !== 'string') return;
9576
+ if (!isSessionRuntimeId(eventArg.reasoningDelta.sessionId)) return;
9577
+ this.applyLiveReasoningDelta(eventArg.projectId, eventArg.reasoningDelta);
8076
9578
  return;
8077
9579
  }
8078
9580
  if (eventArg.type === 'session.text.updated' && eventArg.textUpdate) {
8079
- const update = eventArg.textUpdate;
8080
9581
  if (typeof eventArg.projectId !== 'string') return;
8081
- if (!isSessionRuntimeId(update.sessionId)) return;
8082
- if (
8083
- update.sourceUpdatedAt
8084
- <= (this.liveHistoryBarriers.get(
8085
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8086
- ) ?? Number.NEGATIVE_INFINITY)
8087
- ) return;
8088
- if (!this.acceptLiveMessageEpoch(update.sessionId.harnessId, update.streamEpoch)) return;
8089
- if (controllerRuntimeIdsEqual(update.sessionId, this.subagentModalSessionId)) {
8090
- this.scheduleSubagentModalRefresh();
8091
- }
8092
- if (
8093
- eventArg.projectId !== this.selectedProjectId
8094
- || !controllerRuntimeIdsEqual(update.sessionId, this.selectedSessionId)
8095
- ) return;
8096
- const floor = this.liveMessageHydrationFloors.get(
8097
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8098
- )?.cursor;
8099
- if (
8100
- floor
8101
- && floor.streamEpoch === update.streamEpoch
8102
- && update.revision <= floor.revision
8103
- ) return;
8104
- const key = `${eventArg.projectId ?? ''}\0${controllerRuntimeIdToUiKey(update.sessionId)}\0${controllerRuntimeIdToUiKey(update.partId)}`;
8105
- const previous = this.liveTextUpdates.get(key);
8106
- if (
8107
- !previous
8108
- || update.streamEpoch > previous.streamEpoch
8109
- || (update.streamEpoch === previous.streamEpoch && update.revision > previous.revision)
8110
- ) {
8111
- if (this.storeLiveTextUpdate(key, update)) this.requestUpdate();
8112
- }
8113
- if (update.status === 'completed') this.refreshSessionsNow();
9582
+ if (!isSessionRuntimeId(eventArg.textUpdate.sessionId)) return;
9583
+ this.applyLiveTextUpdate(eventArg.projectId, eventArg.textUpdate);
9584
+ return;
9585
+ }
9586
+ if (eventArg.type === 'session.text.delta' && eventArg.textDelta) {
9587
+ if (typeof eventArg.projectId !== 'string') return;
9588
+ if (!isSessionRuntimeId(eventArg.textDelta.sessionId)) return;
9589
+ this.applyLiveTextDelta(eventArg.projectId, eventArg.textDelta);
8114
9590
  return;
8115
9591
  }
8116
9592
  this.applySessionEventState(eventArg);
@@ -10424,25 +11900,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10424
11900
  <dees-badge type="success" text="Connected" .rounded=${true}></dees-badge>
10425
11901
  <span class="headerSpacer"></span>
10426
11902
  <div class="headerActions">
10427
- <dees-button
10428
- class="refreshAction"
10429
- type="ghost"
10430
- size="sm"
10431
- ?disabled=${this.sessionsLoading || this.mutationPending}
10432
- @clicked=${() => void this.refreshSessions()}
10433
- >
10434
- <dees-icon icon="lucide:RefreshCw" iconSize="14"></dees-icon>
10435
- Refresh
10436
- </dees-button>
10437
- <dees-button
10438
- type="ghost"
10439
- size="sm"
10440
- ?disabled=${!this.selectedSessionId || this.mutationPending}
10441
- @clicked=${this.showDeleteConfirmation}
10442
- >
10443
- <dees-icon icon="lucide:Trash2" iconSize="14"></dees-icon>
10444
- Delete
10445
- </dees-button>
10446
11903
  <dees-button
10447
11904
  type="ghost"
10448
11905
  size="sm"
@@ -10746,9 +12203,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10746
12203
  <dees-icon icon="lucide:Globe2" iconSize="16"></dees-icon>
10747
12204
  <input
10748
12205
  aria-label="Browser address"
10749
- .value=${activeTab?.url ?? ''}
12206
+ .value=${this.browserAddressEditing ? this.browserAddressDraft : activeTab?.url ?? ''}
10750
12207
  placeholder="https://example.com"
10751
12208
  ?disabled=${!this.browserViewId}
12209
+ @focus=${this.handleBrowserAddressFocus}
12210
+ @input=${this.handleBrowserAddressInput}
12211
+ @blur=${this.handleBrowserAddressBlur}
10752
12212
  />
10753
12213
  <dees-button
10754
12214
  type="highlighted"
@@ -10803,6 +12263,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10803
12263
  .reasoningEffort=${this.resolveSessionEffortString() || 'default'}
10804
12264
  .effortOptions=${this.resolveSessionEffortOptions()}
10805
12265
  .showTodosPanel=${false}
12266
+ .markdownWhileStreaming=${false}
10806
12267
  heading=${`New ${sessionHarnessLabel(this.draftHarnessId)} conversation`}
10807
12268
  subheading=${directory
10808
12269
  ? `${sessionHarnessLabel(this.draftHarnessId)} creates this session on first send · starts in ${directory}`
@@ -10876,27 +12337,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10876
12337
  const sessionId = detailArg.session.id;
10877
12338
  const isOpenCode = sessionId.harnessId === 'opencode';
10878
12339
  const readOnly = this.sessionIsReadOnly(sessionId);
10879
- // The question tool call is the transcript's raw record of an ask; the
10880
- // inline question card is its rendering, so the JSON never shows.
10881
- const messages: plugins.deesCatalog.IHarnessMessage[] = detailArg.messages
10882
- .filter((messageArg) => messageArg.toolCall?.name !== 'question')
10883
- .map((messageArg) => this.toHarnessMessage(messageArg, this.selectedProjectId, sessionId));
10884
- const visibleMessageIds = new Set(messages.map((message) => message.id));
10885
- for (const prompt of detailArg.pendingPrompts) {
10886
- const id = controllerRuntimeIdToUiKey(prompt.id);
10887
- if (visibleMessageIds.has(id)) continue;
10888
- messages.push({
10889
- id,
10890
- role: 'user',
10891
- text: prompt.text,
10892
- createdAt: prompt.createdAt,
10893
- markdown: false,
10894
- attachments: prompt.attachments.map((attachment) => ({ ...attachment })),
10895
- });
10896
- }
10897
- this.mergeLiveReasoningMessages(messages, this.selectedProjectId, sessionId);
10898
- this.mergeLiveTextMessages(messages, this.selectedProjectId, sessionId);
10899
- this.mergeLiveToolMessages(messages, this.selectedProjectId, sessionId);
12340
+ // The canonical array and all delta targets remain stable across unrelated app renders.
12341
+ const messages = this.ensureCanonicalChatProjection(
12342
+ detailArg,
12343
+ this.selectedProjectId,
12344
+ sessionId,
12345
+ ).messages;
10900
12346
  const status = this.toHarnessStatus(
10901
12347
  detailArg.session,
10902
12348
  this.activeSubagentCount(messages),
@@ -11120,6 +12566,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
11120
12566
  .reasoningEffort=${this.resolveSessionEffortString(sessionId) || 'default'}
11121
12567
  .effortOptions=${this.resolveSessionEffortOptions(sessionId)}
11122
12568
  .showTodosPanel=${true}
12569
+ .markdownWhileStreaming=${false}
11123
12570
  heading=${detailArg.session.title || `${sessionHarnessLabel(sessionId.harnessId)} session`}
11124
12571
  subheading=${readOnly
11125
12572
  ? `Flex unavailable · saved projection · read-only · ${runtimeIdDisplay(sessionId)}`
@@ -11454,13 +12901,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
11454
12901
  ? (this.authenticated ? 'authenticated' : 'connected')
11455
12902
  : pending ? 'connecting' : 'offline';
11456
12903
  const metrics = this.systemMetrics;
11457
- const memoryPercent = metrics?.memoryUsedBytes !== null
11458
- && metrics?.memoryUsedBytes !== undefined
11459
- && metrics.memoryTotalBytes !== null
11460
- && metrics.memoryTotalBytes !== undefined
11461
- && metrics.memoryTotalBytes > 0
11462
- ? (metrics.memoryUsedBytes / metrics.memoryTotalBytes) * 100
11463
- : null;
11464
12904
  const memoryTitle = metrics?.memoryUsedBytes !== null
11465
12905
  && metrics?.memoryUsedBytes !== undefined
11466
12906
  && metrics.memoryTotalBytes !== null
@@ -11484,19 +12924,19 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
11484
12924
  : 'Whole-system CPU usage'}
11485
12925
  >
11486
12926
  <span class="metricLabel">CPU</span>
11487
- <span>${formatPercentage(metrics?.cpuUsagePercent)}</span>
12927
+ <span class="metricValue">--</span>
11488
12928
  </span>
11489
12929
  <span class="statusMetric memoryMetric" title=${memoryTitle}>
11490
12930
  <span class="metricLabel">RAM</span>
11491
- <span>${formatPercentage(memoryPercent)}</span>
12931
+ <span class="metricValue">--</span>
11492
12932
  </span>
11493
12933
  <span
11494
12934
  class="statusMetric networkMetric"
11495
12935
  title="Aggregate traffic across all system network interfaces"
11496
12936
  >
11497
12937
  <span class="metricLabel">NET</span>
11498
- <span>in ${formatByteRate(metrics?.networkReceiveBytesPerSecond)}</span>
11499
- <span>out ${formatByteRate(metrics?.networkTransmitBytesPerSecond)}</span>
12938
+ <span class="networkReceive">in --</span>
12939
+ <span class="networkTransmit">out --</span>
11500
12940
  </span>
11501
12941
  </div>
11502
12942
  `