diff --git a/apps/desktop/src/lib/trpc/routers/external/index.ts b/apps/desktop/src/lib/trpc/routers/external/index.ts index 80c2afca7a4..c32d9f067d1 100644 --- a/apps/desktop/src/lib/trpc/routers/external/index.ts +++ b/apps/desktop/src/lib/trpc/routers/external/index.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { clipboard, shell } from "electron"; import { db } from "main/lib/db"; import { EXTERNAL_APPS, type ExternalApp } from "main/lib/db/schemas"; +import { terminalManager } from "main/lib/terminal"; import { z } from "zod"; import { publicProcedure, router } from "../.."; @@ -151,7 +152,7 @@ export const createExternalRouter = () => { path: z.string(), line: z.number().optional(), column: z.number().optional(), - cwd: z.string().optional(), + paneId: z.string().optional(), }), ) .mutation(async ({ input }) => { @@ -166,10 +167,20 @@ export const createExternalRouter = () => { } // Convert to absolute path - required for editor commands to work reliably + // Query actual cwd from the terminal's pty process if paneId is provided if (!path.isAbsolute(filePath)) { - filePath = input.cwd - ? path.resolve(input.cwd, filePath) - : path.resolve(filePath); + const cwd = input.paneId + ? terminalManager.getCwd(input.paneId) + : null; + if (!cwd) { + console.warn( + `[openFileInEditor] Cannot resolve relative path "${filePath}": no cwd available for paneId "${input.paneId}"`, + ); + throw new Error( + `Cannot open relative path "${filePath}" - terminal session not found`, + ); + } + filePath = path.resolve(cwd, filePath); } // Build the file location string (file:line:column format for URL schemes) diff --git a/apps/desktop/src/main/lib/terminal/manager.ts b/apps/desktop/src/main/lib/terminal/manager.ts index 5444399585a..0b37aa381b4 100644 --- a/apps/desktop/src/main/lib/terminal/manager.ts +++ b/apps/desktop/src/main/lib/terminal/manager.ts @@ -263,6 +263,28 @@ export class TerminalManager extends EventEmitter { }; } + /** + * Get the current working directory of a terminal session. + * Uses cwd tracked from OSC 7 sequences emitted by the shell. + * Falls back to the initial cwd if no OSC 7 has been received. + */ + getCwd(paneId: string): string | null { + const session = this.sessions.get(paneId); + if (!session || !session.isAlive) { + console.warn( + `[TerminalManager.getCwd] Session not found or not alive for paneId: ${paneId}`, + ); + return null; + } + + // Use tracked cwd from OSC 7 if available, otherwise fall back to initial cwd + const cwd = session.trackedCwd ?? session.cwd; + console.log( + `[TerminalManager.getCwd] paneId: ${paneId}, trackedCwd: ${session.trackedCwd}, initialCwd: ${session.cwd}, returning: ${cwd}`, + ); + return cwd; + } + async killByWorkspaceId( workspaceId: string, ): Promise<{ killed: number; failed: number }> { diff --git a/apps/desktop/src/main/lib/terminal/parse-cwd.ts b/apps/desktop/src/main/lib/terminal/parse-cwd.ts new file mode 100644 index 00000000000..91034188b24 --- /dev/null +++ b/apps/desktop/src/main/lib/terminal/parse-cwd.ts @@ -0,0 +1,36 @@ +/** + * Parse OSC 7 escape sequences to extract the current working directory. + * OSC 7 format: ESC]7;file://hostname/path BEL (or ESC\) + * + * This is emitted by shells when the directory changes. + */ + +const ESC = "\x1b"; +const BEL = "\x07"; + +// Match OSC 7 sequences: ESC]7;file://hostname/path followed by BEL or ST (ESC\) +const OSC7_PATTERN = new RegExp( + `${ESC}\\]7;file://[^/]*((?:/[^${BEL}${ESC}]*)*)(?:${BEL}|${ESC}\\\\)`, + "g", +); + +/** + * Parse terminal output data for OSC 7 directory sequences. + * Returns the last (most recent) directory found, or null if none. + */ +export function parseCwd(data: string): string | null { + let lastMatch: string | null = null; + + for (const match of data.matchAll(OSC7_PATTERN)) { + const path = match[1]; + if (path) { + try { + lastMatch = decodeURIComponent(path); + } catch { + lastMatch = path; + } + } + } + + return lastMatch; +} diff --git a/apps/desktop/src/main/lib/terminal/session.ts b/apps/desktop/src/main/lib/terminal/session.ts index 4c9ea1005d4..0840eb01b7e 100644 --- a/apps/desktop/src/main/lib/terminal/session.ts +++ b/apps/desktop/src/main/lib/terminal/session.ts @@ -9,6 +9,7 @@ import { } from "../terminal-escape-filter"; import { HistoryReader, HistoryWriter } from "../terminal-history"; import { buildTerminalEnv, FALLBACK_SHELL, getDefaultShell } from "./env"; +import { parseCwd } from "./parse-cwd"; import type { InternalCreateSessionParams, TerminalSession } from "./types"; const DEFAULT_COLS = 80; @@ -153,6 +154,12 @@ export function setupDataHandler( dataToStore = extractContentAfterClear(data); } + // Track cwd from OSC 7 sequences + const cwdFromData = parseCwd(data); + if (cwdFromData) { + session.trackedCwd = cwdFromData; + } + const filteredData = session.escapeFilter.filter(dataToStore); session.scrollback += filteredData; session.historyWriter?.write(filteredData); diff --git a/apps/desktop/src/main/lib/terminal/types.ts b/apps/desktop/src/main/lib/terminal/types.ts index f099ed3b2fb..11a18f72029 100644 --- a/apps/desktop/src/main/lib/terminal/types.ts +++ b/apps/desktop/src/main/lib/terminal/types.ts @@ -8,6 +8,8 @@ export interface TerminalSession { paneId: string; workspaceId: string; cwd: string; + /** Current working directory tracked from OSC 7 sequences */ + trackedCwd?: string; cols: number; rows: number; lastActive: number; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index 14bfdda99a6..ca6ceb2193c 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -198,11 +198,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { xterm, fitAddon, cleanup: cleanupQuerySuppression, - } = createTerminalInstance( - container, - workspaceCwd, - initialThemeRef.current, - ); + } = createTerminalInstance(container, paneId, initialThemeRef.current); xtermRef.current = xterm; fitAddonRef.current = fitAddon; isExitedRef.current = false; @@ -397,7 +393,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { xtermRef.current = null; searchAddonRef.current = null; }; - }, [paneId, workspaceId, workspaceCwd]); + }, [paneId, workspaceId]); useEffect(() => { const xterm = xtermRef.current; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts index 4f216287283..c9b36abc519 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts @@ -90,7 +90,7 @@ function loadRenderer(xterm: XTerm): { dispose: () => void } { export function createTerminalInstance( container: HTMLDivElement, - cwd?: string, + paneId: string, initialTheme?: ITheme | null, ): { xterm: XTerm; @@ -148,7 +148,7 @@ export function createTerminalInstance( path, line, column, - cwd, + paneId, }) .catch((error) => { console.error(