diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index 0f6c42aca33f..d8f387a6443c 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -4,10 +4,10 @@ 20530 CLI/cmux.swift 17317 Sources/TerminalController.swift 15956 Sources/ContentView.swift -14636 Sources/AppDelegate.swift +16782 Sources/AppDelegate.swift 13975 Sources/Workspace.swift 13458 Sources/GhosttyTerminalView.swift -10607 Sources/Panels/BrowserPanel.swift +12676 Sources/Panels/BrowserPanel.swift 8285 Sources/cmuxApp.swift 7488 Sources/TabManager.swift 6794 Sources/Panels/BrowserPanelView.swift @@ -19,12 +19,12 @@ 4122 cmuxTests/WorkspaceRemoteConnectionTests.swift 3933 Sources/Panels/FilePreviewPanel.swift 3818 Sources/Feed/FeedPanelView.swift -3588 cmuxTests/BrowserConfigTests.swift +5509 cmuxTests/BrowserConfigTests.swift 3145 cmuxTests/BrowserPanelTests.swift 2917 Sources/CmuxConfig.swift 2863 cmuxTests/WindowAndDragTests.swift 2609 Sources/SessionIndexView.swift -2472 Sources/Panels/CmuxWebView.swift +2521 Sources/Panels/CmuxWebView.swift 2317 cmuxTests/TabManagerUnitTests.swift 2179 Sources/KeyboardShortcutSettings.swift 2172 Sources/TerminalWindowPortal.swift @@ -65,7 +65,7 @@ 705 cmuxUITests/BrowserOmnibarSuggestionsUITests.swift 701 cmuxUITests/BonsplitTabDragUITests.swift 683 Sources/SocketControlSettings.swift -645 Sources/App/ShortcutRoutingSupport.swift +867 Sources/App/ShortcutRoutingSupport.swift 643 Sources/Panels/BrowserPopupWindowController.swift 631 Sources/Feed/FeedCoordinator.swift 613 Sources/PortScanner.swift diff --git a/Sources/App/BrowserShortcutPassthroughPolicy.swift b/Sources/App/BrowserShortcutPassthroughPolicy.swift new file mode 100644 index 000000000000..4cde70876a6e --- /dev/null +++ b/Sources/App/BrowserShortcutPassthroughPolicy.swift @@ -0,0 +1,36 @@ +import AppKit + +/// Per-URL Cmd-modifier passthrough for the embedded browser pane. +/// +/// When the focused browser pane's URL matches the user-configured +/// `browser.shortcutPassthroughHosts` allowlist, Cmd-modifier key equivalents +/// should be handed off to the web content instead of being claimed by cmux's +/// main menu. This is the seam that lets VS Code (running in code-server +/// inside the embedded browser) receive Cmd+P, Cmd+Shift+P, Cmd+F, Cmd+B, +/// Cmd+D, etc. +/// +/// Default behavior (empty allowlist) is unchanged. When a chord is forwarded +/// but the page does NOT consume it (e.g. Cmd+Q has no JS handler), callers +/// fall back to AppKit's standard menu dispatch so system shortcuts still work. +/// +/// `@MainActor` on the function (not just the body) gives compile-time +/// guarantees that callers are on the main actor, instead of only the runtime +/// trap from `MainActor.assumeIsolated`. The function takes a plain `URL?` +/// rather than a `WKWebView` reference so tests can exercise the policy +/// without instantiating a web view. +@MainActor +func shouldPassthroughCommandEquivalentToWebContent( + _ event: NSEvent, + responder: NSResponder? = nil, + url: URL?, + defaults: UserDefaults = .standard +) -> Bool { + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + guard flags.contains(.command) else { return false } + + if cmuxIsLikelyWebInspectorResponder(responder) { + return false + } + + return BrowserLinkOpenSettings.urlMatchesShortcutPassthrough(url, defaults: defaults) +} diff --git a/Sources/App/ShortcutRoutingSupport.swift b/Sources/App/ShortcutRoutingSupport.swift index 5e59b602c759..463b2029e522 100644 --- a/Sources/App/ShortcutRoutingSupport.swift +++ b/Sources/App/ShortcutRoutingSupport.swift @@ -104,10 +104,43 @@ func shouldDispatchBrowserArrowViaFirstResponderKeyDown( return true } - // Keep modified arrow routing narrow to avoid stealing cmux shortcuts such - // as Cmd+Option+Arrow pane focus. Browser document editors own Cmd+Up/Down - // as trusted keyDown navigation to the start/end of the document. - return normalizedFlags == [.command] && (keyCode == 125 || keyCode == 126) + // cmux owns Cmd+Option+Arrow for pane focus navigation + // (focusLeft/focusRight/focusUp/focusDown). Never route those; let cmux's + // shortcut chain claim them. + if normalizedFlags.contains(.command), normalizedFlags.contains(.option) { + return false + } + + // Standard macOS text-navigation / selection chords. AppKit dispatches + // these via interpretKeyEvents -> moveLeftAndModifySelection: / + // moveWordRight: / moveToBeginningOfDocumentAndModifySelection: etc., + // which means WebKit must see them through keyDown (not NSWindow. + // performKeyEquivalent) for selection extension and word jumps to work. + // + // - Shift+arrow: extend selection by character + // - Option+arrow: jump by word (←/→) or paragraph (↑/↓) + // - Shift+Option+arrow: extend selection by word/paragraph + if normalizedFlags == [.shift] { + return true + } + if normalizedFlags == [.option] { + return true + } + if normalizedFlags == [.shift, .option] { + return true + } + + // Browser document editors own Cmd+Up/Down as trusted keyDown navigation + // to the start/end of the document. Cmd+Shift+Up/Down extends selection + // to that same boundary. + if normalizedFlags == [.command] && (keyCode == 125 || keyCode == 126) { + return true + } + if normalizedFlags == [.command, .shift] && (keyCode == 125 || keyCode == 126) { + return true + } + + return false } func shouldDispatchBrowserOmnibarArrowViaFirstResponderKeyDown( diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 7fb33e30a912..b2b2832b429f 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -11846,6 +11846,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent clearConfiguredShortcutChordState() return false } + // Per-URL passthrough — local event monitor entry point. + // + // Why this check exists despite identical checks at the NSWindow and + // CmuxWebView layers: the shortcut monitor (installShortcutMonitor) + // fires BEFORE AppKit sends the event to any window. If we let + // handleCustomShortcut's configured-shortcut routing run on a + // passthrough chord (e.g. Cmd+P with a configured `goToWorkspace` + // binding), the monitor would consume the event by returning nil + // and the chord would never reach the web view. We bail out here + // so the event flows through the normal dispatch chain and the + // downstream NSWindow/CmuxWebView passthrough fast paths take over. + if let focusedWindow = event.window ?? NSApp.keyWindow, + let firstResponder = focusedWindow.firstResponder, + browserOmnibarPanelId(for: firstResponder) == nil, + let webView = NSWindow.cmuxOwningWebView(for: firstResponder, in: focusedWindow, event: event), + shouldPassthroughCommandEquivalentToWebContent( + event, + responder: firstResponder, + url: webView.url + ) { + clearConfiguredShortcutChordState() + return false + } // `charactersIgnoringModifiers` can be nil for some synthetic NSEvents and certain special keys. // Treat nil as "" and rely on keyCode/layout-aware fallback logic where needed. @@ -16093,6 +16116,40 @@ private extension NSWindow { ) return true } + // Per-URL Cmd-modifier passthrough — NSWindow swizzle entry point. + // + // Why this check exists despite identical checks downstream in + // CmuxWebView.performKeyEquivalent: by the time AppKit's standard + // dispatch reaches the web view, several cmux-internal handlers + // further down in this function (stale-menu-shortcut, ghostty + // routing, browser-find/document-editing preflight) can have already + // claimed the event for cmux. We need to short-circuit at the + // window-swizzle layer so passthrough URLs bypass that entire + // ladder. Returning WebKit's actual consumption result here means + // chords the page does NOT handle (e.g. Cmd+Q with no JS handler) + // fall through to AppKit's standard main-menu dispatch and behave + // as users expect. The omnibar (browser address bar) is excluded + // so its own shortcuts keep working. + if let firstResponderWebView, + firstResponderOmnibarPanelId == nil, + shouldPassthroughCommandEquivalentToWebContent( + event, + responder: self.firstResponder, + url: firstResponderWebView.url + ) { + let result = firstResponderWebView.performKeyEquivalent(with: event) +#if DEBUG + cmuxDebugLog( + " → passthrough host match: webView.performKeyEquivalent returned \(result)" + ) +#endif + if result { + return true + } + // Web content did not consume the chord; let AppKit's standard + // menu dispatch run so system shortcuts (Cmd+Q, Cmd+W, Cmd+H, ...) + // still work on passthrough hosts. + } if AppDelegate.shared?.shouldSuppressStaleCmuxMenuShortcut(event: event) == true { if AppDelegate.shared?.handleConfiguredShortcutKeyEquivalent(event) == true { #if DEBUG @@ -16465,7 +16522,11 @@ private extension NSWindow { return nil } - private static func cmuxOwningWebView( + // `fileprivate` (not `private`) so AppDelegate.handleCustomShortcut can + // resolve the focused web view when applying the + // `browser.shortcutPassthroughHosts` policy in the local event monitor + // (which runs on the AppDelegate, not on the NSWindow extension). + fileprivate static func cmuxOwningWebView( for responder: NSResponder, in window: NSWindow, event: NSEvent? diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index 63bf352e923e..481813213d52 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -151,6 +151,7 @@ extension CmuxSettingsFileStore { "browser.hostsToOpenInEmbeddedBrowser", "browser.urlsToAlwaysOpenExternally", "browser.insecureHttpHostsAllowedInEmbeddedBrowser", + "browser.shortcutPassthroughHosts", "browser.showImportHintOnBlankTabs", "browser.reactGrabVersion", "shortcuts.bindings", diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 67dff210fa82..89e82700124e 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -903,6 +903,16 @@ final class CmuxSettingsFileStore { } else if section.keys.contains("insecureHttpHostsAllowedInEmbeddedBrowser") { logInvalid("browser.insecureHttpHostsAllowedInEmbeddedBrowser", sourcePath: sourcePath) } + if let values = jsonStringArray(section["shortcutPassthroughHosts"]) { + let normalized = values + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + snapshot.managedUserDefaults[BrowserLinkOpenSettings.shortcutPassthroughHostsKey] = .string( + normalized.joined(separator: "\n") + ) + } else if section.keys.contains("shortcutPassthroughHosts") { + logInvalid("browser.shortcutPassthroughHosts", sourcePath: sourcePath) + } if let value = jsonBool(section["showImportHintOnBlankTabs"]) { snapshot.managedUserDefaults[BrowserImportHintSettings.showOnBlankTabsKey] = .bool(value) } diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index a382d5ee45aa..6f8256bc56d5 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -772,6 +772,44 @@ final class BrowserProfileStore: ObservableObject { } } +/// Thread-safe cache for `BrowserLinkOpenSettings.shortcutPassthroughHosts`. +/// The raw newline-delimited string is read from UserDefaults on every call +/// (UserDefaults itself is fast — backed by an in-memory dictionary), but the +/// expensive `components(separatedBy:)` + `map(trimmingCharacters)` + `filter` +/// chain is cached and reused while the raw value is unchanged. This matters +/// because the function fires three times per Cmd-modifier keystroke on the +/// passthrough dispatch path (handleCustomShortcut → cmux_performKeyEquivalent +/// → CmuxWebView.performKeyEquivalent). Cache is keyed on the raw string so +/// it self-invalidates the first call after any UserDefaults change, without +/// needing an explicit observer. +private final class ShortcutPassthroughHostsCache: @unchecked Sendable { + static let shared = ShortcutPassthroughHostsCache() + private let lock = NSLock() + private var cachedHosts: [String]? + private var cachedRaw: String? + + func hosts(rawValue: String) -> [String] { + lock.lock() + if let cachedHosts, cachedRaw == rawValue { + let result = cachedHosts + lock.unlock() + return result + } + lock.unlock() + + let parsed = rawValue + .components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + + lock.lock() + cachedHosts = parsed + cachedRaw = rawValue + lock.unlock() + return parsed + } +} + enum BrowserLinkOpenSettings { static let openTerminalLinksInCmuxBrowserKey = "browserOpenTerminalLinksInCmuxBrowser" static let defaultOpenTerminalLinksInCmuxBrowser: Bool = true @@ -790,6 +828,9 @@ enum BrowserLinkOpenSettings { static let browserExternalOpenPatternsKey = "browserExternalOpenPatterns" static let defaultBrowserExternalOpenPatterns: String = "" + static let shortcutPassthroughHostsKey = "browserShortcutPassthroughHosts" + static let defaultShortcutPassthroughHosts: String = "" + static func openTerminalLinksInCmuxBrowser(defaults: UserDefaults = .standard) -> Bool { guard BrowserAvailabilitySettings.isEnabled(defaults: defaults) else { return false } if defaults.object(forKey: openTerminalLinksInCmuxBrowserKey) == nil { @@ -889,6 +930,38 @@ enum BrowserLinkOpenSettings { return false } + static func shortcutPassthroughHosts(defaults: UserDefaults = .standard) -> [String] { + let raw = defaults.string(forKey: shortcutPassthroughHostsKey) ?? defaultShortcutPassthroughHosts + return ShortcutPassthroughHostsCache.shared.hosts(rawValue: raw) + } + + /// Check whether a hostname is on the user's Cmd-shortcut passthrough allowlist. + /// Empty list means "no passthrough" — every Cmd-modifier chord stays with + /// cmux's menus. This is the inverse of `hostMatchesWhitelist`'s "empty + /// allows all" semantics; the default for passthrough is opt-in. + /// Supports exact match and wildcard prefix (`*.example.com`, which also + /// matches the `example.com` apex). + static func hostMatchesShortcutPassthrough(_ host: String, defaults: UserDefaults = .standard) -> Bool { + let rawPatterns = shortcutPassthroughHosts(defaults: defaults) + if rawPatterns.isEmpty { return false } + guard let normalizedHost = BrowserInsecureHTTPSettings.normalizeHost(host) else { return false } + for rawPattern in rawPatterns { + guard let pattern = normalizeWhitelistPattern(rawPattern) else { continue } + if hostMatchesPattern(normalizedHost, pattern: pattern) { + return true + } + } + return false + } + + /// Convenience wrapper around `hostMatchesShortcutPassthrough` that takes a + /// `URL?` and extracts the host. Returns `false` for nil URLs and URLs with + /// no host component (e.g. `about:blank`). + static func urlMatchesShortcutPassthrough(_ url: URL?, defaults: UserDefaults = .standard) -> Bool { + guard let host = url?.host, !host.isEmpty else { return false } + return hostMatchesShortcutPassthrough(host, defaults: defaults) + } + private static func normalizeWhitelistPattern(_ rawPattern: String) -> String? { let trimmed = rawPattern .trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift index 065522c83396..76e5e749045c 100644 --- a/Sources/Panels/CmuxWebView.swift +++ b/Sources/Panels/CmuxWebView.swift @@ -614,6 +614,37 @@ final class CmuxWebView: WKWebView { return finish(super.performKeyEquivalent(with: event)) } + // Per-URL passthrough — CmuxWebView entry point. + // + // This check is NOT dead code despite an identical check existing in + // NSWindow.cmux_performKeyEquivalent. AppKit can reach + // CmuxWebView.performKeyEquivalent via at least two paths: + // (a) NSWindow.performKeyEquivalent walking the responder chain + // (the swizzled cmux_performKeyEquivalent's passthrough block + // calls super → reaches us here), and + // (b) direct programmatic dispatch — including the existing + // browser-find / browser-document-editing preflight at + // AppDelegate.swift:~15985, which calls + // firstResponderWebView.performKeyEquivalent(with: event) + // directly without re-entering cmux_performKeyEquivalent. + // Removing this check would silently make path (b) bypass the + // passthrough rule. Returning super's result lets WebKit consume the + // chord (e.g. VS Code's Cmd+P preventDefault); if super returns false, + // AppKit's standard responder/menu lookup continues. This branch + // deliberately SKIPS the NSApp.mainMenu.performKeyEquivalent + // forwarding below — the cmux-menu forward is what was claiming + // Cmd+P/Cmd+F before this fix. + if shouldPassthroughCommandEquivalentToWebContent( + event, + responder: window?.firstResponder, + url: self.url + ) { +#if DEBUG + cmuxDebugLog("browser.web.performKeyEquivalent: passthrough host match → super") +#endif + return finish(super.performKeyEquivalent(with: event)) + } + if Self.isPasteAsPlainTextCommandEquivalent(event) { if event.timestamp > 0 { lastPasteAsPlainTextPerformKeyEventTimestamp = event.timestamp diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index dd5a6a2bf20f..3ada0f86a166 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -70,6 +70,8 @@ 4472A0024472A0024472A002 /* BrowserScreenshotPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4472B0024472B0024472B002 /* BrowserScreenshotPipeline.swift */; }; 4472A0034472A0034472A003 /* BrowserScreenshotSnapshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4472B0034472B0034472B003 /* BrowserScreenshotSnapshotter.swift */; }; A5008371 /* BrowserSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008370 /* BrowserSearchOverlay.swift */; }; + B5A5A7F0DAC0FBE71B7B0001 /* BrowserShortcutPassthroughPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5A7F0DAC0FBE71B7B0002 /* BrowserShortcutPassthroughPolicy.swift */; }; + B5A5A7E50C0C5A571B7B0001 /* BrowserShortcutPassthroughTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5A7E50C0C5A571B7B0002 /* BrowserShortcutPassthroughTests.swift */; }; A5007422 /* BrowserWebAuthnSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5007423 /* BrowserWebAuthnSupport.swift */; }; A5001534 /* BrowserWindowPortal.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001533 /* BrowserWindowPortal.swift */; }; C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE35530000000000000102 /* BundledCLILinkageTests.swift */; }; @@ -664,6 +666,8 @@ 4472B0024472B0024472B002 /* BrowserScreenshotPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserScreenshotPipeline.swift; sourceTree = ""; }; 4472B0034472B0034472B003 /* BrowserScreenshotSnapshotter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserScreenshotSnapshotter.swift; sourceTree = ""; }; A5008370 /* BrowserSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/BrowserSearchOverlay.swift; sourceTree = ""; }; + B5A5A7F0DAC0FBE71B7B0002 /* BrowserShortcutPassthroughPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/BrowserShortcutPassthroughPolicy.swift; sourceTree = ""; }; + B5A5A7E50C0C5A571B7B0002 /* BrowserShortcutPassthroughTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserShortcutPassthroughTests.swift; sourceTree = ""; }; A5007423 /* BrowserWebAuthnSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserWebAuthnSupport.swift; sourceTree = ""; }; A5001533 /* BrowserWindowPortal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowPortal.swift; sourceTree = ""; }; C0DE35530000000000000102 /* BundledCLILinkageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundledCLILinkageTests.swift; sourceTree = ""; }; @@ -1338,6 +1342,7 @@ 47D5AA7D29C94F5CA865B2BF /* ScreenIdentity.swift */, C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */, B42A82C6AA614E74873D9A5F /* ShortcutRoutingSupport.swift */, + B5A5A7F0DAC0FBE71B7B0002 /* BrowserShortcutPassthroughPolicy.swift */, AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */, E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */, C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */, @@ -1669,6 +1674,7 @@ A50019B3 /* SettingsSearchIndexTests.swift */, D36090010000000000000004 /* SettingsWindowPresenterTests.swift */, 970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */, + B5A5A7E50C0C5A571B7B0002 /* BrowserShortcutPassthroughTests.swift */, D3622001A1B2C3D4E5F60718 /* BrowserArrowKeyForwardingTests.swift */, 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */, D0B1000DA1B2C3D4E5F60001 /* CmuxWebViewDragRoutingTests.swift */, @@ -2087,6 +2093,7 @@ 4472A0024472A0024472A002 /* BrowserScreenshotPipeline.swift in Sources */, 4472A0034472A0034472A003 /* BrowserScreenshotSnapshotter.swift in Sources */, A5008371 /* BrowserSearchOverlay.swift in Sources */, + B5A5A7F0DAC0FBE71B7B0001 /* BrowserShortcutPassthroughPolicy.swift in Sources */, A5007422 /* BrowserWebAuthnSupport.swift in Sources */, A5001534 /* BrowserWindowPortal.swift in Sources */, C46790000000000000000003 /* CLIForwardingLaunchRouter.swift in Sources */, @@ -2453,6 +2460,7 @@ C2B6A97D1F2E4C71A8B9D001 /* BrowserOmnibarPerformanceSupportTests.swift in Sources */, D0B1000EA1B2C3D4E5F60001 /* BrowserPaneDropRoutingTests.swift in Sources */, 1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */, + B5A5A7E50C0C5A571B7B0001 /* BrowserShortcutPassthroughTests.swift in Sources */, C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */, F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */, D3571002A1B2C3D4E5F60718 /* CJKIMEMarkedSelectionTests.swift in Sources */, diff --git a/cmuxTests/BrowserArrowKeyForwardingTests.swift b/cmuxTests/BrowserArrowKeyForwardingTests.swift index 4f48ce0aba5d..80bb4d013fcc 100644 --- a/cmuxTests/BrowserArrowKeyForwardingTests.swift +++ b/cmuxTests/BrowserArrowKeyForwardingTests.swift @@ -42,4 +42,102 @@ final class BrowserArrowKeyForwardingTests: XCTestCase { XCTAssertFalse(shouldDispatchBrowserArrowViaFirstResponderKeyDown(keyCode: 126, firstResponderIsBrowser: true, flags: [.command, .option])) XCTAssertFalse(shouldDispatchBrowserArrowViaFirstResponderKeyDown(keyCode: 125, firstResponderIsBrowser: true, flags: [.command, .option])) } + + // MARK: - Selection / word-jump combos + // Shift+arrow, Option+arrow, Shift+Option+arrow, and Cmd+Shift+Up/Down must + // be routed via firstResponder.keyDown so AppKit's interpretKeyEvents path + // fires moveLeftAndModifySelection: / moveWordRight: / etc. into WebKit. + + func testRoutesShiftArrowForSelectionExtension() { + for keyCode in [123, 124, 125, 126] as [UInt16] { + XCTAssertTrue( + shouldDispatchBrowserArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsBrowser: true, + flags: [.shift] + ), + "Shift+arrow keyCode \(keyCode) must route to browser for selection extension" + ) + } + } + + func testRoutesOptionArrowForWordOrParagraphJump() { + for keyCode in [123, 124, 125, 126] as [UInt16] { + XCTAssertTrue( + shouldDispatchBrowserArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsBrowser: true, + flags: [.option] + ), + "Option+arrow keyCode \(keyCode) must route to browser for word/paragraph jump" + ) + } + } + + func testRoutesShiftOptionArrowForWordParagraphSelection() { + for keyCode in [123, 124, 125, 126] as [UInt16] { + XCTAssertTrue( + shouldDispatchBrowserArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsBrowser: true, + flags: [.shift, .option] + ), + "Shift+Option+arrow keyCode \(keyCode) must route to browser for word/paragraph selection" + ) + } + } + + func testRoutesCommandShiftUpAndDownForDocumentBoundarySelection() { + for keyCode in [125, 126] as [UInt16] { + XCTAssertTrue( + shouldDispatchBrowserArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsBrowser: true, + flags: [.command, .shift] + ), + "Cmd+Shift+vertical arrow keyCode \(keyCode) must route to browser for document-boundary selection" + ) + } + } + + // MARK: - Regression guards for cmux-owned chords / deliberate scope + + func testDoesNotRouteCommandOptionArrowForAnyDirection() { + // cmux uses Cmd+Option+Arrow for focusLeft/focusRight/focusUp/focusDown + // pane navigation. Selection-arrow extensions must not steal these. + for keyCode in [123, 124, 125, 126] as [UInt16] { + XCTAssertFalse( + shouldDispatchBrowserArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsBrowser: true, + flags: [.command, .option] + ), + "Cmd+Option+arrow keyCode \(keyCode) is cmux pane-focus and must NOT route to browser" + ) + XCTAssertFalse( + shouldDispatchBrowserArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsBrowser: true, + flags: [.command, .option, .shift] + ), + "Cmd+Option+Shift+arrow keyCode \(keyCode) must not route; still owned by cmux" + ) + } + } + + func testDoesNotRouteCommandShiftHorizontalArrowsByCurrentScope() { + // Cmd+Shift+Left/Right (extend selection to line start/end) is deliberately + // out of scope until a user reports a bug — symmetric with the existing + // decision to not route plain Cmd+Left/Right. Guard the scope. + for keyCode in [123, 124] as [UInt16] { + XCTAssertFalse( + shouldDispatchBrowserArrowViaFirstResponderKeyDown( + keyCode: keyCode, + firstResponderIsBrowser: true, + flags: [.command, .shift] + ), + "Cmd+Shift+horizontal arrow keyCode \(keyCode) is deliberately out of current routing scope" + ) + } + } } diff --git a/cmuxTests/BrowserConfigTests.swift b/cmuxTests/BrowserConfigTests.swift index de158d48b294..52dcb889a146 100644 --- a/cmuxTests/BrowserConfigTests.swift +++ b/cmuxTests/BrowserConfigTests.swift @@ -1481,6 +1481,128 @@ final class CmuxWebViewKeyEquivalentTests: XCTestCase { ) } + // MARK: - Passthrough host policy (PR #4724 review B2) + + private func withPassthroughHost(_ host: String?, _ body: () -> Void) { + let key = BrowserLinkOpenSettings.shortcutPassthroughHostsKey + let previous = UserDefaults.standard.string(forKey: key) + if let host { + UserDefaults.standard.set(host, forKey: key) + } else { + UserDefaults.standard.removeObject(forKey: key) + } + defer { + if let previous { + UserDefaults.standard.set(previous, forKey: key) + } else { + UserDefaults.standard.removeObject(forKey: key) + } + } + body() + } + + private func cmdEvent(key: String, keyCode: UInt16) -> NSEvent? { + makeKeyDownEvent(key: key, modifiers: [.command], keyCode: keyCode) + } + + @MainActor + func testPassthroughPolicyMatchesAllowlistedHosts() { + // B1 + B2: the policy function correctly identifies passthrough URLs. + // This is what the three production call sites (CmuxWebView, NSWindow + // swizzle, AppDelegate local-monitor) consult before forwarding a + // Cmd-modifier chord to the web view instead of cmux's menu. + withPassthroughHost("127.0.0.1") { + guard let event = cmdEvent(key: "q", keyCode: 12) else { + XCTFail("Failed to construct Cmd+Q event") + return + } + XCTAssertTrue( + shouldPassthroughCommandEquivalentToWebContent( + event, + url: URL(string: "http://127.0.0.1:8888/") + ), + "URL on allowlist must passthrough" + ) + XCTAssertFalse( + shouldPassthroughCommandEquivalentToWebContent( + event, + url: URL(string: "https://github.com/") + ), + "URL not on allowlist must NOT passthrough" + ) + } + } + + @MainActor + func testPassthroughPolicyRejectsNonCommandEvents() { + // Shift+arrow / plain arrow / Option-only chords have a separate + // dispatch path (shouldDispatchBrowserArrowViaFirstResponderKeyDown). + // The passthrough policy is for Cmd-modifier chords only. + withPassthroughHost("127.0.0.1") { + guard let event = makeKeyDownEvent(key: "a", modifiers: [.shift], keyCode: 0) else { + XCTFail("Failed to construct Shift+A event") + return + } + XCTAssertFalse( + shouldPassthroughCommandEquivalentToWebContent( + event, + url: URL(string: "http://127.0.0.1:8888/") + ), + "Non-command events must never trigger Cmd-passthrough" + ) + } + } + + @MainActor + func testPassthroughPolicyEmptyAllowlistMatchesNothing() { + // Default-behavior guarantee: an empty / unset allowlist must return + // false for every URL so cmux's existing menu routing is preserved. + withPassthroughHost(nil) { + guard let event = cmdEvent(key: "p", keyCode: 35) else { + XCTFail("Failed to construct Cmd+P event") + return + } + XCTAssertFalse( + shouldPassthroughCommandEquivalentToWebContent( + event, + url: URL(string: "http://127.0.0.1:8888/") + ), + "Empty passthrough allowlist must NOT passthrough (zero default-behavior change)" + ) + } + } + + @MainActor + func testCmuxWebViewSkipsMainMenuForwardOnPassthroughURL() { + // B2 integration: a CmuxWebView whose .url matches the passthrough + // allowlist must NOT invoke cmux's main-menu forwarding inside + // performKeyEquivalent. The fresh WKWebView has no committed URL + // (so passthrough is NOT triggered) which exercises the negative + // path; the matching URL case is covered by the policy tests above + // and the AppDelegate window-level swizzle test + // (testCmdNRoutesToMainMenuWhenWebViewIsFirstResponder etc.). + // + // This regression guard ensures the non-passthrough path still + // forwards Cmd+P to the main menu. + withPassthroughHost("127.0.0.1") { + let spy = ActionSpy() + installMenu(spy: spy, key: "p", modifiers: [.command]) + let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) + guard let event = cmdEvent(key: "p", keyCode: 35) else { + XCTFail("Failed to construct Cmd+P event") + return + } + // Fresh WKWebView has no URL, so passthrough policy returns false, + // and the existing cmux main-menu forward inside CmuxWebView. + // performKeyEquivalent runs unchanged. + XCTAssertTrue(webView.performKeyEquivalent(with: event)) + XCTAssertTrue( + spy.invoked, + "When the web view has no URL (passthrough cannot match), Cmd+P must still reach cmux's main menu — confirms passthrough did not break existing routing" + ) + } + } + private func installMenu(spy: ActionSpy, key: String, modifiers: NSEvent.ModifierFlags) { installMenu( target: spy, diff --git a/cmuxTests/BrowserShortcutPassthroughTests.swift b/cmuxTests/BrowserShortcutPassthroughTests.swift new file mode 100644 index 000000000000..5043d4bbb9ef --- /dev/null +++ b/cmuxTests/BrowserShortcutPassthroughTests.swift @@ -0,0 +1,94 @@ +import XCTest + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +final class BrowserShortcutPassthroughHostMatchingTests: XCTestCase { + private func makeIsolatedDefaults() -> UserDefaults { + let suiteName = "BrowserShortcutPassthroughHostMatchingTests.\(UUID().uuidString)" + guard let defaults = UserDefaults(suiteName: suiteName) else { + fatalError("Failed to create defaults suite") + } + defaults.removePersistentDomain(forName: suiteName) + addTeardownBlock { + defaults.removePersistentDomain(forName: suiteName) + } + return defaults + } + + func testEmptyAllowlistReturnsFalseSoDefaultBehaviorIsUnchanged() { + let defaults = makeIsolatedDefaults() + XCTAssertFalse( + BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("127.0.0.1", defaults: defaults), + "Empty allowlist must NOT passthrough — that's the safety guarantee." + ) + XCTAssertFalse( + BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("example.com", defaults: defaults) + ) + } + + func testExactHostMatch() { + let defaults = makeIsolatedDefaults() + defaults.set("127.0.0.1\nlocalhost", forKey: BrowserLinkOpenSettings.shortcutPassthroughHostsKey) + + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("127.0.0.1", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("localhost", defaults: defaults)) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("example.com", defaults: defaults)) + } + + func testWildcardSubdomainMatch() { + let defaults = makeIsolatedDefaults() + defaults.set("*.example.com", forKey: BrowserLinkOpenSettings.shortcutPassthroughHostsKey) + + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("dev.example.com", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("a.b.example.com", defaults: defaults)) + XCTAssertTrue( + BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("example.com", defaults: defaults), + "*.example.com should also match the apex host (consistent with hostsToOpenInEmbeddedBrowser behavior)." + ) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("notexample.com", defaults: defaults)) + } + + func testURLAccessorStripsPortAndMatchesHost() { + let defaults = makeIsolatedDefaults() + defaults.set("127.0.0.1", forKey: BrowserLinkOpenSettings.shortcutPassthroughHostsKey) + + XCTAssertTrue( + BrowserLinkOpenSettings.urlMatchesShortcutPassthrough( + URL(string: "http://127.0.0.1:8888/?folder=/x"), + defaults: defaults + ), + "URL host matching should ignore port — code-server on any loopback port is covered by the host entry." + ) + XCTAssertFalse( + BrowserLinkOpenSettings.urlMatchesShortcutPassthrough( + URL(string: "https://example.com/page"), + defaults: defaults + ) + ) + } + + func testURLAccessorReturnsFalseForNilOrSchemeOnlyURL() { + let defaults = makeIsolatedDefaults() + defaults.set("127.0.0.1", forKey: BrowserLinkOpenSettings.shortcutPassthroughHostsKey) + + XCTAssertFalse(BrowserLinkOpenSettings.urlMatchesShortcutPassthrough(nil, defaults: defaults)) + XCTAssertFalse( + BrowserLinkOpenSettings.urlMatchesShortcutPassthrough(URL(string: "about:blank"), defaults: defaults) + ) + } + + func testWhitespaceAndBlankEntriesAreIgnored() { + let defaults = makeIsolatedDefaults() + defaults.set( + " \n127.0.0.1\n\n localhost \n", + forKey: BrowserLinkOpenSettings.shortcutPassthroughHostsKey + ) + + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("127.0.0.1", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesShortcutPassthrough("localhost", defaults: defaults)) + } +} diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index 66ce568bddfd..43c6401c4faa 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -82,7 +82,8 @@ const settingsFileExample = `{ // "customSearchEngineName": "My Search", // "customSearchEngineURLTemplate": "https://search.example.com/?q={query}", // "openTerminalLinksInCmuxBrowser": true, - // "hostsToOpenInEmbeddedBrowser": ["localhost", "*.internal.example"] + // "hostsToOpenInEmbeddedBrowser": ["localhost", "*.internal.example"], + // "shortcutPassthroughHosts": ["127.0.0.1", "localhost"] // }, // "automation": { diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index a88371decf5b..be62f17846d3 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -916,6 +916,15 @@ ], "description": "HTTP hosts allowed in the embedded browser without a warning prompt." }, + "shortcutPassthroughHosts": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "descriptionKey": "schemaDescriptions.browser.shortcutPassthroughHosts", + "description": "Hosts whose pages receive Cmd-modifier shortcuts directly. On a match, Cmd-modifier chords are forwarded to the web view first; if the page does not consume a chord, cmux's normal menu handling resumes (so Cmd+Q still quits). Useful for embedding web IDEs such as VS Code via code-server. Supports exact host match and *.subdomain wildcards (a *.example.com pattern also matches the example.com apex). Port numbers are ignored. Empty list keeps cmux's existing behavior on every page." + }, "showImportHintOnBlankTabs": { "type": "boolean", "default": true, diff --git a/web/messages/ar.json b/web/messages/ar.json index 6fbbba23998a..0df056f8622e 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "محرك البحث الافتراضي لاستعلامات شريط عنوان المتصفح التي ليست عناوين URL. استخدم custom مع customSearchEngineURLTemplate لموفري بحث آخرين.", "customSearchEngineName": "اسم العرض المستخدم عندما تكون defaultSearchEngine هي custom.", - "customSearchEngineURLTemplate": "عنوان URL للبحث عند استخدام custom. أدرج موضع الاستعلام أو %s لعبارة البحث المرمزة. عند عدم وجود موضع، يضيف cmux q= إلى العنوان." + "customSearchEngineURLTemplate": "عنوان URL للبحث عند استخدام custom. أدرج موضع الاستعلام أو %s لعبارة البحث المرمزة. عند عدم وجود موضع، يضيف cmux q= إلى العنوان.", + "shortcutPassthroughHosts": "المضيفون الذين تتلقى صفحاتهم اختصارات معدِّل Cmd مباشرةً. عند المطابقة، تُمرَّر اختصارات Cmd أولاً إلى عرض الويب؛ إذا لم تستهلك الصفحة الاختصار، يُستأنف التعامل العادي مع قائمة cmux (لذا Cmd+Q لا يزال يخرج). مفيد لتضمين بيئات تطوير ويب مثل VS Code عبر code-server. يدعم المطابقة الدقيقة للمضيف ورموز البدل *.subdomain (نمط *.example.com يطابق أيضاً example.com نفسه). تُتجاهل أرقام المنفذ. القائمة الفارغة تحافظ على سلوك cmux الحالي في كل صفحة." } } }, diff --git a/web/messages/bs.json b/web/messages/bs.json index 885489e12691..7a4d44ee1286 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Zadani pretraživač za upite u adresnoj traci preglednika koji nisu URL-ovi. Koristite custom uz customSearchEngineURLTemplate za druge pružatelje.", "customSearchEngineName": "Prikazani naziv koji se koristi kada je defaultSearchEngine custom.", - "customSearchEngineURLTemplate": "URL pretrage koji se koristi kada je defaultSearchEngine custom. Uključite rezervirano mjesto za upit ili %s za kodirani upit. Ako nema rezerviranog mjesta, cmux dodaje q= u URL." + "customSearchEngineURLTemplate": "URL pretrage koji se koristi kada je defaultSearchEngine custom. Uključite rezervirano mjesto za upit ili %s za kodirani upit. Ako nema rezerviranog mjesta, cmux dodaje q= u URL.", + "shortcutPassthroughHosts": "Hostovi čije stranice direktno primaju prečice s Cmd tipkom. Pri podudaranju, Cmd prečice se prvo prosljeđuju u web prikaz; ako stranica ne konzumira prečicu, nastavlja se uobičajeno upravljanje cmux menijem (pa Cmd+Q i dalje izlazi). Korisno za ugrađivanje web IDE-ova poput VS Code preko code-server. Podržava tačno podudaranje hosta i *.poddomen znakove zamjene (obrazac *.example.com također odgovara samom example.com). Brojevi portova se ignoriraju. Prazna lista zadržava postojeće cmux ponašanje na svakoj stranici." } } }, diff --git a/web/messages/da.json b/web/messages/da.json index e4d916f02d9d..193389a5d784 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Standardsøgemaskine for forespørgsler i browserens adresselinje, der ikke er URLer. Brug custom med customSearchEngineURLTemplate til andre udbydere.", "customSearchEngineName": "Visningsnavn brugt når defaultSearchEngine er custom.", - "customSearchEngineURLTemplate": "Søge-URL brugt når defaultSearchEngine er custom. Medtag forespørgselspladsholderen eller %s for den kodede søgning. Uden en pladsholder tilføjer cmux q= til URLen." + "customSearchEngineURLTemplate": "Søge-URL brugt når defaultSearchEngine er custom. Medtag forespørgselspladsholderen eller %s for den kodede søgning. Uden en pladsholder tilføjer cmux q= til URLen.", + "shortcutPassthroughHosts": "Værter hvis sider modtager Cmd-modifikatorgenveje direkte. Ved match videresendes Cmd-modifikatorgenveje først til webvisningen; hvis siden ikke forbruger genvejen, genoptages cmux' normale menuhåndtering (så Cmd+Q stadig afslutter). Nyttig til at indlejre web-IDE'er som VS Code via code-server. Understøtter præcis værtsmatch og *.underdomæne jokertegn (et *.example.com-mønster matcher også selve example.com). Portnumre ignoreres. En tom liste bevarer cmux' eksisterende adfærd på hver side." } } }, diff --git a/web/messages/de.json b/web/messages/de.json index ef98c5ef6803..ec38048d1598 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Standardsuchmaschine für Nicht-URL-Eingaben in der Browser-Adressleiste. Verwenden Sie custom mit customSearchEngineURLTemplate für beliebige Anbieter.", "customSearchEngineName": "Anzeigename, der verwendet wird, wenn defaultSearchEngine custom ist.", - "customSearchEngineURLTemplate": "Such-URL, die verwendet wird, wenn defaultSearchEngine custom ist. Verwenden Sie den Abfrageplatzhalter oder %s für den kodierten Suchbegriff. Ohne Platzhalter fügt cmux q= zur URL hinzu." + "customSearchEngineURLTemplate": "Such-URL, die verwendet wird, wenn defaultSearchEngine custom ist. Verwenden Sie den Abfrageplatzhalter oder %s für den kodierten Suchbegriff. Ohne Platzhalter fügt cmux q= zur URL hinzu.", + "shortcutPassthroughHosts": "Hosts, deren Seiten Cmd-Tastenkürzel direkt erhalten. Bei einer Übereinstimmung werden Cmd-Kürzel zuerst an die Webansicht weitergeleitet; wenn die Seite ein Kürzel nicht verarbeitet, übernimmt die normale Menüverarbeitung von cmux (Cmd+Q beendet also weiterhin). Nützlich zum Einbetten von Web-IDEs wie VS Code über code-server. Unterstützt exakte Host-Übereinstimmung und *.subdomain-Platzhalter (ein Muster *.example.com passt auch auf example.com selbst). Portnummern werden ignoriert. Eine leere Liste behält das bestehende cmux-Verhalten auf jeder Seite bei." } } }, diff --git a/web/messages/en.json b/web/messages/en.json index 31581b3fb9a1..f92379b48b44 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -604,7 +604,8 @@ "browser": { "defaultSearchEngine": "Default search engine for non-URL browser address bar queries. Use custom with customSearchEngineURLTemplate for arbitrary providers.", "customSearchEngineName": "Display name used when defaultSearchEngine is custom.", - "customSearchEngineURLTemplate": "Search URL used when defaultSearchEngine is custom. Include the query placeholder or %s for the encoded query. If omitted, cmux appends q= to the URL." + "customSearchEngineURLTemplate": "Search URL used when defaultSearchEngine is custom. Include the query placeholder or %s for the encoded query. If omitted, cmux appends q= to the URL.", + "shortcutPassthroughHosts": "Hosts whose pages receive Cmd-modifier shortcuts directly. On a match, Cmd-modifier chords are forwarded to the web view first; if the page does not consume a chord, cmux's normal menu handling resumes (so Cmd+Q still quits). Useful for embedding web IDEs such as VS Code via code-server. Supports exact host match and *.subdomain wildcards (a *.example.com pattern also matches the example.com apex). Port numbers are ignored. Empty list keeps cmux's existing behavior on every page." } } }, diff --git a/web/messages/es.json b/web/messages/es.json index 9047544d9810..6f3424185654 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Motor de búsqueda predeterminado para consultas de la barra de direcciones del navegador que no son URL. Usa custom con customSearchEngineURLTemplate para otros proveedores.", "customSearchEngineName": "Nombre visible usado cuando defaultSearchEngine es custom.", - "customSearchEngineURLTemplate": "URL de búsqueda usada cuando defaultSearchEngine es custom. Incluye el marcador de consulta o %s para la consulta codificada. Si no hay marcador, cmux añade q= a la URL." + "customSearchEngineURLTemplate": "URL de búsqueda usada cuando defaultSearchEngine es custom. Incluye el marcador de consulta o %s para la consulta codificada. Si no hay marcador, cmux añade q= a la URL.", + "shortcutPassthroughHosts": "Hosts cuyas páginas reciben directamente los atajos con la tecla Cmd. En una coincidencia, los atajos Cmd se envían primero a la vista web; si la página no consume el atajo, se reanuda el manejo normal del menú de cmux (así Cmd+Q sigue funcionando para salir). Útil para incrustar IDE web como VS Code mediante code-server. Admite coincidencia exacta de host y comodines *.subdominio (un patrón *.example.com también coincide con el ápice example.com). Los números de puerto se ignoran. Una lista vacía mantiene el comportamiento existente de cmux en cada página." } } }, diff --git a/web/messages/fr.json b/web/messages/fr.json index 35a98ed016d9..7e69e03b4f41 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Moteur de recherche par défaut pour les saisies de la barre d’adresse du navigateur qui ne sont pas des URL. Utilisez custom avec customSearchEngineURLTemplate pour d’autres fournisseurs.", "customSearchEngineName": "Nom affiché lorsque defaultSearchEngine vaut custom.", - "customSearchEngineURLTemplate": "URL de recherche utilisée lorsque defaultSearchEngine vaut custom. Incluez l’espace réservé de requête ou %s pour la requête encodée. Sans espace réservé, cmux ajoute q= à l’URL." + "customSearchEngineURLTemplate": "URL de recherche utilisée lorsque defaultSearchEngine vaut custom. Incluez l’espace réservé de requête ou %s pour la requête encodée. Sans espace réservé, cmux ajoute q= à l’URL.", + "shortcutPassthroughHosts": "Hôtes dont les pages reçoivent directement les raccourcis avec touche Cmd. En cas de correspondance, les raccourcis Cmd sont d'abord transmis à la vue web ; si la page ne consomme pas le raccourci, la gestion normale des menus de cmux reprend (Cmd+Q permet donc toujours de quitter). Utile pour héberger des IDE web comme VS Code via code-server. Accepte la correspondance exacte d'hôte et les jokers *.sousdomaine (un motif *.example.com correspond aussi à example.com lui-même). Les numéros de port sont ignorés. Une liste vide conserve le comportement actuel de cmux sur chaque page." } } }, diff --git a/web/messages/it.json b/web/messages/it.json index adf8e52a59fd..262da71677de 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Motore di ricerca predefinito per le query nella barra degli indirizzi del browser che non sono URL. Usa custom con customSearchEngineURLTemplate per altri provider.", "customSearchEngineName": "Nome visualizzato quando defaultSearchEngine è custom.", - "customSearchEngineURLTemplate": "URL di ricerca usato quando defaultSearchEngine è custom. Includi il segnaposto della query o %s per la query codificata. Senza segnaposto, cmux aggiunge q= all’URL." + "customSearchEngineURLTemplate": "URL di ricerca usato quando defaultSearchEngine è custom. Includi il segnaposto della query o %s per la query codificata. Senza segnaposto, cmux aggiunge q= all’URL.", + "shortcutPassthroughHosts": "Host le cui pagine ricevono direttamente le scorciatoie con tasto Cmd. In caso di corrispondenza, le scorciatoie Cmd vengono prima inoltrate alla vista web; se la pagina non consuma una scorciatoia, riprende la normale gestione dei menu di cmux (quindi Cmd+Q continua a far uscire). Utile per ospitare IDE web come VS Code tramite code-server. Supporta corrispondenza host esatta e jolly *.sottodominio (un pattern *.example.com corrisponde anche all'apex example.com). I numeri di porta vengono ignorati. Una lista vuota mantiene il comportamento esistente di cmux su ogni pagina." } } }, diff --git a/web/messages/ja.json b/web/messages/ja.json index 59f8fad45008..624366f752bd 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -563,7 +563,8 @@ "browser": { "defaultSearchEngine": "ブラウザのアドレスバーでURLではない入力に使う既定の検索エンジンです。任意のプロバイダには custom と customSearchEngineURLTemplate を使います。", "customSearchEngineName": "defaultSearchEngine が custom のときに使う表示名です。", - "customSearchEngineURLTemplate": "defaultSearchEngine が custom のときに使う検索URLです。エンコード済み検索語にはクエリ用プレースホルダーまたは %s を含めます。省略すると cmux が URL に q= を追加します。" + "customSearchEngineURLTemplate": "defaultSearchEngine が custom のときに使う検索URLです。エンコード済み検索語にはクエリ用プレースホルダーまたは %s を含めます。省略すると cmux が URL に q= を追加します。", + "shortcutPassthroughHosts": "ページに対して Cmd 修飾キーのショートカットを直接渡すホスト。マッチした場合、Cmd 修飾の組み合わせはまず Web ビューに転送されます。ページがその組み合わせを消費しなかった場合は、cmux の通常のメニュー処理に戻ります(Cmd+Q は引き続き終了します)。code-server 経由の VS Code のような Web IDE を埋め込むのに便利です。完全一致と *.サブドメイン ワイルドカードをサポートします(*.example.com のパターンは example.com 自体にもマッチします)。ポート番号は無視されます。空のリストはすべてのページで既存の cmux の動作を維持します。" } } }, diff --git a/web/messages/km.json b/web/messages/km.json index a6da8881f0e3..7b2e0627ff89 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "ម៉ាស៊ីនស្វែងរកលំនាំដើមសម្រាប់សំណួរនៅក្នុងរបារអាសយដ្ឋានដែលមិនមែនជា URL។ ប្រើ custom ជាមួយ customSearchEngineURLTemplate សម្រាប់អ្នកផ្តល់ផ្សេងៗ។", "customSearchEngineName": "ឈ្មោះបង្ហាញដែលប្រើនៅពេល defaultSearchEngine ជា custom។", - "customSearchEngineURLTemplate": "URL ស្វែងរកដែលប្រើនៅពេល defaultSearchEngine ជា custom។ ដាក់ទីតាំងជំនួសសំណួរ ឬ %s សម្រាប់ពាក្យស្វែងរកដែលបានអ៊ិនកូដ។ ប្រសិនបើគ្មានទីតាំងជំនួស cmux បន្ថែម q= ទៅ URL។" + "customSearchEngineURLTemplate": "URL ស្វែងរកដែលប្រើនៅពេល defaultSearchEngine ជា custom។ ដាក់ទីតាំងជំនួសសំណួរ ឬ %s សម្រាប់ពាក្យស្វែងរកដែលបានអ៊ិនកូដ។ ប្រសិនបើគ្មានទីតាំងជំនួស cmux បន្ថែម q= ទៅ URL។", + "shortcutPassthroughHosts": "ម៉ាស៊ីនមេដែលទំព័ររបស់ពួកវាទទួលផ្លូវកាត់កែប្រែ Cmd ដោយផ្ទាល់។ នៅពេលដែលត្រូវគ្នា ការផ្សំ Cmd ត្រូវបានបញ្ជូនបន្តទៅទិដ្ឋភាពបណ្តាញជាមុនសិន។ បើទំព័រមិនប្រើការផ្សំនេះ ការគ្រប់គ្រងម៉ឺនុយធម្មតារបស់ cmux នឹងបន្ត (ដូច្នេះ Cmd+Q នៅតែបិទកម្មវិធី)។ មានប្រយោជន៍សម្រាប់បង្កប់ IDE បណ្តាញដូចជា VS Code តាមរយៈ code-server។ គាំទ្រការផ្គូផ្គងពិតប្រាកដនិងសញ្ញាជំនួស *.subdomain (លំនាំ *.example.com ក៏ផ្គូផ្គងនឹង example.com ដោយផ្ទាល់ដែរ)។ លេខច្រកត្រូវបានព្រងើយកន្តើយ។ បញ្ជីទទេនឹងរក្សាអាកប្បកិរិយាបច្ចុប្បន្នរបស់ cmux លើគ្រប់ទំព័រ។" } } }, diff --git a/web/messages/ko.json b/web/messages/ko.json index 8d9a4a2e9c38..25e54c5e8fd6 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "브라우저 주소 표시줄에서 URL이 아닌 입력에 사용할 기본 검색 엔진입니다. 다른 제공자는 custom과 customSearchEngineURLTemplate을 사용하세요.", "customSearchEngineName": "defaultSearchEngine이 custom일 때 사용할 표시 이름입니다.", - "customSearchEngineURLTemplate": "defaultSearchEngine이 custom일 때 사용할 검색 URL입니다. 인코딩된 검색어에는 쿼리 자리표시자 또는 %s를 포함하세요. 자리표시자가 없으면 cmux가 URL에 q=를 추가합니다." + "customSearchEngineURLTemplate": "defaultSearchEngine이 custom일 때 사용할 검색 URL입니다. 인코딩된 검색어에는 쿼리 자리표시자 또는 %s를 포함하세요. 자리표시자가 없으면 cmux가 URL에 q=를 추가합니다.", + "shortcutPassthroughHosts": "페이지가 Cmd 수정자 단축키를 직접 받는 호스트입니다. 일치 시 Cmd 수정자 조합은 먼저 웹 뷰로 전달됩니다. 페이지가 조합을 소비하지 않으면 cmux의 일반 메뉴 처리로 복귀합니다(따라서 Cmd+Q는 계속 종료됩니다). code-server를 통한 VS Code 같은 웹 IDE를 임베드할 때 유용합니다. 정확한 호스트 일치와 *.서브도메인 와일드카드를 지원합니다(*.example.com 패턴은 example.com 자체에도 일치합니다). 포트 번호는 무시됩니다. 빈 목록은 모든 페이지에서 기존 cmux 동작을 유지합니다." } } }, diff --git a/web/messages/no.json b/web/messages/no.json index b9ca895d9bbe..6e43138f6ffa 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Standard søkemotor for søk i nettleserens adresselinje som ikke er URLer. Bruk custom med customSearchEngineURLTemplate for andre leverandører.", "customSearchEngineName": "Visningsnavn som brukes når defaultSearchEngine er custom.", - "customSearchEngineURLTemplate": "Søke-URL som brukes når defaultSearchEngine er custom. Inkluder søkeplassholderen eller %s for det kodede søket. Uten plassholder legger cmux til q= i URLen." + "customSearchEngineURLTemplate": "Søke-URL som brukes når defaultSearchEngine er custom. Inkluder søkeplassholderen eller %s for det kodede søket. Uten plassholder legger cmux til q= i URLen.", + "shortcutPassthroughHosts": "Verter hvis sider mottar Cmd-modifikatorsnarveier direkte. Ved en treff videresendes Cmd-snarveier først til nettvisningen; hvis siden ikke konsumerer snarveien, gjenopptas cmux' vanlige menyhåndtering (så Cmd+Q fortsatt avslutter). Nyttig for å bygge inn web-IDE-er som VS Code via code-server. Støtter eksakt vertsmatch og *.underdomene jokertegn (et *.example.com-mønster matcher også selve example.com). Portnumre ignoreres. En tom liste beholder cmux' eksisterende oppførsel på hver side." } } }, diff --git a/web/messages/pl.json b/web/messages/pl.json index 6ba04a060a42..d53ab4671d18 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Domyślna wyszukiwarka dla wpisów w pasku adresu przeglądarki, które nie są URL-ami. Użyj custom z customSearchEngineURLTemplate dla innych dostawców.", "customSearchEngineName": "Nazwa wyświetlana, gdy defaultSearchEngine ma wartość custom.", - "customSearchEngineURLTemplate": "URL wyszukiwania używany, gdy defaultSearchEngine ma wartość custom. Dodaj symbol zastępczy zapytania lub %s dla zakodowanego zapytania. Bez symbolu zastępczego cmux doda q= do URL." + "customSearchEngineURLTemplate": "URL wyszukiwania używany, gdy defaultSearchEngine ma wartość custom. Dodaj symbol zastępczy zapytania lub %s dla zakodowanego zapytania. Bez symbolu zastępczego cmux doda q= do URL.", + "shortcutPassthroughHosts": "Hosty, których strony bezpośrednio otrzymują skróty z klawiszem Cmd. Po dopasowaniu kombinacje Cmd są najpierw przekazywane do widoku internetowego; jeśli strona nie konsumuje kombinacji, wznawiana jest standardowa obsługa menu cmux (więc Cmd+Q nadal zamyka). Przydatne do osadzania internetowych IDE, takich jak VS Code przez code-server. Obsługuje dokładne dopasowanie hosta i symbole wieloznaczne *.subdomena (wzorzec *.example.com pasuje również do samego example.com). Numery portów są ignorowane. Pusta lista zachowuje istniejące zachowanie cmux na każdej stronie." } } }, diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 052b0ed8d047..30eb540924e5 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Mecanismo de busca padrão para consultas na barra de endereço do navegador que não são URLs. Use custom com customSearchEngineURLTemplate para outros provedores.", "customSearchEngineName": "Nome exibido quando defaultSearchEngine é custom.", - "customSearchEngineURLTemplate": "URL de busca usada quando defaultSearchEngine é custom. Inclua o marcador da consulta ou %s para a consulta codificada. Sem marcador, o cmux adiciona q= à URL." + "customSearchEngineURLTemplate": "URL de busca usada quando defaultSearchEngine é custom. Inclua o marcador da consulta ou %s para a consulta codificada. Sem marcador, o cmux adiciona q= à URL.", + "shortcutPassthroughHosts": "Hosts cujas páginas recebem diretamente atalhos com tecla Cmd. Em uma correspondência, os atalhos Cmd são enviados primeiro para a visualização web; se a página não consumir o atalho, o tratamento normal de menu do cmux é retomado (então Cmd+Q continua saindo). Útil para incorporar IDEs web como VS Code via code-server. Aceita correspondência exata de host e curingas *.subdomínio (um padrão *.example.com também corresponde ao ápice example.com). Números de porta são ignorados. Uma lista vazia mantém o comportamento existente do cmux em todas as páginas." } } }, diff --git a/web/messages/ru.json b/web/messages/ru.json index d902d8417c4d..5a49dca97f78 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Поисковая система по умолчанию для запросов в адресной строке браузера, которые не являются URL. Используйте custom с customSearchEngineURLTemplate для других провайдеров.", "customSearchEngineName": "Отображаемое имя, когда defaultSearchEngine имеет значение custom.", - "customSearchEngineURLTemplate": "URL поиска, используемый когда defaultSearchEngine имеет значение custom. Укажите заполнитель запроса или %s для закодированного запроса. Без заполнителя cmux добавит q= к URL." + "customSearchEngineURLTemplate": "URL поиска, используемый когда defaultSearchEngine имеет значение custom. Укажите заполнитель запроса или %s для закодированного запроса. Без заполнителя cmux добавит q= к URL.", + "shortcutPassthroughHosts": "Хосты, страницы которых получают сочетания клавиш с Cmd напрямую. При совпадении сочетания с Cmd сначала передаются в веб-просмотр; если страница не обрабатывает сочетание, возобновляется обычная обработка меню cmux (поэтому Cmd+Q по-прежнему завершает работу). Полезно для встраивания веб-IDE, таких как VS Code через code-server. Поддерживает точное совпадение хоста и подстановочные знаки *.поддомен (шаблон *.example.com также совпадает с самим example.com). Номера портов игнорируются. Пустой список сохраняет существующее поведение cmux на каждой странице." } } }, diff --git a/web/messages/th.json b/web/messages/th.json index 691ac0998f22..da7ffe291b26 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "เครื่องมือค้นหาเริ่มต้นสำหรับคำค้นในแถบที่อยู่ของเบราว์เซอร์ที่ไม่ใช่ URL ใช้ custom กับ customSearchEngineURLTemplate สำหรับผู้ให้บริการอื่น", "customSearchEngineName": "ชื่อที่แสดงเมื่อ defaultSearchEngine เป็น custom", - "customSearchEngineURLTemplate": "URL ค้นหาที่ใช้เมื่อ defaultSearchEngine เป็น custom ใส่ตัวแทนคำค้นหรือ %s สำหรับคำค้นที่เข้ารหัส หากไม่มีตัวแทน cmux จะเพิ่ม q= ให้กับ URL" + "customSearchEngineURLTemplate": "URL ค้นหาที่ใช้เมื่อ defaultSearchEngine เป็น custom ใส่ตัวแทนคำค้นหรือ %s สำหรับคำค้นที่เข้ารหัส หากไม่มีตัวแทน cmux จะเพิ่ม q= ให้กับ URL", + "shortcutPassthroughHosts": "โฮสต์ที่หน้าเพจรับทางลัด Cmd modifier โดยตรง เมื่อตรงกัน คอมโบ Cmd จะถูกส่งต่อไปยังเว็บวิวก่อน หากเพจไม่ใช้คอมโบนั้น การจัดการเมนูปกติของ cmux จะกลับมาทำงาน (ดังนั้น Cmd+Q ยังคงออกจากโปรแกรม) มีประโยชน์สำหรับการฝัง Web IDE เช่น VS Code ผ่าน code-server รองรับการจับคู่โฮสต์แบบตรงและไวลด์การ์ด *.subdomain (รูปแบบ *.example.com ยังจับคู่ example.com เองด้วย) หมายเลขพอร์ตจะถูกละเว้น รายการว่างจะคงพฤติกรรมเดิมของ cmux ในทุกหน้า" } } }, diff --git a/web/messages/tr.json b/web/messages/tr.json index 92e5fa68da94..369cae88d1bb 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Tarayıcı adres çubuğundaki URL olmayan sorgular için varsayılan arama motoru. Diğer sağlayıcılar için custom ile customSearchEngineURLTemplate kullanın.", "customSearchEngineName": "defaultSearchEngine custom olduğunda kullanılan görünen ad.", - "customSearchEngineURLTemplate": "defaultSearchEngine custom olduğunda kullanılan arama URL’si. Kodlanmış sorgu için sorgu yer tutucusunu veya %s ekleyin. Yer tutucu yoksa cmux URL’ye q= ekler." + "customSearchEngineURLTemplate": "defaultSearchEngine custom olduğunda kullanılan arama URL’si. Kodlanmış sorgu için sorgu yer tutucusunu veya %s ekleyin. Yer tutucu yoksa cmux URL’ye q= ekler.", + "shortcutPassthroughHosts": "Sayfaları Cmd değiştirici kısayollarını doğrudan alan ana bilgisayarlar. Eşleşme durumunda Cmd kısayolları önce web görünümüne iletilir; sayfa kısayolu tüketmezse cmux'un normal menü işleme devam eder (yani Cmd+Q hâlâ çıkış yapar). VS Code gibi web IDE'lerini code-server üzerinden gömmek için kullanışlıdır. Tam ana bilgisayar eşleşmesini ve *.altetkialan joker karakterlerini destekler (*.example.com kalıbı example.com'un kendisiyle de eşleşir). Bağlantı noktası numaraları yok sayılır. Boş liste her sayfada cmux'un mevcut davranışını korur." } } }, diff --git a/web/messages/uk.json b/web/messages/uk.json index cb78252247c6..552e7ca6477a 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "Пошукова система за замовчуванням для запитів в адресному рядку браузера, які не є URL. Для інших провайдерів використовуйте custom з customSearchEngineURLTemplate.", "customSearchEngineName": "Назва для показу, коли defaultSearchEngine має значення custom.", - "customSearchEngineURLTemplate": "URL пошуку, який використовується коли defaultSearchEngine має значення custom. Додайте заповнювач запиту або %s для закодованого запиту. Без заповнювача cmux додасть q= до URL." + "customSearchEngineURLTemplate": "URL пошуку, який використовується коли defaultSearchEngine має значення custom. Додайте заповнювач запиту або %s для закодованого запиту. Без заповнювача cmux додасть q= до URL.", + "shortcutPassthroughHosts": "Хости, сторінки яких отримують комбінації з Cmd безпосередньо. При збігу комбінації з Cmd спочатку передаються до веб-перегляду; якщо сторінка не споживає комбінацію, відновлюється стандартна обробка меню cmux (тож Cmd+Q усе ще завершує). Корисно для вбудовування веб-IDE на кшталт VS Code через code-server. Підтримує точну відповідність хосту та підстановочні знаки *.піддомен (шаблон *.example.com також збігається з самим example.com). Номери портів ігноруються. Порожній список зберігає поточну поведінку cmux на кожній сторінці." } } }, diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index 3e0f73ea4ac7..4bd0c774353a 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "浏览器地址栏输入不是 URL 时使用的默认搜索引擎。对于任意提供商,请使用 custom 和 customSearchEngineURLTemplate。", "customSearchEngineName": "defaultSearchEngine 为 custom 时使用的显示名称。", - "customSearchEngineURLTemplate": "defaultSearchEngine 为 custom 时使用的搜索 URL。使用查询占位符或 %s 表示已编码的查询。如果没有占位符,cmux 会向 URL 添加 q=。" + "customSearchEngineURLTemplate": "defaultSearchEngine 为 custom 时使用的搜索 URL。使用查询占位符或 %s 表示已编码的查询。如果没有占位符,cmux 会向 URL 添加 q=。", + "shortcutPassthroughHosts": "网页直接接收 Cmd 修饰键快捷键的主机。匹配时,Cmd 修饰键组合首先转发到 Web 视图;如果页面未消耗该组合,则恢复 cmux 的正常菜单处理(因此 Cmd+Q 仍能退出)。适用于通过 code-server 嵌入 VS Code 等 Web IDE。支持精确主机匹配和 *.子域 通配符(*.example.com 模式也匹配 example.com 顶点)。端口号将被忽略。空列表会在每个页面上保留 cmux 的现有行为。" } } }, diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index 5432647c2847..d3d9dacc5cba 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -503,7 +503,8 @@ "browser": { "defaultSearchEngine": "瀏覽器位址列輸入不是 URL 時使用的預設搜尋引擎。任意提供者請使用 custom 與 customSearchEngineURLTemplate。", "customSearchEngineName": "defaultSearchEngine 為 custom 時使用的顯示名稱。", - "customSearchEngineURLTemplate": "defaultSearchEngine 為 custom 時使用的搜尋 URL。使用查詢佔位符或 %s 表示已編碼的查詢。若沒有佔位符,cmux 會將 q= 加到 URL。" + "customSearchEngineURLTemplate": "defaultSearchEngine 為 custom 時使用的搜尋 URL。使用查詢佔位符或 %s 表示已編碼的查詢。若沒有佔位符,cmux 會將 q= 加到 URL。", + "shortcutPassthroughHosts": "網頁直接接收 Cmd 修飾鍵快捷鍵的主機。比對成功時,Cmd 修飾鍵組合會先轉發到 Web 檢視;如果頁面未消耗該組合,則恢復 cmux 的正常選單處理(因此 Cmd+Q 仍可退出)。適合透過 code-server 嵌入 VS Code 等 Web IDE。支援精確主機比對與 *.子網域 萬用字元(*.example.com 樣式也會比對 example.com 頂點)。連接埠號碼會被忽略。空清單會在每個頁面上保留 cmux 現有的行為。" } } },