From ba9c7eadbfde2e9079bc172df60a90b810fb1243 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:52:27 -0700 Subject: [PATCH] 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 --- src/main/ipc/pty-launch-race-parity.test.ts | 13 +- src/main/ipc/pty-launch-race-test-fixture.ts | 6 - src/renderer/src/assets/main.css | 4 +- .../EditorFileLocationMenuItems.test.tsx | 122 ++++++++++++++++++ .../editor/EditorFileLocationMenuItems.tsx | 50 +++++++ ...st.tsx => EditorPanelActionsMenu.test.tsx} | 44 +++++-- ...onsMenu.tsx => EditorPanelActionsMenu.tsx} | 83 ++++++------ .../editor/EditorPanelHeader.test.tsx | 33 ++++- .../components/editor/EditorPanelHeader.tsx | 13 +- .../editor/EditorPanelHeaderPath.tsx | 73 ++--------- .../src/i18n/en-runtime-required.json | 4 + src/renderer/src/i18n/locales/en.json | 3 + 12 files changed, 320 insertions(+), 128 deletions(-) create mode 100644 src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx create mode 100644 src/renderer/src/components/editor/EditorFileLocationMenuItems.tsx rename src/renderer/src/components/editor/{EditorPanelMarkdownActionsMenu.test.tsx => EditorPanelActionsMenu.test.tsx} (74%) rename src/renderer/src/components/editor/{EditorPanelMarkdownActionsMenu.tsx => EditorPanelActionsMenu.tsx} (62%) diff --git a/src/main/ipc/pty-launch-race-parity.test.ts b/src/main/ipc/pty-launch-race-parity.test.ts index 80c393a3a07..23c439a83d5 100644 --- a/src/main/ipc/pty-launch-race-parity.test.ts +++ b/src/main/ipc/pty-launch-race-parity.test.ts @@ -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() diff --git a/src/main/ipc/pty-launch-race-test-fixture.ts b/src/main/ipc/pty-launch-race-test-fixture.ts index a83b328dcef..dc4586ec75d 100644 --- a/src/main/ipc/pty-launch-race-test-fixture.ts +++ b/src/main/ipc/pty-launch-race-test-fixture.ts @@ -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 = (value: unknown): T => value as T -export const flushMacrotasks = async (rounds = 20): Promise => { - for (let index = 0; index < rounds; index++) { - await new Promise((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}` diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 497a2333fef..19db891cd29 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -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; diff --git a/src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx b/src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx new file mode 100644 index 00000000000..5e1cdf988eb --- /dev/null +++ b/src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx @@ -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 = {}): void { + render( + + + + + + + + + ) +} + +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') + }) +}) diff --git a/src/renderer/src/components/editor/EditorFileLocationMenuItems.tsx b/src/renderer/src/components/editor/EditorFileLocationMenuItems.tsx new file mode 100644 index 00000000000..b110849a674 --- /dev/null +++ b/src/renderer/src/components/editor/EditorFileLocationMenuItems.tsx @@ -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 ( + <> + void revealInFileManager(file.filePath, revealOwner)} + > + + {getRevealInFileManagerLabel()} + {revealBlocked ? : null} + + + + {translate('auto.components.editor.EditorPanelHeader.7c08a1f990', 'Copy Path')} + + void window.api.ui.writeClipboardText(file.relativePath)}> + + {translate('auto.components.editor.EditorPanelHeader.269ce4842b', 'Copy Relative Path')} + + + ) +} diff --git a/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.test.tsx b/src/renderer/src/components/editor/EditorPanelActionsMenu.test.tsx similarity index 74% rename from src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.test.tsx rename to src/renderer/src/components/editor/EditorPanelActionsMenu.test.tsx index b68c8cd6cf3..45df9b6815f 100644 --- a/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.test.tsx +++ b/src/renderer/src/components/editor/EditorPanelActionsMenu.test.tsx @@ -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( + + ) + 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, diff --git a/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx b/src/renderer/src/components/editor/EditorPanelActionsMenu.tsx similarity index 62% rename from src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx rename to src/renderer/src/components/editor/EditorPanelActionsMenu.tsx index 0982f6f7e4f..e8185ca052e 100644 --- a/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx +++ b/src/renderer/src/components/editor/EditorPanelActionsMenu.tsx @@ -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 ( - - - - + + + + + + + + + {menuLabel} + + + {fileLocationActions ? ( + <> + {fileLocationActions} + + + ) : null} {translate( 'auto.components.editor.EditorPanelMarkdownActionsMenu.1eef809708', @@ -88,34 +93,22 @@ export function EditorPanelMarkdownActionsMenu({ )} ) : null} - {hasMarkdownActions ? : null} {canShowMarkdownFrontmatterToggle ? ( - <> - { - event.preventDefault() - onToggleMarkdownFrontmatter() - }} - > - {markdownFrontmatterVisible - ? translate( - 'auto.components.editor.EditorPanelMarkdownActionsMenu.10c39d58c1', - 'Hide front matter' - ) - : translate( - 'auto.components.editor.EditorPanelMarkdownActionsMenu.8c8b7f5ff5', - 'Show front matter' - )} - - {shouldShowMarkdownExportAction ? : null} - + + {translate('auto.components.editor.EditorContent.e4b074749d', 'Front Matter')} + ) : null} + {shouldShowMarkdownExportAction ? : null} {shouldShowMarkdownExportAction ? ( + {translate( 'auto.components.editor.EditorPanelMarkdownActionsMenu.3e0ce48c24', 'Export as PDF' diff --git a/src/renderer/src/components/editor/EditorPanelHeader.test.tsx b/src/renderer/src/components/editor/EditorPanelHeader.test.tsx index 1a4acb389ac..ad17ea8c387 100644 --- a/src/renderer/src/components/editor/EditorPanelHeader.test.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeader.test.tsx @@ -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: () => })) vi.mock('@/components/artifacts/ArtifactPublishButton', () => ({ @@ -102,6 +107,30 @@ function renderHeader(overrides: Partial { + 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() diff --git a/src/renderer/src/components/editor/EditorPanelHeader.tsx b/src/renderer/src/components/editor/EditorPanelHeader.tsx index a605f645a3f..fafe7672291 100644 --- a/src/renderer/src/components/editor/EditorPanelHeader.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeader.tsx @@ -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} - + ) : undefined + } isDiffSurface={isDiffSurface} diffWordWrap={diffWordWrap} diffShowWhitespace={diffShowWhitespace} diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx index e648ac3ccd8..b3b5ce617e5 100644 --- a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx @@ -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({ {!isVirtualEditorTab && ( - <> - { - void window.api.ui.writeClipboardText(activeFile.filePath) - }} - > - - {translate('auto.components.editor.EditorPanelHeader.7c08a1f990', 'Copy Path')} - - { - void window.api.ui.writeClipboardText(activeFile.relativePath) - }} - > - - {translate( - 'auto.components.editor.EditorPanelHeader.269ce4842b', - 'Copy Relative Path' - )} - - - + )} {canShowMarkdownPreview && ( - - - {translate( - 'auto.components.editor.EditorPanelHeader.4157f3cbf3', - 'Open Markdown Preview' - )} - {markdownPreviewShortcutLabel} - - )} - {canShowMarkdownPreview && } - {!isVirtualEditorTab && ( - void revealInFileManager(activeFile.filePath, revealOwner)} - > - - {getRevealInFileManagerLabel()} - {revealBlocked ? : null} - + <> + + + + {translate( + 'auto.components.editor.EditorPanelHeader.4157f3cbf3', + 'Open Markdown Preview' + )} + {markdownPreviewShortcutLabel} + + )} diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 250d01de478..2ef0bb0848c 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -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.", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 164e34c0c5a..d92179681f5 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -15755,6 +15755,9 @@ "2076ecfc9c": "Previous change", "631dab0df3": "Next change" }, + "EditorPanelActionsMenu": { + "fileActions": "File actions" + }, "EditorPanelMarkdownActionsMenu": { "3e0ce48c24": "Export as PDF", "561251019a": "More actions",