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:
Vitor Ribeiro
2026-07-16 17:29:42 -07:00
committed by Neil
co-authored by Claude Sonnet 5
parent 1cdc8e85b3
commit e2a7b5df80
11 changed files with 267 additions and 5 deletions
+45
View File
@@ -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
View File
@@ -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)
)
+3
View File
@@ -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>
+2
View File
@@ -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')
},
+35
View File
@@ -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}
+2 -1
View File
@@ -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,
+32
View File
@@ -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)
})
})
+1
View File
@@ -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: {