@@ -238,6 +238,7 @@ import {
238238import { ThreadErrorBanner } from "./chat/ThreadErrorBanner" ;
239239import { resolveThreadPr } from "./ThreadStatusIndicators" ;
240240import { ComposerBannerStack , type ComposerBannerStackItem } from "./chat/ComposerBannerStack" ;
241+ import { ThreadSyncStatusPill } from "./chat/ThreadSyncStatusPill" ;
241242import {
242243 DRAFT_HERO_TRANSITION_ANIMATION_ID ,
243244 DRAFT_HERO_TRANSITION_DURATION_MS ,
@@ -251,6 +252,7 @@ import {
251252 branchMismatchKey ,
252253 buildExpiredTerminalContextToastCopy ,
253254 buildLocalDraftThread ,
255+ buildLoadingThreadFromShell ,
254256 buildThreadTurnInterruptInput ,
255257 collectUserMessageBlobPreviewUrls ,
256258 createLocalDispatchSnapshot ,
@@ -272,9 +274,11 @@ import {
272274 resolveSendEnvMode ,
273275 revokeBlobPreviewUrl ,
274276 revokeUserMessagePreviewUrls ,
277+ shouldWriteThreadErrorToCurrentServerThread ,
275278 startNewThreadForProject ,
276279 waitForStartedServerThread ,
277280} from "./ChatView.logic" ;
281+ import type { ThreadSyncPhase } from "../threadSync" ;
278282import { useLocalStorage } from "~/hooks/useLocalStorage" ;
279283import { useComposerHandleContext } from "../composerHandleContext" ;
280284import { sanitizeThreadErrorMessage } from "~/rpc/transportError" ;
@@ -465,6 +469,7 @@ type ChatViewProps =
465469 onDiffPanelOpen ?: ( ) => void ;
466470 reserveTitleBarControlInset ?: boolean ;
467471 forceExpandedMobileComposer ?: boolean ;
472+ threadSyncPhase ?: ThreadSyncPhase | null ;
468473 routeKind : "server" ;
469474 draftId ?: never ;
470475 }
@@ -474,6 +479,7 @@ type ChatViewProps =
474479 onDiffPanelOpen ?: ( ) => void ;
475480 reserveTitleBarControlInset ?: boolean ;
476481 forceExpandedMobileComposer ?: boolean ;
482+ threadSyncPhase ?: never ;
477483 routeKind : "draft" ;
478484 draftId : DraftId ;
479485 } ;
@@ -1132,6 +1138,8 @@ function ChatViewContent(props: ChatViewProps) {
11321138 forceExpandedMobileComposer = false ,
11331139 } = props ;
11341140 const draftId = routeKind === "draft" ? props . draftId : null ;
1141+ const threadSyncPhase = routeKind === "server" ? ( props . threadSyncPhase ?? null ) : null ;
1142+ const threadDetailLoading = threadSyncPhase === "loading" ;
11351143 const handleNewThread = useNewThreadHandler ( ) ;
11361144 const routeThreadRef = useMemo (
11371145 ( ) => scopeThreadRef ( environmentId , threadId ) ,
@@ -1188,7 +1196,16 @@ function ChatViewContent(props: ChatViewProps) {
11881196 ? store . getDraftSession ( draftId )
11891197 : null ,
11901198 ) ;
1199+ const routeServerThreadShell = useThreadShell ( routeKind === "server" ? routeThreadRef : null ) ;
11911200 const serverThread = useThread ( routeThreadRef , { waitForShell : draftThread !== null } ) ;
1201+ const loadingServerThread = useMemo (
1202+ ( ) =>
1203+ threadDetailLoading && routeServerThreadShell
1204+ ? buildLoadingThreadFromShell ( routeServerThreadShell )
1205+ : null ,
1206+ [ routeServerThreadShell , threadDetailLoading ] ,
1207+ ) ;
1208+ const activeServerThread = serverThread ?? loadingServerThread ;
11921209 const markThreadVisited = useUiStateStore ( ( store ) => store . markThreadVisited ) ;
11931210 const activeThreadLastVisitedAt = useUiStateStore (
11941211 ( store ) => store . threadLastVisitedAtById [ routeThreadKey ] ,
@@ -1368,7 +1385,7 @@ function ChatViewContent(props: ChatViewProps) {
13681385 ? scopeProjectRef ( draftThread . environmentId , draftThread . projectId )
13691386 : null ;
13701387 const fallbackDraftProject = useProject ( fallbackDraftProjectRef ) ;
1371- const localDraftError = serverThread
1388+ const localDraftError = activeServerThread
13721389 ? null
13731390 : ( ( draftId ? localDraftErrorsByDraftId [ draftId ] ?. message : null ) ?? null ) ;
13741391 const localServerError = localServerErrorsByThreadKey [ routeThreadKey ] ?. message ?? null ;
@@ -1377,7 +1394,7 @@ function ChatViewContent(props: ChatViewProps) {
13771394 // a failed send would silently disappear on promotion. When both keys hold
13781395 // an entry, the most recent write wins.
13791396 useEffect ( ( ) => {
1380- if ( ! serverThread || ! draftId ) {
1397+ if ( ! activeServerThread || ! draftId ) {
13811398 return ;
13821399 }
13831400 const pendingDraftEntry = localDraftErrorsByDraftId [ draftId ] ;
@@ -1406,7 +1423,7 @@ function ChatViewContent(props: ChatViewProps) {
14061423 [ routeThreadKey ] : pendingDraftEntry ,
14071424 } ;
14081425 } ) ;
1409- } , [ draftId , localDraftErrorsByDraftId , routeThreadKey , serverThread ] ) ;
1426+ } , [ activeServerThread , draftId , localDraftErrorsByDraftId , routeThreadKey ] ) ;
14101427 const localDraftThread = useMemo (
14111428 ( ) =>
14121429 draftThread
@@ -1421,10 +1438,10 @@ function ChatViewContent(props: ChatViewProps) {
14211438 // Promotion is data-driven: the draft route keeps rendering while the
14221439 // server thread (same pre-allocated ref) starts, so live state must not
14231440 // depend on which route is mounted.
1424- const isServerThread = serverThread !== null ;
1425- const activeThread = isServerThread ? serverThread : localDraftThread ;
1441+ const isServerThread = activeServerThread !== null ;
1442+ const activeThread = activeServerThread ?? localDraftThread ;
14261443 const threadError = isServerThread
1427- ? ( localServerError ?? serverThread ?. session ?. lastError ?? null )
1444+ ? ( localServerError ?? activeServerThread ?. session ?. lastError ?? null )
14281445 : localDraftError ;
14291446 const runtimeMode = composerRuntimeMode ?? activeThread ?. runtimeMode ?? DEFAULT_RUNTIME_MODE ;
14301447 const interactionMode =
@@ -2490,10 +2507,11 @@ function ChatViewContent(props: ChatViewProps) {
24902507 const nextError = sanitizeThreadErrorMessage ( error ) ;
24912508 const nextEntry : LocalThreadErrorEntry = { message : nextError , at : Date . now ( ) } ;
24922509 if (
2493- serverThread &&
2494- targetThreadId === routeThreadRef . threadId &&
2495- serverThread . environmentId === routeThreadRef . environmentId &&
2496- serverThread . id === targetThreadId
2510+ shouldWriteThreadErrorToCurrentServerThread ( {
2511+ activeServerThread,
2512+ routeThreadRef,
2513+ targetThreadId,
2514+ } )
24972515 ) {
24982516 setLocalServerErrorsByThreadKey ( ( existing ) => {
24992517 if ( ( existing [ routeThreadKey ] ?. message ?? null ) === nextError ) {
@@ -2517,7 +2535,7 @@ function ChatViewContent(props: ChatViewProps) {
25172535 } ;
25182536 } ) ;
25192537 } ,
2520- [ draftId , routeThreadKey , routeThreadRef , serverThread ] ,
2538+ [ activeServerThread , draftId , routeThreadKey , routeThreadRef ] ,
25212539 ) ;
25222540
25232541 const focusComposer = useCallback ( ( ) => {
@@ -4471,6 +4489,7 @@ function ChatViewContent(props: ChatViewProps) {
44714489 ! activeThread ||
44724490 isSendBusy ||
44734491 isConnecting ||
4492+ threadDetailLoading ||
44744493 activeEnvironmentUnavailable ||
44754494 sendInFlightRef . current
44764495 )
@@ -5737,7 +5756,7 @@ function ChatViewContent(props: ChatViewProps) {
57375756 contentInsetEndAdjustment = { composerOverlayHeight }
57385757 onIsAtEndChange = { onIsAtEndChange }
57395758 onManualNavigation = { cancelTimelineLiveFollowForUserNavigation }
5740- hideEmptyPlaceholder = { isDraftHeroState }
5759+ hideEmptyPlaceholder = { isDraftHeroState || threadDetailLoading }
57415760 topFadeEnabled = { ! hasTimelineTopBanner }
57425761 />
57435762
@@ -5798,6 +5817,9 @@ function ChatViewContent(props: ChatViewProps) {
57985817 ) : (
57995818 < ComposerBannerStack className = "relative z-0" items = { composerBannerItems } />
58005819 ) }
5820+ { threadSyncPhase && ! activeEnvironmentUnavailable ? (
5821+ < ThreadSyncStatusPill phase = { threadSyncPhase } />
5822+ ) : null }
58015823 < div
58025824 className = "relative"
58035825 style = {
@@ -5831,6 +5853,7 @@ function ChatViewContent(props: ChatViewProps) {
58315853 phase = { phase }
58325854 isConnecting = { isConnecting }
58335855 isSendBusy = { isSendBusy }
5856+ sendDisabledReason = { threadDetailLoading ? "Messages loading" : null }
58345857 isPreparingWorktree = { isPreparingWorktree }
58355858 environmentUnavailable = { activeEnvironmentUnavailableState }
58365859 activePendingApproval = { activePendingApproval }
0 commit comments