feat: Semantic History — resolve bare filenames against OSC 7 CWD on click - #2200
jiseongnoh wants to merge 4 commits into
Conversation
…click When a user clicks a bare filename (e.g. `README.md`, `src/main.py`) in the terminal, resolve it against the terminal's current working directory (reported via OSC 7) and open it if the file exists on disk. Changes: - `resolveTerminalOpenURLTarget()` now accepts an optional `workingDirectory` parameter. Before falling through to browser URL heuristics, it checks whether `CWD + clicked_text` resolves to an existing file. - Line/column suffixes (e.g. `file.swift:42:10`) are stripped before the file existence check. - The `GHOSTTY_ACTION_OPEN_URL` handler now retrieves the terminal's CWD from `workspace.panelDirectories[surfaceId]` (per-panel) or `workspace.currentDirectory` (workspace-level fallback) and passes it to the resolver. This matches iTerm2's "Semantic History" feature and is especially useful for AI coding agents (Claude Code, Codex, etc.) that output bare filenames. Closes manaflow-ai#2199
|
@jiseongnoh is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughResolve logic for terminal-opened URLs now accepts an optional workingDirectory and, if initial URL parsing fails, treats non-absolute inputs (optionally stripping numeric :line[:column] suffixes) as relative paths resolved against that workingDirectory, returning an external file target only when the canonicalized path exists. Changes
Sequence Diagram(s)sequenceDiagram
participant TerminalView
participant TabManager
participant Workspace
participant Resolver
participant FileSystem
TerminalView->>TabManager: tabManagerFor(tabId)
TabManager->>Workspace: read panelDirectories[surfaceId] / currentDirectory
TabManager-->>TerminalView: terminalCWD
TerminalView->>Resolver: resolveTerminalOpenURLTarget(rawValue, workingDirectory=terminalCWD)
Resolver->>FileSystem: attempt URL parsing / scheme handling
alt parse succeeds
Resolver-->>TerminalView: return external or remote URL target
else parse fails and input not absolute and workingDirectory != nil
Resolver->>Resolver: strip trailing :line[:column] if numeric
Resolver->>FileSystem: join workingDirectory + trimmedPath (canonicalize)
FileSystem-->>Resolver: exists? (yes/no)
alt exists
Resolver-->>TerminalView: return .external(fileURLWithPath:)
else
Resolver-->>TerminalView: return nil / other fallback
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR implements Semantic History — resolving bare filenames and relative paths (e.g. Confidence Score: 4/5PR is safe to merge; core logic is correct, path-traversal is guarded, and all previously flagged issues are resolved. All three P1/P0 issues from prior review rounds are addressed (multi-window lookup, colon-in-filename, empty-pathPart). The only remaining item is a P2 style suggestion to hoist No files require special attention beyond the inline Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["User clicks text in terminal\n(GHOSTTY_ACTION_OPEN_URL)"] --> B["Lookup CWD\ntabManagerFor(tabId) ?? tabManager\n→ panelDirectories[surfaceId]\n ?? currentDirectory"]
B --> C["resolveTerminalOpenURLTarget(text, cwd)"]
C --> D{Absolute path?\nstarts with /}
D -- Yes --> E["Return .external(fileURL)"]
D -- No --> F{Known URL scheme?\nhttp/https/ftp/file/…}
F -- Yes --> G{http/https?}
G -- Yes --> H["Return .embeddedBrowser(url)"]
G -- No --> I["Return .external(url)"]
F -- No --> J{CWD provided?}
J -- No --> N
J -- Yes --> K["Step 1: append raw text to CWD\nstandardize + path-traversal check\nFileManager.fileExists?"]
K -- Exists --> L["Return .external(cwdResolvedURL)"]
K -- Not found --> M["Step 2: strip :line/:col suffix\nretry CWD resolve + exists check"]
M -- Exists --> L
M -- Not found --> N["resolveBrowserNavigableURL()\n(bare-host heuristic)"]
N -- Match --> O["Return .embeddedBrowser / .external"]
N -- No match --> P["Return .external(fallback URL)\nor nil"]
Reviews (3): Last reviewed commit: "fix: comprehensive review round — knownS..." | Re-trigger Greptile |
| let terminalCWD: String? = { | ||
| guard let tabId = surfaceView.tabId, | ||
| let surfaceId = surfaceView.terminalSurface?.id, | ||
| let tabManager = AppDelegate.shared?.tabManager, | ||
| let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else { | ||
| return nil | ||
| } | ||
| return workspace.panelDirectories[surfaceId] | ||
| ?? workspace.currentDirectory | ||
| }() |
There was a problem hiding this comment.
Multi-window CWD lookup misses non-primary windows
The new CWD lookup hardcodes AppDelegate.shared?.tabManager, which is the primary window's tab manager. In a multi-window cmux setup the clicked terminal's tab may live in a secondary window, whose TabManager is a different object. When that happens, tabManager.tabs.first(where: { $0.id == tabId }) will never find the workspace, the guard fails, terminalCWD is nil, and the feature silently degrades to the old behavior (no bare-filename resolution) for every terminal except those in the primary window.
Every other CWD/tab lookup in the same file uses the tabManagerFor(tabId:) ?? tabManager pattern to handle this correctly — e.g. lines 1277, 2322, 2353, 6344–6345.
| let terminalCWD: String? = { | |
| guard let tabId = surfaceView.tabId, | |
| let surfaceId = surfaceView.terminalSurface?.id, | |
| let tabManager = AppDelegate.shared?.tabManager, | |
| let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else { | |
| return nil | |
| } | |
| return workspace.panelDirectories[surfaceId] | |
| ?? workspace.currentDirectory | |
| }() | |
| let terminalCWD: String? = { | |
| guard let tabId = surfaceView.tabId, | |
| let surfaceId = surfaceView.terminalSurface?.id, | |
| let app = AppDelegate.shared, | |
| let tabManager = app.tabManagerFor(tabId: tabId) ?? app.tabManager, | |
| let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else { | |
| return nil | |
| } | |
| return workspace.panelDirectories[surfaceId] | |
| ?? workspace.currentDirectory | |
| }() |
| // Semantic History: resolve bare filenames and relative paths against CWD. | ||
| // Strip trailing line/column suffix (e.g. "file.swift:42:10") before checking. | ||
| if let cwd = workingDirectory, !cwd.isEmpty { | ||
| let pathPart = trimmed.components(separatedBy: ":").first ?? trimmed |
There was a problem hiding this comment.
Colon-split discards the path when the filename itself contains a colon
trimmed.components(separatedBy: ":").first will silently drop everything after the first :. That is fine for file.swift:42, but filenames (especially those with Unicode in them — the PR's own example shows Korean filenames) can legally contain colons on macOS. A file named 사업계획서_초안:v2.md would be checked as 사업계획서_초안, which does not exist, and the resolution would fall through instead of opening the correct file.
A more robust strip is to only drop a suffix that matches the :<digits> line/column pattern:
| let pathPart = trimmed.components(separatedBy: ":").first ?? trimmed | |
| let pathPart: String = { | |
| // Strip trailing ":line" or ":line:col" suffixes only. | |
| // Using regex-free approach: check if every component after | |
| // the first is purely numeric. | |
| let parts = trimmed.components(separatedBy: ":") | |
| var end = parts.count | |
| while end > 1, parts[end - 1].allSatisfy(\.isNumber) { | |
| end -= 1 | |
| } | |
| return parts[..<end].joined(separator: ":") | |
| }() |
…w support P2 fixes from code review: 1. Move CWD-based file resolution AFTER URL/scheme checks so that `localhost:3000`, `http://...`, etc. are never misrouted to local files even if a matching path exists in CWD. 2. Use `tabManagerFor(tabId:)` instead of `AppDelegate.shared?.tabManager` so Semantic History works in detached/secondary windows, not just the primary window.
…lback 1. Colon-split now only strips trailing numeric-only components (`:42`, `:42:10`) instead of splitting on the first colon. Filenames containing colons (legal on macOS) are preserved. 2. Add `?? app.tabManager` fallback to match codebase convention, ensuring CWD lookup works even if tabManagerFor returns nil.
There was a problem hiding this comment.
No issues found across 1 file
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2605-2614: The closure computing terminalCWD currently probes
AppDelegate.shared?.tabManager?.tabs (via surfaceView.tabId and
surfaceView.terminalSurface?.id) which only looks at the active window's
tabManager and can pick the wrong workspace in multi-window cases; replace that
lookup with AppDelegate.shared?.workspaceFor(tabId: tabId) (or equivalent API)
to find the owning Workspace across all mainWindowContexts and perform this
resolution on the main thread before using the result so
panelDirectories[surfaceId] and currentDirectory come from the correct
workspace.
- Around line 518-523: The current logic strips at the first ":"
unconditionally, which collapses whole-string URLs (e.g. "http://..." or
"file:///...") and causes them to be treated as local paths; update the branch
around workingDirectory to first detect full URLs by attempting URL(string:
trimmed) and checking url.scheme != nil (or checking for "://"/"file://"
prefix), and if it is a URL skip the CWD fallback entirely; only perform the
components(separatedBy: ":").first trimming and the
FileManager.default.fileExists(atPath:) resolution for inputs that are not
recognized as full URLs (reference variables/functions: workingDirectory,
trimmed, pathPart, resolved, FileManager.default.fileExists).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cf1109b7-f65d-423e-bab6-b2892853a54b
📒 Files selected for processing (1)
Sources/GhosttyTerminalView.swift
| let terminalCWD: String? = { | ||
| guard let tabId = surfaceView.tabId, | ||
| let surfaceId = surfaceView.terminalSurface?.id, | ||
| let tabManager = AppDelegate.shared?.tabManager, | ||
| let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else { | ||
| return nil | ||
| } | ||
| return workspace.panelDirectories[surfaceId] | ||
| ?? workspace.currentDirectory | ||
| }() |
There was a problem hiding this comment.
Resolve the panel CWD on main from the owning workspace.
This reads AppDelegate.shared?.tabManager?.tabs inside action_cb before hopping to main, and it only consults the active tab manager. In secondary-window cases that can pick the wrong workspace or miss the panel entirely, so bare filenames fall back incorrectly.
Suggested fix
- let terminalCWD: String? = {
- guard let tabId = surfaceView.tabId,
- let surfaceId = surfaceView.terminalSurface?.id,
- let tabManager = AppDelegate.shared?.tabManager,
- let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else {
- return nil
- }
- return workspace.panelDirectories[surfaceId]
- ?? workspace.currentDirectory
- }()
+ let terminalCWD: String? = performOnMain {
+ guard let tabId = callbackTabId ?? surfaceView.tabId,
+ let surfaceId = callbackSurfaceId ?? surfaceView.terminalSurface?.id,
+ let workspace = AppDelegate.shared?.workspaceFor(tabId: tabId) else {
+ return nil
+ }
+ return workspace.panelDirectories[surfaceId] ?? workspace.currentDirectory
+ }Based on learnings, AppDelegate.shared?.workspaceFor(tabId: workspaceId) searches across all mainWindowContexts, whereas self.tabManager is active-window only.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/GhosttyTerminalView.swift` around lines 2605 - 2614, The closure
computing terminalCWD currently probes AppDelegate.shared?.tabManager?.tabs (via
surfaceView.tabId and surfaceView.terminalSurface?.id) which only looks at the
active window's tabManager and can pick the wrong workspace in multi-window
cases; replace that lookup with AppDelegate.shared?.workspaceFor(tabId: tabId)
(or equivalent API) to find the owning Workspace across all mainWindowContexts
and perform this resolution on the main thread before using the result so
panelDirectories[surfaceId] and currentDirectory come from the correct
workspace.
|
@coderabbitai review All issues from the initial review have been addressed in follow-up commits:
Please re-review the latest commit (6f9f3de). |
|
@greptile-apps review All P1 issues have been fixed:
Please re-review the latest state. |
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/GhosttyTerminalView.swift">
<violation number="1" location="Sources/GhosttyTerminalView.swift:542">
P2: Semantic-history CWD resolution is bypassed for bare filenames containing `:line`/`:col` because URL parsing happens first; scheme-like strings such as `file.swift:42` return early as external URLs, so the new suffix-stripping CWD resolution never runs.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
Sources/GhosttyTerminalView.swift (1)
2615-2628:⚠️ Potential issue | 🟠 MajorResolve the panel CWD on main from the callback ids.
This still derives
terminalCWDfromsurfaceViewstate inside the action callback, even thoughcallbackTabIdandcallbackSurfaceIdwere already captured above. During window/panel reattachment that can read a stale workspace/panel mapping and open the wrong file or fall through when the file exists.Suggested fix
- let terminalCWD: String? = { - guard let tabId = surfaceView.tabId, - let surfaceId = surfaceView.terminalSurface?.id, - let app = AppDelegate.shared, - let tabManager = app.tabManagerFor(tabId: tabId) ?? app.tabManager, - let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else { - return nil - } - return workspace.panelDirectories[surfaceId] - ?? workspace.currentDirectory - }() + let terminalCWD: String? = performOnMain { + guard let tabId = callbackTabId ?? surfaceView.tabId, + let surfaceId = callbackSurfaceId ?? surfaceView.terminalSurface?.id, + let workspace = AppDelegate.shared?.workspaceFor(tabId: tabId) else { + return nil + } + return workspace.panelDirectories[surfaceId] ?? workspace.currentDirectory + } guard let target = resolveTerminalOpenURLTarget(urlString, workingDirectory: terminalCWD) else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 2615 - 2628, The code currently computes terminalCWD from surfaceView inside the action callback, which can be stale; instead, on the main thread resolve the workspace and panel directory using the captured callbackTabId and callbackSurfaceId: fetch AppDelegate.shared, call app.tabManagerFor(tabId: callbackTabId) ?? app.tabManager, find the workspace with id == callbackTabId, then derive terminalCWD from workspace.panelDirectories[callbackSurfaceId] ?? workspace.currentDirectory, and pass that into resolveTerminalOpenURLTarget(urlString, workingDirectory:); update references to terminalCWD and remove reliance on surfaceView in this callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 542-559: The existence check currently only tests the stripped
pathPart, which prevents literal filenames like "foo:42" from resolving; update
the logic in the block that uses workingDirectory, trimmed, pathPart and
resolved so it first constructs resolved = (cwd as
NSString).appendingPathComponent(trimmed) and calls
FileManager.default.fileExists(atPath: resolved) and returns
.external(URL(fileURLWithPath: resolved)) if found, and only if that fails then
compute the stripped pathPart (the existing parts/trimming loop) and, if the
stripped result is non-empty and different from trimmed, construct the
alternative resolved and test FileManager.default.fileExists(atPath:) again
before returning .external.
---
Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2615-2628: The code currently computes terminalCWD from
surfaceView inside the action callback, which can be stale; instead, on the main
thread resolve the workspace and panel directory using the captured
callbackTabId and callbackSurfaceId: fetch AppDelegate.shared, call
app.tabManagerFor(tabId: callbackTabId) ?? app.tabManager, find the workspace
with id == callbackTabId, then derive terminalCWD from
workspace.panelDirectories[callbackSurfaceId] ?? workspace.currentDirectory, and
pass that into resolveTerminalOpenURLTarget(urlString, workingDirectory:);
update references to terminalCWD and remove reliance on surfaceView in this
callback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9a452c45-28fc-4449-9e9a-2eea69a50f79
📒 Files selected for processing (1)
Sources/GhosttyTerminalView.swift
🧩 Analysis chain🏁 Script executed: Length of output: 4162 🏁 Script executed: Length of output: 17546
✅ Fix 1 — CWD check after URL/scheme checksThe new block is correctly placed after the URL/scheme resolution path, with an explanatory comment. ✅ Fix 2 — Multi-window support
✅ Fix 3 — Smart colon strippingThe 🔴 Remaining issue — Absolute paths not guarded in CWD blockThe CWD block has no guard for absolute paths (e.g. ("/home/user" as NSString).appendingPathComponent("/tmp/foo.txt")
// → "/home/user/tmp/foo.txt" ← not "/tmp/foo.txt"So an absolute path that isn't caught by the URL/scheme check earlier reaches the CWD block, gets rewritten to Suggested fix — add if let cwd = workingDirectory, !cwd.isEmpty {
let pathPart: String = { ... }()
// Skip absolute paths — they must not be joined against the CWD.
guard !pathPart.hasPrefix("/") else { break }
let resolved = (cwd as NSString).appendingPathComponent(pathPart)
if FileManager.default.fileExists(atPath: resolved) { ... }
}Or equivalently, inline the guard in the if let cwd = workingDirectory, !cwd.isEmpty, !pathPart.hasPrefix("/") {💡 Minor follow-up (not a blocker)The stripped 🧠 Learnings used✅ Actions performedReview triggered.
|
| let parts = trimmed.components(separatedBy: ":") | ||
| var end = parts.count | ||
| while end > 1, parts[end - 1].allSatisfy(\.isNumber) { | ||
| end -= 1 | ||
| } | ||
| return parts[..<end].joined(separator: ":") |
There was a problem hiding this comment.
Empty
pathPart when input starts with :
"".allSatisfy(\.isNumber) returns true in Swift (vacuous truth over an empty collection). This means the stripping loop also removes empty components — not just purely-numeric ones. Concretely, for an input like ":42":
parts = ["", "42"]"42"is all-digits →endgoes to 1""ends the loop (end > 1is false)pathPart = ""
(cwd as NSString).appendingPathComponent("") returns cwd unchanged, and FileManager.default.fileExists(atPath: cwd) will return true for any existing working directory. The result is that clicking ":42" (which can appear in shell error messages like bash: line :42: …) would cause the CWD directory itself to be opened in Finder/editor.
The fix is to add a guard after the closure, or add !last.isEmpty to the while predicate:
guard !pathPart.isEmpty else { /* fall through to resolveBrowserNavigableURL */ }…esolve, path traversal guard, root CWD fix Addresses all findings from 5 AI reviewers (Codex, Greptile, CodeRabbit, cubic-dev, + 3 internal agents): 1. knownSchemes allowlist (cubic-dev P2): URL(string: "file.swift:42") parses scheme="file.swift" via RFC 3986. Only known schemes (http, https, ftp, ssh, mailto, file, etc.) are now treated as real URLs. Unknown schemes fall through to CWD resolution, fixing the most common Semantic History use case (compiler output like `file.swift:42:10`). 2. Raw-first resolve (CodeRabbit Major): Try the literal clicked text against CWD before stripping :line:col suffixes. Handles filenames that legitimately contain colons (legal on macOS). 3. Path traversal guard (security): Resolved paths are validated against CWD prefix via standardizingPath to prevent `../../etc/passwd` from opening files outside the working directory. 4. Root CWD fix (Codex P2): When CWD is "/", cwdPrefix is "/" not "//", preventing the containment check from rejecting all valid child paths. 5. Empty-string guard (Greptile P1): `!parts[end-1].isEmpty` prevents vacuous truth from `"".allSatisfy(\.isNumber)` stripping empty components.
|
@coderabbitai review All findings from all reviewers have been addressed in commit 8be163a:
Please re-review the latest commit. |
🧩 Analysis chain🏁 Script executed: Length of output: 6142
✅ Fix 1 —
|
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/GhosttyTerminalView.swift">
<violation number="1" location="Sources/GhosttyTerminalView.swift:557">
P2: CWD prefix guard blocks legitimate parent-relative paths (e.g. `../foo.swift`), so relative file links outside the CWD subtree no longer resolve even when the file exists.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| // contain colons (e.g. a file literally named "foo:42"). | ||
| let rawResolved = (cwd as NSString).appendingPathComponent(trimmed) | ||
| let rawCanonical = (rawResolved as NSString).standardizingPath | ||
| if rawCanonical.hasPrefix(cwdPrefix) || rawCanonical == cwdCanonical, |
There was a problem hiding this comment.
P2: CWD prefix guard blocks legitimate parent-relative paths (e.g. ../foo.swift), so relative file links outside the CWD subtree no longer resolve even when the file exists.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/GhosttyTerminalView.swift, line 557:
<comment>CWD prefix guard blocks legitimate parent-relative paths (e.g. `../foo.swift`), so relative file links outside the CWD subtree no longer resolve even when the file exists.</comment>
<file context>
@@ -536,26 +544,44 @@ func resolveTerminalOpenURLTarget(_ rawValue: String, workingDirectory: String?
+ // contain colons (e.g. a file literally named "foo:42").
+ let rawResolved = (cwd as NSString).appendingPathComponent(trimmed)
+ let rawCanonical = (rawResolved as NSString).standardizingPath
+ if rawCanonical.hasPrefix(cwdPrefix) || rawCanonical == cwdCanonical,
+ FileManager.default.fileExists(atPath: rawCanonical) {
+ #if DEBUG
</file context>
There was a problem hiding this comment.
This is intentional — the CWD containment check is a deliberate security boundary to prevent path traversal attacks (e.g. a malicious server injecting ../../etc/passwd in terminal output).
../foo.swift being blocked is the expected trade-off. In practice:
- Compiler/linter output uses CWD-relative paths (e.g.
src/main.swift:42), not parent-relative paths - If a user
cds into a subdirectory, OSC 7 updates the CWD, sofile.swiftin the new CWD still resolves correctly
If maintainers prefer a more permissive approach, we could relax the guard to allow realpath-resolved paths that stay within the project root (e.g. git toplevel), but that adds complexity. Happy to adjust based on project preference.
|
Re: @cubic-dev-ai path traversal guard blocking This is intentional in the current implementation. However, iTerm2 Semantic History has no such restriction — it resolves Two options for maintainers: Option A: Keep guard (current) — Option B: Remove guard (iTerm2-style) — Happy to go either direction. If Option B is preferred, removing the |
Summary
When a user clicks a bare filename (e.g.
README.md,src/main.py) in the terminal, resolve it against the terminal's current working directory (reported via OSC 7) and open it if the file exists on disk.This is the terminal equivalent of iTerm2's Semantic History feature, and the #1 workflow gap for AI coding agent users (Claude Code, Codex, etc.) who frequently see bare filenames in output.
Changes
resolveTerminalOpenURLTarget()now accepts an optionalworkingDirectoryparameterresolveBrowserNavigableURL), it checks whetherCWD/clicked_textresolves to an existing file viaFileManager.default.fileExists(atPath:)file.swift:42:10) are stripped before the file existence checkGHOSTTY_ACTION_OPEN_URLhandler now retrieves the terminal's CWD fromworkspace.panelDirectories[surfaceId](per-panel) orworkspace.currentDirectory(workspace-level fallback)Before / After
README.mdhttps://README.md(bug #1154) or no action/Users/me/project/README.mdin editorsrc/main.py:42/Users/me/project/src/main.pyin editor/absolute/path.mdhttps://google.comnonexistent.xyzHow it works
Why this matters
AI coding agents output bare filenames constantly:
In iTerm2, every one of these is Cmd+clickable. In cmux, none of them are. This PR fixes that.
Related
Test plan
file.swift:42→ opens file (line number stripped)🤖 Generated with Claude Code
Summary by CodeRabbit