diff --git a/src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts b/src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts index 712dd7bc1..a022bcf49 100644 --- a/src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts +++ b/src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts @@ -183,12 +183,13 @@ export function openDetectedFilePath( activateAndRevealWorktree(worktreeId) } + const language = detectLanguage(mappedFilePath) store.openFile( { filePath: mappedFilePath, relativePath, worktreeId: worktreeId || '', - language: detectLanguage(mappedFilePath), + language, mode: 'edit', runtimeEnvironmentId }, @@ -196,6 +197,14 @@ export function openDetectedFilePath( ) if (line !== null) { + const openedStore = useAppStore.getState() + // Why: scope the reveal to the opened editor tab id so owner-qualified tabs + // across local/SSH/runtime contexts get it instead of an ambiguous path key. + const fileId = openedStore.activeFileIdByWorktree[worktreeId] ?? mappedFilePath + if (language === 'markdown') { + // Why: rich Markdown has no line-based reveal consumer; line links must mount Monaco. + openedStore.setMarkdownViewMode(fileId, 'source') + } const targetColumn = column ?? 1 store.setPendingEditorReveal(null) schedulePendingEditorReveal(() => { @@ -204,6 +213,7 @@ export function openDetectedFilePath( } store.setPendingEditorReveal({ filePath: mappedFilePath, + fileId, line, column: targetColumn, matchLength: 0 diff --git a/src/renderer/src/components/terminal-pane/terminal-link-handlers.test.ts b/src/renderer/src/components/terminal-pane/terminal-link-handlers.test.ts index d92e4b761..6a6a0fc15 100644 --- a/src/renderer/src/components/terminal-pane/terminal-link-handlers.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-link-handlers.test.ts @@ -38,6 +38,7 @@ const runtimeEnvironmentTransportCallMock = vi.fn() const setActiveWorktreeMock = vi.fn() const createBrowserTabMock = vi.fn() const setPendingEditorRevealMock = vi.fn() +const setMarkdownViewModeMock = vi.fn() const deps = { worktreeId: 'wt-1', worktreePath: '/tmp' } const storeState = { @@ -52,6 +53,8 @@ const storeState = { createBrowserTab: createBrowserTabMock, openFile: openFileMock, setPendingEditorReveal: setPendingEditorRevealMock, + setMarkdownViewMode: setMarkdownViewModeMock, + activeFileIdByWorktree: {} as Record, worktreesByRepo: {} as Record } @@ -62,7 +65,7 @@ vi.mock('@/store', () => ({ })) vi.mock('@/lib/language-detect', () => ({ - detectLanguage: () => 'plaintext' + detectLanguage: (filePath: string) => (filePath.endsWith('.md') ? 'markdown' : 'plaintext') })) // Why: the real helper reads worktreesByRepo/activeRepoId/etc. from the store @@ -108,6 +111,7 @@ beforeEach(() => { vi.mocked(getConnectionId).mockReturnValue(null) openFilePathMock.mockResolvedValue(true) storeState.settings = undefined + storeState.activeFileIdByWorktree = {} storeState.worktreesByRepo = {} registerHttpLinkStoreAccessor(() => storeState) vi.stubGlobal('window', { @@ -371,6 +375,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '/tmp/src/main.ts', + fileId: '/tmp/src/main.ts', line: 42, column: 1, matchLength: 0 @@ -388,6 +393,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '/tmp/src/main.ts', + fileId: '/tmp/src/main.ts', line: 42, column: 7, matchLength: 0 @@ -395,6 +401,50 @@ describe('handleOscLink', () => { expect(openFilePathMock).not.toHaveBeenCalled() }) + it('opens terminal markdown line links in source mode so Monaco can reveal the line', async () => { + setPlatform('Macintosh') + const filePath = '/tmp/docs/terminal-scroll-intent-architecture.md' + const fileId = 'editor:wt-1:runtime-1:terminal-scroll-intent-architecture.md' + openFileMock.mockImplementationOnce(() => { + storeState.activeFileIdByWorktree['wt-1'] = fileId + }) + + openDetectedFilePath(filePath, 230, null, deps) + await flushAsyncWork() + await flushDoubleRaf() + + expect(setMarkdownViewModeMock).toHaveBeenCalledWith(fileId, 'source') + expect(setPendingEditorRevealMock).toHaveBeenLastCalledWith({ + filePath, + fileId, + line: 230, + column: 1, + matchLength: 0 + }) + }) + + it('scopes non-Markdown line reveals to the owner-qualified editor tab', async () => { + setPlatform('Macintosh') + const filePath = '/tmp/src/main.ts' + const fileId = 'editor:wt-1:runtime-1:main.ts' + openFileMock.mockImplementationOnce(() => { + storeState.activeFileIdByWorktree['wt-1'] = fileId + }) + + openDetectedFilePath(filePath, 42, 7, deps) + await flushAsyncWork() + await flushDoubleRaf() + + expect(setMarkdownViewModeMock).not.toHaveBeenCalled() + expect(setPendingEditorRevealMock).toHaveBeenLastCalledWith({ + filePath, + fileId, + line: 42, + column: 7, + matchLength: 0 + }) + }) + it('uses the system default app for shift+cmd/ctrl-click file paths', async () => { setPlatform('Macintosh') @@ -428,6 +478,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '/tmp/src/main.ts', + fileId: '/tmp/src/main.ts', line: 42, column: 7, matchLength: 0 @@ -526,6 +577,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: 'C:/repo/src/index.ts', + fileId: 'C:/repo/src/index.ts', line: 12, column: 3, matchLength: 0 @@ -587,6 +639,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '/tmp/test.txt', + fileId: '/tmp/test.txt', line: 42, column: 1, matchLength: 0 @@ -629,6 +682,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '/tmp/test.txt', + fileId: '/tmp/test.txt', line: 42, column: 7, matchLength: 0 @@ -662,6 +716,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '//server/Share/Repo/src/app.ts', + fileId: '//server/Share/Repo/src/app.ts', line: 12, column: 3, matchLength: 0 @@ -721,6 +776,7 @@ describe('handleOscLink', () => { ) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '//wsl.localhost/Ubuntu/root/workspace/myrepo/README.md', + fileId: '//wsl.localhost/Ubuntu/root/workspace/myrepo/README.md', line: 5, column: 3, matchLength: 0 @@ -752,6 +808,7 @@ describe('handleOscLink', () => { ) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '//wsl.localhost/Ubuntu/root/workspace/myrepo/README.md', + fileId: '//wsl.localhost/Ubuntu/root/workspace/myrepo/README.md', line: 5, column: 3, matchLength: 0 @@ -1048,6 +1105,7 @@ describe('handleOscLink', () => { expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '/tmp/src/second.ts', + fileId: '/tmp/src/second.ts', line: 20, column: 3, matchLength: 0 @@ -1556,6 +1614,7 @@ describe('createFilePathLinkProvider range bounds', () => { ) expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, { filePath: '//wsl.localhost/Ubuntu/root/workspace/myrepo/README.md', + fileId: '//wsl.localhost/Ubuntu/root/workspace/myrepo/README.md', line: 5, column: 3, matchLength: 0 @@ -1774,6 +1833,7 @@ describe('createFilePathLinkProvider range bounds', () => { }) expect(setPendingEditorRevealMock).toHaveBeenLastCalledWith({ filePath: mappedPath, + fileId: mappedPath, line: 5, column: 3, matchLength: 0 diff --git a/src/shared/terminal-file-link-conformance.ts b/src/shared/terminal-file-link-conformance.ts index 26ee9cd76..ec2ea0b9c 100644 --- a/src/shared/terminal-file-link-conformance.ts +++ b/src/shared/terminal-file-link-conformance.ts @@ -32,6 +32,16 @@ export const TERMINAL_FILE_LINK_TAP_CONFORMANCE_CASES: TerminalFileLinkTapConfor tapText: 'Button', expected: { pathText: 'src/components/Button.tsx', line: 12, column: 7 } }, + { + name: 'relative markdown path with line', + lineText: 'documented in docs/terminal-scroll-intent-architecture.md:230', + tapText: 'terminal-scroll', + expected: { + pathText: 'docs/terminal-scroll-intent-architecture.md', + line: 230, + column: null + } + }, { name: 'tilde path', lineText: 'wrote ~/Documents/notes.md',