agl 22.0.3 → 23.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.
@@ -128,6 +128,33 @@ interface IDetailLoadOwnership {
128
128
  messageStreamEpoch: number;
129
129
  }
130
130
 
131
+ interface ILiveMessageBaseline<
132
+ TUpdate extends interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate,
133
+ > {
134
+ update: TUpdate;
135
+ textUtf8Bytes: number;
136
+ bytes: number;
137
+ }
138
+
139
+ interface ICanonicalReasoningTarget {
140
+ message: plugins.deesCatalog.IHarnessMessage;
141
+ part: plugins.deesCatalog.IHarnessReasoningPart;
142
+ }
143
+
144
+ interface ICanonicalChatProjection {
145
+ key: string;
146
+ sourceDetail: IControllerSessionRenderDetail | undefined;
147
+ messages: plugins.deesCatalog.IHarnessMessage[];
148
+ messagesById: Map<string, plugins.deesCatalog.IHarnessMessage>;
149
+ messagesByOrder: Map<string, plugins.deesCatalog.IHarnessMessage>;
150
+ reasoningById: Map<string, ICanonicalReasoningTarget>;
151
+ }
152
+
153
+ interface ILiveMessageDeltaBlock {
154
+ sessionId: TBrowserSessionId;
155
+ streamEpoch: number;
156
+ }
157
+
131
158
  interface ISlashCatalogOwnership {
132
159
  key: string;
133
160
  projectId: string;
@@ -282,6 +309,57 @@ const controllerJsonValuesEqual = (leftArg: unknown, rightArg: unknown): boolean
282
309
  ));
283
310
  };
284
311
 
312
+ const reconcileKeyedJsonArray = <TValue>(
313
+ currentArg: readonly TValue[],
314
+ nextArg: readonly TValue[],
315
+ keyForArg: (valueArg: TValue, indexArg: number) => string,
316
+ ): TValue[] => {
317
+ const currentByKey = new Map(
318
+ currentArg.map((value, index) => [keyForArg(value, index), value]),
319
+ );
320
+ const seenKeys = new Set<string>();
321
+ let unchanged = currentArg.length === nextArg.length;
322
+ const reconciled = nextArg.map((nextValue, index) => {
323
+ const key = keyForArg(nextValue, index);
324
+ const currentValue = seenKeys.has(key) ? undefined : currentByKey.get(key);
325
+ seenKeys.add(key);
326
+ const value = currentValue !== undefined && controllerJsonValuesEqual(currentValue, nextValue)
327
+ ? currentValue
328
+ : nextValue;
329
+ unchanged &&= value === currentArg[index];
330
+ return value;
331
+ });
332
+ return unchanged ? currentArg as TValue[] : reconciled;
333
+ };
334
+
335
+ const browserTextEncoder = new TextEncoder();
336
+
337
+ const controllerTextAfterAppendUtf8Bytes = (
338
+ textArg: string,
339
+ textUtf8BytesArg: number,
340
+ deltaArg: string,
341
+ ): number | undefined => {
342
+ const previousCodeUnit = textArg.charCodeAt(textArg.length - 1);
343
+ const nextCodeUnit = deltaArg.charCodeAt(0);
344
+ // Separately encoded surrogate halves cost six bytes; the joined pair costs four.
345
+ const boundaryCorrection = textArg.length > 0
346
+ && deltaArg.length > 0
347
+ && previousCodeUnit >= 0xD800
348
+ && previousCodeUnit <= 0xDBFF
349
+ && nextCodeUnit >= 0xDC00
350
+ && nextCodeUnit <= 0xDFFF
351
+ ? -2
352
+ : 0;
353
+ const result = textUtf8BytesArg
354
+ + browserTextEncoder.encode(deltaArg).byteLength
355
+ + boundaryCorrection;
356
+ return Number.isSafeInteger(result) && result >= 0 ? result : undefined;
357
+ };
358
+
359
+ const transcriptOrderKey = (orderArg: interfaces.IControllerTranscriptOrder): string => (
360
+ `${orderArg.messageIndex}\0${orderArg.partIndex}`
361
+ );
362
+
285
363
  const controllerNormalizeBrowserAddress = (addressArg: string): string => {
286
364
  const address = addressArg.trim();
287
365
  if (!address) throw new Error('Enter a browser address.');
@@ -628,6 +706,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
628
706
  private highCpuStartedAt: number | undefined;
629
707
  private lastCpuSampleAt: number | undefined;
630
708
  private refreshPending = false;
709
+ private refreshRelatedPending = false;
631
710
  private projectsRequestId = 0;
632
711
  private modelCatalogRequestId = 0;
633
712
  private layoutRequestId = 0;
@@ -757,13 +836,30 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
757
836
  private readonly liveToolOverlays = new Map<string, ILiveToolOverlay>();
758
837
  private readonly liveReasoningUpdates = new Map<string, interfaces.IControllerReasoningUpdate>();
759
838
  private readonly liveTextUpdates = new Map<string, interfaces.IControllerTextUpdate>();
839
+ private readonly liveReasoningBaselines = new Map<
840
+ string,
841
+ ILiveMessageBaseline<interfaces.IControllerReasoningUpdate>
842
+ >();
843
+ private readonly liveTextBaselines = new Map<
844
+ string,
845
+ ILiveMessageBaseline<interfaces.IControllerTextUpdate>
846
+ >();
847
+ private readonly liveReasoningTextUtf8Bytes = new Map<string, number>();
848
+ private readonly liveTextTextUtf8Bytes = new Map<string, number>();
849
+ private readonly liveReasoningStoredBytes = new Map<string, number>();
850
+ private readonly liveTextStoredBytes = new Map<string, number>();
760
851
  private readonly liveToolHydrationFloors = new Map<string, ILiveToolHydrationFloor>();
761
852
  private readonly liveMessageHydrationFloors = new Map<string, ILiveMessageHydrationFloor>();
853
+ private readonly liveMessageDeltaBlocks = new Map<string, ILiveMessageDeltaBlock>();
762
854
  private readonly liveHistoryBarriers = new Map<string, number>();
763
855
  private liveToolOverlayBytes = 0;
764
856
  private liveMessageOverlayBytes = 0;
765
857
  private readonly liveToolStreamEpochs = new Map<TBrowserSessionHarnessId, number>();
766
858
  private readonly liveMessageStreamEpochs = new Map<TBrowserSessionHarnessId, number>();
859
+ private canonicalChatProjection: ICanonicalChatProjection | undefined;
860
+ private canonicalChatProjectionDirty = false;
861
+ private canonicalTimelineRefreshPending = false;
862
+ private readonly canonicalMessageRefreshIds = new Set<string>();
767
863
  private readonly subtaskPreviewEntries = new Map<string, ISubtaskPreviewEntry>();
768
864
  private subtaskPreviewGeneration = 0;
769
865
  private subtaskPreviewHydrationQueue: string[] = [];
@@ -1824,8 +1920,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1824
1920
  padding: 0 var(--dees-spacing-sm);
1825
1921
  }
1826
1922
 
1827
- .brandSubtitle,
1828
- .headerActions .refreshAction {
1923
+ .brandSubtitle {
1829
1924
  display: none;
1830
1925
  }
1831
1926
 
@@ -2016,6 +2111,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2016
2111
  this.draftSessionDetailReadyId = undefined;
2017
2112
  this.authenticated = false;
2018
2113
  this.refreshPending = false;
2114
+ this.refreshRelatedPending = false;
2019
2115
  this.detailRefreshPending = false;
2020
2116
  this.projects = [];
2021
2117
  this.creatingProject = false;
@@ -2843,12 +2939,13 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2843
2939
  }
2844
2940
  }
2845
2941
 
2846
- private refreshSessions(): Promise<void> {
2942
+ private refreshSessions(includeRelatedArg = true): Promise<void> {
2847
2943
  if (!this.authenticated || !this.socketClient.isConnected) {
2848
2944
  return Promise.resolve();
2849
2945
  }
2850
2946
 
2851
2947
  this.refreshPending = true;
2948
+ this.refreshRelatedPending ||= includeRelatedArg;
2852
2949
  if (!this.refreshInFlight) {
2853
2950
  this.refreshInFlight = this.runRefreshQueue();
2854
2951
  }
@@ -2930,8 +3027,16 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2930
3027
  if (harnessIdArg === undefined) {
2931
3028
  this.liveReasoningUpdates.clear();
2932
3029
  this.liveTextUpdates.clear();
3030
+ this.liveReasoningBaselines.clear();
3031
+ this.liveTextBaselines.clear();
3032
+ this.liveReasoningTextUtf8Bytes.clear();
3033
+ this.liveTextTextUtf8Bytes.clear();
3034
+ this.liveReasoningStoredBytes.clear();
3035
+ this.liveTextStoredBytes.clear();
2933
3036
  this.liveMessageHydrationFloors.clear();
3037
+ this.liveMessageDeltaBlocks.clear();
2934
3038
  this.liveMessageOverlayBytes = 0;
3039
+ this.canonicalChatProjectionDirty = true;
2935
3040
  return;
2936
3041
  }
2937
3042
  for (const [key, floor] of this.liveMessageHydrationFloors) {
@@ -2943,6 +3048,18 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2943
3048
  for (const [key, update] of this.liveTextUpdates) {
2944
3049
  if (update.sessionId.harnessId === harnessIdArg) this.deleteLiveTextUpdate(key);
2945
3050
  }
3051
+ for (const [key, baseline] of this.liveReasoningBaselines) {
3052
+ if (baseline.update.sessionId.harnessId === harnessIdArg) {
3053
+ this.deleteLiveReasoningBaseline(key);
3054
+ }
3055
+ }
3056
+ for (const [key, baseline] of this.liveTextBaselines) {
3057
+ if (baseline.update.sessionId.harnessId === harnessIdArg) this.deleteLiveTextBaseline(key);
3058
+ }
3059
+ for (const [key, block] of this.liveMessageDeltaBlocks) {
3060
+ if (block.sessionId.harnessId === harnessIdArg) this.liveMessageDeltaBlocks.delete(key);
3061
+ }
3062
+ this.canonicalChatProjectionDirty = true;
2946
3063
  }
2947
3064
 
2948
3065
  private clearLiveToolSession(
@@ -2967,21 +3084,38 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2967
3084
  && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
2968
3085
  ) this.deleteLiveTextUpdate(key);
2969
3086
  }
3087
+ for (const [key, baseline] of this.liveReasoningBaselines) {
3088
+ if (
3089
+ key.startsWith(`${projectIdArg}\0`)
3090
+ && controllerRuntimeIdsEqual(baseline.update.sessionId, sessionIdArg)
3091
+ ) this.deleteLiveReasoningBaseline(key);
3092
+ }
3093
+ for (const [key, baseline] of this.liveTextBaselines) {
3094
+ if (
3095
+ key.startsWith(`${projectIdArg}\0`)
3096
+ && controllerRuntimeIdsEqual(baseline.update.sessionId, sessionIdArg)
3097
+ ) this.deleteLiveTextBaseline(key);
3098
+ }
3099
+ this.canonicalChatProjectionDirty = true;
2970
3100
  // Keep both hydration floors: they continue fencing snapshots already
2971
3101
  // represented by the last authoritative detail until its replacement lands.
2972
3102
  }
2973
3103
 
2974
3104
  private liveMessageUpdateBytes(updateArg: unknown): number {
2975
- return new TextEncoder().encode(JSON.stringify(updateArg)).byteLength;
3105
+ return browserTextEncoder.encode(JSON.stringify(updateArg)).byteLength;
2976
3106
  }
2977
3107
 
2978
3108
  private deleteLiveReasoningUpdate(keyArg: string): void {
2979
3109
  const update = this.liveReasoningUpdates.get(keyArg);
2980
3110
  if (!update) return;
2981
3111
  this.liveReasoningUpdates.delete(keyArg);
3112
+ this.liveReasoningTextUtf8Bytes.delete(keyArg);
3113
+ const bytes = this.liveReasoningStoredBytes.get(keyArg)
3114
+ ?? this.liveMessageUpdateBytes(update);
3115
+ this.liveReasoningStoredBytes.delete(keyArg);
2982
3116
  this.liveMessageOverlayBytes = Math.max(
2983
3117
  0,
2984
- this.liveMessageOverlayBytes - this.liveMessageUpdateBytes(update),
3118
+ this.liveMessageOverlayBytes - bytes,
2985
3119
  );
2986
3120
  }
2987
3121
 
@@ -2989,29 +3123,61 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2989
3123
  const update = this.liveTextUpdates.get(keyArg);
2990
3124
  if (!update) return;
2991
3125
  this.liveTextUpdates.delete(keyArg);
3126
+ this.liveTextTextUtf8Bytes.delete(keyArg);
3127
+ const bytes = this.liveTextStoredBytes.get(keyArg)
3128
+ ?? this.liveMessageUpdateBytes(update);
3129
+ this.liveTextStoredBytes.delete(keyArg);
2992
3130
  this.liveMessageOverlayBytes = Math.max(
2993
3131
  0,
2994
- this.liveMessageOverlayBytes - this.liveMessageUpdateBytes(update),
3132
+ this.liveMessageOverlayBytes - bytes,
2995
3133
  );
2996
3134
  }
2997
3135
 
3136
+ private deleteLiveReasoningBaseline(keyArg: string): void {
3137
+ const baseline = this.liveReasoningBaselines.get(keyArg);
3138
+ if (!baseline) return;
3139
+ this.liveReasoningBaselines.delete(keyArg);
3140
+ this.liveMessageOverlayBytes = Math.max(0, this.liveMessageOverlayBytes - baseline.bytes);
3141
+ }
3142
+
3143
+ private deleteLiveTextBaseline(keyArg: string): void {
3144
+ const baseline = this.liveTextBaselines.get(keyArg);
3145
+ if (!baseline) return;
3146
+ this.liveTextBaselines.delete(keyArg);
3147
+ this.liveMessageOverlayBytes = Math.max(0, this.liveMessageOverlayBytes - baseline.bytes);
3148
+ }
3149
+
3150
+ private liveMessageEntryCount(): number {
3151
+ return this.liveReasoningUpdates.size
3152
+ + this.liveTextUpdates.size
3153
+ + this.liveReasoningBaselines.size
3154
+ + this.liveTextBaselines.size;
3155
+ }
3156
+
2998
3157
  private storeLiveReasoningUpdate(
2999
3158
  keyArg: string,
3000
3159
  updateArg: interfaces.IControllerReasoningUpdate,
3001
3160
  ): boolean {
3002
3161
  const bytes = this.liveMessageUpdateBytes(updateArg);
3003
3162
  const existing = this.liveReasoningUpdates.get(keyArg);
3004
- const existingBytes = existing ? this.liveMessageUpdateBytes(existing) : 0;
3163
+ const baseline = this.liveReasoningBaselines.get(keyArg);
3164
+ const existingBytes = existing
3165
+ ? this.liveReasoningStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(existing)
3166
+ : baseline?.bytes ?? 0;
3005
3167
  if (
3006
- (!existing && this.liveReasoningUpdates.size + this.liveTextUpdates.size >= maxLiveToolOverlayEntries)
3168
+ (!existing && !baseline && this.liveMessageEntryCount() >= maxLiveToolOverlayEntries)
3007
3169
  || this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes
3008
3170
  ) {
3009
- this.clearLiveMessageState();
3010
- this.refreshSessionsNow();
3011
3171
  return false;
3012
3172
  }
3013
3173
  if (existing) this.deleteLiveReasoningUpdate(keyArg);
3174
+ if (baseline) this.deleteLiveReasoningBaseline(keyArg);
3014
3175
  this.liveReasoningUpdates.set(keyArg, updateArg);
3176
+ this.liveReasoningTextUtf8Bytes.set(
3177
+ keyArg,
3178
+ browserTextEncoder.encode(updateArg.text).byteLength,
3179
+ );
3180
+ this.liveReasoningStoredBytes.set(keyArg, bytes);
3015
3181
  this.liveMessageOverlayBytes += bytes;
3016
3182
  return true;
3017
3183
  }
@@ -3022,21 +3188,168 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3022
3188
  ): boolean {
3023
3189
  const bytes = this.liveMessageUpdateBytes(updateArg);
3024
3190
  const existing = this.liveTextUpdates.get(keyArg);
3025
- const existingBytes = existing ? this.liveMessageUpdateBytes(existing) : 0;
3191
+ const baseline = this.liveTextBaselines.get(keyArg);
3192
+ const existingBytes = existing
3193
+ ? this.liveTextStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(existing)
3194
+ : baseline?.bytes ?? 0;
3026
3195
  if (
3027
- (!existing && this.liveReasoningUpdates.size + this.liveTextUpdates.size >= maxLiveToolOverlayEntries)
3196
+ (!existing && !baseline && this.liveMessageEntryCount() >= maxLiveToolOverlayEntries)
3028
3197
  || this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes
3029
3198
  ) {
3030
- this.clearLiveMessageState();
3031
- this.refreshSessionsNow();
3032
3199
  return false;
3033
3200
  }
3034
3201
  if (existing) this.deleteLiveTextUpdate(keyArg);
3202
+ if (baseline) this.deleteLiveTextBaseline(keyArg);
3035
3203
  this.liveTextUpdates.set(keyArg, updateArg);
3204
+ this.liveTextTextUtf8Bytes.set(keyArg, browserTextEncoder.encode(updateArg.text).byteLength);
3205
+ this.liveTextStoredBytes.set(keyArg, bytes);
3036
3206
  this.liveMessageOverlayBytes += bytes;
3037
3207
  return true;
3038
3208
  }
3039
3209
 
3210
+ private retireLiveReasoningUpdateToBaseline(
3211
+ keyArg: string,
3212
+ updateArg: interfaces.IControllerReasoningUpdate,
3213
+ textArg: string,
3214
+ cursorArg: interfaces.IControllerMessageStreamCursor,
3215
+ ): void {
3216
+ const update = { ...updateArg, text: textArg, revision: cursorArg.revision };
3217
+ const bytes = this.liveMessageUpdateBytes(update);
3218
+ const existingBytes = this.liveReasoningStoredBytes.get(keyArg)
3219
+ ?? this.liveReasoningBaselines.get(keyArg)?.bytes
3220
+ ?? this.liveMessageUpdateBytes(updateArg);
3221
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) {
3222
+ this.deleteLiveReasoningUpdate(keyArg);
3223
+ this.deleteLiveReasoningBaseline(keyArg);
3224
+ return;
3225
+ }
3226
+ this.deleteLiveReasoningUpdate(keyArg);
3227
+ this.deleteLiveReasoningBaseline(keyArg);
3228
+ this.liveReasoningBaselines.set(keyArg, {
3229
+ update,
3230
+ textUtf8Bytes: browserTextEncoder.encode(textArg).byteLength,
3231
+ bytes,
3232
+ });
3233
+ this.liveMessageOverlayBytes += bytes;
3234
+ }
3235
+
3236
+ private retireLiveTextUpdateToBaseline(
3237
+ keyArg: string,
3238
+ updateArg: interfaces.IControllerTextUpdate,
3239
+ textArg: string,
3240
+ cursorArg: interfaces.IControllerMessageStreamCursor,
3241
+ ): void {
3242
+ const update = { ...updateArg, text: textArg, revision: cursorArg.revision };
3243
+ const bytes = this.liveMessageUpdateBytes(update);
3244
+ const existingBytes = this.liveTextStoredBytes.get(keyArg)
3245
+ ?? this.liveTextBaselines.get(keyArg)?.bytes
3246
+ ?? this.liveMessageUpdateBytes(updateArg);
3247
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) {
3248
+ this.deleteLiveTextUpdate(keyArg);
3249
+ this.deleteLiveTextBaseline(keyArg);
3250
+ return;
3251
+ }
3252
+ this.deleteLiveTextUpdate(keyArg);
3253
+ this.deleteLiveTextBaseline(keyArg);
3254
+ this.liveTextBaselines.set(keyArg, {
3255
+ update,
3256
+ textUtf8Bytes: browserTextEncoder.encode(textArg).byteLength,
3257
+ bytes,
3258
+ });
3259
+ this.liveMessageOverlayBytes += bytes;
3260
+ }
3261
+
3262
+ private storeLiveReasoningDeltaState(
3263
+ keyArg: string,
3264
+ updateArg: interfaces.IControllerReasoningUpdate,
3265
+ textUtf8BytesArg: number,
3266
+ deltaArg: string,
3267
+ ): boolean {
3268
+ const active = this.liveReasoningUpdates.get(keyArg);
3269
+ const baseline = this.liveReasoningBaselines.get(keyArg);
3270
+ const existingBytes = active
3271
+ ? this.liveReasoningStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(active)
3272
+ : baseline?.bytes ?? 0;
3273
+ const bytes = existingBytes + Math.max(
3274
+ browserTextEncoder.encode(deltaArg).byteLength,
3275
+ deltaArg.length * 6,
3276
+ );
3277
+ if (!active && !baseline) return false;
3278
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) return false;
3279
+ this.deleteLiveReasoningUpdate(keyArg);
3280
+ this.deleteLiveReasoningBaseline(keyArg);
3281
+ this.liveReasoningUpdates.set(keyArg, updateArg);
3282
+ this.liveReasoningTextUtf8Bytes.set(keyArg, textUtf8BytesArg);
3283
+ this.liveReasoningStoredBytes.set(keyArg, bytes);
3284
+ this.liveMessageOverlayBytes += bytes;
3285
+ return true;
3286
+ }
3287
+
3288
+ private storeLiveTextDeltaState(
3289
+ keyArg: string,
3290
+ updateArg: interfaces.IControllerTextUpdate,
3291
+ textUtf8BytesArg: number,
3292
+ deltaArg: string,
3293
+ ): boolean {
3294
+ const active = this.liveTextUpdates.get(keyArg);
3295
+ const baseline = this.liveTextBaselines.get(keyArg);
3296
+ const existingBytes = active
3297
+ ? this.liveTextStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(active)
3298
+ : baseline?.bytes ?? 0;
3299
+ const bytes = existingBytes + Math.max(
3300
+ browserTextEncoder.encode(deltaArg).byteLength,
3301
+ deltaArg.length * 6,
3302
+ );
3303
+ if (!active && !baseline) return false;
3304
+ if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) return false;
3305
+ this.deleteLiveTextUpdate(keyArg);
3306
+ this.deleteLiveTextBaseline(keyArg);
3307
+ this.liveTextUpdates.set(keyArg, updateArg);
3308
+ this.liveTextTextUtf8Bytes.set(keyArg, textUtf8BytesArg);
3309
+ this.liveTextStoredBytes.set(keyArg, bytes);
3310
+ this.liveMessageOverlayBytes += bytes;
3311
+ return true;
3312
+ }
3313
+
3314
+ private liveMessageUpdateKey(
3315
+ projectIdArg: string,
3316
+ updateArg: interfaces.IControllerReasoningUpdate
3317
+ | interfaces.IControllerTextUpdate
3318
+ | interfaces.IControllerReasoningDelta
3319
+ | interfaces.IControllerTextDelta,
3320
+ ): string {
3321
+ return `${projectIdArg}\0${controllerRuntimeIdToUiKey(updateArg.sessionId)}\0${controllerRuntimeIdToUiKey(updateArg.partId)}`;
3322
+ }
3323
+
3324
+ private blockLiveMessageDeltas(
3325
+ projectIdArg: string,
3326
+ sessionIdArg: TBrowserSessionId,
3327
+ streamEpochArg: number,
3328
+ refreshArg = true,
3329
+ ): void {
3330
+ const sessionKey = this.sessionOperationKey(projectIdArg, sessionIdArg);
3331
+ if (this.liveMessageDeltaBlocks.get(sessionKey)?.streamEpoch === streamEpochArg) return;
3332
+ this.liveMessageDeltaBlocks.set(sessionKey, {
3333
+ sessionId: sessionIdArg,
3334
+ streamEpoch: streamEpochArg,
3335
+ });
3336
+ if (
3337
+ refreshArg
3338
+ && projectIdArg === this.selectedProjectId
3339
+ && controllerRuntimeIdsEqual(sessionIdArg, this.selectedSessionId)
3340
+ ) void this.refreshSelectedSessionDetail();
3341
+ }
3342
+
3343
+ private liveMessageDeltasBlocked(
3344
+ projectIdArg: string,
3345
+ sessionIdArg: TBrowserSessionId,
3346
+ streamEpochArg: number,
3347
+ ): boolean {
3348
+ return this.liveMessageDeltaBlocks.get(
3349
+ this.sessionOperationKey(projectIdArg, sessionIdArg),
3350
+ )?.streamEpoch === streamEpochArg;
3351
+ }
3352
+
3040
3353
  private acceptLiveToolEpoch(
3041
3354
  harnessIdArg: TBrowserSessionHarnessId,
3042
3355
  epochArg: number,
@@ -3067,6 +3380,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3067
3380
  harnessIdArg: TBrowserSessionHarnessId,
3068
3381
  epochArg: number,
3069
3382
  handleBarrierArg = true,
3383
+ refreshSessionListArg = true,
3070
3384
  ): boolean {
3071
3385
  const currentEpoch = this.liveMessageStreamEpochs.get(harnessIdArg) ?? 0;
3072
3386
  if (epochArg < currentEpoch) return false;
@@ -3083,7 +3397,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3083
3397
  this.invalidateDetailEnrichment();
3084
3398
  void this.refreshSelectedSessionDetail();
3085
3399
  }
3086
- this.refreshSessionsNow();
3400
+ if (refreshSessionListArg) this.refreshSessionsNow();
3087
3401
  }
3088
3402
  }
3089
3403
  return true;
@@ -3153,6 +3467,20 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3153
3467
  updateArg: interfaces.IControllerReasoningUpdate,
3154
3468
  detailArg: IControllerSessionRenderDetail,
3155
3469
  ): boolean {
3470
+ const target = this.authoritativeReasoningTarget(updateArg, detailArg);
3471
+ return target !== undefined
3472
+ && (target.part.text === updateArg.text || target.part.text.startsWith(updateArg.text))
3473
+ && (
3474
+ updateArg.status === 'running'
3475
+ || target.part.endedAt !== undefined
3476
+ || target.message.streaming !== true
3477
+ );
3478
+ }
3479
+
3480
+ private authoritativeReasoningTarget(
3481
+ updateArg: interfaces.IControllerReasoningUpdate,
3482
+ detailArg: IControllerSessionRenderDetail,
3483
+ ): { message: interfaces.IControllerMessage; part: interfaces.IControllerReasoningPart } | undefined {
3156
3484
  const message = detailArg.messages.find((messageArg) => (
3157
3485
  messageArg.role === 'assistant'
3158
3486
  && (
@@ -3169,16 +3497,24 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3169
3497
  const part = message?.reasoning?.find((partArg) => (
3170
3498
  controllerRuntimeIdsEqual(partArg.id, updateArg.partId)
3171
3499
  ));
3172
- return part !== undefined
3173
- && (part.text === updateArg.text || part.text.startsWith(updateArg.text))
3174
- && (updateArg.status === 'running' || part.endedAt !== undefined || message?.streaming !== true);
3500
+ return message && part ? { message, part } : undefined;
3175
3501
  }
3176
3502
 
3177
3503
  private authoritativeTextUpdateCovers(
3178
3504
  updateArg: interfaces.IControllerTextUpdate,
3179
3505
  detailArg: IControllerSessionRenderDetail,
3180
3506
  ): boolean {
3181
- const message = detailArg.messages.find((messageArg) => (
3507
+ const message = this.authoritativeTextTarget(updateArg, detailArg);
3508
+ return message !== undefined
3509
+ && (message.text === updateArg.text || message.text.startsWith(updateArg.text))
3510
+ && (updateArg.status === 'running' || message.streaming !== true);
3511
+ }
3512
+
3513
+ private authoritativeTextTarget(
3514
+ updateArg: interfaces.IControllerTextUpdate,
3515
+ detailArg: IControllerSessionRenderDetail,
3516
+ ): interfaces.IControllerMessage | undefined {
3517
+ return detailArg.messages.find((messageArg) => (
3182
3518
  messageArg.role === 'assistant'
3183
3519
  && (
3184
3520
  controllerRuntimeIdsEqual(messageArg.id, updateArg.partId)
@@ -3189,9 +3525,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3189
3525
  )
3190
3526
  )
3191
3527
  ));
3192
- return message !== undefined
3193
- && (message.text === updateArg.text || message.text.startsWith(updateArg.text))
3194
- && (updateArg.status === 'running' || message.streaming !== true);
3195
3528
  }
3196
3529
 
3197
3530
  private liveToolSnapshotIsNewer(
@@ -3259,6 +3592,257 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3259
3592
  return this.authoritativeToolCallCovers(merged, toolCallArg) ? undefined : merged;
3260
3593
  }
3261
3594
 
3595
+ private ensureCanonicalChatProjection(
3596
+ detailArg: IControllerSessionRenderDetail,
3597
+ projectIdArg: string,
3598
+ sessionIdArg: TBrowserSessionId,
3599
+ ): ICanonicalChatProjection {
3600
+ const key = this.sessionOperationKey(projectIdArg, sessionIdArg);
3601
+ let projection = this.canonicalChatProjection;
3602
+ if (!projection || projection.key !== key) {
3603
+ projection = {
3604
+ key,
3605
+ sourceDetail: undefined,
3606
+ messages: [],
3607
+ messagesById: new Map(),
3608
+ messagesByOrder: new Map(),
3609
+ reasoningById: new Map(),
3610
+ };
3611
+ this.canonicalChatProjection = projection;
3612
+ this.canonicalChatProjectionDirty = true;
3613
+ }
3614
+ if (projection.sourceDetail === detailArg && !this.canonicalChatProjectionDirty) {
3615
+ return projection;
3616
+ }
3617
+
3618
+ const messages = detailArg.messages
3619
+ .filter((messageArg) => messageArg.toolCall?.name !== 'question')
3620
+ .map((messageArg) => this.toHarnessMessage(messageArg, projectIdArg, sessionIdArg));
3621
+ const visibleMessageIds = new Set(messages.map((message) => message.id));
3622
+ for (const prompt of detailArg.pendingPrompts) {
3623
+ const id = controllerRuntimeIdToUiKey(prompt.id);
3624
+ if (visibleMessageIds.has(id)) continue;
3625
+ visibleMessageIds.add(id);
3626
+ messages.push({
3627
+ id,
3628
+ role: 'user',
3629
+ text: prompt.text,
3630
+ createdAt: prompt.createdAt,
3631
+ markdown: false,
3632
+ attachments: prompt.attachments.map((attachment) => ({ ...attachment })),
3633
+ });
3634
+ }
3635
+ this.mergeLiveReasoningMessages(messages, projectIdArg, sessionIdArg);
3636
+ this.mergeLiveTextMessages(messages, projectIdArg, sessionIdArg);
3637
+ this.mergeLiveToolMessages(messages, projectIdArg, sessionIdArg);
3638
+ const structureChanged = this.canonicalHarnessStructureChanged(
3639
+ projection.messages,
3640
+ messages,
3641
+ );
3642
+ this.reconcileCanonicalHarnessMessages(projection, messages);
3643
+ this.canonicalTimelineRefreshPending ||= structureChanged;
3644
+ projection.sourceDetail = detailArg;
3645
+ this.canonicalChatProjectionDirty = false;
3646
+ return projection;
3647
+ }
3648
+
3649
+ private canonicalHarnessStructureChanged(
3650
+ currentArg: readonly plugins.deesCatalog.IHarnessMessage[],
3651
+ nextArg: readonly plugins.deesCatalog.IHarnessMessage[],
3652
+ ): boolean {
3653
+ return currentArg.length !== nextArg.length || currentArg.some((current, index) => {
3654
+ const next = nextArg[index];
3655
+ return !next
3656
+ || current.id !== next.id
3657
+ || current.role !== next.role
3658
+ || current.toolCall?.id !== next.toolCall?.id
3659
+ || current.toolCall?.name !== next.toolCall?.name
3660
+ || !this.liveMessageOrdersEqual(current.order, next.order);
3661
+ });
3662
+ }
3663
+
3664
+ private reconcileCanonicalHarnessMessages(
3665
+ projectionArg: ICanonicalChatProjection,
3666
+ nextMessagesArg: plugins.deesCatalog.IHarnessMessage[],
3667
+ ): void {
3668
+ const currentById = new Map(
3669
+ projectionArg.messages.map((message) => [message.id, message]),
3670
+ );
3671
+ const reconciled = nextMessagesArg.map((nextMessage) => {
3672
+ const current = currentById.get(nextMessage.id);
3673
+ if (!current) return nextMessage;
3674
+ if (!controllerJsonValuesEqual(current, nextMessage)) {
3675
+ this.canonicalMessageRefreshIds.add(current.id);
3676
+ }
3677
+ this.reconcileCanonicalHarnessMessage(current, nextMessage);
3678
+ return current;
3679
+ });
3680
+ projectionArg.messages.splice(0, projectionArg.messages.length, ...reconciled);
3681
+ this.rebuildCanonicalChatIndexes(projectionArg);
3682
+ }
3683
+
3684
+ private reconcileCanonicalHarnessMessage(
3685
+ targetArg: plugins.deesCatalog.IHarnessMessage,
3686
+ sourceArg: plugins.deesCatalog.IHarnessMessage,
3687
+ ): void {
3688
+ const targetReasoning = targetArg.reasoning;
3689
+ const sourceReasoning = sourceArg.reasoning;
3690
+ const targetToolCall = targetArg.toolCall;
3691
+ const sourceToolCall = sourceArg.toolCall;
3692
+ const target = targetArg as unknown as Record<string, unknown>;
3693
+ const source = sourceArg as unknown as Record<string, unknown>;
3694
+ for (const key of Object.keys(target)) {
3695
+ if (!(key in source)) delete target[key];
3696
+ }
3697
+ for (const [key, value] of Object.entries(source)) {
3698
+ if (key !== 'reasoning' && key !== 'toolCall') target[key] = value;
3699
+ }
3700
+
3701
+ if (sourceReasoning) {
3702
+ const currentById = new Map((targetReasoning ?? []).map((part) => [part.id, part]));
3703
+ const reasoning = sourceReasoning.map((sourcePart) => {
3704
+ const currentPart = currentById.get(sourcePart.id);
3705
+ if (!currentPart) return sourcePart;
3706
+ const current = currentPart as unknown as Record<string, unknown>;
3707
+ const next = sourcePart as unknown as Record<string, unknown>;
3708
+ for (const key of Object.keys(current)) {
3709
+ if (!(key in next)) delete current[key];
3710
+ }
3711
+ Object.assign(current, next);
3712
+ return currentPart;
3713
+ });
3714
+ if (targetReasoning) targetReasoning.splice(0, targetReasoning.length, ...reasoning);
3715
+ else targetArg.reasoning = reasoning;
3716
+ }
3717
+
3718
+ if (sourceToolCall) {
3719
+ if (targetToolCall?.id === sourceToolCall.id) {
3720
+ const current = targetToolCall as unknown as Record<string, unknown>;
3721
+ const next = sourceToolCall as unknown as Record<string, unknown>;
3722
+ for (const key of Object.keys(current)) {
3723
+ if (!(key in next)) delete current[key];
3724
+ }
3725
+ Object.assign(current, next);
3726
+ targetArg.toolCall = targetToolCall;
3727
+ } else {
3728
+ targetArg.toolCall = sourceToolCall;
3729
+ }
3730
+ }
3731
+ }
3732
+
3733
+ private rebuildCanonicalChatIndexes(projectionArg: ICanonicalChatProjection): void {
3734
+ projectionArg.messagesById.clear();
3735
+ projectionArg.messagesByOrder.clear();
3736
+ projectionArg.reasoningById.clear();
3737
+ for (const message of projectionArg.messages) {
3738
+ projectionArg.messagesById.set(message.id, message);
3739
+ if (message.order && !projectionArg.messagesByOrder.has(transcriptOrderKey(message.order))) {
3740
+ projectionArg.messagesByOrder.set(transcriptOrderKey(message.order), message);
3741
+ }
3742
+ for (const part of message.reasoning ?? []) {
3743
+ projectionArg.reasoningById.set(part.id, { message, part });
3744
+ }
3745
+ }
3746
+ }
3747
+
3748
+ private selectedCanonicalChatProjection(
3749
+ projectIdArg: string,
3750
+ sessionIdArg: TBrowserSessionId,
3751
+ ): ICanonicalChatProjection | undefined {
3752
+ const detail = this.sessionDetail;
3753
+ if (
3754
+ projectIdArg !== this.selectedProjectId
3755
+ || !detail
3756
+ || !controllerRuntimeIdsEqual(detail.session.id, sessionIdArg)
3757
+ || !controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg)
3758
+ ) return undefined;
3759
+ return this.ensureCanonicalChatProjection(detail, projectIdArg, sessionIdArg);
3760
+ }
3761
+
3762
+ private canonicalTextTarget(
3763
+ projectionArg: ICanonicalChatProjection,
3764
+ updateArg: interfaces.IControllerTextUpdate | interfaces.IControllerTextDelta,
3765
+ ): plugins.deesCatalog.IHarnessMessage | undefined {
3766
+ return projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.messageId))
3767
+ ?? projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.partId))
3768
+ ?? (updateArg.order
3769
+ ? projectionArg.messagesByOrder.get(transcriptOrderKey(updateArg.order))
3770
+ : undefined);
3771
+ }
3772
+
3773
+ private canonicalReasoningTarget(
3774
+ projectionArg: ICanonicalChatProjection,
3775
+ updateArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerReasoningDelta,
3776
+ ): ICanonicalReasoningTarget | undefined {
3777
+ const reasoningId = controllerRuntimeIdToUiKey(updateArg.partId);
3778
+ const partTarget = projectionArg.reasoningById.get(reasoningId);
3779
+ const message = projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.messageId))
3780
+ ?? partTarget?.message
3781
+ ?? (updateArg.order
3782
+ ? projectionArg.messagesByOrder.get(transcriptOrderKey(updateArg.order))
3783
+ : undefined);
3784
+ if (!message) return undefined;
3785
+ const part = partTarget?.message === message
3786
+ ? partTarget.part
3787
+ : message.reasoning?.find((candidate) => candidate.id === reasoningId);
3788
+ return part ? { message, part } : undefined;
3789
+ }
3790
+
3791
+ private mountedCanonicalChat(
3792
+ projectionArg: ICanonicalChatProjection,
3793
+ ): InstanceType<typeof plugins.deesCatalog.DeesHarnessChat> | undefined {
3794
+ const chat = this.shadowRoot?.querySelector('dees-harness-chat');
3795
+ return chat instanceof plugins.deesCatalog.DeesHarnessChat
3796
+ && chat.messages === projectionArg.messages
3797
+ && chat.transcriptKey === projectionArg.key
3798
+ ? chat
3799
+ : undefined;
3800
+ }
3801
+
3802
+ private applyCanonicalTextDelta(
3803
+ projectionArg: ICanonicalChatProjection,
3804
+ messageArg: plugins.deesCatalog.IHarnessMessage,
3805
+ deltaArg: string,
3806
+ ): boolean {
3807
+ const previousText = messageArg.text;
3808
+ const expectedText = `${previousText}${deltaArg}`;
3809
+ const chat = this.mountedCanonicalChat(projectionArg);
3810
+ chat?.applyDelta({ type: 'text', messageId: messageArg.id, delta: deltaArg });
3811
+ if (messageArg.text === previousText) {
3812
+ messageArg.text = expectedText;
3813
+ messageArg.streaming = true;
3814
+ }
3815
+ if (messageArg.text === expectedText) return true;
3816
+ messageArg.text = previousText;
3817
+ return false;
3818
+ }
3819
+
3820
+ private applyCanonicalReasoningDelta(
3821
+ projectionArg: ICanonicalChatProjection,
3822
+ targetArg: ICanonicalReasoningTarget,
3823
+ deltaArg: string,
3824
+ ): boolean {
3825
+ const previousText = targetArg.part.text;
3826
+ const expectedText = `${previousText}${deltaArg}`;
3827
+ const chat = this.mountedCanonicalChat(projectionArg);
3828
+ chat?.applyDelta({
3829
+ type: 'reasoning',
3830
+ messageId: targetArg.message.id,
3831
+ partId: targetArg.part.id,
3832
+ delta: deltaArg,
3833
+ });
3834
+ if (targetArg.part.text === previousText) targetArg.part.text = expectedText;
3835
+ if (targetArg.part.text === expectedText) return true;
3836
+ targetArg.part.text = previousText;
3837
+ return false;
3838
+ }
3839
+
3840
+ private requestCanonicalChatReconciliation(structureMayChangeArg = true): void {
3841
+ this.canonicalChatProjectionDirty = true;
3842
+ this.canonicalTimelineRefreshPending ||= structureMayChangeArg;
3843
+ this.requestUpdate();
3844
+ }
3845
+
3262
3846
  private applyLiveToolExecution(
3263
3847
  projectIdArg: string,
3264
3848
  executionArg: interfaces.IControllerToolExecution,
@@ -3332,24 +3916,22 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3332
3916
  }
3333
3917
  this.reconcileSubtaskPreviews(projectIdArg, executionArg.sessionId);
3334
3918
 
3335
- const chat = this.shadowRoot?.querySelector('dees-harness-chat');
3919
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, executionArg.sessionId);
3920
+ if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate();
3336
3921
  const messageId = controllerRuntimeIdToUiKey(execution.partId);
3337
3922
  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)!;
3923
+ const message = projection?.messagesById.get(messageId);
3924
+ if (projection && message) {
3343
3925
  message.toolCall = harnessToolCall;
3344
3926
  message.streaming = execution.status === 'pending' || execution.status === 'running';
3345
3927
  message.updatedAt = execution.finishedAt ?? execution.sourceUpdatedAt;
3346
- chat.applyDelta({
3928
+ this.mountedCanonicalChat(projection)?.applyDelta({
3347
3929
  type: 'tool-update',
3348
3930
  messageId,
3349
3931
  tool: harnessToolCall,
3350
3932
  });
3351
3933
  } else {
3352
- this.requestUpdate();
3934
+ this.requestCanonicalChatReconciliation();
3353
3935
  }
3354
3936
  }
3355
3937
 
@@ -3388,23 +3970,59 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3388
3970
  if (cursorArg.streamEpoch < (this.liveMessageStreamEpochs.get(harnessId) ?? 0)) return false;
3389
3971
  if (!this.acceptLiveMessageEpoch(harnessId, cursorArg.streamEpoch, false)) return false;
3390
3972
  const sessionKey = this.liveToolSessionKey(projectIdArg, sessionIdArg);
3973
+ const deltaBlockKey = this.sessionOperationKey(projectIdArg, sessionIdArg);
3974
+ const recoveringDeltaGap = this.liveMessageDeltaBlocks.get(deltaBlockKey)?.streamEpoch
3975
+ === cursorArg.streamEpoch;
3391
3976
  this.liveMessageHydrationFloors.set(sessionKey, { sessionId: sessionIdArg, cursor: cursorArg });
3392
3977
  for (const [key, update] of this.liveReasoningUpdates) {
3978
+ const target = this.authoritativeReasoningTarget(update, detailArg);
3393
3979
  if (
3394
3980
  key.startsWith(`${projectIdArg}\0`)
3395
3981
  && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
3396
3982
  && this.hydrationCursorCovers(cursorArg, update)
3397
- && this.authoritativeReasoningUpdateCovers(update, detailArg)
3398
- ) this.deleteLiveReasoningUpdate(key);
3983
+ && target !== undefined
3984
+ && (recoveringDeltaGap || this.authoritativeReasoningUpdateCovers(update, detailArg))
3985
+ ) this.retireLiveReasoningUpdateToBaseline(key, update, target.part.text, cursorArg);
3399
3986
  }
3400
3987
  for (const [key, update] of this.liveTextUpdates) {
3988
+ const target = this.authoritativeTextTarget(update, detailArg);
3401
3989
  if (
3402
3990
  key.startsWith(`${projectIdArg}\0`)
3403
3991
  && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
3404
3992
  && this.hydrationCursorCovers(cursorArg, update)
3405
- && this.authoritativeTextUpdateCovers(update, detailArg)
3406
- ) this.deleteLiveTextUpdate(key);
3993
+ && target !== undefined
3994
+ && (recoveringDeltaGap || this.authoritativeTextUpdateCovers(update, detailArg))
3995
+ ) this.retireLiveTextUpdateToBaseline(key, update, target.text, cursorArg);
3996
+ }
3997
+ for (const [key, baseline] of [...this.liveReasoningBaselines]) {
3998
+ const update = baseline.update;
3999
+ if (
4000
+ !key.startsWith(`${projectIdArg}\0`)
4001
+ || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
4002
+ || !this.hydrationCursorCovers(cursorArg, update)
4003
+ ) continue;
4004
+ const target = this.authoritativeReasoningTarget(update, detailArg);
4005
+ if (target) {
4006
+ this.retireLiveReasoningUpdateToBaseline(key, update, target.part.text, cursorArg);
4007
+ } else if (recoveringDeltaGap) {
4008
+ this.deleteLiveReasoningBaseline(key);
4009
+ }
4010
+ }
4011
+ for (const [key, baseline] of [...this.liveTextBaselines]) {
4012
+ const update = baseline.update;
4013
+ if (
4014
+ !key.startsWith(`${projectIdArg}\0`)
4015
+ || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)
4016
+ || !this.hydrationCursorCovers(cursorArg, update)
4017
+ ) continue;
4018
+ const target = this.authoritativeTextTarget(update, detailArg);
4019
+ if (target) {
4020
+ this.retireLiveTextUpdateToBaseline(key, update, target.text, cursorArg);
4021
+ } else if (recoveringDeltaGap) {
4022
+ this.deleteLiveTextBaseline(key);
4023
+ }
3407
4024
  }
4025
+ this.liveMessageDeltaBlocks.delete(deltaBlockKey);
3408
4026
  return true;
3409
4027
  }
3410
4028
 
@@ -3448,6 +4066,30 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3448
4066
  || !controllerRuntimeIdsEqual(update.sessionId, sessionId)
3449
4067
  ) this.deleteLiveTextUpdate(key);
3450
4068
  }
4069
+ for (const [key, baseline] of this.liveReasoningBaselines) {
4070
+ if (
4071
+ !projectId
4072
+ || !isSessionRuntimeId(sessionId)
4073
+ || !key.startsWith(`${projectId}\0`)
4074
+ || !controllerRuntimeIdsEqual(baseline.update.sessionId, sessionId)
4075
+ ) this.deleteLiveReasoningBaseline(key);
4076
+ }
4077
+ for (const [key, baseline] of this.liveTextBaselines) {
4078
+ if (
4079
+ !projectId
4080
+ || !isSessionRuntimeId(sessionId)
4081
+ || !key.startsWith(`${projectId}\0`)
4082
+ || !controllerRuntimeIdsEqual(baseline.update.sessionId, sessionId)
4083
+ ) this.deleteLiveTextBaseline(key);
4084
+ }
4085
+ for (const [key, block] of this.liveMessageDeltaBlocks) {
4086
+ if (
4087
+ !projectId
4088
+ || !isSessionRuntimeId(sessionId)
4089
+ || key !== this.sessionOperationKey(projectId, sessionId)
4090
+ || !controllerRuntimeIdsEqual(block.sessionId, sessionId)
4091
+ ) this.liveMessageDeltaBlocks.delete(key);
4092
+ }
3451
4093
  }
3452
4094
 
3453
4095
  private subtaskPreviewKey(
@@ -3667,7 +4309,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3667
4309
  if (isBusy) this.queueSubtaskPreviewHydration(entry);
3668
4310
  changed = true;
3669
4311
  }
3670
- if (changed) this.requestUpdate();
4312
+ if (changed) this.requestCanonicalChatReconciliation();
3671
4313
  }
3672
4314
 
3673
4315
  private queueSubtaskPreviewHydration(entryArg: ISubtaskPreviewEntry): void {
@@ -3795,7 +4437,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3795
4437
  messages,
3796
4438
  ...((page.truncated === true || page.nextCursor !== undefined) ? { truncated: true } : {}),
3797
4439
  };
3798
- this.requestUpdate();
4440
+ this.requestCanonicalChatReconciliation();
3799
4441
  } catch {
3800
4442
  if (abortControllerArg.signal.aborted) return;
3801
4443
  if (!this.subtaskPreviewEntryIsCurrent(entryArg)) return;
@@ -3807,7 +4449,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3807
4449
  status: entryArg.stream.status,
3808
4450
  notice: subtaskPreviewUnavailableNotice,
3809
4451
  };
3810
- this.requestUpdate();
4452
+ this.requestCanonicalChatReconciliation();
3811
4453
  }
3812
4454
  }
3813
4455
 
@@ -3840,7 +4482,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3840
4482
  this.queueSubtaskPreviewHydration(entry);
3841
4483
  }, subtaskPreviewRefreshDelayMs);
3842
4484
  }
3843
- if (changed) this.requestUpdate();
4485
+ if (changed) this.requestCanonicalChatReconciliation();
3844
4486
  }
3845
4487
 
3846
4488
  private clearSubtaskPreviewState(): void {
@@ -3851,6 +4493,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
3851
4493
  }
3852
4494
  this.subtaskPreviewEntries.clear();
3853
4495
  this.subtaskPreviewHydrationQueue = [];
4496
+ this.canonicalChatProjectionDirty = true;
3854
4497
  }
3855
4498
 
3856
4499
  private applySessionEventState(eventArg: interfaces.IControllerEvent): void {
@@ -4001,7 +4644,9 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4001
4644
  && this.socketClient.isConnected
4002
4645
  ) {
4003
4646
  this.refreshPending = false;
4004
- await this.performSessionRefresh();
4647
+ const includeRelated = this.refreshRelatedPending;
4648
+ this.refreshRelatedPending = false;
4649
+ await this.performSessionRefresh(includeRelated);
4005
4650
  }
4006
4651
  } finally {
4007
4652
  this.refreshInFlight = undefined;
@@ -4041,7 +4686,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4041
4686
  }
4042
4687
  }
4043
4688
 
4044
- private async performSessionRefresh(): Promise<void> {
4689
+ private async performSessionRefresh(includeRelatedArg = true): Promise<void> {
4045
4690
  const projectId = this.selectedProjectId;
4046
4691
  if (!isNonEmptyString(projectId)) {
4047
4692
  this.sessions = [];
@@ -4062,10 +4707,14 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4062
4707
  && controllerRuntimeIdsEqual(sessionArg.id, selectedSessionAtStart)
4063
4708
  && sessionArg.archivedAt !== undefined
4064
4709
  ));
4065
- if (selectedSessionAtStart) void this.refreshSelectedSessionDetail(false);
4066
- void this.loadSessionGroups();
4067
- void this.loadTerminals();
4068
- void this.loadResources();
4710
+ if (includeRelatedArg && selectedSessionAtStart) {
4711
+ void this.refreshSelectedSessionDetail(false);
4712
+ }
4713
+ if (includeRelatedArg) {
4714
+ void this.loadSessionGroups();
4715
+ void this.loadTerminals();
4716
+ void this.loadResources();
4717
+ }
4069
4718
  // The loading state is for a list we cannot show yet; background refreshes
4070
4719
  // must not flip it, or the sidebar flashes on every pushed event.
4071
4720
  const initialLoad = this.sessionsLoadedForProjectId !== projectId;
@@ -4091,11 +4740,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4091
4740
  ? response.sessions.filter(hasSessionRuntimeId)
4092
4741
  : [];
4093
4742
  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
- }
4743
+ const reconciledSessions = reconcileKeyedJsonArray(
4744
+ this.sessions,
4745
+ nextSessions,
4746
+ (session) => controllerRuntimeIdToUiKey(session.id),
4747
+ );
4748
+ if (reconciledSessions !== this.sessions) this.sessions = reconciledSessions;
4099
4749
  const selectedSessionBecameArchived = selectedSessionAtStart !== undefined
4100
4750
  && !selectedSessionWasArchived
4101
4751
  && this.sessions.some((sessionArg) => (
@@ -4171,6 +4821,107 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4171
4821
  }
4172
4822
  }
4173
4823
 
4824
+ private reconcileControllerMessages(
4825
+ currentArg: readonly interfaces.IControllerMessage[],
4826
+ nextArg: readonly interfaces.IControllerMessage[],
4827
+ ): interfaces.IControllerMessage[] {
4828
+ return reconcileKeyedJsonArray(
4829
+ currentArg,
4830
+ nextArg,
4831
+ (message) => controllerRuntimeIdToUiKey(message.id),
4832
+ );
4833
+ }
4834
+
4835
+ private reconcileControllerMessageBundles(
4836
+ currentArg: readonly interfaces.IControllerMessageBundle[],
4837
+ nextArg: readonly interfaces.IControllerMessageBundle[],
4838
+ ): interfaces.IControllerMessageBundle[] {
4839
+ const currentByKey = new Map(currentArg.map((bundle) => [
4840
+ controllerRuntimeIdToUiKey(bundle.sourceMessageId),
4841
+ bundle,
4842
+ ]));
4843
+ let unchanged = currentArg.length === nextArg.length;
4844
+ const bundles = nextArg.map((nextBundle, index) => {
4845
+ const current = currentByKey.get(controllerRuntimeIdToUiKey(nextBundle.sourceMessageId));
4846
+ if (!current) {
4847
+ unchanged = false;
4848
+ return nextBundle;
4849
+ }
4850
+ const messages = this.reconcileControllerMessages(current.messages, nextBundle.messages);
4851
+ const bundle = current.structuralDigest === nextBundle.structuralDigest
4852
+ && messages === current.messages
4853
+ && controllerRuntimeIdsEqual(current.sourceMessageId, nextBundle.sourceMessageId)
4854
+ ? current
4855
+ : { ...nextBundle, messages };
4856
+ unchanged &&= bundle === currentArg[index];
4857
+ return bundle;
4858
+ });
4859
+ return unchanged ? currentArg as interfaces.IControllerMessageBundle[] : bundles;
4860
+ }
4861
+
4862
+ private reconcileSessionRenderDetail(
4863
+ currentArg: IControllerSessionRenderDetail | undefined,
4864
+ nextArg: IControllerSessionRenderDetail,
4865
+ ): IControllerSessionRenderDetail {
4866
+ if (!currentArg || !controllerRuntimeIdsEqual(currentArg.session.id, nextArg.session.id)) {
4867
+ return nextArg;
4868
+ }
4869
+ const reuse = <TValue>(currentValueArg: TValue, nextValueArg: TValue): TValue => (
4870
+ controllerJsonValuesEqual(currentValueArg, nextValueArg) ? currentValueArg : nextValueArg
4871
+ );
4872
+ const messages = this.reconcileControllerMessages(currentArg.messages, nextArg.messages);
4873
+ const pendingPrompts = reconcileKeyedJsonArray(
4874
+ currentArg.pendingPrompts,
4875
+ nextArg.pendingPrompts,
4876
+ (prompt) => controllerRuntimeIdToUiKey(prompt.id),
4877
+ );
4878
+ const permissions = reconcileKeyedJsonArray(
4879
+ currentArg.permissions,
4880
+ nextArg.permissions,
4881
+ (permission) => controllerRuntimeIdToUiKey(permission.id),
4882
+ );
4883
+ const questions = reconcileKeyedJsonArray(
4884
+ currentArg.questions,
4885
+ nextArg.questions,
4886
+ (question) => controllerRuntimeIdToUiKey(question.id),
4887
+ );
4888
+ const todos = reconcileKeyedJsonArray(
4889
+ currentArg.todos,
4890
+ nextArg.todos,
4891
+ (todo, index) => todo.id ? controllerRuntimeIdToUiKey(todo.id) : `index:${index}`,
4892
+ );
4893
+ const intelligenceExchanges = reconcileKeyedJsonArray(
4894
+ currentArg.intelligenceExchanges,
4895
+ nextArg.intelligenceExchanges,
4896
+ (exchange) => exchange.id,
4897
+ );
4898
+ const detail: IControllerSessionRenderDetail = {
4899
+ ...nextArg,
4900
+ session: reuse(currentArg.session, nextArg.session),
4901
+ messages,
4902
+ pendingPrompts,
4903
+ permissions,
4904
+ questions,
4905
+ todos,
4906
+ scratchpad: reuse(currentArg.scratchpad, nextArg.scratchpad),
4907
+ intelligenceExchanges,
4908
+ sessionMetrics: reuse(currentArg.sessionMetrics, nextArg.sessionMetrics),
4909
+ toolStreamCursor: reuse(currentArg.toolStreamCursor, nextArg.toolStreamCursor),
4910
+ messageStreamCursor: reuse(currentArg.messageStreamCursor, nextArg.messageStreamCursor),
4911
+ ...(nextArg.modelChoice === undefined
4912
+ ? {}
4913
+ : { modelChoice: reuse(currentArg.modelChoice, nextArg.modelChoice) }),
4914
+ };
4915
+ const current = currentArg as unknown as Record<string, unknown>;
4916
+ const reconciled = detail as unknown as Record<string, unknown>;
4917
+ const currentKeys = Object.keys(current);
4918
+ const nextKeys = Object.keys(reconciled);
4919
+ return currentKeys.length === nextKeys.length
4920
+ && nextKeys.every((key) => Object.is(current[key], reconciled[key]))
4921
+ ? currentArg
4922
+ : detail;
4923
+ }
4924
+
4174
4925
  private async loadSessionDetail(
4175
4926
  sessionIdArg: TBrowserSessionId,
4176
4927
  restartEnrichmentArg = true,
@@ -4240,9 +4991,14 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4240
4991
  const historyBecameTerminal = sameSession
4241
4992
  && this.detailHistoryCursor !== undefined
4242
4993
  && response.messagePage.nextCursor === undefined;
4994
+ const sessionBecameSettled = sameSession
4995
+ && (currentDetail!.session.status === 'busy' || currentDetail!.session.status === 'retry')
4996
+ && response.session.status !== 'busy'
4997
+ && response.session.status !== 'retry';
4243
4998
  const restartEnrichment = restartEnrichmentArg
4244
4999
  || coreBoundaryChanged
4245
5000
  || historyBecameTerminal
5001
+ || sessionBecameSettled
4246
5002
  || toolEpochAdvanced
4247
5003
  || messageEpochAdvanced;
4248
5004
  const nextCoreBundleKeySet = new Set(nextCoreBundleKeys);
@@ -4279,6 +5035,10 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4279
5035
  ));
4280
5036
  provisionalBundleKeys.clear();
4281
5037
  }
5038
+ accumulated.bundles = this.reconcileControllerMessageBundles(
5039
+ this.detailBundles,
5040
+ accumulated.bundles,
5041
+ );
4282
5042
  const { messagePage, ...coreDetail } = response;
4283
5043
  const renderDetail: IControllerSessionRenderDetail = {
4284
5044
  ...coreDetail,
@@ -4351,9 +5111,9 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4351
5111
  unavailable: this.detailHistoryLimits.unavailable,
4352
5112
  };
4353
5113
  }
4354
- if (JSON.stringify(renderDetail) !== JSON.stringify(this.sessionDetail)) {
4355
- this.sessionDetail = renderDetail;
4356
- }
5114
+ const reconciledDetail = this.reconcileSessionRenderDetail(this.sessionDetail, renderDetail);
5115
+ if (reconciledDetail !== this.sessionDetail) this.sessionDetail = reconciledDetail;
5116
+ this.ensureCanonicalChatProjection(reconciledDetail, projectId, sessionIdArg);
4357
5117
  this.reconcileSubtaskPreviews(projectId, sessionIdArg);
4358
5118
  // A draft that materialized keeps its placeholder view until this first
4359
5119
  // real detail arrives; now the actual chat takes over.
@@ -4417,6 +5177,10 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4417
5177
  this.detailHistoryOwnership = undefined;
4418
5178
  this.detailHistoryLimits = emptyDetailHistoryLimits();
4419
5179
  this.detailHistoryStatus = 'idle';
5180
+ this.canonicalChatProjection = undefined;
5181
+ this.canonicalChatProjectionDirty = false;
5182
+ this.canonicalTimelineRefreshPending = false;
5183
+ this.canonicalMessageRefreshIds.clear();
4420
5184
  }
4421
5185
 
4422
5186
  private invalidateDetailEnrichment(): void {
@@ -4546,14 +5310,19 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
4546
5310
  (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId),
4547
5311
  );
4548
5312
  if (provisionalContentChanged || !sameStringArray(previousKeys, nextKeys)) {
4549
- this.detailBundles = bounded.bundles;
5313
+ this.detailBundles = this.reconcileControllerMessageBundles(
5314
+ this.detailBundles,
5315
+ bounded.bundles,
5316
+ );
4550
5317
  this.detailBundleKeys = new Set(nextKeys);
4551
5318
  const detail = this.sessionDetail;
4552
5319
  if (!detail || !controllerRuntimeIdsEqual(detail.session.id, ownership.sessionId)) return;
4553
- this.sessionDetail = {
5320
+ const nextDetail = this.reconcileSessionRenderDetail(detail, {
4554
5321
  ...detail,
4555
- messages: bounded.bundles.flatMap((bundle) => bundle.messages),
4556
- };
5322
+ messages: this.detailBundles.flatMap((bundle) => bundle.messages),
5323
+ });
5324
+ if (nextDetail !== detail) this.sessionDetail = nextDetail;
5325
+ this.ensureCanonicalChatProjection(nextDetail, ownership.projectId, ownership.sessionId);
4557
5326
  }
4558
5327
  this.detailHistoryCursor = nextCursor;
4559
5328
  if (nextCursor !== undefined) this.detailHistorySeenCursors.add(nextCursor);
@@ -6702,9 +7471,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
6702
7471
  const terminals = Array.isArray(response.terminals)
6703
7472
  ? response.terminals.filter(hasTerminalRuntimeId)
6704
7473
  : [];
6705
- if (JSON.stringify(terminals) !== JSON.stringify(this.terminals)) {
6706
- this.terminals = terminals;
6707
- }
7474
+ const reconciledTerminals = reconcileKeyedJsonArray(
7475
+ this.terminals,
7476
+ terminals,
7477
+ (terminal) => controllerRuntimeIdToUiKey(terminal.id),
7478
+ );
7479
+ if (reconciledTerminals !== this.terminals) this.terminals = reconciledTerminals;
6708
7480
  this.terminalsLoadedForProjectId = projectId;
6709
7481
  if (
6710
7482
  this.selectedTerminalId !== undefined
@@ -6976,6 +7748,17 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
6976
7748
  public updated(changedPropertiesArg: Map<PropertyKey, unknown>): void {
6977
7749
  super.updated(changedPropertiesArg);
6978
7750
  this.flushComposerFocus();
7751
+ const projection = this.canonicalChatProjection;
7752
+ const chat = projection ? this.mountedCanonicalChat(projection) : undefined;
7753
+ if (this.canonicalTimelineRefreshPending) {
7754
+ this.canonicalTimelineRefreshPending = false;
7755
+ this.canonicalMessageRefreshIds.clear();
7756
+ chat?.refreshMessages();
7757
+ } else if (this.canonicalMessageRefreshIds.size > 0) {
7758
+ const messageIds = [...this.canonicalMessageRefreshIds];
7759
+ this.canonicalMessageRefreshIds.clear();
7760
+ chat?.refreshMessages(messageIds);
7761
+ }
6979
7762
  if (
6980
7763
  changedPropertiesArg.has('selectedProjectId')
6981
7764
  || changedPropertiesArg.has('selectedSessionId')
@@ -7928,6 +8711,417 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
7928
8711
  }
7929
8712
  }
7930
8713
 
8714
+ private liveMessageOrdersEqual(
8715
+ leftArg: interfaces.IControllerTranscriptOrder | undefined,
8716
+ rightArg: interfaces.IControllerTranscriptOrder | undefined,
8717
+ ): boolean {
8718
+ return leftArg === undefined
8719
+ ? rightArg === undefined
8720
+ : rightArg !== undefined
8721
+ && leftArg.messageIndex === rightArg.messageIndex
8722
+ && leftArg.partIndex === rightArg.partIndex;
8723
+ }
8724
+
8725
+ private liveMessageIdentityMatches(
8726
+ previousArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate,
8727
+ deltaArg: interfaces.IControllerReasoningDelta | interfaces.IControllerTextDelta,
8728
+ ): boolean {
8729
+ return controllerRuntimeIdsEqual(previousArg.sessionId, deltaArg.sessionId)
8730
+ && controllerRuntimeIdsEqual(previousArg.messageId, deltaArg.messageId)
8731
+ && controllerRuntimeIdsEqual(previousArg.partId, deltaArg.partId)
8732
+ && this.liveMessageOrdersEqual(previousArg.order, deltaArg.order);
8733
+ }
8734
+
8735
+ private liveMessageEventPassesBarriers(
8736
+ projectIdArg: string,
8737
+ updateArg: interfaces.IControllerReasoningUpdate
8738
+ | interfaces.IControllerTextUpdate
8739
+ | interfaces.IControllerReasoningDelta
8740
+ | interfaces.IControllerTextDelta,
8741
+ ): { accepted: boolean; epochAdvanced: boolean } {
8742
+ if (!isSessionRuntimeId(updateArg.sessionId)) {
8743
+ return { accepted: false, epochAdvanced: false };
8744
+ }
8745
+ if (
8746
+ updateArg.sourceUpdatedAt
8747
+ <= (this.liveHistoryBarriers.get(
8748
+ this.liveToolSessionKey(projectIdArg, updateArg.sessionId),
8749
+ ) ?? Number.NEGATIVE_INFINITY)
8750
+ ) return { accepted: false, epochAdvanced: false };
8751
+ const previousEpoch = this.liveMessageStreamEpochs.get(updateArg.sessionId.harnessId) ?? 0;
8752
+ const accepted = this.acceptLiveMessageEpoch(
8753
+ updateArg.sessionId.harnessId,
8754
+ updateArg.streamEpoch,
8755
+ true,
8756
+ false,
8757
+ );
8758
+ return { accepted, epochAdvanced: accepted && updateArg.streamEpoch > previousEpoch };
8759
+ }
8760
+
8761
+ private refreshAfterTerminalLiveMessage(
8762
+ projectIdArg: string,
8763
+ sessionIdArg: TBrowserSessionId,
8764
+ ): void {
8765
+ if (
8766
+ projectIdArg !== this.selectedProjectId
8767
+ || !controllerRuntimeIdsEqual(sessionIdArg, this.selectedSessionId)
8768
+ ) return;
8769
+ void this.refreshSelectedSessionDetail();
8770
+ void this.refreshSessions(false);
8771
+ }
8772
+
8773
+ private applyLiveReasoningUpdate(
8774
+ projectIdArg: string,
8775
+ updateArg: interfaces.IControllerReasoningUpdate,
8776
+ ): void {
8777
+ if (!isSessionRuntimeId(updateArg.sessionId)) return;
8778
+ const sessionId = updateArg.sessionId;
8779
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, updateArg);
8780
+ if (!barrier.accepted) return;
8781
+ if (controllerRuntimeIdsEqual(updateArg.sessionId, this.subagentModalSessionId)) {
8782
+ this.scheduleSubagentModalRefresh();
8783
+ }
8784
+ if (
8785
+ projectIdArg !== this.selectedProjectId
8786
+ || !controllerRuntimeIdsEqual(updateArg.sessionId, this.selectedSessionId)
8787
+ ) return;
8788
+ const floor = this.liveMessageHydrationFloors.get(
8789
+ this.liveToolSessionKey(projectIdArg, updateArg.sessionId),
8790
+ )?.cursor;
8791
+ if (
8792
+ floor
8793
+ && floor.streamEpoch === updateArg.streamEpoch
8794
+ && updateArg.revision <= floor.revision
8795
+ ) return;
8796
+ const key = this.liveMessageUpdateKey(projectIdArg, updateArg);
8797
+ const previous = this.liveReasoningUpdates.get(key)
8798
+ ?? this.liveReasoningBaselines.get(key)?.update;
8799
+ if (
8800
+ previous
8801
+ && previous.streamEpoch === updateArg.streamEpoch
8802
+ && updateArg.revision <= previous.revision
8803
+ ) return;
8804
+ if (!this.storeLiveReasoningUpdate(key, updateArg)) {
8805
+ this.blockLiveMessageDeltas(
8806
+ projectIdArg,
8807
+ sessionId,
8808
+ updateArg.streamEpoch,
8809
+ !barrier.epochAdvanced,
8810
+ );
8811
+ return;
8812
+ }
8813
+
8814
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
8815
+ if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate();
8816
+ const target = projection ? this.canonicalReasoningTarget(projection, updateArg) : undefined;
8817
+ if (!projection || !target) {
8818
+ this.requestCanonicalChatReconciliation();
8819
+ } else if (!this.harnessMessageIsTerminal(target.message)) {
8820
+ const previousStreaming = target.message.streaming === true;
8821
+ const previousText = target.part.text;
8822
+ let correction = !updateArg.text.startsWith(previousText);
8823
+ if (!correction && updateArg.text.length > previousText.length) {
8824
+ correction = !this.applyCanonicalReasoningDelta(
8825
+ projection,
8826
+ target,
8827
+ updateArg.text.slice(previousText.length),
8828
+ );
8829
+ }
8830
+ if (correction) target.part.text = updateArg.text;
8831
+ target.part.startedAt = updateArg.sourceUpdatedAt;
8832
+ if (updateArg.status === 'running') delete target.part.endedAt;
8833
+ target.message.streaming = updateArg.status === 'running'
8834
+ || (updateArg.sessionId.harnessId === 'opencode' && previousStreaming);
8835
+ if (
8836
+ !correction
8837
+ && updateArg.status === 'running'
8838
+ && !previousStreaming
8839
+ && updateArg.text === previousText
8840
+ ) {
8841
+ this.mountedCanonicalChat(projection)?.applyDelta({
8842
+ type: 'reasoning',
8843
+ messageId: target.message.id,
8844
+ partId: target.part.id,
8845
+ delta: '',
8846
+ });
8847
+ }
8848
+ if (updateArg.status !== 'running') {
8849
+ this.mountedCanonicalChat(projection)?.applyDelta({
8850
+ type: 'reasoning-end',
8851
+ messageId: target.message.id,
8852
+ partId: target.part.id,
8853
+ endedAt: updateArg.sourceUpdatedAt,
8854
+ });
8855
+ target.part.endedAt = updateArg.sourceUpdatedAt;
8856
+ }
8857
+ if (correction) {
8858
+ this.canonicalMessageRefreshIds.add(target.message.id);
8859
+ this.requestCanonicalChatReconciliation(false);
8860
+ }
8861
+ }
8862
+ if (updateArg.status !== 'running') {
8863
+ this.refreshAfterTerminalLiveMessage(projectIdArg, sessionId);
8864
+ }
8865
+ }
8866
+
8867
+ private applyLiveTextUpdate(
8868
+ projectIdArg: string,
8869
+ updateArg: interfaces.IControllerTextUpdate,
8870
+ ): void {
8871
+ if (!isSessionRuntimeId(updateArg.sessionId)) return;
8872
+ const sessionId = updateArg.sessionId;
8873
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, updateArg);
8874
+ if (!barrier.accepted) return;
8875
+ if (controllerRuntimeIdsEqual(updateArg.sessionId, this.subagentModalSessionId)) {
8876
+ this.scheduleSubagentModalRefresh();
8877
+ }
8878
+ if (
8879
+ projectIdArg !== this.selectedProjectId
8880
+ || !controllerRuntimeIdsEqual(updateArg.sessionId, this.selectedSessionId)
8881
+ ) return;
8882
+ const floor = this.liveMessageHydrationFloors.get(
8883
+ this.liveToolSessionKey(projectIdArg, updateArg.sessionId),
8884
+ )?.cursor;
8885
+ if (
8886
+ floor
8887
+ && floor.streamEpoch === updateArg.streamEpoch
8888
+ && updateArg.revision <= floor.revision
8889
+ ) return;
8890
+ const key = this.liveMessageUpdateKey(projectIdArg, updateArg);
8891
+ const previous = this.liveTextUpdates.get(key) ?? this.liveTextBaselines.get(key)?.update;
8892
+ if (
8893
+ previous
8894
+ && previous.streamEpoch === updateArg.streamEpoch
8895
+ && updateArg.revision <= previous.revision
8896
+ ) return;
8897
+ if (!this.storeLiveTextUpdate(key, updateArg)) {
8898
+ this.blockLiveMessageDeltas(
8899
+ projectIdArg,
8900
+ sessionId,
8901
+ updateArg.streamEpoch,
8902
+ !barrier.epochAdvanced,
8903
+ );
8904
+ return;
8905
+ }
8906
+
8907
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
8908
+ if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate();
8909
+ const target = projection ? this.canonicalTextTarget(projection, updateArg) : undefined;
8910
+ if (!projection || !target) {
8911
+ this.requestCanonicalChatReconciliation();
8912
+ } else if (!this.harnessMessageIsTerminal(target)) {
8913
+ const previousStreaming = target.streaming === true;
8914
+ const previousText = target.text;
8915
+ let correction = !updateArg.text.startsWith(previousText);
8916
+ if (!correction && updateArg.text.length > previousText.length) {
8917
+ correction = !this.applyCanonicalTextDelta(
8918
+ projection,
8919
+ target,
8920
+ updateArg.text.slice(previousText.length),
8921
+ );
8922
+ }
8923
+ if (correction) target.text = updateArg.text;
8924
+ if (updateArg.status === 'running') {
8925
+ target.streaming = true;
8926
+ if (!correction && !previousStreaming && updateArg.text === previousText) {
8927
+ this.mountedCanonicalChat(projection)?.applyDelta({
8928
+ type: 'text',
8929
+ messageId: target.id,
8930
+ delta: '',
8931
+ });
8932
+ }
8933
+ } else {
8934
+ this.mountedCanonicalChat(projection)?.applyDelta({
8935
+ type: 'message-end',
8936
+ messageId: target.id,
8937
+ });
8938
+ target.streaming = false;
8939
+ target.updatedAt = updateArg.sourceUpdatedAt;
8940
+ }
8941
+ if (correction) {
8942
+ this.canonicalMessageRefreshIds.add(target.id);
8943
+ this.requestCanonicalChatReconciliation(false);
8944
+ }
8945
+ }
8946
+ if (updateArg.status === 'completed') {
8947
+ this.refreshAfterTerminalLiveMessage(projectIdArg, sessionId);
8948
+ }
8949
+ }
8950
+
8951
+ private applyLiveReasoningDelta(
8952
+ projectIdArg: string,
8953
+ deltaArg: interfaces.IControllerReasoningDelta,
8954
+ ): void {
8955
+ if (!isSessionRuntimeId(deltaArg.sessionId)) return;
8956
+ const sessionId = deltaArg.sessionId;
8957
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, deltaArg);
8958
+ if (!barrier.accepted) return;
8959
+ if (
8960
+ projectIdArg !== this.selectedProjectId
8961
+ || !controllerRuntimeIdsEqual(deltaArg.sessionId, this.selectedSessionId)
8962
+ || this.liveMessageDeltasBlocked(projectIdArg, sessionId, deltaArg.streamEpoch)
8963
+ ) return;
8964
+ const floor = this.liveMessageHydrationFloors.get(
8965
+ this.liveToolSessionKey(projectIdArg, deltaArg.sessionId),
8966
+ )?.cursor;
8967
+ if (
8968
+ floor
8969
+ && floor.streamEpoch === deltaArg.streamEpoch
8970
+ && deltaArg.revision <= floor.revision
8971
+ ) return;
8972
+ const key = this.liveMessageUpdateKey(projectIdArg, deltaArg);
8973
+ const active = this.liveReasoningUpdates.get(key);
8974
+ const baseline = this.liveReasoningBaselines.get(key);
8975
+ const previous = active ?? baseline?.update;
8976
+ if (
8977
+ previous
8978
+ && previous.streamEpoch === deltaArg.streamEpoch
8979
+ && deltaArg.revision <= previous.revision
8980
+ ) return;
8981
+ const textUtf8Bytes = active
8982
+ ? this.liveReasoningTextUtf8Bytes.get(key)
8983
+ ?? browserTextEncoder.encode(active.text).byteLength
8984
+ : baseline?.textUtf8Bytes;
8985
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
8986
+ const target = projection ? this.canonicalReasoningTarget(projection, deltaArg) : undefined;
8987
+ const nextTextUtf8Bytes = previous && textUtf8Bytes !== undefined
8988
+ ? controllerTextAfterAppendUtf8Bytes(previous.text, textUtf8Bytes, deltaArg.delta)
8989
+ : undefined;
8990
+ if (
8991
+ !previous
8992
+ || previous.status !== 'running'
8993
+ || previous.streamEpoch !== deltaArg.streamEpoch
8994
+ || deltaArg.revision <= previous.revision
8995
+ || deltaArg.sourceUpdatedAt < previous.sourceUpdatedAt
8996
+ || !this.liveMessageIdentityMatches(previous, deltaArg)
8997
+ || textUtf8Bytes !== deltaArg.baseTextUtf8Bytes
8998
+ || nextTextUtf8Bytes !== deltaArg.textUtf8Bytes
8999
+ || !projection
9000
+ || !target
9001
+ || this.harnessMessageIsTerminal(target.message)
9002
+ || target.part.text !== previous.text
9003
+ || (deltaArg.order !== undefined
9004
+ && (!target.message.order
9005
+ || transcriptOrderKey(target.message.order) !== transcriptOrderKey(deltaArg.order)))
9006
+ ) {
9007
+ this.blockLiveMessageDeltas(
9008
+ projectIdArg,
9009
+ sessionId,
9010
+ deltaArg.streamEpoch,
9011
+ !barrier.epochAdvanced,
9012
+ );
9013
+ return;
9014
+ }
9015
+ if (!this.applyCanonicalReasoningDelta(projection, target, deltaArg.delta)) {
9016
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9017
+ return;
9018
+ }
9019
+ target.message.streaming = true;
9020
+ const update: interfaces.IControllerReasoningUpdate = {
9021
+ ...previous,
9022
+ text: `${previous.text}${deltaArg.delta}`,
9023
+ sourceUpdatedAt: deltaArg.sourceUpdatedAt,
9024
+ revision: deltaArg.revision,
9025
+ streamEpoch: deltaArg.streamEpoch,
9026
+ };
9027
+ if (!this.storeLiveReasoningDeltaState(
9028
+ key,
9029
+ update,
9030
+ deltaArg.textUtf8Bytes,
9031
+ deltaArg.delta,
9032
+ )) {
9033
+ target.part.text = previous.text;
9034
+ this.requestCanonicalChatReconciliation();
9035
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9036
+ }
9037
+ }
9038
+
9039
+ private applyLiveTextDelta(
9040
+ projectIdArg: string,
9041
+ deltaArg: interfaces.IControllerTextDelta,
9042
+ ): void {
9043
+ if (!isSessionRuntimeId(deltaArg.sessionId)) return;
9044
+ const sessionId = deltaArg.sessionId;
9045
+ const barrier = this.liveMessageEventPassesBarriers(projectIdArg, deltaArg);
9046
+ if (!barrier.accepted) return;
9047
+ if (
9048
+ projectIdArg !== this.selectedProjectId
9049
+ || !controllerRuntimeIdsEqual(deltaArg.sessionId, this.selectedSessionId)
9050
+ || this.liveMessageDeltasBlocked(projectIdArg, sessionId, deltaArg.streamEpoch)
9051
+ ) return;
9052
+ const floor = this.liveMessageHydrationFloors.get(
9053
+ this.liveToolSessionKey(projectIdArg, deltaArg.sessionId),
9054
+ )?.cursor;
9055
+ if (
9056
+ floor
9057
+ && floor.streamEpoch === deltaArg.streamEpoch
9058
+ && deltaArg.revision <= floor.revision
9059
+ ) return;
9060
+ const key = this.liveMessageUpdateKey(projectIdArg, deltaArg);
9061
+ const active = this.liveTextUpdates.get(key);
9062
+ const baseline = this.liveTextBaselines.get(key);
9063
+ const previous = active ?? baseline?.update;
9064
+ if (
9065
+ previous
9066
+ && previous.streamEpoch === deltaArg.streamEpoch
9067
+ && deltaArg.revision <= previous.revision
9068
+ ) return;
9069
+ const textUtf8Bytes = active
9070
+ ? this.liveTextTextUtf8Bytes.get(key)
9071
+ ?? browserTextEncoder.encode(active.text).byteLength
9072
+ : baseline?.textUtf8Bytes;
9073
+ const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId);
9074
+ const target = projection ? this.canonicalTextTarget(projection, deltaArg) : undefined;
9075
+ const nextTextUtf8Bytes = previous && textUtf8Bytes !== undefined
9076
+ ? controllerTextAfterAppendUtf8Bytes(previous.text, textUtf8Bytes, deltaArg.delta)
9077
+ : undefined;
9078
+ if (
9079
+ !previous
9080
+ || previous.status !== 'running'
9081
+ || previous.streamEpoch !== deltaArg.streamEpoch
9082
+ || deltaArg.revision <= previous.revision
9083
+ || deltaArg.sourceUpdatedAt < previous.sourceUpdatedAt
9084
+ || !this.liveMessageIdentityMatches(previous, deltaArg)
9085
+ || textUtf8Bytes !== deltaArg.baseTextUtf8Bytes
9086
+ || nextTextUtf8Bytes !== deltaArg.textUtf8Bytes
9087
+ || !projection
9088
+ || !target
9089
+ || this.harnessMessageIsTerminal(target)
9090
+ || target.text !== previous.text
9091
+ || (deltaArg.order !== undefined
9092
+ && (!target.order || transcriptOrderKey(target.order) !== transcriptOrderKey(deltaArg.order)))
9093
+ ) {
9094
+ this.blockLiveMessageDeltas(
9095
+ projectIdArg,
9096
+ sessionId,
9097
+ deltaArg.streamEpoch,
9098
+ !barrier.epochAdvanced,
9099
+ );
9100
+ return;
9101
+ }
9102
+ if (!this.applyCanonicalTextDelta(projection, target, deltaArg.delta)) {
9103
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9104
+ return;
9105
+ }
9106
+ const update: interfaces.IControllerTextUpdate = {
9107
+ ...previous,
9108
+ text: `${previous.text}${deltaArg.delta}`,
9109
+ sourceUpdatedAt: deltaArg.sourceUpdatedAt,
9110
+ revision: deltaArg.revision,
9111
+ streamEpoch: deltaArg.streamEpoch,
9112
+ };
9113
+ if (!this.storeLiveTextDeltaState(
9114
+ key,
9115
+ update,
9116
+ deltaArg.textUtf8Bytes,
9117
+ deltaArg.delta,
9118
+ )) {
9119
+ target.text = previous.text;
9120
+ this.requestCanonicalChatReconciliation();
9121
+ this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch);
9122
+ }
9123
+ }
9124
+
7931
9125
  private async handleControllerEvent(eventArg: interfaces.IControllerEvent): Promise<void> {
7932
9126
  if (!this.authenticated) {
7933
9127
  return;
@@ -8033,79 +9227,27 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
8033
9227
  return;
8034
9228
  }
8035
9229
  if (eventArg.type === 'session.reasoning.updated' && eventArg.reasoningUpdate) {
8036
- const update = eventArg.reasoningUpdate;
8037
9230
  if (typeof eventArg.projectId !== 'string') return;
8038
- if (!isSessionRuntimeId(update.sessionId)) return;
8039
- if (
8040
- update.sourceUpdatedAt
8041
- <= (this.liveHistoryBarriers.get(
8042
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8043
- ) ?? Number.NEGATIVE_INFINITY)
8044
- ) return;
8045
- if (!this.acceptLiveMessageEpoch(update.sessionId.harnessId, update.streamEpoch)) return;
8046
- if (controllerRuntimeIdsEqual(update.sessionId, this.subagentModalSessionId)) {
8047
- this.scheduleSubagentModalRefresh();
8048
- }
8049
- if (
8050
- eventArg.projectId !== this.selectedProjectId
8051
- || !controllerRuntimeIdsEqual(update.sessionId, this.selectedSessionId)
8052
- ) return;
8053
- const floor = this.liveMessageHydrationFloors.get(
8054
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8055
- )?.cursor;
8056
- if (
8057
- floor
8058
- && floor.streamEpoch === update.streamEpoch
8059
- && update.revision <= floor.revision
8060
- ) return;
8061
- const key = `${eventArg.projectId ?? ''}\0${controllerRuntimeIdToUiKey(update.sessionId)}\0${controllerRuntimeIdToUiKey(update.partId)}`;
8062
- const previous = this.liveReasoningUpdates.get(key);
8063
- if (
8064
- !previous
8065
- || update.streamEpoch > previous.streamEpoch
8066
- || (update.streamEpoch === previous.streamEpoch && update.revision > previous.revision)
8067
- ) {
8068
- if (this.storeLiveReasoningUpdate(key, update)) this.requestUpdate();
8069
- }
8070
- if (update.status !== 'running') this.refreshSessionsNow();
9231
+ if (!isSessionRuntimeId(eventArg.reasoningUpdate.sessionId)) return;
9232
+ this.applyLiveReasoningUpdate(eventArg.projectId, eventArg.reasoningUpdate);
9233
+ return;
9234
+ }
9235
+ if (eventArg.type === 'session.reasoning.delta' && eventArg.reasoningDelta) {
9236
+ if (typeof eventArg.projectId !== 'string') return;
9237
+ if (!isSessionRuntimeId(eventArg.reasoningDelta.sessionId)) return;
9238
+ this.applyLiveReasoningDelta(eventArg.projectId, eventArg.reasoningDelta);
8071
9239
  return;
8072
9240
  }
8073
9241
  if (eventArg.type === 'session.text.updated' && eventArg.textUpdate) {
8074
- const update = eventArg.textUpdate;
8075
9242
  if (typeof eventArg.projectId !== 'string') return;
8076
- if (!isSessionRuntimeId(update.sessionId)) return;
8077
- if (
8078
- update.sourceUpdatedAt
8079
- <= (this.liveHistoryBarriers.get(
8080
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8081
- ) ?? Number.NEGATIVE_INFINITY)
8082
- ) return;
8083
- if (!this.acceptLiveMessageEpoch(update.sessionId.harnessId, update.streamEpoch)) return;
8084
- if (controllerRuntimeIdsEqual(update.sessionId, this.subagentModalSessionId)) {
8085
- this.scheduleSubagentModalRefresh();
8086
- }
8087
- if (
8088
- eventArg.projectId !== this.selectedProjectId
8089
- || !controllerRuntimeIdsEqual(update.sessionId, this.selectedSessionId)
8090
- ) return;
8091
- const floor = this.liveMessageHydrationFloors.get(
8092
- this.liveToolSessionKey(eventArg.projectId, update.sessionId),
8093
- )?.cursor;
8094
- if (
8095
- floor
8096
- && floor.streamEpoch === update.streamEpoch
8097
- && update.revision <= floor.revision
8098
- ) return;
8099
- const key = `${eventArg.projectId ?? ''}\0${controllerRuntimeIdToUiKey(update.sessionId)}\0${controllerRuntimeIdToUiKey(update.partId)}`;
8100
- const previous = this.liveTextUpdates.get(key);
8101
- if (
8102
- !previous
8103
- || update.streamEpoch > previous.streamEpoch
8104
- || (update.streamEpoch === previous.streamEpoch && update.revision > previous.revision)
8105
- ) {
8106
- if (this.storeLiveTextUpdate(key, update)) this.requestUpdate();
8107
- }
8108
- if (update.status === 'completed') this.refreshSessionsNow();
9243
+ if (!isSessionRuntimeId(eventArg.textUpdate.sessionId)) return;
9244
+ this.applyLiveTextUpdate(eventArg.projectId, eventArg.textUpdate);
9245
+ return;
9246
+ }
9247
+ if (eventArg.type === 'session.text.delta' && eventArg.textDelta) {
9248
+ if (typeof eventArg.projectId !== 'string') return;
9249
+ if (!isSessionRuntimeId(eventArg.textDelta.sessionId)) return;
9250
+ this.applyLiveTextDelta(eventArg.projectId, eventArg.textDelta);
8109
9251
  return;
8110
9252
  }
8111
9253
  this.applySessionEventState(eventArg);
@@ -10419,25 +11561,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10419
11561
  <dees-badge type="success" text="Connected" .rounded=${true}></dees-badge>
10420
11562
  <span class="headerSpacer"></span>
10421
11563
  <div class="headerActions">
10422
- <dees-button
10423
- class="refreshAction"
10424
- type="ghost"
10425
- size="sm"
10426
- ?disabled=${this.sessionsLoading || this.mutationPending}
10427
- @clicked=${() => void this.refreshSessions()}
10428
- >
10429
- <dees-icon icon="lucide:RefreshCw" iconSize="14"></dees-icon>
10430
- Refresh
10431
- </dees-button>
10432
- <dees-button
10433
- type="ghost"
10434
- size="sm"
10435
- ?disabled=${!this.selectedSessionId || this.mutationPending}
10436
- @clicked=${this.showDeleteConfirmation}
10437
- >
10438
- <dees-icon icon="lucide:Trash2" iconSize="14"></dees-icon>
10439
- Delete
10440
- </dees-button>
10441
11564
  <dees-button
10442
11565
  type="ghost"
10443
11566
  size="sm"
@@ -10798,6 +11921,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10798
11921
  .reasoningEffort=${this.resolveSessionEffortString() || 'default'}
10799
11922
  .effortOptions=${this.resolveSessionEffortOptions()}
10800
11923
  .showTodosPanel=${false}
11924
+ .markdownWhileStreaming=${false}
10801
11925
  heading=${`New ${sessionHarnessLabel(this.draftHarnessId)} conversation`}
10802
11926
  subheading=${directory
10803
11927
  ? `${sessionHarnessLabel(this.draftHarnessId)} creates this session on first send · starts in ${directory}`
@@ -10871,27 +11995,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10871
11995
  const sessionId = detailArg.session.id;
10872
11996
  const isOpenCode = sessionId.harnessId === 'opencode';
10873
11997
  const readOnly = this.sessionIsReadOnly(sessionId);
10874
- // The question tool call is the transcript's raw record of an ask; the
10875
- // inline question card is its rendering, so the JSON never shows.
10876
- const messages: plugins.deesCatalog.IHarnessMessage[] = detailArg.messages
10877
- .filter((messageArg) => messageArg.toolCall?.name !== 'question')
10878
- .map((messageArg) => this.toHarnessMessage(messageArg, this.selectedProjectId, sessionId));
10879
- const visibleMessageIds = new Set(messages.map((message) => message.id));
10880
- for (const prompt of detailArg.pendingPrompts) {
10881
- const id = controllerRuntimeIdToUiKey(prompt.id);
10882
- if (visibleMessageIds.has(id)) continue;
10883
- messages.push({
10884
- id,
10885
- role: 'user',
10886
- text: prompt.text,
10887
- createdAt: prompt.createdAt,
10888
- markdown: false,
10889
- attachments: prompt.attachments.map((attachment) => ({ ...attachment })),
10890
- });
10891
- }
10892
- this.mergeLiveReasoningMessages(messages, this.selectedProjectId, sessionId);
10893
- this.mergeLiveTextMessages(messages, this.selectedProjectId, sessionId);
10894
- this.mergeLiveToolMessages(messages, this.selectedProjectId, sessionId);
11998
+ // The canonical array and all delta targets remain stable across unrelated app renders.
11999
+ const messages = this.ensureCanonicalChatProjection(
12000
+ detailArg,
12001
+ this.selectedProjectId,
12002
+ sessionId,
12003
+ ).messages;
10895
12004
  const status = this.toHarnessStatus(
10896
12005
  detailArg.session,
10897
12006
  this.activeSubagentCount(messages),
@@ -11089,12 +12198,14 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
11089
12198
  .intelligenceBusy=${this.intelligenceAskIdsBySession.has(operationKey)
11090
12199
  || detailArg.intelligenceExchanges.some((exchange) => exchange.status === 'running')}
11091
12200
  .intelligenceError=${intelligenceError}
11092
- .todos=${isOpenCode ? detailArg.todos.map((todoArg) => ({
12201
+ .todos=${detailArg.todos.map((todoArg) => ({
11093
12202
  ...todoArg,
11094
12203
  ...(todoArg.id ? { id: controllerRuntimeIdToUiKey(todoArg.id) } : {}),
11095
- })) : []}
12204
+ }))}
12205
+ .todosAuthoritative=${true}
11096
12206
  .busy=${detailArg.session.status === 'busy' || detailArg.session.status === 'retry'}
11097
12207
  .queuedCount=${detailArg.pendingPrompts.length}
12208
+ .steeringEnabled=${false}
11098
12209
  .disabled=${this.detailLoading
11099
12210
  || this.mutationPending
11100
12211
  || this.sessionModelPending
@@ -11112,7 +12223,8 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
11112
12223
  .modelOptions=${this.modelOptionsForHarness(sessionId.harnessId, account)}
11113
12224
  .reasoningEffort=${this.resolveSessionEffortString(sessionId) || 'default'}
11114
12225
  .effortOptions=${this.resolveSessionEffortOptions(sessionId)}
11115
- .showTodosPanel=${isOpenCode}
12226
+ .showTodosPanel=${true}
12227
+ .markdownWhileStreaming=${false}
11116
12228
  heading=${detailArg.session.title || `${sessionHarnessLabel(sessionId.harnessId)} session`}
11117
12229
  subheading=${readOnly
11118
12230
  ? `Flex unavailable · saved projection · read-only · ${runtimeIdDisplay(sessionId)}`