From 78e3ab9744b5a377b7fbd0d89d7690e85f40a903 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 7 Sep 2026 16:48:52 +0300 Subject: [PATCH] fix(chat): keep the dirty-branch tooltip quiet for an auto-opened draft At cold launch the container opens a new-session draft as a placeholder until the last session restores. The draft's dirty-directory warning announced itself by opening its tooltip for five seconds, so on mobile the tooltip appeared alone over an almost empty screen and vanished with the draft once the session came back. The draft state now records that the app opened it automatically, and the warning does not flash for such a draft. The icon still shows and the tooltip stays reachable by hover or long press; a draft the user opens flashes as before. Testing: ui type-check and lint; session store and composer tests; web build. --- packages/ui/src/components/chat/ChatInput.tsx | 4 ++++ .../chat/composer/ui/DraftTargetSelectors.tsx | 23 +++++++++++++------ packages/ui/src/sync/session-ui-store.ts | 3 +++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index d9980561..d05b8b6c 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -455,6 +455,7 @@ const ChatInputComponent: React.FC = ({ ); const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft); const newSessionDraftOpen = Boolean(newSessionDraft?.open); + const newSessionDraftAnnouncesDirtyState = newSessionDraftOpen && newSessionDraft?.openedAutomatically !== true; const draftPermissionAutoAcceptEnabled = useSessionUIStore((s) => ( s.newSessionDraft?.open ? s.newSessionDraft.permissionAutoAcceptEnabled === true : false )); @@ -3119,6 +3120,7 @@ const ChatInputComponent: React.FC = ({ selectedBranchLabel={selectedDraftBranchLabel} selectedBranchIsKnown={selectedDraftBranchIsKnown} hasUncommittedChanges={selectedDraftDirectoryHasUncommittedChanges} + announceDirtyState={newSessionDraftAnnouncesDirtyState} projectRootBranchOption={projectRootBranchOption} worktreeBranchOptions={worktreeBranchOptions} branchItems={draftBranchItems} @@ -3134,6 +3136,7 @@ const ChatInputComponent: React.FC = ({ selectedProject={selectedDraftProject} selectedBranchLabel={selectedDraftBranchLabel} hasUncommittedChanges={selectedDraftDirectoryHasUncommittedChanges} + announceDirtyState={newSessionDraftAnnouncesDirtyState} showBranchSelector={shouldShowDraftBranchSelector} theme={currentTheme} onOpenPicker={setMobileDraftPicker} @@ -3550,6 +3553,7 @@ const ChatInputComponent: React.FC = ({ selectedBranchLabel={selectedDraftBranchLabel} selectedBranchIsKnown={selectedDraftBranchIsKnown} hasUncommittedChanges={selectedDraftDirectoryHasUncommittedChanges} + announceDirtyState={newSessionDraftAnnouncesDirtyState} projectRootBranchOption={projectRootBranchOption} worktreeBranchOptions={worktreeBranchOptions} branchItems={draftBranchItems} diff --git a/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx b/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx index a025ae4c..5c6e2578 100644 --- a/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx +++ b/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx @@ -46,6 +46,14 @@ export interface DraftTargetProps { selectedBranchLabel: string | null; selectedBranchIsKnown: boolean; hasUncommittedChanges: boolean; + /** + * Whether the dirty warning may announce itself by opening its tooltip + * unprompted. Off for a draft the app opened on its own at boot: that + * draft is often only a placeholder until the last session restores, and + * a tooltip on an otherwise empty screen reads as a glitch. The warning + * icon still shows and the tooltip stays reachable by hover or long press. + */ + announceDirtyState: boolean; projectRootBranchOption: BranchOption | null; worktreeBranchOptions: readonly BranchOption[]; branchItems: readonly BranchOption[]; @@ -100,26 +108,27 @@ const DIRTY_TOOLTIP_FLASH_MS = 5000; /** * Opens the tooltip for a few seconds when the dirty state first appears, so * the warning is seen without hovering, then hands control back to hover. + * Only when the draft may announce itself — see `announceDirtyState`. */ -function useDirtyFlashTooltip(hasUncommittedChanges: boolean) { +function useDirtyFlashTooltip(hasUncommittedChanges: boolean, announce: boolean) { const [open, setOpen] = React.useState(false); React.useEffect(() => { - if (!hasUncommittedChanges) { + if (!hasUncommittedChanges || !announce) { setOpen(false); return; } setOpen(true); const timer = window.setTimeout(() => setOpen(false), DIRTY_TOOLTIP_FLASH_MS); return () => window.clearTimeout(timer); - }, [hasUncommittedChanges]); + }, [announce, hasUncommittedChanges]); return { open, onOpenChange: setOpen }; } export function DraftTargetSelectors(props: DraftTargetProps) { const { t } = useI18n(); - const dirtyTooltip = useDirtyFlashTooltip(props.hasUncommittedChanges); + const dirtyTooltip = useDirtyFlashTooltip(props.hasUncommittedChanges, props.announceDirtyState); const { projects, selectedProject, @@ -271,12 +280,12 @@ export function DraftTargetSelectors(props: DraftTargetProps) { /** Mobile: buttons that open the bottom sheets below. */ export function MobileDraftTargetTriggers( - props: Pick + props: Pick & { onOpenPicker: (picker: 'project' | 'branch') => void }, ) { const { t } = useI18n(); - const { selectedProject, selectedBranchLabel, showBranchSelector, hasUncommittedChanges, theme, onOpenPicker } = props; - const dirtyTooltip = useDirtyFlashTooltip(hasUncommittedChanges); + const { selectedProject, selectedBranchLabel, showBranchSelector, hasUncommittedChanges, announceDirtyState, theme, onOpenPicker } = props; + const dirtyTooltip = useDirtyFlashTooltip(hasUncommittedChanges, announceDirtyState); return (
diff --git a/packages/ui/src/sync/session-ui-store.ts b/packages/ui/src/sync/session-ui-store.ts index 5af3b683..4dd49c7b 100644 --- a/packages/ui/src/sync/session-ui-store.ts +++ b/packages/ui/src/sync/session-ui-store.ts @@ -327,6 +327,8 @@ export type NewSessionDraftState = { projectContextPins?: { notes: string[]; plans: string[] } target: NewSessionDraftTarget preparedChatDirectory?: string | null + /** Opened as a programmatic fallback (no session active at boot), not by the user. */ + openedAutomatically?: boolean } export type ViewportAnchor = { @@ -1280,6 +1282,7 @@ export const useSessionUIStore = create()((set, get) => ({ syntheticParts: options?.syntheticParts, targetFolderId: options?.targetFolderId, projectContextPins: options?.projectContextPins, + openedAutomatically: options?.automatic === true, } set({