From d8dddf1954077e9174067bd41d0a99596e84dbd3 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 03:00:38 +0000 Subject: [PATCH 1/5] fix(web): thread undo notice replaces the sidebar wordmark instead of pushing the list --- .../src/components/sidebar/SidebarChrome.tsx | 6 ++++-- .../SidebarThreadUndoNotice.browser.test.tsx | 2 +- .../sidebar/SidebarThreadUndoNotice.tsx | 21 ++++++++++++------- apps/web/src/components/ui/button.tsx | 1 + 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index 811848e24218b..00cf5b236e864 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { memo } from "react"; import { Link, useLocation, useNavigate } from "@tanstack/react-router"; +import { useThreadUndoNotice } from "../../hooks/showThreadUndoNotice"; import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { usePullRequestsSupported } from "../../state/environments"; @@ -37,6 +38,7 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ isElectron: boolean; }) { const stageLabel = useEnvironmentStageLabel(); + const showingUndoNotice = useThreadUndoNotice((state) => state.notice !== null); const environmentIdentificationMode = useEnvironmentIdentificationMode(); const backdropVariant = resolveSidebarStageBackdropVariant( stageLabel, @@ -63,7 +65,8 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ />
- + + {showingUndoNotice ? null : } {pillLabel ? (
@@ -231,7 +234,6 @@ const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { export const SidebarChromeFooter = memo(function SidebarChromeFooter() { return ( - diff --git a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.browser.test.tsx b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.browser.test.tsx index 049ef07ad8056..5633b7b06c785 100644 --- a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.browser.test.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.browser.test.tsx @@ -12,7 +12,7 @@ const spoken = () => describe("SidebarThreadUndoNotice", () => { it("speaks the notice, and drops it when the notice clears before it is spoken", async () => { using _notice = { [Symbol.dispose]: () => useThreadUndoNotice.setState({ notice: null }) }; - await using _screen = await render(); + await using _screen = await render(); using _timers = fakeTimers(); const showSettled = () => act(async () => diff --git a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx index 71818e154a1c1..3ba6716be0a82 100644 --- a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx @@ -4,11 +4,11 @@ import { useEffect } from "react"; import { undoLatestThreadAction, useThreadUndoNotice } from "../../hooks/showThreadUndoNotice"; import { shortcutLabelForCommand } from "../../keybindings"; import { announce, cancelAnnouncement } from "../../lib/announce"; +import { cn } from "../../lib/utils"; import { primaryServerKeybindingsAtom } from "../../state/server"; -import { Alert, AlertDescription } from "../ui/alert"; import { InlineButton } from "../ui/button"; -export function SidebarThreadUndoNotice() { +export function SidebarThreadUndoNotice({ onBackdrop }: { onBackdrop: boolean }) { const notice = useThreadUndoNotice((state) => state.notice); const keybindings = useAtomValue(primaryServerKeybindingsAtom); @@ -28,10 +28,17 @@ export function SidebarThreadUndoNotice() { if (!summary) return null; return ( - - - {summary}, {undoLabel} - - +
+ {summary},{" "} + + {undoLabel} + +
); } diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 5637baf28abbc..1b93336b8de06 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -103,6 +103,7 @@ const inlineButtonVariants = cva( variants: { tone: { default: "text-foreground", + inherit: "text-inherit", muted: "text-muted-foreground hover:text-foreground", destructive: "text-destructive/80 hover:text-destructive", picker: From 15d32903795a9df84c1a051dbfe355d0b542d7d2 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 03:38:42 +0000 Subject: [PATCH 2/5] fix(web): thread undo notice sits above the bottom shelves instead of the header --- apps/web/src/components/LegacySidebar.tsx | 2 +- apps/web/src/components/Sidebar.tsx | 5 +++++ apps/web/src/components/sidebar/SidebarChrome.tsx | 12 +++++++----- .../src/components/sidebar/SidebarListMarkers.tsx | 8 +++++++- .../sidebar/SidebarThreadUndoNotice.browser.test.tsx | 2 +- .../components/sidebar/SidebarThreadUndoNotice.tsx | 7 ++----- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index bf5a40095f2a1..ad0d3350daf5c 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -3692,7 +3692,7 @@ export default function LegacySidebar({ routeReveal }: { routeReveal: SidebarRou attachProjectListAutoAnimateRef={attachProjectListAutoAnimateRef} projectsLength={projectsLength} /> - + ); } diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index ca767d2c88909..24fb5dcadea3b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3138,6 +3138,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve className="mt-auto" label="Working" count={workingThreads.length} + hostsUndoNotice toggle={{ expanded: workingShelfExpanded, onToggle: toggleWorkingShelf, @@ -3153,6 +3154,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve className={cn(workingThreads.length === 0 && "mt-auto")} label="Snoozed" count={snoozedThreads.length} + hostsUndoNotice={workingThreads.length === 0} toggle={{ expanded: snoozedShelfExpanded, onToggle: toggleSnoozedShelf, @@ -3170,6 +3172,9 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve )} label="Settled" count={settledCount} + hostsUndoNotice={ + workingThreads.length + snoozedThreads.length === 0 + } dragging={from !== null} isDropTarget={dragTargetSection === "settled"} toggle={{ diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index 00cf5b236e864..2ebe84961eb86 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -3,7 +3,6 @@ import type { ReactNode } from "react"; import { memo } from "react"; import { Link, useLocation, useNavigate } from "@tanstack/react-router"; -import { useThreadUndoNotice } from "../../hooks/showThreadUndoNotice"; import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { usePullRequestsSupported } from "../../state/environments"; @@ -38,7 +37,6 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ isElectron: boolean; }) { const stageLabel = useEnvironmentStageLabel(); - const showingUndoNotice = useThreadUndoNotice((state) => state.notice !== null); const environmentIdentificationMode = useEnvironmentIdentificationMode(); const backdropVariant = resolveSidebarStageBackdropVariant( stageLabel, @@ -65,8 +63,7 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ />
- - {showingUndoNotice ? null : } + {pillLabel ? (
@@ -231,9 +228,14 @@ const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { ); }); -export const SidebarChromeFooter = memo(function SidebarChromeFooter() { +export const SidebarChromeFooter = memo(function SidebarChromeFooter({ + showUndoNotice = false, +}: { + showUndoNotice?: boolean; +}) { return ( + {showUndoNotice ? : null} diff --git a/apps/web/src/components/sidebar/SidebarListMarkers.tsx b/apps/web/src/components/sidebar/SidebarListMarkers.tsx index 611219b16e63e..23ea1e7285072 100644 --- a/apps/web/src/components/sidebar/SidebarListMarkers.tsx +++ b/apps/web/src/components/sidebar/SidebarListMarkers.tsx @@ -10,6 +10,7 @@ import { type SidebarListMarker, } from "../Sidebar.logic"; import { SIDEBAR_SORTABLE_DRAG_DISABLED } from "./SidebarThreadRow"; +import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice"; function SortableSidebarMarker(props: { marker: SidebarListMarker; @@ -109,6 +110,8 @@ export function SidebarSectionHeader(props: { dragging?: boolean; isDropTarget?: boolean; toggle: { expanded: boolean; onToggle: () => void }; + /** The first shelf at the bottom of the list hosts the undo notice just above itself. */ + hostsUndoNotice?: boolean; }) { const shelf = props.marker === "working-header" @@ -122,8 +125,11 @@ export function SidebarSectionHeader(props: { + {props.hostsUndoNotice ? ( + + ) : null} describe("SidebarThreadUndoNotice", () => { it("speaks the notice, and drops it when the notice clears before it is spoken", async () => { using _notice = { [Symbol.dispose]: () => useThreadUndoNotice.setState({ notice: null }) }; - await using _screen = await render(); + await using _screen = await render(); using _timers = fakeTimers(); const showSettled = () => act(async () => diff --git a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx index 3ba6716be0a82..28a370ea43aa8 100644 --- a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx @@ -8,7 +8,7 @@ import { cn } from "../../lib/utils"; import { primaryServerKeybindingsAtom } from "../../state/server"; import { InlineButton } from "../ui/button"; -export function SidebarThreadUndoNotice({ onBackdrop }: { onBackdrop: boolean }) { +export function SidebarThreadUndoNotice({ className }: { className?: string }) { const notice = useThreadUndoNotice((state) => state.notice); const keybindings = useAtomValue(primaryServerKeybindingsAtom); @@ -30,10 +30,7 @@ export function SidebarThreadUndoNotice({ onBackdrop }: { onBackdrop: boolean }) return (
{summary},{" "} From b541d9cea0740e0cea0be9d301ba245a641a3092 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 03:45:09 +0000 Subject: [PATCH 3/5] fix(web): undo notice falls back to the footer while searching or when the list is empty --- apps/web/src/components/Sidebar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 24fb5dcadea3b..9fed2f0f8007e 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3261,7 +3261,9 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve ) : null} - + ); } From 5c36b3e283cb23ae56425dccadebae02818495f6 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 04:00:51 +0000 Subject: [PATCH 4/5] fix(web): undo notice fades in and out and keeps the undo link emphasized --- .../sidebar/SidebarThreadUndoNotice.tsx | 24 +++++++++++++------ apps/web/src/components/ui/button.tsx | 1 - 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx index 28a370ea43aa8..de0ca354274ec 100644 --- a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx @@ -1,5 +1,5 @@ import { useAtomValue } from "@effect/atom-react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { undoLatestThreadAction, useThreadUndoNotice } from "../../hooks/showThreadUndoNotice"; import { shortcutLabelForCommand } from "../../keybindings"; @@ -25,17 +25,27 @@ export function SidebarThreadUndoNotice({ className }: { className?: string }) { else cancelAnnouncement("thread-undo-notice"); }, [summary, undoLabel]); - if (!summary) return null; + // Keeps the last text on screen while it fades out, then unmounts on transitionend. + const [shownSummary, setShownSummary] = useState(summary); + if (summary && summary !== shownSummary) setShownSummary(summary); + const text = summary ?? shownSummary; + if (!text) return null; return (
{ + if (!summary && event.target === event.currentTarget) setShownSummary(null); + }} + className={cn( + "truncate px-2 text-2xs leading-6 text-sidebar-muted-foreground transition-opacity duration-200 ease-out starting:opacity-0", + !summary && "opacity-0", + className, + )} > - {summary},{" "} - - {undoLabel} - + {text}, {undoLabel}
); } diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 1b93336b8de06..5637baf28abbc 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -103,7 +103,6 @@ const inlineButtonVariants = cva( variants: { tone: { default: "text-foreground", - inherit: "text-inherit", muted: "text-muted-foreground hover:text-foreground", destructive: "text-destructive/80 hover:text-destructive", picker: From 6c2d9f2d46cb322446aead47bf8425f6f62f1053 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 05:17:39 +0000 Subject: [PATCH 5/5] fix(web): undo notice truncates the summary and keeps the undo link visible --- apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx index 0b7694075e492..72b5754a1936d 100644 --- a/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx @@ -31,12 +31,12 @@ export function SidebarThreadUndoNotice({ className }: { className?: string }) { if (!summary && event.target === event.currentTarget) setShownSummary(null); }} className={cn( - "truncate px-2 text-2xs leading-6 text-sidebar-muted-foreground transition-opacity duration-200 ease-out starting:opacity-0", + "flex gap-1 px-2 text-2xs leading-6 text-sidebar-muted-foreground transition-opacity duration-200 ease-out starting:opacity-0", !summary && "opacity-0", className, )} > - {text},{" "} + {text}, {threadUndoLabel(keybindings)}
);