@truefoundry/assistant-ui-runtime 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +3 -2
  2. package/dist/{chunk-Q2SHKMLM.js → chunk-SQDOTGP2.js} +32 -10
  3. package/dist/chunk-SQDOTGP2.js.map +1 -0
  4. package/dist/index.d.ts +19 -23
  5. package/dist/index.js +256 -162
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +3 -6
  8. package/dist/plugins/truefoundry-agent-server-adapter/index.js +1 -1
  9. package/dist/server/index.d.ts +2 -2
  10. package/dist/{types-BfiFf8O1.d.ts → types-DbNsU075.d.ts} +196 -9
  11. package/package.json +1 -1
  12. package/src/{private → draft}/agentSpec.ts +14 -17
  13. package/src/{private → draft}/draftSessionBridge.ts +1 -2
  14. package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
  15. package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +2 -1
  16. package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
  17. package/src/draftAgentConfig.test.ts +2 -1
  18. package/src/index.ts +30 -7
  19. package/src/plugins/truefoundry-agent-server-adapter/index.ts +51 -11
  20. package/src/plugins/truefoundry-agent-server-adapter/types.ts +6 -4
  21. package/src/server/index.ts +23 -0
  22. package/src/server/types.ts +256 -11
  23. package/src/truefoundryExtras.ts +4 -1
  24. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +1 -1
  25. package/src/types.ts +1 -2
  26. package/src/useTrueFoundryAgentMessages.test.tsx +261 -1
  27. package/src/useTrueFoundryAgentMessages.ts +284 -176
  28. package/src/useTrueFoundryAgentRuntime.ts +31 -21
  29. package/dist/chunk-Q2SHKMLM.js.map +0 -1
  30. /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  isTfyMcpToolInfo,
11
11
  isTfySystemToolInfo,
12
12
  isTfyToolInfo
13
- } from "./chunk-Q2SHKMLM.js";
13
+ } from "./chunk-SQDOTGP2.js";
14
14
 
15
15
  // src/useTrueFoundryAgentRuntime.ts
16
16
  import {
@@ -2386,7 +2386,7 @@ function repositoryItemsFromMessages(messages) {
2386
2386
  return items;
2387
2387
  }
2388
2388
 
2389
- // src/private/draftSessionBridge.ts
2389
+ // src/draft/draftSessionBridge.ts
2390
2390
  var DRAFT_SESSION_LAST_UPDATED_AT_HEADER = "x-tfy-session-last-updated-at";
2391
2391
  function createDraftSessionBridge(server) {
2392
2392
  return {
@@ -2409,7 +2409,7 @@ function createDraftSessionBridge(server) {
2409
2409
  };
2410
2410
  }
2411
2411
 
2412
- // src/private/agentSpec.ts
2412
+ // src/draft/agentSpec.ts
2413
2413
  function mergeAgentSpec(base, update) {
2414
2414
  const { model: modelUpdate, ...rest } = update;
2415
2415
  const next = {
@@ -2437,7 +2437,7 @@ function sessionListStartTimestamp() {
2437
2437
  return start.toISOString();
2438
2438
  }
2439
2439
 
2440
- // src/private/truefoundryDraftThreadListAdapter.ts
2440
+ // src/draft/truefoundryDraftThreadListAdapter.ts
2441
2441
  var THREAD_LIST_PAGE_SIZE = 20;
2442
2442
  function createTrueFoundryDraftThreadListAdapter(options) {
2443
2443
  const { server, defaultAgentSpec, getAgentSpec } = options;
@@ -2502,6 +2502,7 @@ var trueFoundryExtras = createRuntimeExtras(
2502
2502
  var EMPTY_DRAFT_EXTRAS = {
2503
2503
  agentSpec: null,
2504
2504
  draftSessionId: void 0,
2505
+ isSpecLoading: false,
2505
2506
  isSpecSyncing: false,
2506
2507
  specError: null,
2507
2508
  updateAgentSpec: () => {
@@ -2597,7 +2598,7 @@ function resolveTrueFoundryAgentRuntimeOptions(options) {
2597
2598
  };
2598
2599
  }
2599
2600
 
2600
- // src/private/useDraftAgentSpec.ts
2601
+ // src/draft/useDraftAgentSpec.ts
2601
2602
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2602
2603
  var SPEC_SYNC_DEBOUNCE_MS = 400;
2603
2604
  function useDraftAgentSpec({
@@ -2609,6 +2610,7 @@ function useDraftAgentSpec({
2609
2610
  }) {
2610
2611
  const enabled = draftBridge != null;
2611
2612
  const [agentSpec, setAgentSpec] = useState(defaultAgentSpec);
2613
+ const [isSpecLoading, setIsSpecLoading] = useState(false);
2612
2614
  const [isSpecSyncing, setIsSpecSyncing] = useState(false);
2613
2615
  const [specError, setSpecError] = useState(null);
2614
2616
  const agentSpecRef = useRef(agentSpec);
@@ -2649,12 +2651,14 @@ function useDraftAgentSpec({
2649
2651
  setAgentSpec(defaultAgentSpec);
2650
2652
  localDirtyRef.current = false;
2651
2653
  setSpecError(null);
2654
+ setIsSpecLoading(false);
2652
2655
  return;
2653
2656
  }
2654
2657
  if (loadedDraftIdRef.current === draftSessionId) {
2655
2658
  return;
2656
2659
  }
2657
2660
  let cancelled = false;
2661
+ setIsSpecLoading(true);
2658
2662
  void (async () => {
2659
2663
  try {
2660
2664
  const loaded = await draftBridge.getDraftAgentSpec(draftSessionId);
@@ -2666,19 +2670,23 @@ function useDraftAgentSpec({
2666
2670
  scheduleSpecSyncRef.current?.(draftSessionId, agentSpecRef.current);
2667
2671
  localDirtyRef.current = false;
2668
2672
  setSpecError(null);
2673
+ setIsSpecLoading(false);
2669
2674
  return;
2670
2675
  }
2671
2676
  setAgentSpec(loaded);
2672
2677
  setSpecError(null);
2678
+ setIsSpecLoading(false);
2673
2679
  } catch (error) {
2674
2680
  if (!cancelled) {
2675
2681
  onError?.(error);
2676
2682
  setSpecError(error);
2683
+ setIsSpecLoading(false);
2677
2684
  }
2678
2685
  }
2679
2686
  })();
2680
2687
  return () => {
2681
2688
  cancelled = true;
2689
+ setIsSpecLoading(false);
2682
2690
  };
2683
2691
  }, [defaultAgentSpec, draftBridge, draftSessionId, enabled, onError]);
2684
2692
  const flushSpecSync = useCallback(
@@ -2781,6 +2789,7 @@ function useDraftAgentSpec({
2781
2789
  () => ({
2782
2790
  agentSpec: enabled ? agentSpec : null,
2783
2791
  draftSessionId: enabled ? draftSessionId : void 0,
2792
+ isSpecLoading: enabled ? isSpecLoading : false,
2784
2793
  isSpecSyncing: enabled ? isSpecSyncing : false,
2785
2794
  specError: enabled ? specError : null,
2786
2795
  updateAgentSpec,
@@ -2790,6 +2799,7 @@ function useDraftAgentSpec({
2790
2799
  agentSpec,
2791
2800
  draftSessionId,
2792
2801
  enabled,
2802
+ isSpecLoading,
2793
2803
  isSpecSyncing,
2794
2804
  specError,
2795
2805
  takeTurnHeaderTimestamp,
@@ -3058,6 +3068,7 @@ function useTrueFoundryAgentMessages({
3058
3068
  server,
3059
3069
  sessionId,
3060
3070
  isMain,
3071
+ isInitialSession,
3061
3072
  listEventsConcurrency,
3062
3073
  onError,
3063
3074
  initializeSession,
@@ -3066,7 +3077,9 @@ function useTrueFoundryAgentMessages({
3066
3077
  }) {
3067
3078
  const [snapshot, setSnapshot] = useState2(createEmptySessionSnapshot);
3068
3079
  const [isRunning, setIsRunning] = useState2(false);
3069
- const [isLoading, setIsLoading] = useState2(false);
3080
+ const [isLoading, setIsLoading] = useState2(
3081
+ sessionId != null && (isMain !== false || isInitialSession === true)
3082
+ );
3070
3083
  const [isLoadingOlderHistory, setIsLoadingOlderHistory] = useState2(false);
3071
3084
  const [loadRetryTrigger, setLoadRetryTrigger] = useState2(0);
3072
3085
  const snapshotRef = useRef2(snapshot);
@@ -3087,6 +3100,8 @@ function useTrueFoundryAgentMessages({
3087
3100
  const loadGenerationRef = useRef2(0);
3088
3101
  const streamGenerationRef = useRef2(0);
3089
3102
  const lazilyCreatedSessionIdRef = useRef2(void 0);
3103
+ const initialLoadStartedForRef = useRef2(void 0);
3104
+ const skipInitialPromotionLoadForRef = useRef2(void 0);
3090
3105
  const projectOptions = useMemo2(
3091
3106
  () => ({
3092
3107
  getCreatedAt: (messageId, fallback) => {
@@ -3197,12 +3212,24 @@ function useTrueFoundryAgentMessages({
3197
3212
  [onError]
3198
3213
  );
3199
3214
  const load = useCallback2(async () => {
3215
+ void loadRetryTrigger;
3200
3216
  if (sessionId == null) {
3201
3217
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3202
3218
  setSnapshot(createEmptySessionSnapshot());
3203
3219
  return;
3204
3220
  }
3205
- if (isMain === false) return;
3221
+ const isEarlyInitialLoad = isMain === false && isInitialSession === true && initialLoadStartedForRef.current !== sessionId;
3222
+ if (isMain === false) {
3223
+ if (!isEarlyInitialLoad) return;
3224
+ initialLoadStartedForRef.current = sessionId;
3225
+ skipInitialPromotionLoadForRef.current = sessionId;
3226
+ } else if (isMain === true && skipInitialPromotionLoadForRef.current === sessionId) {
3227
+ skipInitialPromotionLoadForRef.current = void 0;
3228
+ return;
3229
+ }
3230
+ if (isInitialSession === true) {
3231
+ initialLoadStartedForRef.current = sessionId;
3232
+ }
3206
3233
  if (lazilyCreatedSessionIdRef.current != null && sessionId !== lazilyCreatedSessionIdRef.current) {
3207
3234
  lazilyCreatedSessionIdRef.current = void 0;
3208
3235
  }
@@ -3211,6 +3238,7 @@ function useTrueFoundryAgentMessages({
3211
3238
  }
3212
3239
  const generation = ++loadGenerationRef.current;
3213
3240
  ++streamGenerationRef.current;
3241
+ setIsRunning(false);
3214
3242
  abortControllerRef.current?.abort();
3215
3243
  loadOlderInflightRef.current = null;
3216
3244
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
@@ -3257,6 +3285,10 @@ function useTrueFoundryAgentMessages({
3257
3285
  }
3258
3286
  } catch (error) {
3259
3287
  if (generation === loadGenerationRef.current) {
3288
+ if (isEarlyInitialLoad) {
3289
+ initialLoadStartedForRef.current = void 0;
3290
+ skipInitialPromotionLoadForRef.current = void 0;
3291
+ }
3260
3292
  onErrorRef.current?.(error);
3261
3293
  }
3262
3294
  throw error;
@@ -3265,134 +3297,178 @@ function useTrueFoundryAgentMessages({
3265
3297
  setIsLoading(false);
3266
3298
  }
3267
3299
  }
3268
- }, [server, runStream, sessionId, loadRetryTrigger, isMain]);
3300
+ }, [
3301
+ server,
3302
+ runStream,
3303
+ sessionId,
3304
+ loadRetryTrigger,
3305
+ isMain,
3306
+ isInitialSession
3307
+ ]);
3269
3308
  useEffect2(() => {
3270
3309
  void load().catch(() => void 0);
3271
3310
  }, [load]);
3272
3311
  const sendTurn = useCallback2(
3273
3312
  async (options) => {
3274
- let activeSessionId = sessionId;
3275
- if (activeSessionId == null) {
3276
- if (initializeSessionRef.current == null) {
3277
- throw new Error("Cannot send a turn without an active session.");
3313
+ let gatewayTurnAccepted = false;
3314
+ let pendingUserWasSet = false;
3315
+ let runStreamStarted = false;
3316
+ let pendingUserTurnId;
3317
+ try {
3318
+ let activeSessionId = sessionId;
3319
+ if (activeSessionId == null) {
3320
+ if (initializeSessionRef.current == null) {
3321
+ throw new Error("Cannot send a turn without an active session.");
3322
+ }
3323
+ const { remoteId } = await initializeSessionRef.current();
3324
+ activeSessionId = remoteId;
3325
+ lazilyCreatedSessionIdRef.current = remoteId;
3278
3326
  }
3279
- const { remoteId } = await initializeSessionRef.current();
3280
- activeSessionId = remoteId;
3281
- lazilyCreatedSessionIdRef.current = remoteId;
3282
- }
3283
- const conversationSessionId = await resolveActiveSessionId(
3284
- activeSessionId,
3285
- resolveConversationSessionIdRef.current
3286
- );
3287
- const turnHeaders = await getTurnHeadersRef.current?.();
3288
- const streamHeaders = turnHeaders != null ? { headers: turnHeaders } : {};
3289
- const isContinuation = "inputs" in options || "resumeMcpAuth" in options && options.resumeMcpAuth === true;
3290
- const continuationTurnId = snapshotRef.current.activeStream?.turnId;
3291
- const turnId = isContinuation && continuationTurnId != null ? continuationTurnId : generateId();
3292
- const isFirstTurnInSession = "userMessage" in options && options.previousTurnId === void 0 && snapshotRef.current.turns.length === 0 && snapshotRef.current.pendingUser == null && snapshotRef.current.activeStream == null;
3293
- const turnIdRef = { current: turnId };
3294
- if ("inputs" in options) {
3295
- applyUserToolResponsesToFold(
3296
- snapshotRef.current.fold,
3297
- options.inputs
3298
- );
3299
- }
3300
- const branchBase = "userMessage" in options ? options.branchFromSnapshot : void 0;
3301
- let groupRootBaseline;
3302
- if (branchBase != null && "userMessage" in options) {
3303
- const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
3304
- groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3305
- const nextSnapshot = replaceSessionSnapshot(branchBase, {
3306
- pendingUser: {
3307
- turnId,
3308
- content: options.userMessage,
3309
- createdAt: /* @__PURE__ */ new Date()
3310
- },
3311
- activeStream: void 0,
3312
- groupRootBaseline
3313
- });
3314
- snapshotRef.current = nextSnapshot;
3315
- setSnapshot(nextSnapshot);
3316
- } else {
3317
- setSnapshot(
3318
- (prev) => commitActiveStream(
3319
- prev,
3320
- "inputs" in options ? options.inputs : void 0
3321
- )
3327
+ const conversationSessionId = await resolveActiveSessionId(
3328
+ activeSessionId,
3329
+ resolveConversationSessionIdRef.current
3322
3330
  );
3323
- if ("userMessage" in options) {
3324
- const rootBucket = snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
3331
+ const turnHeaders = await getTurnHeadersRef.current?.();
3332
+ const streamHeaders = turnHeaders != null ? { headers: turnHeaders } : {};
3333
+ const isContinuation = "inputs" in options || "resumeMcpAuth" in options && options.resumeMcpAuth === true;
3334
+ const continuationTurnId = snapshotRef.current.activeStream?.turnId;
3335
+ const turnId = isContinuation && continuationTurnId != null ? continuationTurnId : generateId();
3336
+ const isFirstTurnInSession = "userMessage" in options && options.previousTurnId === void 0 && snapshotRef.current.turns.length === 0 && snapshotRef.current.pendingUser == null && snapshotRef.current.activeStream == null;
3337
+ const turnIdRef = { current: turnId };
3338
+ if ("inputs" in options) {
3339
+ applyUserToolResponsesToFold(
3340
+ snapshotRef.current.fold,
3341
+ options.inputs
3342
+ );
3343
+ }
3344
+ const branchBase = "userMessage" in options ? options.branchFromSnapshot : void 0;
3345
+ let groupRootBaseline;
3346
+ if (branchBase != null && "userMessage" in options) {
3347
+ const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
3325
3348
  groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3326
- setSnapshot((prev) => {
3327
- const next = replaceSessionSnapshot(prev, {
3328
- pendingUser: {
3329
- turnId,
3330
- content: options.userMessage,
3331
- createdAt: /* @__PURE__ */ new Date()
3332
- },
3333
- activeStream: void 0,
3334
- groupRootBaseline
3335
- });
3336
- snapshotRef.current = next;
3337
- return next;
3349
+ const nextSnapshot = replaceSessionSnapshot(branchBase, {
3350
+ pendingUser: {
3351
+ turnId,
3352
+ content: options.userMessage,
3353
+ createdAt: /* @__PURE__ */ new Date()
3354
+ },
3355
+ activeStream: void 0,
3356
+ groupRootBaseline
3338
3357
  });
3358
+ snapshotRef.current = nextSnapshot;
3359
+ setSnapshot(nextSnapshot);
3360
+ pendingUserWasSet = true;
3361
+ pendingUserTurnId = turnId;
3339
3362
  } else {
3340
- groupRootBaseline = snapshotRef.current.groupRootBaseline ?? computeGroupRootBaseline(snapshotRef.current.turns);
3341
- }
3342
- }
3343
- await runStream(
3344
- (signal) => {
3345
- if ("inputs" in options) {
3346
- return streamTurnContent(
3347
- server,
3348
- conversationSessionId,
3349
- snapshotRef.current.fold,
3350
- { inputs: options.inputs, ...streamHeaders },
3351
- signal,
3352
- groupRootBaseline
3353
- );
3363
+ setSnapshot(
3364
+ (prev) => commitActiveStream(
3365
+ prev,
3366
+ "inputs" in options ? options.inputs : void 0
3367
+ )
3368
+ );
3369
+ if ("userMessage" in options) {
3370
+ const rootBucket = snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
3371
+ groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3372
+ setSnapshot((prev) => {
3373
+ const next = replaceSessionSnapshot(prev, {
3374
+ pendingUser: {
3375
+ turnId,
3376
+ content: options.userMessage,
3377
+ createdAt: /* @__PURE__ */ new Date()
3378
+ },
3379
+ activeStream: void 0,
3380
+ groupRootBaseline
3381
+ });
3382
+ snapshotRef.current = next;
3383
+ return next;
3384
+ });
3385
+ pendingUserWasSet = true;
3386
+ pendingUserTurnId = turnId;
3387
+ } else {
3388
+ groupRootBaseline = snapshotRef.current.groupRootBaseline ?? computeGroupRootBaseline(snapshotRef.current.turns);
3354
3389
  }
3355
- if ("resumeMcpAuth" in options) {
3390
+ }
3391
+ runStreamStarted = true;
3392
+ await runStream(
3393
+ (signal) => {
3394
+ if ("inputs" in options) {
3395
+ return streamTurnContent(
3396
+ server,
3397
+ conversationSessionId,
3398
+ snapshotRef.current.fold,
3399
+ { inputs: options.inputs, ...streamHeaders },
3400
+ signal,
3401
+ groupRootBaseline
3402
+ );
3403
+ }
3404
+ if ("resumeMcpAuth" in options) {
3405
+ return streamTurnContent(
3406
+ server,
3407
+ conversationSessionId,
3408
+ snapshotRef.current.fold,
3409
+ { resumeMcpAuth: true, ...streamHeaders },
3410
+ signal,
3411
+ groupRootBaseline
3412
+ );
3413
+ }
3356
3414
  return streamTurnContent(
3357
3415
  server,
3358
3416
  conversationSessionId,
3359
3417
  snapshotRef.current.fold,
3360
- { resumeMcpAuth: true, ...streamHeaders },
3418
+ {
3419
+ userMessage: options.userMessage,
3420
+ ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId ?? "none" } : isFirstTurnInSession ? { previousTurnId: "none" } : {},
3421
+ ...streamHeaders
3422
+ },
3361
3423
  signal,
3362
- groupRootBaseline
3424
+ groupRootBaseline,
3425
+ // Rename the optimistic local ID to the gateway turn ID
3426
+ // so that edit/retry can resolve the turn via the gateway.
3427
+ (gatewayTurnId) => {
3428
+ const oldId = turnIdRef.current;
3429
+ gatewayTurnAccepted = true;
3430
+ if (gatewayTurnId === oldId) return;
3431
+ turnIdRef.current = gatewayTurnId;
3432
+ const renamePendingUser = (prev) => {
3433
+ if (prev.pendingUser?.turnId !== oldId) return prev;
3434
+ return replaceSessionSnapshot(prev, {
3435
+ pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId }
3436
+ });
3437
+ };
3438
+ snapshotRef.current = renamePendingUser(snapshotRef.current);
3439
+ setSnapshot(renamePendingUser);
3440
+ }
3363
3441
  );
3442
+ },
3443
+ turnIdRef,
3444
+ isContinuation
3445
+ );
3446
+ } catch (error) {
3447
+ if ("userMessage" in options && !gatewayTurnAccepted) {
3448
+ const branchRollbackSnapshot = options.branchRollbackSnapshot;
3449
+ const canRestoreBranch = branchRollbackSnapshot != null && (snapshotRef.current === options.branchFromSnapshot || snapshotRef.current.pendingUser?.turnId === pendingUserTurnId);
3450
+ if (canRestoreBranch) {
3451
+ snapshotRef.current = branchRollbackSnapshot;
3452
+ setSnapshot(branchRollbackSnapshot);
3453
+ } else if (pendingUserWasSet) {
3454
+ const clearPendingUser = (previous) => {
3455
+ if (previous.pendingUser?.turnId !== pendingUserTurnId) {
3456
+ return previous;
3457
+ }
3458
+ return replaceSessionSnapshot(previous, {
3459
+ pendingUser: void 0
3460
+ });
3461
+ };
3462
+ snapshotRef.current = clearPendingUser(snapshotRef.current);
3463
+ setSnapshot(clearPendingUser);
3364
3464
  }
3365
- return streamTurnContent(
3366
- server,
3367
- conversationSessionId,
3368
- snapshotRef.current.fold,
3369
- {
3370
- userMessage: options.userMessage,
3371
- ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId ?? "none" } : isFirstTurnInSession ? { previousTurnId: "none" } : {},
3372
- ...streamHeaders
3373
- },
3374
- signal,
3375
- groupRootBaseline,
3376
- // Rename the optimistic local ID to the gateway turn ID
3377
- // so that edit/retry can resolve the turn via the gateway.
3378
- (gatewayTurnId) => {
3379
- const oldId = turnIdRef.current;
3380
- if (gatewayTurnId === oldId) return;
3381
- turnIdRef.current = gatewayTurnId;
3382
- const renamePendingUser = (prev) => {
3383
- if (prev.pendingUser?.turnId !== oldId) return prev;
3384
- return replaceSessionSnapshot(prev, {
3385
- pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId }
3386
- });
3387
- };
3388
- snapshotRef.current = renamePendingUser(snapshotRef.current);
3389
- setSnapshot(renamePendingUser);
3390
- }
3391
- );
3392
- },
3393
- turnIdRef,
3394
- isContinuation
3395
- );
3465
+ options.onPreTurnFailure?.();
3466
+ }
3467
+ if (!runStreamStarted) {
3468
+ onErrorRef.current?.(error);
3469
+ }
3470
+ throw error;
3471
+ }
3396
3472
  },
3397
3473
  [server, runStream, sessionId]
3398
3474
  );
@@ -3422,7 +3498,7 @@ function useTrueFoundryAgentMessages({
3422
3498
  }
3423
3499
  const inputs = collectRequiredActionInputs(paused);
3424
3500
  if (inputs.length > 0) {
3425
- void sendTurn({ inputs }).catch((error) => onErrorRef.current?.(error));
3501
+ void sendTurn({ inputs }).catch(() => void 0);
3426
3502
  }
3427
3503
  },
3428
3504
  [projectOptions, sendTurn]
@@ -3483,35 +3559,44 @@ function useTrueFoundryAgentMessages({
3483
3559
  }, [runStream, server]);
3484
3560
  const branchFromTurn = useCallback2(
3485
3561
  async (turnId, userMessage) => {
3486
- let activeSessionId = sessionId;
3487
- if (activeSessionId == null) {
3488
- throw new Error("Cannot branch from a turn without an active session.");
3562
+ let committed;
3563
+ let previousTurnId;
3564
+ let rewound;
3565
+ try {
3566
+ let activeSessionId = sessionId;
3567
+ if (activeSessionId == null) {
3568
+ throw new Error("Cannot branch from a turn without an active session.");
3569
+ }
3570
+ committed = commitActiveStream(snapshotRef.current);
3571
+ setSnapshot(committed);
3572
+ await cancel();
3573
+ const conversationSessionId = await resolveActiveSessionId(
3574
+ activeSessionId,
3575
+ resolveConversationSessionIdRef.current
3576
+ );
3577
+ previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
3578
+ server,
3579
+ conversationSessionId,
3580
+ turnId
3581
+ );
3582
+ rewound = await buildSnapshotBeforeTurn(
3583
+ server,
3584
+ conversationSessionId,
3585
+ turnId,
3586
+ listEventsConcurrency
3587
+ );
3588
+ createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3589
+ snapshotRef.current = rewound;
3590
+ setSnapshot(rewound);
3591
+ } catch (error) {
3592
+ onErrorRef.current?.(error);
3593
+ throw error;
3489
3594
  }
3490
- const committed = commitActiveStream(snapshotRef.current);
3491
- setSnapshot(committed);
3492
- await cancel();
3493
- const conversationSessionId = await resolveActiveSessionId(
3494
- activeSessionId,
3495
- resolveConversationSessionIdRef.current
3496
- );
3497
- const previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
3498
- server,
3499
- conversationSessionId,
3500
- turnId
3501
- );
3502
- const rewound = await buildSnapshotBeforeTurn(
3503
- server,
3504
- conversationSessionId,
3505
- turnId,
3506
- listEventsConcurrency
3507
- );
3508
- createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3509
- snapshotRef.current = rewound;
3510
- setSnapshot(rewound);
3511
3595
  await sendTurn({
3512
3596
  userMessage,
3513
3597
  previousTurnId,
3514
- branchFromSnapshot: rewound
3598
+ branchFromSnapshot: rewound,
3599
+ branchRollbackSnapshot: committed
3515
3600
  });
3516
3601
  },
3517
3602
  [
@@ -3527,7 +3612,9 @@ function useTrueFoundryAgentMessages({
3527
3612
  const committed = commitActiveStream(snapshotRef.current);
3528
3613
  const originalInput = resolveTurnInput(committed, turnId);
3529
3614
  if (originalInput == null) {
3530
- throw new Error(`Turn ${turnId} not found in session snapshot`);
3615
+ const error = new Error(`Turn ${turnId} not found in session snapshot`);
3616
+ onErrorRef.current?.(error);
3617
+ throw error;
3531
3618
  }
3532
3619
  const userMessage = extractTurnUserMessageContent(originalInput);
3533
3620
  await branchFromTurn(turnId, userMessage);
@@ -3539,7 +3626,9 @@ function useTrueFoundryAgentMessages({
3539
3626
  const committed = commitActiveStream(snapshotRef.current);
3540
3627
  const originalInput = resolveTurnInput(committed, turnId);
3541
3628
  if (originalInput == null) {
3542
- throw new Error(`Turn ${turnId} not found in session snapshot`);
3629
+ const error = new Error(`Turn ${turnId} not found in session snapshot`);
3630
+ onErrorRef.current?.(error);
3631
+ throw error;
3543
3632
  }
3544
3633
  const userMessage = buildEditedUserMessageContent(
3545
3634
  editedText,
@@ -3638,6 +3727,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3638
3727
  const isMain = useAuiState(
3639
3728
  (state) => state.threads.mainThreadId === state.threadListItem.id
3640
3729
  );
3730
+ const isInitialSession = sessionId != null && sessionId === options.initialSessionId;
3641
3731
  const draftSpec = useDraftAgentSpec({
3642
3732
  draftSessionId,
3643
3733
  draftBridge: draftBridgeRef.current,
@@ -3683,6 +3773,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3683
3773
  server,
3684
3774
  sessionId,
3685
3775
  isMain,
3776
+ isInitialSession,
3686
3777
  listEventsConcurrency,
3687
3778
  onError,
3688
3779
  initializeSession,
@@ -3708,20 +3799,16 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3708
3799
  const downloadSandboxFile = useCallback3(
3709
3800
  async (path) => {
3710
3801
  if (server.downloadSandboxFile == null) {
3711
- const error = new Error(
3802
+ throw new Error(
3712
3803
  "Downloading a sandbox file requires AgentChatServer.downloadSandboxFile."
3713
3804
  );
3714
- onError?.(error);
3715
- throw error;
3716
3805
  }
3717
3806
  if (sandboxId == null) {
3718
- const error = new Error("No sandbox is available yet for this session.");
3719
- onError?.(error);
3720
- throw error;
3807
+ throw new Error("No sandbox is available yet for this session.");
3721
3808
  }
3722
3809
  return await server.downloadSandboxFile(sandboxId, { path });
3723
3810
  },
3724
- [server, sandboxId, onError]
3811
+ [server, sandboxId]
3725
3812
  );
3726
3813
  const draftExtras = useMemo3(() => {
3727
3814
  if (agent.mode !== "draft") {
@@ -3730,6 +3817,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3730
3817
  return {
3731
3818
  agentSpec: draftSpec.agentSpec,
3732
3819
  draftSessionId: draftSpec.draftSessionId,
3820
+ isSpecLoading: draftSpec.isSpecLoading,
3733
3821
  isSpecSyncing: draftSpec.isSpecSyncing,
3734
3822
  specError: draftSpec.specError,
3735
3823
  updateAgentSpec: draftSpec.updateAgentSpec
@@ -3750,9 +3838,8 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3750
3838
  resumeMcpAuth,
3751
3839
  downloadSandboxFile,
3752
3840
  cancel,
3753
- resetFromTurn: (turnId) => resetFromTurn(turnId).catch((error) => {
3754
- onError?.(error);
3755
- }),
3841
+ // resetFromTurn/branchFromTurn/sendTurn already report via onError.
3842
+ resetFromTurn: (turnId) => resetFromTurn(turnId).catch(() => void 0),
3756
3843
  reload: retryLoad,
3757
3844
  hasOlderHistory,
3758
3845
  isLoadingOlderHistory,
@@ -3777,7 +3864,19 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3777
3864
  await sendTurn({ resumeMcpAuth: true });
3778
3865
  return;
3779
3866
  }
3780
- await sendTurn({ userMessage: buildUserMessageContent(message) });
3867
+ const userMessage = buildUserMessageContent(message);
3868
+ await sendTurn({
3869
+ userMessage,
3870
+ // The composer clears before onNew runs. Restore its text only when
3871
+ // the turn failed before turn.created registered it in the backend.
3872
+ onPreTurnFailure: () => {
3873
+ const text = userMessageContentToText(userMessage);
3874
+ const composer = aui.thread().composer();
3875
+ if (text && !composer.getState().text.trim()) {
3876
+ composer.setText(text);
3877
+ }
3878
+ }
3879
+ });
3781
3880
  },
3782
3881
  onCancel: async () => {
3783
3882
  await cancel();
@@ -3795,12 +3894,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3795
3894
  }
3796
3895
  const turnId = parseTurnIdFromMessageId(sourceId);
3797
3896
  const editedText = extractEditedText(message);
3798
- try {
3799
- await editFromTurn(turnId, editedText);
3800
- } catch (error) {
3801
- onError?.(error);
3802
- throw error;
3803
- }
3897
+ await editFromTurn(turnId, editedText);
3804
3898
  }
3805
3899
  });
3806
3900
  }