@@ -12,16 +12,20 @@ import type { Thread } from "../types";
1212import {
1313 MAX_HIDDEN_MOUNTED_PREVIEW_THREADS ,
1414 MAX_HIDDEN_MOUNTED_TERMINAL_THREADS ,
15+ branchMismatchKey ,
1516 buildExpiredTerminalContextToastCopy ,
1617 buildThreadTurnInterruptInput ,
1718 createLocalDispatchSnapshot ,
1819 deriveComposerSendState ,
20+ dismissBranchMismatchForSession ,
1921 getStartedThreadModelChangeBlockReason ,
2022 hasServerAcknowledgedLocalDispatch ,
23+ isBranchMismatchDismissedForSession ,
2124 reconcileMountedTerminalThreadIds ,
2225 reconcileRetainedMountedThreadIds ,
2326 resolveThreadMetadataUpdateForNextTurn ,
2427 resolveSendEnvMode ,
28+ shouldShowBranchMismatchBanner ,
2529 shouldWriteThreadErrorToCurrentServerThread ,
2630} from "./ChatView.logic" ;
2731
@@ -292,6 +296,61 @@ describe("resolveSendEnvMode", () => {
292296 } ) ;
293297} ) ;
294298
299+ describe ( "branchMismatchKey" , ( ) => {
300+ it ( "builds a key from thread id and both branches" , ( ) => {
301+ expect ( branchMismatchKey ( "thread-1" , { threadBranch : "feat/a" , currentBranch : "feat/b" } ) ) . toBe (
302+ "thread-1:feat/a:feat/b" ,
303+ ) ;
304+ } ) ;
305+
306+ it ( "returns null without a thread or mismatch" , ( ) => {
307+ expect ( branchMismatchKey ( null , { threadBranch : "a" , currentBranch : "b" } ) ) . toBeNull ( ) ;
308+ expect ( branchMismatchKey ( "thread-1" , null ) ) . toBeNull ( ) ;
309+ } ) ;
310+ } ) ;
311+
312+ describe ( "shouldShowBranchMismatchBanner" , ( ) => {
313+ const base = {
314+ hasMismatch : true ,
315+ isDismissed : false ,
316+ composerHasContent : false ,
317+ wasShownForCurrentMismatch : false ,
318+ } ;
319+
320+ it ( "stays hidden during passive browsing (even though the composer autofocuses)" , ( ) => {
321+ expect ( shouldShowBranchMismatchBanner ( base ) ) . toBe ( false ) ;
322+ } ) ;
323+
324+ it ( "shows once the composer has draft content" , ( ) => {
325+ expect ( shouldShowBranchMismatchBanner ( { ...base , composerHasContent : true } ) ) . toBe ( true ) ;
326+ } ) ;
327+
328+ it ( "stays mounted after the draft clears once shown for the current mismatch" , ( ) => {
329+ expect ( shouldShowBranchMismatchBanner ( { ...base , wasShownForCurrentMismatch : true } ) ) . toBe (
330+ true ,
331+ ) ;
332+ } ) ;
333+
334+ it ( "never shows when dismissed or without a mismatch" , ( ) => {
335+ expect (
336+ shouldShowBranchMismatchBanner ( { ...base , composerHasContent : true , isDismissed : true } ) ,
337+ ) . toBe ( false ) ;
338+ expect (
339+ shouldShowBranchMismatchBanner ( { ...base , composerHasContent : true , hasMismatch : false } ) ,
340+ ) . toBe ( false ) ;
341+ } ) ;
342+ } ) ;
343+
344+ describe ( "session branch mismatch dismissal" , ( ) => {
345+ it ( "tracks dismissed keys and treats other keys as active" , ( ) => {
346+ expect ( isBranchMismatchDismissedForSession ( "t1:a:b" ) ) . toBe ( false ) ;
347+ dismissBranchMismatchForSession ( "t1:a:b" ) ;
348+ expect ( isBranchMismatchDismissedForSession ( "t1:a:b" ) ) . toBe ( true ) ;
349+ expect ( isBranchMismatchDismissedForSession ( "t1:a:c" ) ) . toBe ( false ) ;
350+ expect ( isBranchMismatchDismissedForSession ( null ) ) . toBe ( false ) ;
351+ } ) ;
352+ } ) ;
353+
295354describe ( "reconcileMountedTerminalThreadIds" , ( ) => {
296355 it ( "keeps open threads and makes the active thread most recent" , ( ) => {
297356 expect (
0 commit comments