diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index ec945a595de32452a36c05e498f3b202e4782558..6d8f6fe1f1aa2a29f87423ca971768b20531b739 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -38,6 +38,7 @@ import { gitStatusQueryOptions, invalidateGitQueries, } from "~/lib/gitReactQuery"; +import { useStore } from "~/store"; import { preferredTerminalEditor, resolvePathLinkTarget } from "~/terminal-links"; interface GitActionsControlProps { @@ -136,6 +137,12 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) { } export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) { + const { state } = useStore(); + const activeThreadId = state.activeThreadId; + const threadToastData = useMemo( + () => (activeThreadId ? { threadId: activeThreadId } : undefined), + [activeThreadId], + ); const queryClient = useQueryClient(); const [activeDialogAction, setActiveDialogAction] = useState(null); const [dialogCommitMessage, setDialogCommitMessage] = useState(""); @@ -190,25 +197,34 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp const openExistingPr = useCallback(async () => { if (!api) { - toastManager.add({ type: "error", title: "Link opening is unavailable." }); + toastManager.add({ + type: "error", + title: "Link opening is unavailable.", + data: threadToastData, + }); return; } const prUrl = gitStatus?.openPr?.url ?? null; if (!prUrl) { - toastManager.add({ type: "error", title: "No open PR found." }); + toastManager.add({ + type: "error", + title: "No open PR found.", + data: threadToastData, + }); return; } const promise = api.shell.openExternal(prUrl); toastManager.promise(promise, { - loading: { title: "Opening PR..." }, - success: { title: "PR opened" }, + loading: { title: "Opening PR...", data: threadToastData }, + success: { title: "PR opened", data: threadToastData }, error: (err) => ({ title: "Unable to open PR link", description: err instanceof Error ? err.message : "An error occurred.", + data: threadToastData, }), }); void promise.catch(() => undefined); - }, [api, gitStatus?.openPr?.url]); + }, [api, gitStatus?.openPr?.url, threadToastData]); const runGitActionWithToast = useCallback( async ({ @@ -238,6 +254,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp type: "loading", title: progressStages[0] ?? "Running git action...", timeout: 0, + data: threadToastData, }); let stageIndex = 0; @@ -247,6 +264,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp title: progressStages[stageIndex] ?? "Running git action...", type: "loading", timeout: 0, + data: threadToastData, }); }, 1100); @@ -279,6 +297,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp title: resultToast.title, description: resultToast.description, timeout: 0, + data: threadToastData, ...(shouldOfferPushCta ? { actionProps: { @@ -320,6 +339,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp type: "error", title: "Action failed", description: err instanceof Error ? err.message : "An error occurred.", + data: threadToastData, }); } }, @@ -332,6 +352,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp maybeConfirmPushToDefaultBranch, runImmediateGitActionMutation, setActiveDialogAction, + threadToastData, ], ); @@ -343,17 +364,19 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp if (quickAction.kind === "run_pull") { const promise = pullMutation.mutateAsync(); toastManager.promise(promise, { - loading: { title: "Pulling..." }, + loading: { title: "Pulling...", data: threadToastData }, success: (result) => ({ title: result.status === "pulled" ? "Pulled" : "Already up to date", description: result.status === "pulled" ? `Updated ${result.branch} from ${result.upstreamBranch ?? "upstream"}` : `${result.branch} is already synchronized.`, + data: threadToastData, }), error: (err) => ({ title: "Pull failed", description: err instanceof Error ? err.message : "An error occurred.", + data: threadToastData, }), }); void promise.catch(() => undefined); @@ -364,13 +387,14 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp type: "info", title: quickAction.label, description: quickAction.hint, + data: threadToastData, }); return; } if (quickAction.action) { void runGitActionWithToast({ action: quickAction.action }); } - }, [openExistingPr, pullMutation, quickAction, runGitActionWithToast]); + }, [openExistingPr, pullMutation, quickAction, runGitActionWithToast, threadToastData]); const openDialogForMenuItem = useCallback( (item: GitActionMenuItem) => { @@ -414,6 +438,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp toastManager.add({ type: "error", title: "Editor opening is unavailable.", + data: threadToastData, }); return; } @@ -423,10 +448,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp type: "error", title: "Unable to open file", description: error instanceof Error ? error.message : "An error occurred.", + data: threadToastData, }); }); }, - [api, gitCwd], + [api, gitCwd, threadToastData], ); if (!gitCwd) return null; diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx index 8c406bcbe9e75ed966a647479089e3e16a23952f..6c9614a91a0034a10f764b873e5fd6de64b6dd67 100644 --- a/apps/web/src/components/ui/toast.tsx +++ b/apps/web/src/components/ui/toast.tsx @@ -11,9 +11,15 @@ import { import { cn } from "~/lib/utils"; import { buttonVariants } from "~/components/ui/button"; +import { useStore } from "~/store"; -const toastManager = Toast.createToastManager(); -const anchoredToastManager = Toast.createToastManager(); +type ThreadToastData = { + threadId?: string | null; + tooltipStyle?: boolean; +}; + +const toastManager = Toast.createToastManager(); +const anchoredToastManager = Toast.createToastManager(); const TOAST_ICONS = { error: CircleAlertIcon, @@ -35,6 +41,15 @@ interface ToastProviderProps extends Toast.Provider.Props { position?: ToastPosition; } +function shouldRenderForActiveThread( + data: ThreadToastData | undefined, + activeThreadId: string | null, +): boolean { + const toastThreadId = data?.threadId; + if (!toastThreadId) return true; + return toastThreadId === activeThreadId; +} + function ToastProvider({ children, position = "top-right", ...props }: ToastProviderProps) { return ( @@ -45,7 +60,9 @@ function ToastProvider({ children, position = "top-right", ...props }: ToastProv } function Toasts({ position = "top-right" }: { position: ToastPosition }) { - const { toasts } = Toast.useToastManager(); + const { state } = useStore(); + const { toasts } = Toast.useToastManager(); + const activeThreadId = state.activeThreadId; const isTop = position.startsWith("top"); return ( @@ -64,91 +81,96 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) { data-position={position} data-slot="toast-viewport" > - {toasts.map((toast) => { - const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null; + {toasts + .filter((toast) => shouldRenderForActiveThread(toast.data, activeThreadId)) + .map((toast) => { + const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null; - return ( - - -
- {Icon && ( -
- + return ( + + +
+ {Icon && ( +
+ +
+ )} + +
+ +
- )} - -
- -
-
- {toast.actionProps && ( - - {toast.actionProps.children} - - )} -
-
- ); - })} + {toast.actionProps && ( + + {toast.actionProps.children} + + )} + + + ); + })} ); @@ -164,76 +186,80 @@ function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) { } function AnchoredToasts() { - const { toasts } = Toast.useToastManager(); + const { state } = useStore(); + const { toasts } = Toast.useToastManager(); + const activeThreadId = state.activeThreadId; return ( - {toasts.map((toast) => { - const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null; - const tooltipStyle = (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false; - const positionerProps = toast.positionerProps; + {toasts + .filter((toast) => shouldRenderForActiveThread(toast.data, activeThreadId)) + .map((toast) => { + const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null; + const tooltipStyle = toast.data?.tooltipStyle ?? false; + const positionerProps = toast.positionerProps; - if (!positionerProps?.anchor) { - return null; - } + if (!positionerProps?.anchor) { + return null; + } - return ( - - - {tooltipStyle ? ( - - - - ) : ( - -
- {Icon && ( -
- + + {tooltipStyle ? ( + + + + ) : ( + +
+ {Icon && ( +
+ +
+ )} + +
+ +
+
+ {toast.actionProps && ( + + {toast.actionProps.children} + )} - -
- - -
-
- {toast.actionProps && ( - - {toast.actionProps.children} - - )} - - )} - - - ); - })} + + )} + + + ); + })} );