From a594cfb91e07394dd6f352baaefa8c1d82f89b3d Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:31:48 -0700 Subject: [PATCH] Simplify markdown rename to inline field without breadcrumb or buttons Replaces breadcrumb-morph rename with confirm/cancel buttons with a simple inline field accepting full filenames. Commits on Enter, blur, or Escape to cancel, matching tab bar and file explorer behavior. Adds renameCancelledRef to prevent blur-commit after Escape. Removes unused i18n strings for buttons and simplifies state by dropping extension pinning and breadcrumb display. --- AGENTS.md | 2 +- docs/reference/remote-wire-compatibility.md | 2 +- .../usage-scan-worker-event-loop.test.ts | 7 +- .../editor/EditorPanelHeaderPath.test.tsx | 146 ++++------------- .../editor/EditorPanelHeaderPath.tsx | 153 ++++++------------ .../editor/editor-header-file-rename.ts | 97 ++++------- src/renderer/src/i18n/locales/en.json | 4 +- src/renderer/src/i18n/locales/es.json | 7 +- src/renderer/src/i18n/locales/fr.json | 7 +- src/renderer/src/i18n/locales/ja.json | 7 +- src/renderer/src/i18n/locales/ko.json | 7 +- src/renderer/src/i18n/locales/zh.json | 7 +- 12 files changed, 123 insertions(+), 323 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 99c56e74108..c8705c903b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh - **Keyboard shortcuts**: Never hardcode `e.metaKey`. Use a platform check (`navigator.userAgent.includes('Mac')`) to pick `metaKey` on Mac and `ctrlKey` on Linux/Windows. Electron menu accelerators should use `CmdOrCtrl`. - **Shortcut labels in UI**: Display `⌘` / `⇧` on Mac and `Ctrl+` / `Shift+` on other platforms. - **File paths**: Use `path.join` or Electron/Node path utilities — never assume `/` or `\`. -- **Windows terminal shells**: `--shell` picks the shell a terminal *is*; `--command` is typed into whatever shell the host spawned, so a shell choice routed through `command` silently becomes a child process. See [`docs/reference/windows-terminal-shell-selection.md`](./docs/reference/windows-terminal-shell-selection.md). +- **Windows terminal shells**: `--shell` picks the shell a terminal _is_; `--command` is typed into whatever shell the host spawned, so a shell choice routed through `command` silently becomes a child process. See [`docs/reference/windows-terminal-shell-selection.md`](./docs/reference/windows-terminal-shell-selection.md). - **Windows setup scripts**: the setup/issue-command runner is a `.cmd` batch file unless the script starts with a `#!` line — never derive that from the user's terminal-shell preference, and never launch a `.cmd` runner with a bare `cmd.exe /c` from a Git Bash pane (MSYS rewrites the `/c`). See [`docs/reference/windows-setup-shell.md`](./docs/reference/windows-setup-shell.md). - **Windows child processes**: start them through `runProcess`/`spawnProcess` in `src/shared/child-process/` — never `child_process` directly. It pins `windowsHide`, refuses `shell: true`, and encodes `.cmd`/`.bat` arguments so neither `CommandLineToArgvW` nor `cmd.exe` mangles them. A ratchet test fails on any new direct import. Recognised npm/pnpm `.cmd` shims are resolved to their real target so the spawn skips `cmd.exe` entirely; see [`docs/reference/windows-cmd-shim-resolution.md`](./docs/reference/windows-cmd-shim-resolution.md) before adding a shim shape or debugging one. - **Windows process enumeration**: read the table through `src/main/windows/windows-process-table.ts`, never by forking `powershell.exe`. See [`docs/reference/windows-process-enumeration.md`](./docs/reference/windows-process-enumeration.md). diff --git a/docs/reference/remote-wire-compatibility.md b/docs/reference/remote-wire-compatibility.md index ef029aea9f5..c6bbdb47825 100644 --- a/docs/reference/remote-wire-compatibility.md +++ b/docs/reference/remote-wire-compatibility.md @@ -102,7 +102,7 @@ gate, and a conflicted worktree that looks clean is granted a hosted-review crea should not have. Withholding an affordance is a degrade; removing the evidence a gate reads is not. -A fallback is only ever allowed to shape a *reading*. If the member is sent back to the +A fallback is only ever allowed to shape a _reading_. If the member is sent back to the host — a token the client echoes into a later call's params — pass it through as `z.string()` and let the send site keep it verbatim. `hostedReview`'s `provider` is the case: the eligibility reply names it and the create call returns it, so an diff --git a/src/main/usage/usage-scan-worker-event-loop.test.ts b/src/main/usage/usage-scan-worker-event-loop.test.ts index 7157d26373e..3f39a95ebbf 100644 --- a/src/main/usage/usage-scan-worker-event-loop.test.ts +++ b/src/main/usage/usage-scan-worker-event-loop.test.ts @@ -33,7 +33,12 @@ const EXPECTED_EVENTS = FILE_COUNT * EVENTS_PER_FILE const TOKENS_PER_EVENT = 200 const WORKTREES: UsageScanWorktreeRef[] = [ - { repoId: 'repo-1', worktreeId: 'wt-1', path: '/tmp/orca-usage-oracle-project', displayName: 'demo' } + { + repoId: 'repo-1', + worktreeId: 'wt-1', + path: '/tmp/orca-usage-oracle-project', + displayName: 'demo' + } ] let corpusRoot = '' diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx b/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx index c68632a5523..2c5cbe5f288 100644 --- a/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx @@ -84,7 +84,7 @@ function openRenameInput(): void { fireEvent.click(screen.getByRole('menuitem', { name: 'Rename' })) } -describe('EditorPanelHeaderPath breadcrumb morph rename', () => { +describe('EditorPanelHeaderPath inline rename', () => { beforeEach(() => { renameFileOnDiskMock.mockReset() Object.assign(window, { api: { ui: { writeClipboardText: vi.fn() } } }) @@ -95,40 +95,21 @@ describe('EditorPanelHeaderPath breadcrumb morph rename', () => { vi.stubGlobal('cancelAnimationFrame', vi.fn()) }) - it('morphs into a breadcrumb strip with the basename editable', () => { + it('opens a field holding the whole name', () => { renderPath(baseFile()) openRenameInput() const input = getRenameInput('Rename file notes.md') - expect(input.value).toBe('notes') - expect(screen.getByText('.md')).toBeDefined() - expect(screen.getByText('repo /')).toBeDefined() + expect(input.value).toBe('notes.md') }) - it('shows the full crumb chain without ellipsis cuts', () => { - renderPath( - baseFile({ - id: '/repo/docs/marketing/notes.md', - filePath: '/repo/docs/marketing/notes.md', - relativePath: 'docs/marketing/notes.md' - }) - ) - openRenameInput() - - expect(screen.getByText('repo / docs / marketing /')).toBeDefined() - }) - - it('lets the strip claim the full header width instead of capping at 520px', () => { + it('lets the field claim the full header width', () => { renderPath(baseFile()) openRenameInput() const input = getRenameInput('Rename file notes.md') - const strip = input.parentElement - if (!strip) { - throw new Error('Missing rename strip') - } - expect(strip.className).toContain('w-full') - expect(strip.className).toContain('max-w-full') + expect(input.className).toContain('w-full') + expect(input.className).toContain('max-w-full') }) it('selects the basename so typing replaces just the name', () => { @@ -141,60 +122,42 @@ describe('EditorPanelHeaderPath breadcrumb morph rename', () => { expect(input.selectionEnd).toBe('notes'.length) }) - it('re-attaches the pinned extension to a bare basename', () => { - renderPath(baseFile()) - openRenameInput() - - const input = getRenameInput('Rename file notes.md') - fireEvent.change(input, { target: { value: 'renamed' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - expect(renameFileOnDiskMock).toHaveBeenCalledWith({ - oldPath: '/repo/notes.md', - newName: 'renamed.md', - worktreeId: 'wt-1', - worktreePath: '/repo' - }) - }) - - it('respects an explicitly typed extension and drops the pinned suffix', () => { + it('renames to the typed name verbatim', () => { renderPath(baseFile()) openRenameInput() const input = getRenameInput('Rename file notes.md') fireEvent.change(input, { target: { value: 'renamed.mdx' } }) - expect(screen.queryByText('.md')).toBeNull() - fireEvent.keyDown(input, { key: 'Enter' }) - expect(renameFileOnDiskMock).toHaveBeenCalledWith( - expect.objectContaining({ newName: 'renamed.mdx' }) - ) + + expect(renameFileOnDiskMock).toHaveBeenCalledWith({ + oldPath: '/repo/notes.md', + newName: 'renamed.mdx', + worktreeId: 'wt-1', + worktreePath: '/repo' + }) }) - it('shows the same name it commits for a dotted basename', () => { + it('commits on blur like the tab bar and file explorer', () => { renderPath(baseFile()) openRenameInput() const input = getRenameInput('Rename file notes.md') - fireEvent.change(input, { target: { value: 'v1.2' } }) - expect(screen.queryByText('.md')).toBeNull() + fireEvent.change(input, { target: { value: 'renamed.md' } }) + fireEvent.blur(input) - fireEvent.keyDown(input, { key: 'Enter' }) - expect(renameFileOnDiskMock).toHaveBeenCalledWith(expect.objectContaining({ newName: 'v1.2' })) + expect(renameFileOnDiskMock).toHaveBeenCalledWith( + expect.objectContaining({ newName: 'renamed.md' }) + ) }) - it('keeps the pinned suffix for a leading-dot name', () => { + it('does not request a rename when the name was not edited', () => { renderPath(baseFile()) openRenameInput() - const input = getRenameInput('Rename file notes.md') - fireEvent.change(input, { target: { value: '.notes' } }) - expect(screen.getByText('.md')).toBeDefined() + fireEvent.keyDown(getRenameInput('Rename file notes.md'), { key: 'Enter' }) - fireEvent.keyDown(input, { key: 'Enter' }) - expect(renameFileOnDiskMock).toHaveBeenCalledWith( - expect.objectContaining({ newName: '.notes.md' }) - ) + expect(renameFileOnDiskMock).not.toHaveBeenCalled() }) it('ignores an Enter that only confirms an IME candidate', () => { @@ -202,7 +165,7 @@ describe('EditorPanelHeaderPath breadcrumb morph rename', () => { openRenameInput() const input = getRenameInput('Rename file notes.md') - fireEvent.change(input, { target: { value: 'renamed' } }) + fireEvent.change(input, { target: { value: 'renamed.md' } }) fireEvent.keyDown(input, { key: 'Enter', keyCode: 229 }) expect(renameFileOnDiskMock).not.toHaveBeenCalled() @@ -212,23 +175,28 @@ describe('EditorPanelHeaderPath breadcrumb morph rename', () => { ) }) - it('does not commit when the field merely loses focus', () => { + it('cancels on Escape without a trailing blur-commit, and ignores empty renames', () => { renderPath(baseFile()) openRenameInput() const input = getRenameInput('Rename file notes.md') - fireEvent.change(input, { target: { value: 'renamed' } }) + fireEvent.change(input, { target: { value: 'renamed.md' } }) + fireEvent.keyDown(input, { key: 'Escape' }) fireEvent.blur(input) - expect(renameFileOnDiskMock).not.toHaveBeenCalled() - expect(screen.getByLabelText('Rename file notes.md')).toBeDefined() + expect(screen.queryByLabelText('Rename file notes.md')).toBeNull() + + openRenameInput() + fireEvent.change(getRenameInput('Rename file notes.md'), { target: { value: ' ' } }) + fireEvent.keyDown(getRenameInput('Rename file notes.md'), { key: 'Enter' }) + expect(renameFileOnDiskMock).not.toHaveBeenCalled() }) it('drops rename mode when the active file changes', () => { const rerenderPath = renderPath(baseFile()) openRenameInput() - fireEvent.change(getRenameInput('Rename file notes.md'), { target: { value: 'renamed' } }) + fireEvent.change(getRenameInput('Rename file notes.md'), { target: { value: 'renamed.md' } }) rerenderPath( baseFile({ id: '/repo/other.md', @@ -242,52 +210,6 @@ describe('EditorPanelHeaderPath breadcrumb morph rename', () => { expect(renameFileOnDiskMock).not.toHaveBeenCalled() }) - it('commits via the confirm button', () => { - renderPath(baseFile()) - openRenameInput() - - fireEvent.change(getRenameInput('Rename file notes.md'), { - target: { value: 'renamed.md' } - }) - fireEvent.mouseDown(screen.getByRole('button', { name: 'Confirm rename' })) - fireEvent.click(screen.getByRole('button', { name: 'Confirm rename' })) - - expect(renameFileOnDiskMock).toHaveBeenCalledTimes(1) - expect(renameFileOnDiskMock).toHaveBeenCalledWith( - expect.objectContaining({ newName: 'renamed.md' }) - ) - }) - - it('cancels via the cancel button without renaming', () => { - renderPath(baseFile()) - openRenameInput() - - fireEvent.change(getRenameInput('Rename file notes.md'), { - target: { value: 'renamed.md' } - }) - fireEvent.mouseDown(screen.getByRole('button', { name: 'Cancel rename' })) - fireEvent.click(screen.getByRole('button', { name: 'Cancel rename' })) - - expect(renameFileOnDiskMock).not.toHaveBeenCalled() - expect(screen.queryByLabelText('Rename file notes.md')).toBeNull() - }) - - it('cancels on Escape and ignores empty renames', () => { - renderPath(baseFile()) - openRenameInput() - - const input = getRenameInput('Rename file notes.md') - fireEvent.change(input, { target: { value: 'renamed.md' } }) - fireEvent.keyDown(input, { key: 'Escape' }) - expect(renameFileOnDiskMock).not.toHaveBeenCalled() - expect(screen.queryByLabelText('Rename file notes.md')).toBeNull() - - openRenameInput() - fireEvent.change(getRenameInput('Rename file notes.md'), { target: { value: ' ' } }) - fireEvent.keyDown(getRenameInput('Rename file notes.md'), { key: 'Enter' }) - expect(renameFileOnDiskMock).not.toHaveBeenCalled() - }) - it('selects the whole name when there is no extension', () => { const file = baseFile({ id: '/repo/Makefile', diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx index 7e05626683f..0986062ffde 100644 --- a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import { Check, Copy, ExternalLink, Eye, Pencil, X } from 'lucide-react' +import { Copy, ExternalLink, Eye, Pencil } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -61,13 +61,9 @@ export function EditorPanelHeaderPath({ const { canRename, currentFileName, - currentBaseName, - pinnedExtension, - breadcrumbSegments, isRenaming, renameInputRef, openRenameInput, - setRenameDraft, commitRename, cancelRename } = useEditorHeaderFileRename(activeFile) @@ -90,101 +86,41 @@ export function EditorPanelHeaderPath({ }} > {isRenaming ? ( -