diff --git a/Packages/CmuxFoundation/Package.swift b/Packages/CmuxFoundation/Package.swift
new file mode 100644
index 000000000000..35bcc7161c28
--- /dev/null
+++ b/Packages/CmuxFoundation/Package.swift
@@ -0,0 +1,30 @@
+// swift-tools-version: 6.0
+
+import PackageDescription
+
+let package = Package(
+ name: "CmuxFoundation",
+ platforms: [
+ .macOS(.v14),
+ ],
+ products: [
+ .library(
+ name: "CmuxFoundation",
+ targets: ["CmuxFoundation"]
+ ),
+ ],
+ targets: [
+ .target(
+ name: "CmuxFoundation",
+ swiftSettings: [
+ .swiftLanguageMode(.v6),
+ .enableUpcomingFeature("ExistentialAny"),
+ .enableUpcomingFeature("InternalImportsByDefault"),
+ ]
+ ),
+ .testTarget(
+ name: "CmuxFoundationTests",
+ dependencies: ["CmuxFoundation"]
+ ),
+ ]
+)
diff --git a/Packages/CmuxFoundation/README.md b/Packages/CmuxFoundation/README.md
new file mode 100644
index 000000000000..a72f8e6a4743
--- /dev/null
+++ b/Packages/CmuxFoundation/README.md
@@ -0,0 +1,38 @@
+# CmuxFoundation
+
+Shared low-level primitives for cmux with no internal package dependencies. This is the
+bottom of the package dependency graph: encoding/text helpers, value types, and other
+cross-cutting utilities that several domains need, with nothing in here depending on AppKit,
+SwiftUI, or another cmux package.
+
+It exists as the leaf every other package and the app target can depend on without creating
+a cycle. Keep it dependency-free.
+
+Foundation helpers are exposed as extensions on existing types rather than free functions,
+so call sites read naturally (`value.javaScriptStringLiteral`, not `f(value)`).
+
+## Contents
+
+- `String.javaScriptStringLiteral` — the string encoded as a quoted JavaScript string literal.
+
+## Usage
+
+```swift
+import CmuxFoundation
+
+let literal = userText?.javaScriptStringLiteral ?? "null"
+webView.evaluateJavaScript("setValue(\(literal))")
+```
+
+## Testing
+
+Everything here is a pure value transform, so tests need no app, no AppKit, and no filesystem:
+
+```swift
+import Testing
+import CmuxFoundation
+
+@Test func plainStringIsQuoted() {
+ #expect("hello".javaScriptStringLiteral == "\"hello\"")
+}
+```
diff --git a/Packages/CmuxFoundation/Sources/CmuxFoundation/String+JavaScriptStringLiteral.swift b/Packages/CmuxFoundation/Sources/CmuxFoundation/String+JavaScriptStringLiteral.swift
new file mode 100644
index 000000000000..d3b1c07feb3c
--- /dev/null
+++ b/Packages/CmuxFoundation/Sources/CmuxFoundation/String+JavaScriptStringLiteral.swift
@@ -0,0 +1,26 @@
+import Foundation
+
+public extension String {
+ /// This string encoded as a quoted JavaScript string literal, ready to splice into JS source.
+ ///
+ /// The value includes the surrounding double quotes and escapes any characters (quotes,
+ /// backslashes, control characters) that would otherwise break out of the literal. Encoding
+ /// goes through `JSONSerialization`, so the result is also a valid JSON string.
+ ///
+ /// ```swift
+ /// "a\"b".javaScriptStringLiteral // -> "\"a\\\"b\""
+ /// webView.evaluateJavaScript("setValue(\(id?.javaScriptStringLiteral ?? "null"))")
+ /// ```
+ ///
+ /// - Returns: The quoted JS string literal, or `nil` if the string cannot be encoded.
+ var javaScriptStringLiteral: String? {
+ // [self] is always valid JSON; reuse JSON's escaping and drop the array brackets to
+ // get a quoted JS string literal.
+ guard let data = try? JSONSerialization.data(withJSONObject: [self]),
+ let arrayLiteral = String(data: data, encoding: .utf8),
+ arrayLiteral.count >= 2 else {
+ return nil
+ }
+ return String(arrayLiteral.dropFirst().dropLast())
+ }
+}
diff --git a/Packages/CmuxFoundation/Tests/CmuxFoundationTests/String+JavaScriptStringLiteralTests.swift b/Packages/CmuxFoundation/Tests/CmuxFoundationTests/String+JavaScriptStringLiteralTests.swift
new file mode 100644
index 000000000000..994fb492ae45
--- /dev/null
+++ b/Packages/CmuxFoundation/Tests/CmuxFoundationTests/String+JavaScriptStringLiteralTests.swift
@@ -0,0 +1,25 @@
+import Testing
+
+import CmuxFoundation
+
+@Suite struct StringJavaScriptStringLiteralTests {
+ @Test func plainStringIsQuoted() {
+ #expect("hello".javaScriptStringLiteral == "\"hello\"")
+ }
+
+ @Test func emptyStringIsEmptyQuotes() {
+ #expect("".javaScriptStringLiteral == "\"\"")
+ }
+
+ @Test func escapesDoubleQuotesAndBackslashes() {
+ #expect(#"a"b\c"#.javaScriptStringLiteral == #""a\"b\\c""#)
+ }
+
+ @Test func escapesNewlines() {
+ #expect("line1\nline2".javaScriptStringLiteral == #""line1\nline2""#)
+ }
+
+ @Test func preservesUnicode() {
+ #expect("café".javaScriptStringLiteral == "\"café\"")
+ }
+}
diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift
index e64a408726c7..6e4aa2025f27 100644
--- a/Sources/AppDelegate.swift
+++ b/Sources/AppDelegate.swift
@@ -9,17 +9,7 @@ import WebKit
import Combine
import ObjectiveC.runtime
import Darwin
-
-func cmuxJavaScriptStringLiteral(_ value: String?) -> String? {
- guard let value else { return nil }
- // Serialize as a JSON array, then strip the outer brackets to get a quoted JS string literal.
- guard let data = try? JSONSerialization.data(withJSONObject: [value]),
- let arrayLiteral = String(data: data, encoding: .utf8),
- arrayLiteral.count >= 2 else {
- return nil
- }
- return String(arrayLiteral.dropFirst().dropLast())
-}
+import CmuxFoundation
private struct MultiWindowRouteCLIResult {
let status: String
@@ -10524,7 +10514,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
awaitingInputId: String? = nil,
completion: @escaping ([String: String]) -> Void
) {
- let expectedInputIdLiteral = cmuxJavaScriptStringLiteral(awaitingInputId) ?? "null"
+ let expectedInputIdLiteral = awaitingInputId?.javaScriptStringLiteral ?? "null"
let script = """
(() => {
const expectedInputId = \(expectedInputIdLiteral);
diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj
index 29e65084c744..9264eb69735e 100644
--- a/cmux.xcodeproj/project.pbxproj
+++ b/cmux.xcodeproj/project.pbxproj
@@ -140,6 +140,7 @@
E7E000000000000000000001 /* CmuxEventStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E000000000000000000002 /* CmuxEventStream.swift */; };
C0DE46000000000000000001 /* CMUXExtensionClient in Frameworks */ = {isa = PBXBuildFile; productRef = C0DE46000000000000000002 /* CMUXExtensionClient */; };
C0DE45000000000000000001 /* CmuxExtensionKit in Frameworks */ = {isa = PBXBuildFile; productRef = C0DE45000000000000000002 /* CmuxExtensionKit */; };
+ CF00F00000000000000000A3 /* CmuxFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CF00F00000000000000000A2 /* CmuxFoundation */; };
C0DE34020000000000000001 /* CmuxHelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000003 /* CmuxHelpCommands.swift */; };
C0DE34020000000000000002 /* CmuxHelpResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000004 /* CmuxHelpResource.swift */; };
C0DE46010000000000000001 /* CMUXInstalledExtensionSidebarHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE46010000000000000002 /* CMUXInstalledExtensionSidebarHostView.swift */; };
@@ -1216,6 +1217,7 @@
5EDB6027B346C46521A93C74 /* CMUXAuthCore in Frameworks */,
C0DE46000000000000000001 /* CMUXExtensionClient in Frameworks */,
C0DE45000000000000000001 /* CmuxExtensionKit in Frameworks */,
+ CF00F00000000000000000A3 /* CmuxFoundation in Frameworks */,
3069F1D10000000000000005 /* CMUXPasteboardFidelity in Frameworks */,
A5C0DE0000000000000000A3 /* CMUXProjectModel in Frameworks */,
CD0CFE5300000000CD0CFE53 /* CmuxSettings in Frameworks */,
@@ -1935,6 +1937,7 @@
A500D013A1B2C3D4E5F60718 /* CMUXDebugLog */,
CD0CFE5200000000CD0CFE52 /* CmuxSettings */,
CD0CFE5500000000CD0CFE55 /* CmuxSettingsUI */,
+ CF00F00000000000000000A2 /* CmuxFoundation */,
A8BD195031FC4B82B4354297 /* StackAuth */,
);
productName = cmux;
@@ -2072,6 +2075,7 @@
A5354304A5354304A5354304 /* XCLocalSwiftPackageReference "CMUXSocketPathDomain" */,
CD0CFE5100000000CD0CFE51 /* XCLocalSwiftPackageReference "CmuxSettings" */,
CD0CFE5400000000CD0CFE54 /* XCLocalSwiftPackageReference "CmuxSettingsUI" */,
+ CF00F00000000000000000A1 /* XCLocalSwiftPackageReference "CmuxFoundation" */,
28B798BB9086C8E6B60C3355 /* XCLocalSwiftPackageReference "stack-auth-swift-sdk-prerelease" */,
);
productRefGroup = A5001042 /* Products */;
@@ -3198,6 +3202,10 @@
isa = XCLocalSwiftPackageReference;
relativePath = Packages/CmuxSettingsUI;
};
+ CF00F00000000000000000A1 /* XCLocalSwiftPackageReference "CmuxFoundation" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Packages/CmuxFoundation;
+ };
A5001260 /* XCLocalSwiftPackageReference "bonsplit" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = vendor/bonsplit;
@@ -3300,6 +3308,11 @@
package = CD0CFE5400000000CD0CFE54 /* XCLocalSwiftPackageReference "CmuxSettingsUI" */;
productName = CmuxSettingsUI;
};
+ CF00F00000000000000000A2 /* CmuxFoundation */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CF00F00000000000000000A1 /* XCLocalSwiftPackageReference "CmuxFoundation" */;
+ productName = CmuxFoundation;
+ };
A5001231 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = A5001232 /* XCRemoteSwiftPackageReference "Sparkle" */;
diff --git a/cmux.xcworkspace/contents.xcworkspacedata b/cmux.xcworkspace/contents.xcworkspacedata
index 2ad47a26aa42..559dcbd02b42 100644
--- a/cmux.xcworkspace/contents.xcworkspacedata
+++ b/cmux.xcworkspace/contents.xcworkspacedata
@@ -25,6 +25,9 @@
+
+