@@ -264,7 +264,13 @@ import {
264264 ProviderStatusBanner ,
265265 shouldShowProviderStatusBanner ,
266266} from "./chat/ProviderStatusBanner" ;
267- import { ThreadErrorBanner } from "./chat/ThreadErrorBanner" ;
267+ import {
268+ dismissThreadErrorBannerForSession ,
269+ getThreadErrorBannerKey ,
270+ isThreadErrorBannerDismissedForSession ,
271+ shouldShowThreadErrorBanner ,
272+ ThreadErrorBanner ,
273+ } from "./chat/ThreadErrorBanner" ;
268274import { resolveThreadPr } from "./ThreadStatusIndicators" ;
269275import { ComposerBannerStack , type ComposerBannerStackItem } from "./chat/ComposerBannerStack" ;
270276import { ThreadSyncStatusPill } from "./chat/ThreadSyncStatusPill" ;
@@ -1493,6 +1499,24 @@ function ChatViewContent(props: ChatViewProps) {
14931499 const threadError = isServerThread
14941500 ? ( localServerError ?? activeServerThread ?. session ?. lastError ?? null )
14951501 : localDraftError ;
1502+ // Dismissals can only mask the shown error, never clear it: a server thread
1503+ // keeps its error in session.lastError, so clearing the local shadow would
1504+ // just fall through to the persisted one. Mask the current error until a
1505+ // different error arrives, mirroring the provider status banner.
1506+ const threadErrorBannerKey = getThreadErrorBannerKey ( routeThreadKey , threadError ) ;
1507+ const visibleThreadError = shouldShowThreadErrorBanner (
1508+ routeThreadKey ,
1509+ threadError ,
1510+ isThreadErrorBannerDismissedForSession ( threadErrorBannerKey ) ,
1511+ )
1512+ ? threadError
1513+ : null ;
1514+ // Dismissing only mutates the session-scoped mask set, which does not
1515+ // trigger a render on its own; setThreadError(null) can also bail when the
1516+ // local shadow is already empty and the banner is driven purely by
1517+ // session.lastError. Bump a tick so the banner hides immediately. Mirrors
1518+ // the branch mismatch banner.
1519+ const [ , setThreadErrorBannerDismissTick ] = useState ( 0 ) ;
14961520 const runtimeMode = composerRuntimeMode ?? activeThread ?. runtimeMode ?? DEFAULT_RUNTIME_MODE ;
14971521 // Plan mode is legacy (Settings → Beta). With the flag off the effective
14981522 // mode is forced to "default" — even for threads with a stored plan mode —
@@ -2618,7 +2642,7 @@ function ChatViewContent(props: ChatViewProps) {
26182642 )
26192643 ? activeProviderStatus
26202644 : null ;
2621- const hasTimelineTopBanner = Boolean ( threadError ) || visibleProviderStatus !== null ;
2645+ const hasTimelineTopBanner = Boolean ( visibleThreadError ) || visibleProviderStatus !== null ;
26222646 const activeProjectCwd = activeProject ?. workspaceRoot ?? null ;
26232647 const activeThreadWorktreePath = activeThread ?. worktreePath ?? null ;
26242648 const activeWorkspaceRoot = activeThreadWorktreePath ?? activeProjectCwd ?? undefined ;
@@ -6146,8 +6170,12 @@ function ChatViewContent(props: ChatViewProps) {
61466170 </ header >
61476171
61486172 < ThreadErrorBanner
6149- error = { threadError }
6150- onDismiss = { ( ) => setThreadError ( activeThread . id , null ) }
6173+ error = { visibleThreadError }
6174+ onDismiss = { ( ) => {
6175+ setThreadError ( activeThread . id , null ) ;
6176+ dismissThreadErrorBannerForSession ( threadErrorBannerKey ) ;
6177+ setThreadErrorBannerDismissTick ( ( tick ) => tick + 1 ) ;
6178+ } }
61516179 />
61526180 { /* Main content area with optional plan sidebar */ }
61536181 < div className = "flex min-h-0 min-w-0 flex-1" >
0 commit comments