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({