diff --git a/src/renderer/src/components/browser-pane/BrowserPane.tsx b/src/renderer/src/components/browser-pane/BrowserPane.tsx index 5a0cb9d0615..0e786d64544 100644 --- a/src/renderer/src/components/browser-pane/BrowserPane.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPane.tsx @@ -2632,6 +2632,7 @@ function RemoteBrowserPagePane({ onClick={() => (markup.isActive ? markup.cancel() : void markup.start())} disabled={!frameUrl} active={markup.isActive} + surfaceActive={isActive} className="h-7 w-7" /> @@ -5122,6 +5123,7 @@ function BrowserPagePane({ onClick={() => (markup.isActive ? markup.cancel() : void markup.start())} disabled={isBlankTab || grab.state !== 'idle'} active={markup.isActive} + surfaceActive={isActive} /> + ) + + // Discovery uses a dismissable popover (outside click / Escape / blur / act) + // instead of a force-open tooltip — tooltips stay stuck when controlled open + // and mis-position against hidden triggers. Styled like BrowserImportHint + // (surface + title + body + actions), not an inverted tooltip chip. + // + // Why: Popover and Tooltip stay mounted as one persistent tree (matching + // WorkspaceStatusAppearancePopover's Tooltip-wrapping-Popover pattern) + // instead of switching root element types on `hintOpen` — swapping roots + // would unmount/remount the trigger button at the exact moments the hint + // is meant to draw the user's attention to it. return ( - - - {/* Why: wrap in a span so tooltip hover still fires when disabled. */} - - - - - - {hintOpen - ? translate( - 'auto.components.browser-pane.markup.drawHint', - 'New — draw on the page, then copy it to your agent' - ) - : label} - - + { + if (!open) { + dismissHint() + } + }} + > + + + + {/* Why: wrap in a span so the trigger still fires when disabled. */} + {button} + + + + {label} + + + { + // Why: don't steal focus from the address bar / webview on first show. + event.preventDefault() + }} + > +
+
+
+ + {translate('auto.components.browser-pane.markup.drawHintBadge', 'New')} + +
{label}
+
+

+ {translate( + 'auto.components.browser-pane.markup.drawHint', + 'Draw on the page, then copy the markup to paste into your agent.' + )} +

+
+
+ + +
+
+
+
) } diff --git a/src/renderer/src/components/browser-pane/markup/use-markup-draw-hint.test.ts b/src/renderer/src/components/browser-pane/markup/use-markup-draw-hint.test.ts new file mode 100644 index 00000000000..309a109b03b --- /dev/null +++ b/src/renderer/src/components/browser-pane/markup/use-markup-draw-hint.test.ts @@ -0,0 +1,61 @@ +/** @vitest-environment happy-dom */ +import { act, renderHook } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const HINT_KEY = 'orca.browser.markup-draw-hint-seen' + +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: { persistedUIReady: boolean }) => unknown) => + selector({ persistedUIReady: true }) +})) + +import { useMarkupDrawHint } from './use-markup-draw-hint' + +describe('useMarkupDrawHint', () => { + beforeEach(() => { + window.localStorage.removeItem(HINT_KEY) + }) + + afterEach(() => { + window.localStorage.removeItem(HINT_KEY) + }) + + it('opens once when eligible and records the seen flag', () => { + const { result } = renderHook(() => useMarkupDrawHint(true)) + + expect(result.current.hintOpen).toBe(true) + expect(window.localStorage.getItem(HINT_KEY)).toBe('true') + }) + + it('does not open when the surface is not eligible', () => { + const { result } = renderHook(() => useMarkupDrawHint(false)) + + expect(result.current.hintOpen).toBe(false) + expect(window.localStorage.getItem(HINT_KEY)).toBeNull() + }) + + it('closes when eligibility drops so a hidden pane cannot keep it open', () => { + const { result, rerender } = renderHook(({ eligible }) => useMarkupDrawHint(eligible), { + initialProps: { eligible: true } + }) + + expect(result.current.hintOpen).toBe(true) + + rerender({ eligible: false }) + expect(result.current.hintOpen).toBe(false) + }) + + it('dismisses without reopening after the first view', () => { + const first = renderHook(() => useMarkupDrawHint(true)) + expect(first.result.current.hintOpen).toBe(true) + + act(() => { + first.result.current.dismissHint() + }) + expect(first.result.current.hintOpen).toBe(false) + first.unmount() + + const second = renderHook(() => useMarkupDrawHint(true)) + expect(second.result.current.hintOpen).toBe(false) + }) +}) diff --git a/src/renderer/src/components/browser-pane/markup/use-markup-draw-hint.ts b/src/renderer/src/components/browser-pane/markup/use-markup-draw-hint.ts index f6d1ab2257e..7709c196984 100644 --- a/src/renderer/src/components/browser-pane/markup/use-markup-draw-hint.ts +++ b/src/renderer/src/components/browser-pane/markup/use-markup-draw-hint.ts @@ -2,11 +2,13 @@ import { useCallback, useEffect, useState } from 'react' import { useAppStore } from '@/store' // One-time discovery highlight for the screenshot-markup Draw button. Shows once -// per install — the first time the button is usable — so users notice the new -// tool. Gated on its own localStorage flag (not a contextual tour), so it fires -// for everyone, including users who already finished the capped browser tour. -// It stays open until the user acts (clicks Draw) or the button stops being -// usable; there is no auto-timeout. +// per install — the first time the button is usable and its surface is active — +// so users notice the new tool. Gated on its own localStorage flag (not a +// contextual tour), so it fires for everyone, including users who already +// finished the capped browser tour. +// +// Stays open until the user dismisses it (outside click / Escape / blur), clicks +// Draw, or the button stops being eligible. No auto-timeout. const MARKUP_DRAW_HINT_SEEN_KEY = 'orca.browser.markup-draw-hint-seen' // Records the first-ever view and returns whether this call is that first view. @@ -31,9 +33,10 @@ export function useMarkupDrawHint(eligible: boolean): MarkupDrawHint { const [hintOpen, setHintOpen] = useState(false) useEffect(() => { - // Why: only nudge once the app is ready and the button is usable. If it stops - // being usable while showing (grab started, markup open, blank tab), close it - // so the forced-open tooltip can't get stuck over a now-disabled button. + // Why: only nudge once the app is ready and the button is usable on a + // visible surface. If eligibility drops mid-hint (tab switch, grab + // started, markup open, blank tab), close it so a forced-open floating + // layer can't stick over a hidden or disabled control at (0,0). if (!persistedUIReady || !eligible) { setHintOpen(false) return diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 904551378c9..257200dcc77 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -13028,7 +13028,10 @@ "copiedToast": "Markup copied — paste it into your agent ({{value0}})", "copy": "Copy Markup", "drawButton": "Draw on screenshot", - "drawHint": "New — draw on the page, then copy it to your agent", + "drawHint": "Draw on the page, then copy the markup to paste into your agent.", + "drawHintBadge": "New", + "drawHintDismiss": "Got it", + "drawHintTry": "Try it", "errorAttach": "Could not attach the markup screenshot.", "errorCapture": "Could not capture the page to draw on.", "errorUnavailable": "Screenshot markup is not available on this page.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 4561dd25d61..5c0fc46acc2 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -13028,7 +13028,10 @@ "copiedToast": "Marcado copiado: pégalo en tu agente ({{value0}})", "copy": "Copiar marcado", "drawButton": "Dibujar en la captura", - "drawHint": "Nuevo: dibuja en la página y cópialo en tu agente", + "drawHint": "Dibuja en la página y copia el markup para pegarlo en tu agente.", + "drawHintBadge": "Nuevo", + "drawHintDismiss": "Entendido", + "drawHintTry": "Probar", "errorAttach": "No se pudo adjuntar la captura de pantalla con marcado.", "errorCapture": "No se pudo capturar la página para dibujar.", "errorUnavailable": "El marcado de captura de pantalla no está disponible en esta página.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index f012157d588..75efd9ce994 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -13028,7 +13028,10 @@ "copiedToast": "注釈をコピーしました — エージェントに貼り付けてください({{value0}})", "copy": "注釈をコピー", "drawButton": "スクリーンショットに描き込み", - "drawHint": "新機能 — ページに描き込み、エージェントに貼り付けられます", + "drawHint": "ページに描き込み、マークアップをコピーしてエージェントに貼り付けられます。", + "drawHintBadge": "新機能", + "drawHintDismiss": "了解", + "drawHintTry": "試してみる", "errorAttach": "注釈付きスクリーンショットを添付できませんでした。", "errorCapture": "描画用にページを取得できませんでした。", "errorUnavailable": "このページではスクリーンショット注釈を利用できません。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 2fae5839230..2ddefe47f79 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -13028,7 +13028,10 @@ "copiedToast": "마크업을 복사했습니다 — 에이전트에 붙여넣으세요 ({{value0}})", "copy": "마크업 복사", "drawButton": "스크린샷에 그리기", - "drawHint": "새 기능 — 페이지에 그린 다음 에이전트에 붙여넣으세요", + "drawHint": "페이지에 그린 다음 마크업을 복사해 에이전트에 붙여넣으세요.", + "drawHintBadge": "새 기능", + "drawHintDismiss": "확인", + "drawHintTry": "사용해 보기", "errorAttach": "마크업 스크린샷을 첨부하지 못했습니다.", "errorCapture": "그릴 페이지를 캡처하지 못했습니다.", "errorUnavailable": "이 페이지에서는 스크린샷 마크업을 사용할 수 없습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index e23f6853fe3..9e74716b26a 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -13028,7 +13028,10 @@ "copiedToast": "已复制标注 — 粘贴到你的智能体({{value0}})", "copy": "复制标注", "drawButton": "在截图上绘制", - "drawHint": "新功能 — 在页面上绘制,然后复制到你的智能体", + "drawHint": "在页面上绘制,然后复制标记并粘贴到你的智能体。", + "drawHintBadge": "新功能", + "drawHintDismiss": "知道了", + "drawHintTry": "试一试", "errorAttach": "无法附加标注后的截图。", "errorCapture": "无法捕获页面进行绘制。", "errorUnavailable": "此页面无法使用截图标注。",