From 104d5da573ee5acfe4dca1498e6bb10cf89216c1 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Fri, 12 Jun 2026 17:49:37 -0700 Subject: [PATCH] Improve Linear setup toast layout --- .../LinearAgentSkillSetupPrompt.test.tsx | 69 ++++++++++++------- .../sidebar/LinearAgentSkillSetupPrompt.tsx | 23 ++++--- ...-agent-skill-setup-reminder-toast-body.tsx | 26 +++++++ 3 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 src/renderer/src/components/sidebar/linear-agent-skill-setup-reminder-toast-body.tsx diff --git a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx index 0ce5c8c8a10..46cd46380cf 100644 --- a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx +++ b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment happy-dom -import { act, type ComponentProps, type ReactNode } from 'react' +import { act, isValidElement, type ComponentProps, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { toast } from 'sonner' import type { CliInstallStatus } from '../../../../shared/cli-install-types' @@ -137,6 +137,16 @@ async function clickBodyButton(label: string): Promise { }) } +type ReminderToastDescriptionProps = { + description?: string + onOpen?: () => void +} + +function getLastReminderToastDescriptionProps(): ReminderToastDescriptionProps { + const description = vi.mocked(toast.warning).mock.calls.at(-1)?.[1]?.description + return isValidElement(description) ? (description.props as ReminderToastDescriptionProps) : {} +} + describe('LinearAgentSkillSetupPrompt', () => { beforeEach(() => { mocks.skillState.installed = false @@ -415,14 +425,14 @@ describe('LinearAgentSkillSetupPrompt', () => { 'Orca CLI and Linear skill are missing', expect.objectContaining({ id: 'linear-agent-skill-setup-orca.linearTicketsSkill.setupDismissed.host', - description: - 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI.', - action: { - label: 'Set up Linear access', - onClick: expect.any(Function) - } + description: expect.anything() }) ) + expect(getLastReminderToastDescriptionProps().description).toBe( + 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI.' + ) + expect(getLastReminderToastDescriptionProps().onOpen).toEqual(expect.any(Function)) + expect(vi.mocked(toast.warning).mock.calls.at(-1)?.[1]?.action).toBeUndefined() }) it('treats closing the modal-only dialog as a casual dismissal', async () => { @@ -442,10 +452,12 @@ describe('LinearAgentSkillSetupPrompt', () => { 'Orca CLI and Linear skill are missing', expect.objectContaining({ id: 'linear-agent-skill-setup-orca.linearTicketsSkill.setupDismissed.host', - description: - 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI.' + description: expect.anything() }) ) + expect(getLastReminderToastDescriptionProps().description).toBe( + 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI.' + ) }) it('keeps remote setup nuance in reminder toast copy', async () => { @@ -457,10 +469,12 @@ describe('LinearAgentSkillSetupPrompt', () => { expect(toast.warning).toHaveBeenCalledWith( 'Orca CLI and Linear skill are missing', expect.objectContaining({ - description: - 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI. Remote agent environments may need their own setup.' + description: expect.anything() }) ) + expect(getLastReminderToastDescriptionProps().description).toBe( + 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI. Remote agent environments may need their own setup.' + ) }) it('keeps WSL target nuance in reminder toast copy', async () => { @@ -490,10 +504,12 @@ describe('LinearAgentSkillSetupPrompt', () => { expect(toast.warning).toHaveBeenCalledWith( 'Orca CLI and Linear skill are missing', expect.objectContaining({ - description: - 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI. This setup runs in the selected WSL agent runtime.' + description: expect.anything() }) ) + expect(getLastReminderToastDescriptionProps().description).toBe( + 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI. This setup runs in the selected WSL agent runtime.' + ) }) it('emits at most one reminder toast for a single eligible modal-only activation', async () => { @@ -530,17 +546,18 @@ describe('LinearAgentSkillSetupPrompt', () => { await unmountPrompt() await renderPrompt({ linked: true, remote: false, surface: 'modal' }) - const action = vi.mocked(toast.warning).mock.calls.at(-1)?.[1]?.action as - | { onClick?: () => void } - | undefined + const onOpen = getLastReminderToastDescriptionProps().onOpen await act(async () => { - action?.onClick?.() + onOpen?.() }) expect(document.body.textContent).toContain( 'Enable agents to read and edit the attached Linear ticket.' ) expect(document.body.textContent).toContain('Mock install') + expect(toast.dismiss).toHaveBeenCalledWith( + 'linear-agent-skill-setup-orca.linearTicketsSkill.setupDismissed.host' + ) }) it('permanently dismisses modal-only reminders after a toast has appeared', async () => { @@ -549,16 +566,18 @@ describe('LinearAgentSkillSetupPrompt', () => { await unmountPrompt() await renderPrompt({ linked: true, remote: false, surface: 'modal' }) - const action = vi.mocked(toast.warning).mock.calls.at(-1)?.[1]?.action as - | { onClick?: () => void } - | undefined + const onOpen = getLastReminderToastDescriptionProps().onOpen await act(async () => { - action?.onClick?.() + onOpen?.() }) + expect(toast.dismiss).toHaveBeenCalledWith( + 'linear-agent-skill-setup-orca.linearTicketsSkill.setupDismissed.host' + ) + mocks.toastDismiss.mockClear() await clickBodyButton("Don't show again") expect(window.localStorage.getItem(HOST_DISMISS_STORAGE_KEY)).toBe('1') - expect(toast.dismiss).toHaveBeenCalledWith('linear-setup-toast-id') + expect(toast.dismiss).not.toHaveBeenCalled() expect(document.body.textContent).not.toContain( 'Enable agents to read and edit the attached Linear ticket.' ) @@ -618,9 +637,11 @@ describe('LinearAgentSkillSetupPrompt', () => { 'Orca CLI and Linear skill are missing', expect.objectContaining({ id: 'linear-agent-skill-setup-orca.linearTicketsSkill.setupDismissed.host', - description: - 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI.' + description: expect.anything() }) ) + expect(getLastReminderToastDescriptionProps().description).toBe( + 'Install the Orca CLI and the Linear skill to enable your agents to read and edit Linear tasks through the Orca CLI.' + ) }) }) diff --git a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx index 699af1717be..ddc7e9c1aec 100644 --- a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx +++ b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx @@ -44,6 +44,7 @@ import { getLinearAgentSkillSetupToastTitle, getLinearAgentSkillSetupToastDescription } from './linear-agent-skill-setup-copy' +import { LinearAgentSkillSetupReminderToastBody } from './linear-agent-skill-setup-reminder-toast-body' import { getCurrentPlatform, getLinearPromptAgentRuntime, @@ -206,16 +207,20 @@ export function LinearAgentSkillSetupPrompt({ } state.toastCount += 1 state.lastToastActivationId = activationId + const toastId = `linear-agent-skill-setup-${localDismissStorageKey}` + const openSetupFromToast = (): void => { + toast.dismiss(toastId) + state.activeToastId = undefined + setSetupDialogOpen(true) + } state.activeToastId = toast.warning(toastTitle, { - id: `linear-agent-skill-setup-${localDismissStorageKey}`, - description: toastDescription, - action: { - label: translate( - 'auto.components.sidebar.LinearAgentSkillSetupPrompt.openSetup', - 'Set up Linear access' - ), - onClick: () => setSetupDialogOpen(true) - } + id: toastId, + description: ( + + ) }) }, [localDismissStorageKey, missingSetup, setupDialogOpen, surface, toastDescription, toastTitle]) diff --git a/src/renderer/src/components/sidebar/linear-agent-skill-setup-reminder-toast-body.tsx b/src/renderer/src/components/sidebar/linear-agent-skill-setup-reminder-toast-body.tsx new file mode 100644 index 00000000000..eff4cf4a673 --- /dev/null +++ b/src/renderer/src/components/sidebar/linear-agent-skill-setup-reminder-toast-body.tsx @@ -0,0 +1,26 @@ +import { Button } from '@/components/ui/button' +import { translate } from '@/i18n/i18n' + +// Why: Sonner's native action sits beside the description and pinches multi-line +// copy; this keeps the CTA in a full-width footer below the text. +export function LinearAgentSkillSetupReminderToastBody({ + description, + onOpen +}: { + description: string + onOpen: () => void +}): React.JSX.Element { + return ( +
+

{description}

+
+ +
+
+ ) +}