Skip to content

[Bug]: Hover-only "Discard draft" X in the sidebar deletes the draft on one click, no confirmation or undo #12735

Description

@myfunc

Area

apps/web (sidebar, shared with the desktop app)

Summary

Each thread row with an unsent draft shows an X ("Discard draft") on hover, at the right edge next to Snooze and Settle. One click on it deletes the draft text and attachments. There is no confirmation, no undo, no toast. The X only appears on hover, so the first time you learn it exists is when your message is gone. I have lost drafts to it several times already.

Steps to reproduce

  1. Open a thread, type a message in the composer, do not send.
  2. Open another thread. The first row now shows the amber draft marker.
  3. Move the pointer onto the first row to go back to it. The hover strip at the right edge shows X, Snooze and Settle over the time/status label.
  4. Click while the pointer is on the X.

Expected behavior

A destructive action on user text needs a guard: a confirmation before, or an undo (toast) after. Alternatively the X should not sit inside the row's own click target, so a click meant to open the thread cannot delete its draft.

Actual behavior

The draft is cleared at once and the row loses its marker. Nothing brings the text back. The standalone new-thread draft rows above the pinned block have the same X with the same result.

Hover state right before the click. The X sits where the pointer lands when going back to the thread:

Sidebar thread row on hover: Discard draft X next to Snooze and Settle

Source

Checked at main @ 7445aa733.

  • Thread row: X is rendered only on hover, inside the row, next to Snooze/Settle:
    {props.settlementSupported || showSnoozeButton || hasUnsentDraft ? (
    <span
    className={cn(
    // focus-visible, not focus-within: a mouse click leaves
    // the Settle button focused, and a plain focus-within
    // would keep the controls pinned over the status label
    // once the pointer moves away (e.g. after a failed
    // settle) instead of cross-fading back.
    "pointer-events-none absolute inset-y-0 right-0 flex items-stretch opacity-0 transition-opacity has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:static has-[:focus-visible]:opacity-100 group-hover/sidebar-row:pointer-events-auto group-hover/sidebar-row:static group-hover/sidebar-row:opacity-100",
    snoozeMenuOpen && "pointer-events-auto static opacity-100",
    )}
    >
    {hasUnsentDraft ? (
    <Tooltip>
    <TooltipTrigger
    render={
    <button
    type="button"
    aria-label="Discard draft"
    onClick={handleDiscardDraftClick}
    className="inline-flex cursor-pointer items-center rounded-md bg-transparent px-1.5 text-xs text-muted-foreground hover:text-foreground"
    />
    }
    >
    <XIcon className="size-3.5" />
    </TooltipTrigger>
    <TooltipPopup side="top">Discard draft</TooltipPopup>
    </Tooltip>
    ) : null}
  • Its handler releases uploads and clears the composer, nothing else:
    const handleDiscardDraftClick = useCallback(
    (event: ReactMouseEvent) => {
    event.preventDefault();
    event.stopPropagation();
    releaseComposerDraftUploads(threadRef);
    clearComposerContent(threadRef);
    },
    [clearComposerContent, threadRef],
    );
  • New-thread draft row: same X, calls clearDraftThread:
    <Tooltip>
    <TooltipTrigger
    render={
    <button
    type="button"
    aria-label="Discard draft"
    onClick={handleDiscard}
    className="pointer-events-none inline-flex cursor-pointer items-center rounded-md bg-transparent px-1 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/sidebar-row:pointer-events-auto group-hover/sidebar-row:opacity-100"
    >
    <XIcon className="size-3" />
    </button>
    }
    />
    <TooltipPopup side="top">Discard draft</TooltipPopup>
    and
    const handleDiscard = useCallback(
    (draftId: DraftId) => {
    // The /draft/$draftId route redirects home on its own when the draft
    // it renders disappears, so discarding the open draft needs no
    // special-casing here.
    releaseComposerDraftUploads(draftId);
    clearDraftThread(draftId);
    },
    [clearDraftThread],
    );
  • Introduced in feat(web): show which sidebar threads hold an unsent draft #9658 (thread rows) and feat(web): keep unsent drafts one click away in the sidebar #5777 (draft rows). docs/user/thread-sidebar.md does not mention the control.

Relation to existing issues

Impact

Major degradation or frequent failure

Version or commit

Desktop 0.0.42, source checked at main @ 7445aa733

Environment

Windows 11 Home 10.0.26200, T3 Code desktop (Alpha), provider Claude Code

Workaround

None after the click. Prevention only: click the left part of the row, or copy the text before leaving the thread.

lovely composed in pair with Claude Fable 5.1 in Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions