mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
fix: open markdown file shortcut does nothing in main window
Cmd/Ctrl+Shift+O only had a handler inside the Floating Terminal panel's own keydown listener, so it did nothing when pressed with a normal worktree tab focused. Add a main-window picker (IPC handler, store action, keydown branch) that mirrors the existing tab.newMarkdown wiring. Fixes #8532 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Neil
co-authored by
Claude Sonnet 5
parent
1cdc8e85b3
commit
e2a7b5df80
@@ -356,4 +356,49 @@ describe('registerAppHandlers', () => {
|
||||
})
|
||||
expect(grantFloatingWorkspaceDirectoryMock).toHaveBeenCalledWith(store, '/Users/kaylee/notes')
|
||||
})
|
||||
|
||||
it('picks a markdown file rooted at the given worktree cwd', async () => {
|
||||
showOpenDialogMock.mockResolvedValue({
|
||||
canceled: false,
|
||||
filePaths: ['/Users/kaylee/repo/docs/notes.md']
|
||||
})
|
||||
registerAppHandlers({} as never)
|
||||
|
||||
await expect(
|
||||
handlers.get('app:pickWorktreeMarkdownDocument')?.({ sender: {} }, '/Users/kaylee/repo')
|
||||
).resolves.toMatchObject({
|
||||
filePath: '/Users/kaylee/repo/docs/notes.md',
|
||||
relativePath: 'docs/notes.md',
|
||||
basename: 'notes.md',
|
||||
name: 'notes'
|
||||
})
|
||||
expect(showOpenDialogMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultPath: '/Users/kaylee/repo',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Markdown', extensions: ['md', 'mdx', 'markdown'] }]
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('returns null when the markdown file picker is cancelled', async () => {
|
||||
showOpenDialogMock.mockResolvedValue({ canceled: true, filePaths: [] })
|
||||
registerAppHandlers({} as never)
|
||||
|
||||
await expect(
|
||||
handlers.get('app:pickWorktreeMarkdownDocument')?.({ sender: {} }, '/Users/kaylee/repo')
|
||||
).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('rejects a picked file that is not a markdown document', async () => {
|
||||
showOpenDialogMock.mockResolvedValue({
|
||||
canceled: false,
|
||||
filePaths: ['/Users/kaylee/repo/notes.txt']
|
||||
})
|
||||
registerAppHandlers({} as never)
|
||||
|
||||
await expect(
|
||||
handlers.get('app:pickWorktreeMarkdownDocument')?.({ sender: {} }, '/Users/kaylee/repo')
|
||||
).rejects.toThrow('Selected file is not a markdown document.')
|
||||
})
|
||||
})
|
||||
|
||||
+21
-3
@@ -37,10 +37,10 @@ type RegisterAppHandlersOptions = {
|
||||
onBeforeRelaunch?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
async function pickFloatingMarkdownDocument(
|
||||
event: IpcMainInvokeEvent
|
||||
async function pickMarkdownDocumentAt(
|
||||
event: IpcMainInvokeEvent,
|
||||
cwd: string
|
||||
): Promise<MarkdownDocument | null> {
|
||||
const cwd = await ensureDefaultFloatingWorkspacePath()
|
||||
const options = {
|
||||
defaultPath: cwd,
|
||||
properties: ['openFile'],
|
||||
@@ -61,6 +61,20 @@ async function pickFloatingMarkdownDocument(
|
||||
return markdownDocumentFromFilePath(cwd, filePath, { outsideRootRelativePath: 'basename' })
|
||||
}
|
||||
|
||||
async function pickFloatingMarkdownDocument(
|
||||
event: IpcMainInvokeEvent
|
||||
): Promise<MarkdownDocument | null> {
|
||||
const cwd = await ensureDefaultFloatingWorkspacePath()
|
||||
return pickMarkdownDocumentAt(event, cwd)
|
||||
}
|
||||
|
||||
async function pickWorktreeMarkdownDocument(
|
||||
event: IpcMainInvokeEvent,
|
||||
cwd: string
|
||||
): Promise<MarkdownDocument | null> {
|
||||
return pickMarkdownDocumentAt(event, cwd)
|
||||
}
|
||||
|
||||
async function pickFloatingWorkspaceDirectory(
|
||||
event: IpcMainInvokeEvent,
|
||||
store: Store
|
||||
@@ -342,6 +356,10 @@ export function registerAppHandlers(store: Store, options: RegisterAppHandlersOp
|
||||
|
||||
ipcMain.handle('app:pickFloatingMarkdownDocument', (event) => pickFloatingMarkdownDocument(event))
|
||||
|
||||
ipcMain.handle('app:pickWorktreeMarkdownDocument', (event, cwd: string) =>
|
||||
pickWorktreeMarkdownDocument(event, cwd)
|
||||
)
|
||||
|
||||
ipcMain.handle('app:pickFloatingWorkspaceDirectory', (event) =>
|
||||
pickFloatingWorkspaceDirectory(event, store)
|
||||
)
|
||||
|
||||
@@ -913,6 +913,9 @@ export type AppApi = {
|
||||
/** Opens a native picker for markdown documents, rooted in the floating
|
||||
* workspace, and authorizes the selected file for editor reads/writes. */
|
||||
pickFloatingMarkdownDocument: () => Promise<MarkdownDocument | null>
|
||||
/** Opens a native picker for markdown documents, rooted in the given
|
||||
* worktree path, and authorizes the selected file for editor reads/writes. */
|
||||
pickWorktreeMarkdownDocument: (cwd: string) => Promise<MarkdownDocument | null>
|
||||
/** Opens a native directory picker and authorizes the selected directory
|
||||
* for Floating Workspace markdown file creation. */
|
||||
pickFloatingWorkspaceDirectory: () => Promise<string | null>
|
||||
|
||||
@@ -503,6 +503,8 @@ const api = {
|
||||
ipcRenderer.invoke('app:getFloatingMarkdownDirectory'),
|
||||
pickFloatingMarkdownDocument: (): Promise<MarkdownDocument | null> =>
|
||||
ipcRenderer.invoke('app:pickFloatingMarkdownDocument'),
|
||||
pickWorktreeMarkdownDocument: (cwd: string): Promise<MarkdownDocument | null> =>
|
||||
ipcRenderer.invoke('app:pickWorktreeMarkdownDocument', cwd),
|
||||
pickFloatingWorkspaceDirectory: (): Promise<string | null> =>
|
||||
ipcRenderer.invoke('app:pickFloatingWorkspaceDirectory')
|
||||
},
|
||||
|
||||
@@ -119,6 +119,7 @@ import {
|
||||
createFloatingWorkspaceTerminalTab,
|
||||
handleEmptyFloatingWorkspacePanelCloseShortcut,
|
||||
isFloatingWorkspacePanelFocused,
|
||||
openFloatingWorkspaceMarkdownTab,
|
||||
switchFloatingWorkspaceTab
|
||||
} from '@/lib/floating-workspace-terminal-actions'
|
||||
import {
|
||||
@@ -308,6 +309,7 @@ function Terminal(): React.JSX.Element | null {
|
||||
(s) => s.openNewBrowserTabInActiveWorkspace
|
||||
)
|
||||
const openNewMarkdownInActiveWorkspace = useAppStore((s) => s.openNewMarkdownInActiveWorkspace)
|
||||
const openMarkdownFileInActiveWorkspace = useAppStore((s) => s.openMarkdownFileInActiveWorkspace)
|
||||
const openNewTerminalTabInActiveWorkspace = useAppStore(
|
||||
(s) => s.openNewTerminalTabInActiveWorkspace
|
||||
)
|
||||
@@ -1444,6 +1446,19 @@ function Terminal(): React.JSX.Element | null {
|
||||
await openNewMarkdownInActiveWorkspace(targetGroupId)
|
||||
}, [activeWorktreeId, openNewMarkdownInActiveWorkspace])
|
||||
|
||||
const handleOpenFile = useCallback(async () => {
|
||||
if (!activeWorktreeId) {
|
||||
return
|
||||
}
|
||||
const targetGroupId =
|
||||
useAppStore.getState().activeGroupIdByWorktree[activeWorktreeId] ??
|
||||
useAppStore.getState().groupsByWorktree[activeWorktreeId]?.[0]?.id
|
||||
if (!targetGroupId) {
|
||||
return
|
||||
}
|
||||
await openMarkdownFileInActiveWorkspace(targetGroupId)
|
||||
}, [activeWorktreeId, openMarkdownFileInActiveWorkspace])
|
||||
|
||||
const handleCloseTab = useCallback((tabId: string) => {
|
||||
closeTerminalTab(tabId)
|
||||
}, [])
|
||||
@@ -1899,6 +1914,24 @@ function Terminal(): React.JSX.Element | null {
|
||||
return
|
||||
}
|
||||
|
||||
// Cmd/Ctrl+Shift+O - open existing markdown file
|
||||
if (!e.repeat && matchShortcut('tab.openMarkdown')) {
|
||||
e.preventDefault()
|
||||
notifyTerminalCapture('tab.openMarkdown')
|
||||
if (floatingWorkspaceFocused) {
|
||||
void openFloatingWorkspaceMarkdownTab(useAppStore.getState()).catch((err) => {
|
||||
toast.error(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: translate('auto.components.Terminal.3a460c3322', 'Failed to open markdown file.')
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
void handleOpenFile()
|
||||
return
|
||||
}
|
||||
|
||||
if (handleEmptyFloatingWorkspacePanelCloseShortcut(e, shortcutPlatform, keybindings)) {
|
||||
return
|
||||
}
|
||||
@@ -2040,6 +2073,7 @@ function Terminal(): React.JSX.Element | null {
|
||||
handleNewBrowserTab,
|
||||
handleNewSimulatorTab,
|
||||
handleNewFile,
|
||||
handleOpenFile,
|
||||
handleNewTab,
|
||||
handleNewAgentTab,
|
||||
handleCloseTab,
|
||||
@@ -2199,6 +2233,7 @@ function Terminal(): React.JSX.Element | null {
|
||||
onNewSimulatorTab={mobileEmulatorEnabled ? handleNewSimulatorTab : undefined}
|
||||
onOpenEntry={handleOpenEntry}
|
||||
onNewFileTab={handleNewFile}
|
||||
onOpenFileTab={handleOpenFile}
|
||||
onSetCustomTitle={setTabCustomTitle}
|
||||
onSetTabColor={setTabColor}
|
||||
expandedPaneByTabId={expandedPaneByTabId}
|
||||
|
||||
@@ -1776,7 +1776,8 @@
|
||||
"61ed600d29": "\"{{value0}}\" has unsaved changes. Do you want to save before closing?",
|
||||
"cdc9ac4b2d": "editor",
|
||||
"e57db40c11": "Could not build launch command for {{value0}}.",
|
||||
"5b2c1a9e44": "No agent CLI detected — install one or pick a default agent in Settings."
|
||||
"5b2c1a9e44": "No agent CLI detected — install one or pick a default agent in Settings.",
|
||||
"3a460c3322": "Failed to open markdown file."
|
||||
},
|
||||
"TerminalSearch": {
|
||||
"db234b7519": "Close",
|
||||
|
||||
@@ -82,3 +82,28 @@ export async function createFloatingWorkspaceMarkdownTab(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function openFloatingWorkspaceMarkdownTab(
|
||||
store: FloatingWorkspaceMarkdownStore
|
||||
): Promise<void> {
|
||||
const targetGroupId = store.activeGroupIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]
|
||||
const document = await window.api.app.pickFloatingMarkdownDocument()
|
||||
if (!document) {
|
||||
return
|
||||
}
|
||||
store.openFile(
|
||||
{
|
||||
filePath: document.filePath,
|
||||
relativePath: document.relativePath,
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
language: detectLanguage(document.relativePath),
|
||||
mode: 'edit',
|
||||
runtimeEnvironmentId: null
|
||||
},
|
||||
{
|
||||
preview: false,
|
||||
targetGroupId,
|
||||
suppressActiveRuntimeFallback: true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import { keybindingMatchesAction, type KeybindingOverrides } from '../../../shar
|
||||
export {
|
||||
createFloatingWorkspaceBrowserTab,
|
||||
createFloatingWorkspaceMarkdownTab,
|
||||
createFloatingWorkspaceTerminalTab
|
||||
createFloatingWorkspaceTerminalTab,
|
||||
openFloatingWorkspaceMarkdownTab
|
||||
} from './floating-workspace-tab-creation'
|
||||
export {
|
||||
isFloatingWorkspacePanelShortcut,
|
||||
|
||||
@@ -493,6 +493,7 @@ export type EditorSlice = {
|
||||
}
|
||||
) => void
|
||||
openNewMarkdownInActiveWorkspace: (groupId: string) => Promise<void>
|
||||
openMarkdownFileInActiveWorkspace: (groupId: string) => Promise<void>
|
||||
// Why: dispatcher for markdown link activation. Lives on the slice because it
|
||||
// sequences openFile, setMarkdownViewMode, and setPendingEditorReveal around
|
||||
// an async Monaco remount — all reading/writing state in this slice. See
|
||||
@@ -1890,6 +1891,37 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
}
|
||||
},
|
||||
|
||||
openMarkdownFileInActiveWorkspace: async (groupId) => {
|
||||
const state = get()
|
||||
const worktreeId = state.activeWorktreeId
|
||||
if (!worktreeId) {
|
||||
return
|
||||
}
|
||||
const worktree = state.getKnownWorktreeById(worktreeId)
|
||||
if (!worktree) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const document = await window.api.app.pickWorktreeMarkdownDocument(worktree.path)
|
||||
if (!document) {
|
||||
return
|
||||
}
|
||||
get().openFile(
|
||||
{
|
||||
filePath: document.filePath,
|
||||
relativePath: document.relativePath,
|
||||
worktreeId,
|
||||
language: detectLanguage(document.relativePath),
|
||||
mode: 'edit',
|
||||
runtimeEnvironmentId: null
|
||||
},
|
||||
{ preview: false, targetGroupId: groupId }
|
||||
)
|
||||
} catch (err) {
|
||||
toast.error(extractIpcErrorMessage(err, 'Failed to open markdown file.'))
|
||||
}
|
||||
},
|
||||
|
||||
openMarkdownPreview: (file, options) => {
|
||||
const initialState = get()
|
||||
const resolvedRuntimeEnvironmentId =
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { toast } from 'sonner'
|
||||
import { createTestStore, makeWorktree, TEST_REPO } from './store-test-helpers'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
success: vi.fn(),
|
||||
warning: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
const wt1 = makeWorktree({ id: 'wt-1', repoId: TEST_REPO.id, path: '/repo1/wt1' })
|
||||
|
||||
function seedActiveWorktree(store: ReturnType<typeof createTestStore>): void {
|
||||
store.setState({
|
||||
repos: [TEST_REPO],
|
||||
worktreesByRepo: { [TEST_REPO.id]: [wt1] },
|
||||
activeWorktreeId: wt1.id
|
||||
})
|
||||
}
|
||||
|
||||
describe('openMarkdownFileInActiveWorkspace', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(toast.error).mockClear()
|
||||
})
|
||||
|
||||
it('opens the picked file rooted at the active worktree path', async () => {
|
||||
const pickWorktreeMarkdownDocument = vi.fn().mockResolvedValue({
|
||||
filePath: '/repo1/wt1/notes.md',
|
||||
relativePath: 'notes.md'
|
||||
})
|
||||
globalThis.window.api = {
|
||||
app: { pickWorktreeMarkdownDocument }
|
||||
} as unknown as Window['api']
|
||||
|
||||
const store = createTestStore()
|
||||
seedActiveWorktree(store)
|
||||
|
||||
await store.getState().openMarkdownFileInActiveWorkspace('group-1')
|
||||
|
||||
expect(pickWorktreeMarkdownDocument).toHaveBeenCalledWith(wt1.path)
|
||||
expect(store.getState().openFiles).toHaveLength(1)
|
||||
expect(store.getState().openFiles[0]).toMatchObject({
|
||||
filePath: '/repo1/wt1/notes.md',
|
||||
relativePath: 'notes.md',
|
||||
worktreeId: wt1.id,
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
})
|
||||
})
|
||||
|
||||
it('does nothing when the picker is cancelled', async () => {
|
||||
const pickWorktreeMarkdownDocument = vi.fn().mockResolvedValue(null)
|
||||
globalThis.window.api = {
|
||||
app: { pickWorktreeMarkdownDocument }
|
||||
} as unknown as Window['api']
|
||||
|
||||
const store = createTestStore()
|
||||
seedActiveWorktree(store)
|
||||
|
||||
await store.getState().openMarkdownFileInActiveWorkspace('group-1')
|
||||
|
||||
expect(store.getState().openFiles).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('does nothing when there is no active worktree', async () => {
|
||||
const pickWorktreeMarkdownDocument = vi.fn()
|
||||
globalThis.window.api = {
|
||||
app: { pickWorktreeMarkdownDocument }
|
||||
} as unknown as Window['api']
|
||||
|
||||
const store = createTestStore()
|
||||
store.setState({ repos: [TEST_REPO], activeWorktreeId: null })
|
||||
|
||||
await store.getState().openMarkdownFileInActiveWorkspace('group-1')
|
||||
|
||||
expect(pickWorktreeMarkdownDocument).not.toHaveBeenCalled()
|
||||
expect(store.getState().openFiles).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('shows a toast error when the picker rejects', async () => {
|
||||
const pickWorktreeMarkdownDocument = vi.fn().mockRejectedValue(new Error('boom'))
|
||||
globalThis.window.api = {
|
||||
app: { pickWorktreeMarkdownDocument }
|
||||
} as unknown as Window['api']
|
||||
|
||||
const store = createTestStore()
|
||||
seedActiveWorktree(store)
|
||||
|
||||
await store.getState().openMarkdownFileInActiveWorkspace('group-1')
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('boom')
|
||||
expect(store.getState().openFiles).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -499,6 +499,7 @@ function createWebPreloadApi(): Partial<PreloadApi> {
|
||||
getFloatingTerminalCwd: () => Promise.resolve(''),
|
||||
getFloatingMarkdownDirectory: () => Promise.resolve(''),
|
||||
pickFloatingMarkdownDocument: () => Promise.resolve(null),
|
||||
pickWorktreeMarkdownDocument: () => Promise.resolve(null),
|
||||
pickFloatingWorkspaceDirectory: () => Promise.resolve(null)
|
||||
},
|
||||
starNag: {
|
||||
|
||||
Reference in New Issue
Block a user