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.
This commit is contained in:
Bohdan Triapitsyn
2026-09-07 17:58:03 +03:00
parent 4915658de1
commit 78e3ab9744
3 changed files with 23 additions and 7 deletions
@@ -455,6 +455,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
);
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<ChatInputProps> = ({
selectedBranchLabel={selectedDraftBranchLabel}
selectedBranchIsKnown={selectedDraftBranchIsKnown}
hasUncommittedChanges={selectedDraftDirectoryHasUncommittedChanges}
announceDirtyState={newSessionDraftAnnouncesDirtyState}
projectRootBranchOption={projectRootBranchOption}
worktreeBranchOptions={worktreeBranchOptions}
branchItems={draftBranchItems}
@@ -3134,6 +3136,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
selectedProject={selectedDraftProject}
selectedBranchLabel={selectedDraftBranchLabel}
hasUncommittedChanges={selectedDraftDirectoryHasUncommittedChanges}
announceDirtyState={newSessionDraftAnnouncesDirtyState}
showBranchSelector={shouldShowDraftBranchSelector}
theme={currentTheme}
onOpenPicker={setMobileDraftPicker}
@@ -3550,6 +3553,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
selectedBranchLabel={selectedDraftBranchLabel}
selectedBranchIsKnown={selectedDraftBranchIsKnown}
hasUncommittedChanges={selectedDraftDirectoryHasUncommittedChanges}
announceDirtyState={newSessionDraftAnnouncesDirtyState}
projectRootBranchOption={projectRootBranchOption}
worktreeBranchOptions={worktreeBranchOptions}
branchItems={draftBranchItems}
@@ -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<DraftTargetProps, 'selectedProject' | 'selectedBranchLabel' | 'showBranchSelector' | 'hasUncommittedChanges' | 'theme'>
props: Pick<DraftTargetProps, 'selectedProject' | 'selectedBranchLabel' | 'showBranchSelector' | 'hasUncommittedChanges' | 'announceDirtyState' | 'theme'>
& { 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 (
<div className="mb-1.5 flex min-w-0 items-center gap-x-2 px-0.5">
+3
View File
@@ -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<SessionUIState>()((set, get) => ({
syntheticParts: options?.syntheticParts,
targetFolderId: options?.targetFolderId,
projectContextPins: options?.projectContextPins,
openedAutomatically: options?.automatic === true,
}
set({