Repository navigation
Fix inline VS Code command palette and WebSocket startup #5595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f5f9d5d
659bd5a
5aafe1c
bfd5319
9794961
7e40494
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -183,10 +183,11 @@ enum TerminalDirectoryOpenTarget: String, CaseIterable { | |
| func isAvailable(in environment: DetectionEnvironment = .live) -> Bool { | ||
| guard let applicationPath = applicationPath(in: environment) else { return false } | ||
| guard self == .vscodeInline else { return true } | ||
| return VSCodeCLILaunchConfigurationBuilder.launchConfiguration( | ||
| vscodeApplicationURL: URL(fileURLWithPath: applicationPath, isDirectory: true), | ||
| isExecutableAtPath: environment.isExecutableFileAtPath | ||
| ) != nil | ||
| // Keep menu/palette availability cheap. Cached code-server discovery does | ||
| // disk I/O and belongs to the actual launch path on the launch queue. | ||
| let codeTunnelURL = URL(fileURLWithPath: applicationPath, isDirectory: true) | ||
| .appendingPathComponent("Contents/Resources/app/bin/code-tunnel", isDirectory: false) | ||
| return environment.isExecutableFileAtPath(codeTunnelURL.path) | ||
| } | ||
|
|
||
| func applicationURL(in environment: DetectionEnvironment = .live) -> URL? { | ||
|
|
@@ -329,17 +330,61 @@ struct VSCodeCLILaunchConfiguration { | |
| } | ||
|
|
||
| enum VSCodeCLILaunchConfigurationBuilder { | ||
| private struct VSCodeProductMetadata: Decodable { | ||
| let dataFolderName: String? | ||
| } | ||
|
|
||
| static func launchConfiguration( | ||
| vscodeApplicationURL: URL, | ||
| homeDirectoryURL: URL = FileManager.default.homeDirectoryForCurrentUser, | ||
| baseEnvironment: [String: String] = ProcessInfo.processInfo.environment, | ||
| isExecutableAtPath: (String) -> Bool = { FileManager.default.isExecutableFile(atPath: $0) } | ||
| isExecutableAtPath: (String) -> Bool = { FileManager.default.isExecutableFile(atPath: $0) }, | ||
| dataAtURL: (URL) -> Data? = { try? Data(contentsOf: $0) }, | ||
| contentsOfDirectoryAtURL: (URL) -> [URL] = { url in | ||
| (try? FileManager.default.contentsOfDirectory( | ||
| at: url, | ||
| includingPropertiesForKeys: [.contentModificationDateKey], | ||
| options: [.skipsHiddenFiles] | ||
| )) ?? [] | ||
| }, | ||
| contentModificationDateAtURL: (URL) -> Date? = { url in | ||
| (try? url.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate | ||
| } | ||
| ) -> VSCodeCLILaunchConfiguration? { | ||
| let contentsURL = vscodeApplicationURL.appendingPathComponent("Contents", isDirectory: true) | ||
| let environment = nodeSafeEnvironment(from: baseEnvironment) | ||
|
|
||
| if let codeServerURL = preferredCachedCodeServerURL( | ||
| contentsURL: contentsURL, | ||
| homeDirectoryURL: homeDirectoryURL, | ||
| isExecutableAtPath: isExecutableAtPath, | ||
| dataAtURL: dataAtURL, | ||
| contentsOfDirectoryAtURL: contentsOfDirectoryAtURL, | ||
| contentModificationDateAtURL: contentModificationDateAtURL | ||
| ) { | ||
| var codeServerEnvironment = environment | ||
| codeServerEnvironment.removeValue(forKey: "ELECTRON_RUN_AS_NODE") | ||
| return VSCodeCLILaunchConfiguration( | ||
| executableURL: codeServerURL, | ||
| argumentsPrefix: [], | ||
| environment: codeServerEnvironment | ||
| ) | ||
| } | ||
|
|
||
| let codeTunnelURL = contentsURL.appendingPathComponent("Resources/app/bin/code-tunnel", isDirectory: false) | ||
| guard isExecutableAtPath(codeTunnelURL.path) else { return nil } | ||
| var codeTunnelEnvironment = environment | ||
| codeTunnelEnvironment["ELECTRON_RUN_AS_NODE"] = "1" | ||
|
|
||
| return VSCodeCLILaunchConfiguration( | ||
| executableURL: codeTunnelURL, | ||
| argumentsPrefix: ["serve-web"], | ||
| environment: codeTunnelEnvironment | ||
| ) | ||
| } | ||
|
|
||
| private static func nodeSafeEnvironment(from baseEnvironment: [String: String]) -> [String: String] { | ||
| var environment = baseEnvironment | ||
| environment["ELECTRON_RUN_AS_NODE"] = "1" | ||
| environment.removeValue(forKey: "VSCODE_NODE_OPTIONS") | ||
| environment.removeValue(forKey: "VSCODE_NODE_REPL_EXTERNAL_MODULE") | ||
| if let nodeOptions = environment["NODE_OPTIONS"] { | ||
|
|
@@ -350,12 +395,87 @@ enum VSCodeCLILaunchConfigurationBuilder { | |
| } | ||
| environment.removeValue(forKey: "NODE_OPTIONS") | ||
| environment.removeValue(forKey: "NODE_REPL_EXTERNAL_MODULE") | ||
|
|
||
| return VSCodeCLILaunchConfiguration( | ||
| executableURL: codeTunnelURL, | ||
| argumentsPrefix: [], | ||
| environment: environment | ||
| return environment | ||
| } | ||
|
|
||
| private static func preferredCachedCodeServerURL( | ||
| contentsURL: URL, | ||
| homeDirectoryURL: URL, | ||
| isExecutableAtPath: (String) -> Bool, | ||
| dataAtURL: (URL) -> Data?, | ||
| contentsOfDirectoryAtURL: (URL) -> [URL], | ||
| contentModificationDateAtURL: (URL) -> Date? | ||
| ) -> URL? { | ||
| let dataFolderName = vscodeDataFolderName( | ||
| contentsURL: contentsURL, | ||
| dataAtURL: dataAtURL | ||
| ) | ||
| let serveWebCacheURL = homeDirectoryURL | ||
| .appendingPathComponent(dataFolderName, isDirectory: true) | ||
| .appendingPathComponent("cli/serve-web", isDirectory: true) | ||
|
|
||
| if let orderedCacheIDs = serveWebLRUCacheIDs( | ||
| serveWebCacheURL: serveWebCacheURL, | ||
| dataAtURL: dataAtURL | ||
| ) { | ||
| for cacheID in orderedCacheIDs { | ||
| let codeServerURL = serveWebCacheURL | ||
| .appendingPathComponent(cacheID, isDirectory: true) | ||
| .appendingPathComponent("bin/code-server", isDirectory: false) | ||
| if isExecutableAtPath(codeServerURL.path) { | ||
| return codeServerURL | ||
|
Comment on lines
+425
to
+426
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For users whose Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
| } | ||
|
|
||
| let candidates = contentsOfDirectoryAtURL(serveWebCacheURL) | ||
| .map { | ||
| $0.appendingPathComponent("bin/code-server", isDirectory: false) | ||
| } | ||
| .filter { | ||
| isExecutableAtPath($0.path) | ||
| } | ||
| .sorted { lhs, rhs in | ||
| let lhsDate = contentModificationDateAtURL(lhs) ?? .distantPast | ||
| let rhsDate = contentModificationDateAtURL(rhs) ?? .distantPast | ||
| if lhsDate != rhsDate { | ||
| return lhsDate > rhsDate | ||
| } | ||
| return lhs.path > rhs.path | ||
| } | ||
|
|
||
| return candidates.first | ||
| } | ||
|
|
||
| private static func vscodeDataFolderName( | ||
| contentsURL: URL, | ||
| dataAtURL: (URL) -> Data? | ||
| ) -> String { | ||
| let productURL = contentsURL.appendingPathComponent("Resources/app/product.json", isDirectory: false) | ||
| guard let data = dataAtURL(productURL), | ||
| let product = try? JSONDecoder().decode(VSCodeProductMetadata.self, from: data), | ||
| let dataFolderName = product.dataFolderName, | ||
| isSafePathComponent(dataFolderName) else { | ||
| return ".vscode" | ||
| } | ||
| return dataFolderName | ||
| } | ||
|
|
||
| private static func serveWebLRUCacheIDs( | ||
| serveWebCacheURL: URL, | ||
| dataAtURL: (URL) -> Data? | ||
| ) -> [String]? { | ||
| let lruURL = serveWebCacheURL.appendingPathComponent("lru.json", isDirectory: false) | ||
| guard let data = dataAtURL(lruURL), | ||
| let cacheIDs = try? JSONDecoder().decode([String].self, from: data) else { | ||
| return nil | ||
| } | ||
| return cacheIDs.filter(isSafePathComponent) | ||
| } | ||
|
|
||
| private static func isSafePathComponent(_ component: String) -> Bool { | ||
| guard !component.isEmpty, component != ".", component != ".." else { return false } | ||
| return component.rangeOfCharacter(from: CharacterSet(charactersIn: "/\\")) == nil | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -544,6 +664,14 @@ final class VSCodeServeWebController { | |
| ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL, completion: completion) | ||
| } | ||
|
|
||
| func isServeWebURL(_ candidateURL: URL?) -> Bool { | ||
| guard let candidateURL else { return false } | ||
| let serveWebURL = queue.sync { | ||
| self.serveWebURL | ||
| } | ||
| return Self.urlsShareLoopbackOrigin(candidateURL, serveWebURL) | ||
| } | ||
|
Comment on lines
+667
to
+673
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Rule Used: Flag new blocking or timing-based synchronization ... (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Comment on lines
+667
to
+673
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | 💤 Low value Add documentation for the new public method. The 📝 Suggested documentation+ /// Returns `true` if the candidate URL shares the same loopback origin as the
+ /// currently running VS Code serve-web instance. Origin matching requires both
+ /// URLs to be HTTP with the same explicit port on loopback addresses.
+ ///
+ /// - Parameter candidateURL: The URL to test, typically a browser's current page URL.
+ /// - Returns: `true` if the candidate matches the serve-web origin, `false` otherwise.
func isServeWebURL(_ candidateURL: URL?) -> Bool {🤖 Prompt for AI Agents |
||
|
|
||
| private func launchServeWebProcess( | ||
| vscodeApplicationURL: URL, | ||
| expectedGeneration: UInt64 | ||
|
|
@@ -563,7 +691,6 @@ final class VSCodeServeWebController { | |
| let process = Process() | ||
| process.executableURL = launchConfiguration.executableURL | ||
| process.arguments = launchConfiguration.argumentsPrefix + [ | ||
| "serve-web", | ||
| "--accept-server-license-terms", | ||
| "--host", "127.0.0.1", | ||
| "--port", "0", | ||
|
Comment on lines
693
to
696
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the cached Useful? React with 👍 / 👎. |
||
|
|
@@ -711,6 +838,21 @@ final class VSCodeServeWebController { | |
| private static func removeConnectionTokenFile(at url: URL) { | ||
| try? FileManager.default.removeItem(at: url) | ||
| } | ||
|
|
||
| private static func urlsShareLoopbackOrigin(_ lhs: URL, _ rhs: URL?) -> Bool { | ||
| guard let rhs else { return false } | ||
| guard lhs.scheme?.lowercased() == "http", | ||
| rhs.scheme?.lowercased() == "http" else { | ||
| return false | ||
| } | ||
| guard lhs.port == rhs.port, lhs.port != nil else { return false } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | 💤 Low value Document the strict port requirement. The port check requires both URLs to have explicit non-nil ports. This is stricter than typical origin comparison (which treats 📝 Suggested clarifying comment+ // Require both URLs to have explicit non-nil ports. VS Code serve-web always
+ // binds to an ephemeral port (e.g., :54321), so both the stored serve-web URL
+ // and any matching browser navigation will have explicit ports. This strict
+ // check avoids false matches with default-port (implicit :80) localhost URLs.
guard lhs.port == rhs.port, lhs.port != nil else { return false }🤖 Prompt for AI Agents |
||
| guard let lhsHost = BrowserInsecureHTTPSettings.normalizeHost(lhs.host ?? ""), | ||
| let rhsHost = BrowserInsecureHTTPSettings.normalizeHost(rhs.host ?? "") else { | ||
| return false | ||
| } | ||
| return RemoteLoopbackProxyAlias.isLoopbackHost(lhsHost) | ||
| && RemoteLoopbackProxyAlias.isLoopbackHost(rhsHost) | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| final class ServeWebOutputCollector { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -711,6 +711,11 @@ final class CmuxWebView: WKWebView { | |
| } | ||
| } | ||
|
|
||
| if shouldRouteInlineVSCodeCommandPaletteShortcutThroughWebContentFirst(event, pageURL: url) { | ||
| _ = super.performKeyEquivalent(with: event) | ||
| return finish(true) | ||
|
Comment on lines
+714
to
+716
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an inline VS Code page receives Cmd+Shift+P but Useful? React with 👍 / 👎. |
||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| if !shouldRouteCommandEquivalentDirectlyToMainMenu(event) { | ||
| return finish(super.performKeyEquivalent(with: event)) | ||
| } | ||
|
|
@@ -785,6 +790,17 @@ final class CmuxWebView: WKWebView { | |
| } | ||
| } | ||
|
|
||
| // Inline VS Code owns Cmd+Shift+P for its in-page command palette. | ||
| // If this path reaches keyDown, forward it to WebKit instead of cmux. | ||
| if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.command), | ||
| shouldRouteInlineVSCodeCommandPaletteShortcutThroughWebContentFirst(event, pageURL: url) { | ||
| #if DEBUG | ||
| route = "inlineVSCode" | ||
| #endif | ||
| super.keyDown(with: event) | ||
| return | ||
| } | ||
|
|
||
| // Some Cmd-based key paths in WebKit don't consistently invoke performKeyEquivalent. | ||
| // Route them through the same app-level shortcut handler as a fallback. | ||
| if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.command), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.