diff --git a/src/main/window/editable-context-menu.test.ts b/src/main/window/editable-context-menu.test.ts index 409792d39..35b597858 100644 --- a/src/main/window/editable-context-menu.test.ts +++ b/src/main/window/editable-context-menu.test.ts @@ -1,13 +1,4 @@ import { describe, expect, it, vi } from 'vitest' - -const { writeTextMock } = vi.hoisted(() => ({ - writeTextMock: vi.fn() -})) - -vi.mock('electron', () => ({ - clipboard: { writeText: writeTextMock } -})) - import { buildEditableContextMenuTemplate } from './editable-context-menu' import { richMarkdownContextMenuCommandChannel } from '../../shared/rich-markdown-context-menu' @@ -148,31 +139,6 @@ describe('buildEditableContextMenuTemplate', () => { ).toEqual([]) }) - it('builds a native read-only text menu for selected markdown text and links', () => { - const template = buildEditableContextMenuTemplate( - contextParams({ - isEditable: false, - linkURL: 'https://example.com/docs', - selectionText: 'https://example.com/docs' - }), - { - replaceMisspelling: vi.fn(), - send: vi.fn(), - session: { addWordToSpellCheckerDictionary: vi.fn() } as unknown as Electron.Session - } - ) - - expect(template.map((item) => item.label ?? item.role ?? item.type)).toEqual([ - 'Copy URL', - 'separator', - 'copy', - 'selectAll' - ]) - - template[0].click?.({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent) - expect(writeTextMock).toHaveBeenCalledWith('https://example.com/docs') - }) - it('keeps regular text inputs to spelling and native edit actions', () => { const template = buildEditableContextMenuTemplate( contextParams({ formControlType: 'input-text' }), diff --git a/src/main/window/editable-context-menu.ts b/src/main/window/editable-context-menu.ts index ff4141b91..f7f60e95f 100644 --- a/src/main/window/editable-context-menu.ts +++ b/src/main/window/editable-context-menu.ts @@ -1,4 +1,3 @@ -import { clipboard } from 'electron' import { richMarkdownContextMenuCommandChannel, type RichMarkdownContextMenuCommand, @@ -84,34 +83,12 @@ function buildNativeEditMenuTemplate(): Electron.MenuItemConstructorOptions[] { ] } -function buildReadOnlyTextMenuTemplate( - params: Electron.ContextMenuParams -): Electron.MenuItemConstructorOptions[] { - const template: Electron.MenuItemConstructorOptions[] = [] - if (params.linkURL) { - template.push({ - label: 'Copy URL', - click: () => clipboard.writeText(params.linkURL) - }) - } - if (params.selectionText) { - if (template.length > 0) { - template.push({ type: 'separator' }) - } - template.push({ role: 'copy' }) - } - if (template.length > 0) { - template.push({ role: 'selectAll' }) - } - return template -} - export function buildEditableContextMenuTemplate( params: Electron.ContextMenuParams, webContents: EditableContextMenuWebContents ): Electron.MenuItemConstructorOptions[] { if (!params.isEditable) { - return buildReadOnlyTextMenuTemplate(params) + return [] } const suggestions = params.dictionarySuggestions.slice(0, 5) diff --git a/src/renderer/src/components/dashboard/DashboardAgentHoverCardContent.test.tsx b/src/renderer/src/components/dashboard/DashboardAgentHoverCardContent.test.tsx deleted file mode 100644 index 86d2b36cc..000000000 --- a/src/renderer/src/components/dashboard/DashboardAgentHoverCardContent.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it } from 'vitest' -import { TooltipProvider } from '@/components/ui/tooltip' -import { DashboardAgentHoverCardContent } from './DashboardAgentHoverCardContent' -import type { DashboardAgentRow } from './useDashboardData' - -function makeAgentRow(): DashboardAgentRow { - const now = Date.UTC(2026, 4, 20) - const paneKey = 'tab-1:11111111-1111-4111-8111-111111111111' - const prompt = 'Review **markdown** formatting' - - return { - paneKey, - agentType: 'codex', - state: 'working', - startedAt: now - 60_000, - tab: { - id: 'tab-1', - ptyId: null, - worktreeId: 'wt-1', - title: 'Codex', - customTitle: null, - color: null, - sortOrder: 0, - createdAt: now - }, - entry: { - state: 'working', - prompt, - updatedAt: now, - stateStartedAt: now - 60_000, - agentType: 'codex', - paneKey, - stateHistory: [{ state: 'working', prompt, startedAt: now - 60_000 }], - toolName: 'Bash', - toolInput: 'pnpm test', - lastAssistantMessage: 'Rendered **markdown** in the hover card.' - } - } -} - -describe('DashboardAgentHoverCardContent', () => { - it('renders prompt and assistant markdown in structured sections', () => { - const markup = renderToStaticMarkup( - - {}} - /> - - ) - - expect(markup).toContain('Prompt') - expect(markup).toContain('Current tool') - expect(markup).toContain('Latest message') - expect(markup).toContain('Copy prompt') - expect(markup).toContain('Copy latest message') - expect(markup).toContain('data-slot="scroll-area"') - expect(markup).toContain('data-slot="scroll-area-viewport"') - expect(markup).toContain('whitespace-pre') - expect(markup).toContain('[&_pre]:max-w-none') - expect(markup).not.toContain('overflow-auto') - expect(markup).toContain('markdown') - expect(markup).toContain('pnpm test') - }) -}) diff --git a/src/renderer/src/components/dashboard/DashboardAgentHoverCardContent.tsx b/src/renderer/src/components/dashboard/DashboardAgentHoverCardContent.tsx deleted file mode 100644 index 02c8353c6..000000000 --- a/src/renderer/src/components/dashboard/DashboardAgentHoverCardContent.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React from 'react' -import { ArrowRight, Check, Copy } from 'lucide-react' -import { AgentStateDot, type AgentDotState } from '@/components/AgentStateDot' -import { AgentIcon } from '@/lib/agent-catalog' -import { agentTypeToIconAgent, formatAgentTypeLabel } from '@/lib/agent-status' -import CommentMarkdown from '@/components/sidebar/CommentMarkdown' -import { WORKTREE_CONTEXT_MENU_SCOPE_ATTR } from '@/components/sidebar/worktree-context-menu-scope' -import { Button } from '@/components/ui/button' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import type { DashboardAgentRow as DashboardAgentRowData } from './useDashboardData' - -type AgentHoverSectionProps = { - title: string - children: React.ReactNode - copyLabel?: string - copied?: boolean - onCopy?: (event: React.MouseEvent) => void -} - -function AgentHoverSection({ - title, - children, - copyLabel, - copied = false, - onCopy -}: AgentHoverSectionProps): React.JSX.Element { - return ( -
-
-
- {title} -
- {onCopy && copyLabel && ( - - - - - - {copied ? 'Copied' : copyLabel} - - - )} -
-
{children}
-
- ) -} - -type DashboardAgentHoverCardContentProps = { - agent: DashboardAgentRowData - dotState: AgentDotState - prompt: string - isWorking: boolean - toolName: string - toolInput: string - lastAssistantMessage: string - headerTimestamp: string | null - onActivate: (event: React.SyntheticEvent) => void -} - -export function DashboardAgentHoverCardContent({ - agent, - dotState, - prompt, - isWorking, - toolName, - toolInput, - lastAssistantMessage, - headerTimestamp, - onActivate -}: DashboardAgentHoverCardContentProps): React.JSX.Element { - const copyResetTimerRef = React.useRef | null>(null) - const [copiedSection, setCopiedSection] = React.useState<'prompt' | 'latest' | null>(null) - const agentLabel = formatAgentTypeLabel(agent.agentType) - const hasToolDetails = isWorking && (toolName.length > 0 || toolInput.length > 0) - const hasBodyDetails = prompt.length > 0 || hasToolDetails || lastAssistantMessage.length > 0 - const copySectionText = React.useCallback((section: 'prompt' | 'latest', text: string) => { - return async (event: React.MouseEvent) => { - event.stopPropagation() - await window.api.ui.writeClipboardText(text) - setCopiedSection(section) - if (copyResetTimerRef.current) { - clearTimeout(copyResetTimerRef.current) - } - copyResetTimerRef.current = setTimeout(() => setCopiedSection(null), 1200) - } - }, []) - const handleHeaderKeyDown = React.useCallback( - (event: React.KeyboardEvent) => { - if (event.key !== 'Enter' && event.key !== ' ') { - return - } - event.preventDefault() - event.stopPropagation() - onActivate(event) - }, - [onActivate] - ) - - React.useEffect(() => { - return () => { - if (copyResetTimerRef.current) { - clearTimeout(copyResetTimerRef.current) - } - } - }, []) - - return ( -
event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onContextMenuCapture={(event) => event.stopPropagation()} - {...{ [WORKTREE_CONTEXT_MENU_SCOPE_ATTR]: 'agent-hover-card' }} - > -
-
- - - - -
- {agentLabel} -
- {agent.entry.interrupted && ( - - interrupted - - )} - - {headerTimestamp && ( - - {headerTimestamp} - - )} - - - Open - - -
-
- -
- {prompt && ( - - - - )} - {hasToolDetails && ( - -
- {toolName && ( - - {toolName} - - )} - {toolInput && ( -
-                    {toolInput}
-                  
- )} -
-
- )} - {lastAssistantMessage && ( - - - - )} - {!hasBodyDetails && ( -
No agent details yet.
- )} -
-
-
- ) -} diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx index 0db21019d..051a1c2c2 100644 --- a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx +++ b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx @@ -5,8 +5,6 @@ import { AgentStateDot, agentStateLabel, type AgentDotState } from '@/components import { AgentIcon } from '@/lib/agent-catalog' import { agentTypeToIconAgent, formatAgentTypeLabel } from '@/lib/agent-status' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' -import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' -import { DashboardAgentHoverCardContent } from './DashboardAgentHoverCardContent' import type { AgentStatusState } from '../../../../shared/agent-status-types' import type { DashboardAgentRow as DashboardAgentRowData } from './useDashboardData' @@ -117,7 +115,6 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({ hideExpand = false }: Props) { const [expanded, setExpanded] = useState(false) - const [hoverCardOpen, setHoverCardOpen] = useState(false) // Why: stop propagation so clicking the X doesn't also fire the worktree // card's click handler (which navigates away from the dashboard). const handleDismiss = useCallback( @@ -160,25 +157,16 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({ }, [onActivate, agent.tab.id, agent.paneKey] ) - const handleHoverCardActivate = useCallback( - (e: React.SyntheticEvent) => { - e.stopPropagation() - setHoverCardOpen(false) - onActivate(agent.tab.id, agent.paneKey) - }, - [onActivate, agent.tab.id, agent.paneKey] - ) const startedAt = agent.startedAt > 0 ? agent.startedAt : null const doneAt = lastEnteredDoneAt(agent) const prompt = agent.entry.prompt.trim() - const dotState = asDotState(agent.state) // Why: `agent.entry.prompt` is normalized to '' when the prompt is unknown // (fresh agent, missing telemetry). Rendering the row with an empty primary // slot would collapse the text column and leave the row with no human- // readable label — just a state dot and icon. Fall back to the state label // ("Working", "Done", "Waiting", …) so every row is identifiable at a // glance. - const displayLabel = prompt || agentStateLabel(dotState) + const displayLabel = prompt || agentStateLabel(asDotState(agent.state)) // Why: the tool row describes what the agent is *currently* doing; once it // leaves working, that line goes stale and misleads (a done row showing // "Bash: pnpm test" reads as if the command is still running). Gate tool @@ -202,17 +190,11 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({ if (doneAt !== null) { tsParts.push(`done ${formatTimeAgo(doneAt, now)}`) } - const headerTimestamp = - doneAt !== null - ? formatTimeAgo(doneAt, now) - : startedAt !== null - ? formatTimeAgo(startedAt, now) - : null - const row = ( + return ( // Why: NOT role="button" / tabIndex={0}. The row contains real