fix(desktop): preserve Welcome banner dismissal (#5406)

## Summary

- remove the complete Welcome guidance surface when dismissal reaches
`hidden`
- preserve dismissal across the private and starter Welcome channels for
the active identity
- assert the starter channel's actual `welcome-everyone` title on
re-entry

## Why

PR #5330 introduced two deterministic Desktop E2E failures:

- the inner banner unmounted, but `welcome-composer-guidance-layer`
remained
- the re-entry test expected case-sensitive `Welcome` while navigating
to `welcome-everyone`

The state hook also scoped completion to channel IDs while `ChannelPane`
remounts during navigation. The Welcome guidance is one experience
spanning both Welcome channels, so completion now survives that remount
while remaining identity-scoped.

## Validation

At `b577eb42edffe889f63566f2457eacea720f3593`:

- `pnpm -C desktop typecheck`
- focused Biome check for all four changed files
- E2E build
- both `welcome-everywhere banner` integration tests repeated three
times: **6/6 passed**
- mandatory pre-push desktop check, typecheck, and full desktop unit
suite: **4,535 passed**
- `git diff --check`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-08-09 09:05:47 -07:00
committed by GitHub
co-authored by Carl
parent 5bf78671f4
commit 97aa9e3185
4 changed files with 39 additions and 14 deletions
@@ -216,7 +216,11 @@ export const ChannelPane = React.memo(function ChannelPane({
bannerState: welcomeComposerBannerState,
completeBanner: completeWelcomeComposerBanner,
dismissBanner: handleDismissWelcomeBanner,
} = useWelcomeComposerBanner(activeChannelId, isActiveWelcomeChannel);
} = useWelcomeComposerBanner(
activeChannelId,
isActiveWelcomeChannel,
currentPubkey ?? null,
);
const isEditInThread =
editTarget != null &&
threadHeadMessage != null &&
@@ -443,6 +443,10 @@ export function WelcomeComposerGuidanceLayer({
settingUp,
state,
}: WelcomeComposerGuidanceLayerProps) {
if (state === "hidden") {
return null;
}
return (
<div className="relative" data-testid="welcome-composer-guidance-layer">
<ComposerDockGlassBackdrop
@@ -8,11 +8,14 @@ import {
type WelcomeComposerBannerState,
} from "@/features/channels/ui/WelcomeComposerBanner";
const completedWelcomeComposerIdentityPubkeys = new Set<string>();
/**
* Manages the Welcome-channel composer hint banner's state machine.
*
* Tracks which channels have been completed within the session so the banner
* stays hidden on re-entry. Exposes three transitions:
* Remembers completion across the Welcome experience per identity for this app
* session, so the hint stays hidden while moving between the private and
* starter Welcome channels without leaking dismissal to another identity.
* - `completeBanner`: agent-mention path — plays the "Nice work." success
* animation before auto-dismissing.
* - `dismissBanner`: manual X-button path — immediately begins the slide-down
@@ -21,12 +24,12 @@ import {
export function useWelcomeComposerBanner(
activeChannelId: string | null,
isActiveWelcomeChannel: boolean,
identityPubkey: string | null,
): {
bannerState: WelcomeComposerBannerState;
completeBanner: () => void;
dismissBanner: () => void;
} {
const completedChannelIdsRef = React.useRef(new Set<string>());
const dismissTimerRef = React.useRef<number | null>(null);
const hideTimerRef = React.useRef<number | null>(null);
const [bannerState, setBannerState] =
@@ -48,15 +51,15 @@ export function useWelcomeComposerBanner(
React.useEffect(() => {
clearTimers();
if (
activeChannelId &&
isActiveWelcomeChannel &&
completedChannelIdsRef.current.has(activeChannelId)
identityPubkey &&
completedWelcomeComposerIdentityPubkeys.has(identityPubkey)
) {
setBannerState("hidden");
return;
}
setBannerState("prompt");
}, [activeChannelId, clearTimers, isActiveWelcomeChannel]);
}, [clearTimers, identityPubkey, isActiveWelcomeChannel]);
const scheduleHide = React.useCallback(() => {
hideTimerRef.current = window.setTimeout(
@@ -70,30 +73,42 @@ export function useWelcomeComposerBanner(
}, []);
const completeBanner = React.useCallback(() => {
if (!activeChannelId || !isActiveWelcomeChannel) {
if (!activeChannelId || !isActiveWelcomeChannel || !identityPubkey) {
return;
}
clearTimers();
completedChannelIdsRef.current.add(activeChannelId);
completedWelcomeComposerIdentityPubkeys.add(identityPubkey);
setBannerState("complete");
dismissTimerRef.current = window.setTimeout(() => {
setBannerState("dismissing");
dismissTimerRef.current = null;
scheduleHide();
}, WELCOME_PERSONA_ROTATION_MS + WELCOME_COMPOSER_BANNER_SUCCESS_SETTLE_MS);
}, [activeChannelId, clearTimers, isActiveWelcomeChannel, scheduleHide]);
}, [
activeChannelId,
clearTimers,
identityPubkey,
isActiveWelcomeChannel,
scheduleHide,
]);
const dismissBanner = React.useCallback(() => {
if (!activeChannelId || !isActiveWelcomeChannel) {
if (!activeChannelId || !isActiveWelcomeChannel || !identityPubkey) {
return;
}
clearTimers();
completedChannelIdsRef.current.add(activeChannelId);
completedWelcomeComposerIdentityPubkeys.add(identityPubkey);
setBannerState("dismissing");
scheduleHide();
}, [activeChannelId, clearTimers, isActiveWelcomeChannel, scheduleHide]);
}, [
activeChannelId,
clearTimers,
identityPubkey,
isActiveWelcomeChannel,
scheduleHide,
]);
return { bannerState, completeBanner, dismissBanner };
}
+3 -1
View File
@@ -3218,7 +3218,9 @@ test("welcome-everywhere banner: dismiss persists after channel re-entry", async
// Return — banner must stay hidden.
await page.getByTestId("channel-welcome-everyone").click();
await expect(page.getByTestId("chat-title")).toContainText("Welcome");
await expect(page.getByTestId("chat-title")).toContainText(
"welcome-everyone",
);
await expect(banner).toHaveCount(0);
});