Add Reveal in Finder to the file header menu (#30129)

* Add file location actions to the editor header

* Wait for the terminal spawn join before releasing race-test gates
This commit is contained in:
Neil
2026-10-17 16:52:27 -07:00
committed by GitHub
parent d71e3e9d45
commit ba9c7eadbf
12 changed files with 320 additions and 128 deletions
+10 -3
View File
@@ -3,11 +3,11 @@ import { setupPtyIpcSuite } from './pty-ipc-test-harness'
import { getHiddenRendererPtyIds, isHiddenRendererPty } from './pty-hidden-delivery-gate'
import { ptySizes } from './pty/delivery/visibility-state'
import { clearProviderPtyState } from './pty/provider/state-cleanup'
import * as paneSpawnReservations from './pty/pane/spawn-reservation'
import { WORKTREE_TERMINALS_SLEEPING_ERROR } from '../runtime/worktree-terminals-sleeping-error'
import {
LAUNCH_RACE_LANES,
createLaunchRaceRuntime,
flushMacrotasks,
gatedClaudeAuth,
launchRacePane,
registerLaunchRaceLanes,
@@ -96,8 +96,13 @@ describe('launch race parity, both spawn lanes', () => {
const winner = lanes.spawn.ipc(pane.args)
await vi.waitFor(() => expect(spawn).toHaveBeenCalledOnce())
const joinPaneSpawn = vi.spyOn(paneSpawnReservations, 'joinPaneSpawn')
onTestFinished(() => joinPaneSpawn.mockRestore())
const joiner = lanes.spawn[lane]({ ...pane.args, command: 'claude' })
await flushMacrotasks()
if (lane === 'runtime') {
// Runtime preflight does filesystem work before it can join the reserved pane.
await vi.waitFor(() => expect(joinPaneSpawn).toHaveBeenCalledOnce())
}
finishSpawn()
await expect(winner).resolves.toMatchObject({ id: 'pty-join-winner' })
@@ -128,8 +133,10 @@ describe('launch race parity, both spawn lanes', () => {
const automatic = lanes.spawn.runtime({ ...pane.args, refuseSleptWorktree: true })
await vi.waitFor(() => expect(lock.acquireWorktreeTerminalSpawn).toHaveBeenCalledOnce())
const joinPaneSpawn = vi.spyOn(paneSpawnReservations, 'joinPaneSpawn')
onTestFinished(() => joinPaneSpawn.mockRestore())
const joiner = lanes.spawn.runtime(pane.args)
await flushMacrotasks()
await vi.waitFor(() => expect(joinPaneSpawn).toHaveBeenCalledOnce())
refused = true
lock.refuse()
@@ -45,12 +45,6 @@ export const LAUNCH_RACE_LANES: LaunchRaceLane[] = ['ipc', 'runtime']
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test doubles implement only the members the two spawn lanes read.
const testDouble = <T>(value: unknown): T => value as T
export const flushMacrotasks = async (rounds = 20): Promise<void> => {
for (let index = 0; index < rounds; index++) {
await new Promise<void>((resolve) => setImmediate(resolve))
}
}
/** One stable pane per test; reservation maps are module state, so names must not repeat. */
export function launchRacePane(tag: string, leafId: string) {
const worktreeId = `repo-1::/tmp/${tag}`
+2 -2
View File
@@ -1288,8 +1288,8 @@ html.native-shell .app-layout {
align-items: center;
/* Keep adjacent actions aligned with the app's compact toolbar spacing. */
gap: 4px;
min-height: 36px;
padding: 6px 14px;
min-height: 32px;
padding: 3px 14px;
background: var(--editor-surface);
border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
flex-shrink: 0;
@@ -0,0 +1,122 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { EditorFileLocationMenuItems } from './EditorFileLocationMenuItems'
const store = vi.hoisted((): { state: WorktreeRuntimeOwnerState } => ({ state: {} }))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: WorktreeRuntimeOwnerState) => unknown) => selector(store.state)
}))
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
const openInFileManager = vi.fn().mockResolvedValue({ ok: true })
const writeClipboardText = vi.fn().mockResolvedValue(undefined)
const onCopyPath = vi.fn()
const file: OpenFile = {
id: '/repo/src/file.ts',
filePath: '/repo/src/file.ts',
relativePath: 'src/file.ts',
worktreeId: 'repo::/repo',
language: 'typescript',
mode: 'edit',
isDirty: false
}
function renderMenu(overrides: Partial<OpenFile> = {}): void {
render(
<DropdownMenu open modal={false}>
<DropdownMenuTrigger asChild>
<button>File actions</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<EditorFileLocationMenuItems file={{ ...file, ...overrides }} onCopyPath={onCopyPath} />
</DropdownMenuContent>
</DropdownMenu>
)
}
beforeEach(() => {
store.state = {
repos: [{ id: 'repo', connectionId: null }],
worktreesByRepo: {
repo: [{ id: file.worktreeId, repoId: 'repo', hostId: 'local' }]
},
folderWorkspaces: [],
projectGroups: []
}
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Macintosh')
Object.defineProperty(window, 'api', {
configurable: true,
value: { shell: { openInFileManager }, ui: { writeClipboardText } }
})
})
afterEach(() => {
cleanup()
vi.clearAllMocks()
vi.restoreAllMocks()
})
describe('EditorFileLocationMenuItems', () => {
it('reveals the open file through the existing host-aware shell action', () => {
renderMenu()
fireEvent.click(screen.getByRole('menuitem', { name: 'Reveal in Finder' }))
expect(openInFileManager).toHaveBeenCalledWith(file.filePath, 'local')
})
it('uses the header copy feedback and copies the workspace-relative path separately', () => {
renderMenu()
fireEvent.click(screen.getByRole('menuitem', { name: 'Copy Path' }))
expect(onCopyPath).toHaveBeenCalledOnce()
fireEvent.click(screen.getByRole('menuitem', { name: 'Copy Relative Path' }))
expect(writeClipboardText).toHaveBeenCalledWith('src/file.ts')
})
it.each([{ externalSshTargetId: 'remote' }, { runtimeEnvironmentId: 'paired-server' }])(
'disables local reveal for a remote file: %j',
(overrides) => {
renderMenu(overrides)
const reveal = screen.getByRole('menuitem', { name: 'Reveal in Finder Local only' })
expect(reveal.getAttribute('aria-disabled')).toBe('true')
fireEvent.click(reveal)
expect(openInFileManager).not.toHaveBeenCalled()
expect(
screen.getByRole('menuitem', { name: 'Copy Relative Path' }).hasAttribute('data-disabled')
).toBe(false)
}
)
it('refuses local reveal while the owning workspace is unresolved', () => {
store.state = { repos: [], worktreesByRepo: {}, folderWorkspaces: [], projectGroups: [] }
renderMenu()
expect(
screen.getByRole('menuitem', { name: /Reveal in Finder/ }).getAttribute('aria-disabled')
).toBe('true')
})
it('reveals local files even while a remote runtime is selected', () => {
store.state.settings = { activeRuntimeEnvironmentId: 'paired-server' }
renderMenu()
fireEvent.click(screen.getByRole('menuitem', { name: 'Reveal in Finder' }))
expect(openInFileManager).toHaveBeenCalledWith(file.filePath, 'local')
})
it('reveals a file in a folder workspace without a git worktree', () => {
store.state = {
repos: [],
worktreesByRepo: {},
folderWorkspaces: [{ id: 'folder', projectGroupId: 'group', executionHostId: 'local' }],
projectGroups: [{ id: 'group', connectionId: null }]
}
renderMenu({ worktreeId: 'folder:folder' })
fireEvent.click(screen.getByRole('menuitem', { name: 'Reveal in Finder' }))
expect(openInFileManager).toHaveBeenCalledWith(file.filePath, 'local')
})
})
@@ -0,0 +1,50 @@
import { Copy, ExternalLink } from 'lucide-react'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { LocalOnlyMenuHint } from '@/components/local-only-menu-hint'
import { useAppStore } from '@/store'
import type { OpenFile } from '@/store/slices/editor'
import { getConnectionIdFromState } from '@/lib/connection-context'
import { isRevealOrEditorOpenBlocked } from '@/lib/local-path-open-guard'
import {
getRevealInFileManagerLabel,
getWorkspaceFileRevealOwner,
revealInFileManager
} from '@/lib/reveal-in-file-manager'
import { translate } from '@/i18n/i18n'
export function EditorFileLocationMenuItems({
file,
onCopyPath
}: {
file: OpenFile
onCopyPath: () => void
}): React.JSX.Element {
const revealOwner = useAppStore((state) =>
getWorkspaceFileRevealOwner(state, file.worktreeId, {
connectionId: file.externalSshTargetId ?? getConnectionIdFromState(state, file.worktreeId),
runtimeEnvironmentId: file.runtimeEnvironmentId
})
)
const revealBlocked = isRevealOrEditorOpenBlocked(revealOwner)
return (
<>
<DropdownMenuItem
disabled={revealBlocked}
onSelect={() => void revealInFileManager(file.filePath, revealOwner)}
>
<ExternalLink />
{getRevealInFileManagerLabel()}
{revealBlocked ? <LocalOnlyMenuHint /> : null}
</DropdownMenuItem>
<DropdownMenuItem onSelect={onCopyPath}>
<Copy />
{translate('auto.components.editor.EditorPanelHeader.7c08a1f990', 'Copy Path')}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => void window.api.ui.writeClipboardText(file.relativePath)}>
<Copy />
{translate('auto.components.editor.EditorPanelHeader.269ce4842b', 'Copy Relative Path')}
</DropdownMenuItem>
</>
)
}
@@ -1,7 +1,7 @@
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { EditorPanelMarkdownActionsMenu } from './EditorPanelMarkdownActionsMenu'
import { EditorPanelActionsMenu } from './EditorPanelActionsMenu'
const checkboxItems = vi.hoisted(() => ({
list: [] as {
@@ -41,17 +41,47 @@ vi.mock('@/components/ui/dropdown-menu', async () => {
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
describe('EditorPanelMarkdownActionsMenu', () => {
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipContent: () => null
}))
describe('EditorPanelActionsMenu', () => {
beforeEach(() => {
checkboxItems.list = []
})
it('shows front matter as a view preference beside word wrap', () => {
const onToggleMarkdownFrontmatter = vi.fn()
renderToStaticMarkup(
<EditorPanelActionsMenu
isDiffSurface={false}
diffWordWrap={false}
diffShowWhitespace={false}
editorWordWrap
shouldShowMarkdownExportAction
canExportMarkdownToPdf
canShowMarkdownFrontmatterToggle
markdownFrontmatterVisible
onToggleDiffWordWrap={vi.fn()}
onToggleDiffWhitespace={vi.fn()}
onToggleEditorWordWrap={vi.fn()}
onToggleMarkdownFrontmatter={onToggleMarkdownFrontmatter}
onExportMarkdownToPdf={vi.fn()}
/>
)
expect(checkboxItems.list.map(({ label }) => label)).toEqual(['Word Wrap', 'Front Matter'])
expect(checkboxItems.list[1]?.checked).toBe(true)
checkboxItems.list[1]?.onCheckedChange?.(false)
expect(onToggleMarkdownFrontmatter).toHaveBeenCalledOnce()
})
it('shows Word Wrap for normal file tabs using editorWordWrap (#9974)', () => {
const onToggleDiffWordWrap = vi.fn()
const onToggleEditorWordWrap = vi.fn()
renderToStaticMarkup(
React.createElement(EditorPanelMarkdownActionsMenu, {
isMarkdown: false,
React.createElement(EditorPanelActionsMenu, {
isDiffSurface: false,
diffWordWrap: false,
diffShowWhitespace: false,
@@ -79,8 +109,7 @@ describe('EditorPanelMarkdownActionsMenu', () => {
const onToggleDiffWordWrap = vi.fn()
const onToggleEditorWordWrap = vi.fn()
renderToStaticMarkup(
React.createElement(EditorPanelMarkdownActionsMenu, {
isMarkdown: false,
React.createElement(EditorPanelActionsMenu, {
isDiffSurface: true,
diffWordWrap: true,
diffShowWhitespace: false,
@@ -107,8 +136,7 @@ describe('EditorPanelMarkdownActionsMenu', () => {
it('shows and binds Show Whitespace on diff surfaces', () => {
const onToggleDiffWhitespace = vi.fn()
renderToStaticMarkup(
React.createElement(EditorPanelMarkdownActionsMenu, {
isMarkdown: false,
React.createElement(EditorPanelActionsMenu, {
isDiffSurface: true,
diffWordWrap: false,
diffShowWhitespace: true,
@@ -1,5 +1,5 @@
import type React from 'react'
import { MoreHorizontal } from 'lucide-react'
import { FileDown, MoreHorizontal } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -9,9 +9,11 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
type EditorPanelMarkdownActionsMenuProps = {
isMarkdown: boolean
type EditorPanelActionsMenuProps = {
fileLocationActions?: React.ReactNode
isDiffSurface: boolean
/** Diff-only wrap preference; ignored for normal file tabs. */
diffWordWrap: boolean
@@ -30,8 +32,8 @@ type EditorPanelMarkdownActionsMenuProps = {
onExportMarkdownToPdf: () => void
}
export function EditorPanelMarkdownActionsMenu({
isMarkdown,
export function EditorPanelActionsMenu({
fileLocationActions,
isDiffSurface,
diffWordWrap,
diffShowWhitespace,
@@ -45,32 +47,35 @@ export function EditorPanelMarkdownActionsMenu({
onToggleEditorWordWrap,
onToggleMarkdownFrontmatter,
onExportMarkdownToPdf
}: EditorPanelMarkdownActionsMenuProps): React.JSX.Element | null {
const hasMarkdownActions =
isMarkdown && (shouldShowMarkdownExportAction || canShowMarkdownFrontmatterToggle)
}: EditorPanelActionsMenuProps): React.JSX.Element {
// Why: normal files always get Word Wrap so long/structured lines can unwrap without Settings (#9974).
const wordWrapChecked = isDiffSurface ? diffWordWrap : editorWordWrap
const onToggleWordWrap = isDiffSurface ? onToggleDiffWordWrap : onToggleEditorWordWrap
const menuLabel = fileLocationActions
? translate('auto.components.editor.EditorPanelActionsMenu.fileActions', 'File actions')
: translate('auto.components.editor.EditorPanelMarkdownActionsMenu.561251019a', 'More actions')
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
aria-label={translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.561251019a',
'More actions'
)}
title={translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.561251019a',
'More actions'
)}
>
<MoreHorizontal size={14} />
</button>
</DropdownMenuTrigger>
<DropdownMenu modal={false}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-xs" aria-label={menuLabel}>
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{menuLabel}
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" sideOffset={4}>
{fileLocationActions ? (
<>
{fileLocationActions}
<DropdownMenuSeparator />
</>
) : null}
<DropdownMenuCheckboxItem checked={wordWrapChecked} onCheckedChange={onToggleWordWrap}>
{translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.1eef809708',
@@ -88,34 +93,22 @@ export function EditorPanelMarkdownActionsMenu({
)}
</DropdownMenuCheckboxItem>
) : null}
{hasMarkdownActions ? <DropdownMenuSeparator /> : null}
{canShowMarkdownFrontmatterToggle ? (
<>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault()
onToggleMarkdownFrontmatter()
}}
>
{markdownFrontmatterVisible
? translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.10c39d58c1',
'Hide front matter'
)
: translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.8c8b7f5ff5',
'Show front matter'
)}
</DropdownMenuItem>
{shouldShowMarkdownExportAction ? <DropdownMenuSeparator /> : null}
</>
<DropdownMenuCheckboxItem
checked={markdownFrontmatterVisible}
onCheckedChange={onToggleMarkdownFrontmatter}
>
{translate('auto.components.editor.EditorContent.e4b074749d', 'Front Matter')}
</DropdownMenuCheckboxItem>
) : null}
{shouldShowMarkdownExportAction ? <DropdownMenuSeparator /> : null}
{shouldShowMarkdownExportAction ? (
<DropdownMenuItem
// Why: source/Monaco fallbacks have no rendered document DOM to export.
disabled={!canExportMarkdownToPdf}
onSelect={onExportMarkdownToPdf}
>
<FileDown />
{translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.3e0ce48c24',
'Export as PDF'
@@ -38,8 +38,13 @@ vi.mock('./EditorPanelHeaderPath', () => ({
EditorPanelHeaderPath: () => null
}))
vi.mock('./EditorPanelMarkdownActionsMenu', () => ({
EditorPanelMarkdownActionsMenu: () => null
vi.mock('./EditorPanelActionsMenu', () => ({
EditorPanelActionsMenu: ({ fileLocationActions }: { fileLocationActions?: React.ReactNode }) =>
fileLocationActions ?? null
}))
vi.mock('./EditorFileLocationMenuItems', () => ({
EditorFileLocationMenuItems: () => <span data-file-location-actions />
}))
vi.mock('@/components/artifacts/ArtifactPublishButton', () => ({
@@ -102,6 +107,30 @@ function renderHeader(overrides: Partial<ComponentProps<typeof EditorPanelHeader
}
describe('EditorPanelHeader', () => {
it.each(['edit', 'markdown-preview'] as const)('offers file actions for %s tabs', (mode) => {
expect(renderHeader({ activeFile: { ...activeFile, mode } })).toContain(
'data-file-location-actions'
)
})
it.each(['diff', 'conflict-review', 'check-details'] as const)(
'does not offer on-disk file actions for %s documents',
(mode) => {
expect(renderHeader({ activeFile: { ...activeFile, mode } })).not.toContain(
'data-file-location-actions'
)
}
)
it('offers file actions for a diff with an openable working file', () => {
expect(renderHeader({ isSingleDiff: true, openFileState: { canOpen: true } })).toContain(
'data-file-location-actions'
)
expect(renderHeader({ isSingleDiff: true, openFileState: { canOpen: false } })).not.toContain(
'data-file-location-actions'
)
})
it('shares one tooltip provider across the diff header controls', () => {
const html = renderHeader()
@@ -11,7 +11,8 @@ import EditorViewToggle, {
import type { EditorToggleValue } from './EditorViewToggle'
import type { EditorHeaderOpenFileState } from './editor-header'
import { DiffNotesSendMenu } from './DiffNotesSendMenu'
import { EditorPanelMarkdownActionsMenu } from './EditorPanelMarkdownActionsMenu'
import { EditorPanelActionsMenu } from './EditorPanelActionsMenu'
import { EditorFileLocationMenuItems } from './EditorFileLocationMenuItems'
import { translate } from '@/i18n/i18n'
import { EditorPanelHeaderPath } from './EditorPanelHeaderPath'
import { useDiffNavigation } from './diff-navigation-context'
@@ -319,8 +320,14 @@ export function EditorPanelHeader({
createRequest={createArtifactRequest}
/>
) : null}
<EditorPanelMarkdownActionsMenu
isMarkdown={isMarkdown}
<EditorPanelActionsMenu
fileLocationActions={
activeFile.mode === 'edit' ||
activeFile.mode === 'markdown-preview' ||
(isSingleDiff && openFileState.canOpen) ? (
<EditorFileLocationMenuItems file={activeFile} onCopyPath={onCopyPath} />
) : undefined
}
isDiffSurface={isDiffSurface}
diffWordWrap={diffWordWrap}
diffShowWhitespace={diffShowWhitespace}
@@ -1,6 +1,6 @@
import { ImeInput } from '@/lib/ime-text-field'
import { useEffect, useRef, useState } from 'react'
import { Copy, ExternalLink, Eye, Pencil } from 'lucide-react'
import { Eye, Pencil } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -12,21 +12,13 @@ import {
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { isImeCompositionKeyDown } from '@/lib/ime-composition-keyboard-event'
import { translate } from '@/i18n/i18n'
import { LocalOnlyMenuHint } from '@/components/local-only-menu-hint'
import { getConnectionIdFromState } from '@/lib/connection-context'
import {
getRevealInFileManagerLabel,
getWorkspaceFileRevealOwner,
revealInFileManager
} from '@/lib/reveal-in-file-manager'
import { isRevealOrEditorOpenBlocked } from '@/lib/local-path-open-guard'
import { useAppStore } from '@/store'
import type { OpenFile } from '@/store/slices/editor'
import { isVirtualEditorFile } from '@/store/slices/editor/tabs/editor-tab-content-type'
import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/lib/close-all-context-menus'
import { useEditorHeaderFileRename } from './editor-header-file-rename'
import { getEditorHeaderCopyState } from './editor-header'
import { splitPathForDisplay } from './editor-path-display'
import { EditorFileLocationMenuItems } from './EditorFileLocationMenuItems'
type EditorPanelHeaderPathProps = {
activeFile: OpenFile
@@ -51,14 +43,6 @@ export function EditorPanelHeaderPath({
const canCopyHeaderPath = headerCopyState.copyText !== null
// Why: virtual editor tabs use synthetic ids instead of on-disk paths.
const isVirtualEditorTab = isVirtualEditorFile(activeFile)
const revealOwner = useAppStore((s) =>
getWorkspaceFileRevealOwner(s, activeFile.worktreeId, {
connectionId:
activeFile.externalSshTargetId ?? getConnectionIdFromState(s, activeFile.worktreeId),
runtimeEnvironmentId: activeFile.runtimeEnvironmentId
})
)
const revealBlocked = isRevealOrEditorOpenBlocked(revealOwner)
const markdownPreviewShortcutLabel = useShortcutLabel('editor.markdownPreview')
const {
canRename,
@@ -180,49 +164,20 @@ export function EditorPanelHeaderPath({
</DropdownMenuItem>
<DropdownMenuSeparator />
{!isVirtualEditorTab && (
<>
<DropdownMenuItem
onSelect={() => {
void window.api.ui.writeClipboardText(activeFile.filePath)
}}
>
<Copy className="w-3.5 h-3.5 mr-1.5" />
{translate('auto.components.editor.EditorPanelHeader.7c08a1f990', 'Copy Path')}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
void window.api.ui.writeClipboardText(activeFile.relativePath)
}}
>
<Copy className="w-3.5 h-3.5 mr-1.5" />
{translate(
'auto.components.editor.EditorPanelHeader.269ce4842b',
'Copy Relative Path'
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
<EditorFileLocationMenuItems file={activeFile} onCopyPath={onCopyPath} />
)}
{canShowMarkdownPreview && (
<DropdownMenuItem onSelect={onOpenMarkdownPreview}>
<Eye className="w-3.5 h-3.5 mr-1.5" />
{translate(
'auto.components.editor.EditorPanelHeader.4157f3cbf3',
'Open Markdown Preview'
)}
<DropdownMenuShortcut>{markdownPreviewShortcutLabel}</DropdownMenuShortcut>
</DropdownMenuItem>
)}
{canShowMarkdownPreview && <DropdownMenuSeparator />}
{!isVirtualEditorTab && (
<DropdownMenuItem
disabled={revealBlocked}
onSelect={() => void revealInFileManager(activeFile.filePath, revealOwner)}
>
<ExternalLink className="w-3.5 h-3.5 mr-1.5" />
{getRevealInFileManagerLabel()}
{revealBlocked ? <LocalOnlyMenuHint /> : null}
</DropdownMenuItem>
<>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={onOpenMarkdownPreview}>
<Eye className="w-3.5 h-3.5 mr-1.5" />
{translate(
'auto.components.editor.EditorPanelHeader.4157f3cbf3',
'Open Markdown Preview'
)}
<DropdownMenuShortcut>{markdownPreviewShortcutLabel}</DropdownMenuShortcut>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
+4
View File
@@ -386,6 +386,10 @@
"d07e4b8553": "branch",
"d16e037f40": "rich"
},
"EditorPanelMarkdownActionsMenu": {
"10c39d58c1": "Hide front matter",
"8c8b7f5ff5": "Show front matter"
},
"ReviewNotesSendMenuContent": {
"50f7e753ea": "Sending notes to active agent...",
"bb9c69a0c9": "Notes sent to active agent.",
+3
View File
@@ -15755,6 +15755,9 @@
"2076ecfc9c": "Previous change",
"631dab0df3": "Next change"
},
"EditorPanelActionsMenu": {
"fileActions": "File actions"
},
"EditorPanelMarkdownActionsMenu": {
"3e0ce48c24": "Export as PDF",
"561251019a": "More actions",