diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index abd93a92984b..b4becc73358e 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -163,6 +163,12 @@ FE003101 /* SessionIndexStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003001 /* SessionIndexStore.swift */; }; FE003102 /* SessionIndexView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003002 /* SessionIndexView.swift */; }; FE003103 /* RightSidebarPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003003 /* RightSidebarPanelView.swift */; }; + E7000001A1B2C3D4E5F60718 /* RightSidebarExtensionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7000000A1B2C3D4E5F60718 /* RightSidebarExtensionDemo.swift */; }; + E7000006A1B2C3D4E5F60718 /* RightSidebarDemoExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7000002A1B2C3D4E5F60718 /* RightSidebarDemoExtension.swift */; }; + E7000007A1B2C3D4E5F60718 /* RightSidebarExtensionPoints.xcappextensionpoints in Resources */ = {isa = PBXBuildFile; fileRef = E7000004A1B2C3D4E5F60718 /* RightSidebarExtensionPoints.xcappextensionpoints */; }; + E7000008A1B2C3D4E5F60718 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; }; + E7000009A1B2C3D4E5F60718 /* RightSidebarDemoExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = E7000005A1B2C3D4E5F60718 /* RightSidebarDemoExtension.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E7000016A1B2C3D4E5F60718 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = E7000015A1B2C3D4E5F60718 /* InfoPlist.xcstrings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -187,6 +193,13 @@ remoteGlobalIDString = D1320AA0D1320AA0D1320AA8; remoteInfo = CmuxDockTilePlugin; }; + E700000AA1B2C3D4E5F60718 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5001070 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E7000010A1B2C3D4E5F60718; + remoteInfo = RightSidebarDemoExtension; + }; F1000008A1B2C3D4E5F60718 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = A5001070 /* Project object */; @@ -231,6 +244,17 @@ name = "Copy Dock Tile Plugin"; runOnlyForDeploymentPostprocessing = 0; }; + E700000CA1B2C3D4E5F60718 /* Embed ExtensionKit Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + E7000009A1B2C3D4E5F60718 /* RightSidebarDemoExtension.appex in Embed ExtensionKit Extensions */, + ); + name = "Embed ExtensionKit Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -387,6 +411,14 @@ FE003001 /* SessionIndexStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIndexStore.swift; sourceTree = ""; }; FE003002 /* SessionIndexView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIndexView.swift; sourceTree = ""; }; FE003003 /* RightSidebarPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarPanelView.swift; sourceTree = ""; }; + E7000000A1B2C3D4E5F60718 /* RightSidebarExtensionDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarExtensionDemo.swift; sourceTree = ""; }; + E7000002A1B2C3D4E5F60718 /* RightSidebarDemoExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarDemoExtension.swift; sourceTree = ""; }; + E7000003A1B2C3D4E5F60718 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E7000004A1B2C3D4E5F60718 /* RightSidebarExtensionPoints.xcappextensionpoints */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xcappextensionpoints; path = RightSidebarExtensionPoints.xcappextensionpoints; sourceTree = ""; }; + E7000005A1B2C3D4E5F60718 /* RightSidebarDemoExtension.appex */ = {isa = PBXFileReference; explicitFileType = wrapper.extensionkit-extension; includeInIndex = 0; path = RightSidebarDemoExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + E7000015A1B2C3D4E5F60718 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; + E7000017A1B2C3D4E5F60718 /* RightSidebarDemoExtension-Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RightSidebarDemoExtension-Debug.entitlements"; sourceTree = ""; }; + E7000018A1B2C3D4E5F60718 /* RightSidebarDemoExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RightSidebarDemoExtension.entitlements; sourceTree = ""; }; IC000002 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = AppIcon.icon; sourceTree = ""; }; /* End PBXFileReference section */ @@ -427,6 +459,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E700000EA1B2C3D4E5F60718 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F1000006A1B2C3D4E5F60718 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -445,6 +484,7 @@ C1ADE00001A1B2C3D4E5F719 /* claude */, DA7A10CA710E000000000001 /* Localizable.xcstrings */, DA7A10CA710E000000000002 /* InfoPlist.xcstrings */, + E7000004A1B2C3D4E5F60718 /* RightSidebarExtensionPoints.xcappextensionpoints */, A5001622 /* cmux.sdef */, ); path = Resources; @@ -491,6 +531,7 @@ children = ( A5001041 /* Sources */, B9000003A1B2C3D4E5F60719 /* CLI */, + E7000011A1B2C3D4E5F60718 /* RightSidebarDemoExtension */, 087C454FFF74443AB06942C3 /* Resources */, A5001101 /* Assets.xcassets */, IC000002 /* AppIcon.icon */, @@ -588,6 +629,7 @@ FE003001 /* SessionIndexStore.swift */, FE003002 /* SessionIndexView.swift */, FE003003 /* RightSidebarPanelView.swift */, + E7000000A1B2C3D4E5F60718 /* RightSidebarExtensionDemo.swift */, 105D8421DF4E2E09D255D785 /* Auth */, ); path = Sources; @@ -599,12 +641,25 @@ A5001000 /* cmux.app */, B9000004A1B2C3D4E5F60719 /* cmux */, D1320AA0D1320AA0D1320AA5 /* CmuxDockTilePlugin.plugin */, + E7000005A1B2C3D4E5F60718 /* RightSidebarDemoExtension.appex */, 7E7E6EF344A568AC7FEE3715 /* cmuxUITests.xctest */, F1000002A1B2C3D4E5F60718 /* cmuxTests.xctest */, ); name = Products; sourceTree = ""; }; + E7000011A1B2C3D4E5F60718 /* RightSidebarDemoExtension */ = { + isa = PBXGroup; + children = ( + E7000002A1B2C3D4E5F60718 /* RightSidebarDemoExtension.swift */, + E7000003A1B2C3D4E5F60718 /* Info.plist */, + E7000015A1B2C3D4E5F60718 /* InfoPlist.xcstrings */, + E7000017A1B2C3D4E5F60718 /* RightSidebarDemoExtension-Debug.entitlements */, + E7000018A1B2C3D4E5F60718 /* RightSidebarDemoExtension.entitlements */, + ); + path = RightSidebarDemoExtension; + sourceTree = ""; + }; B9000003A1B2C3D4E5F60719 /* CLI */ = { isa = PBXGroup; children = ( @@ -664,6 +719,7 @@ A5001030 /* Frameworks */, A5001102 /* Resources */, D1320AA0D1320AA0D1320AA6 /* Copy Dock Tile Plugin */, + E700000CA1B2C3D4E5F60718 /* Embed ExtensionKit Extensions */, A5001020 /* Embed Frameworks */, B900000AA1B2C3D4E5F60719 /* Copy CLI */, A5001300A1B2C3D4E5F60719 /* ShellScript */, @@ -673,6 +729,7 @@ dependencies = ( B900000EA1B2C3D4E5F60719 /* PBXTargetDependency */, D1320AA0D1320AA0D1320AB1 /* PBXTargetDependency */, + E700000BA1B2C3D4E5F60718 /* PBXTargetDependency */, ); name = GhosttyTabs; packageProductDependencies = ( @@ -742,6 +799,25 @@ productReference = D1320AA0D1320AA0D1320AA5 /* CmuxDockTilePlugin.plugin */; productType = "com.apple.product-type.bundle"; }; + E7000010A1B2C3D4E5F60718 /* RightSidebarDemoExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = E7000014A1B2C3D4E5F60718 /* Build configuration list for PBXNativeTarget "RightSidebarDemoExtension" */; + buildPhases = ( + E700000DA1B2C3D4E5F60718 /* Sources */, + E700000EA1B2C3D4E5F60718 /* Frameworks */, + E700000FA1B2C3D4E5F60718 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RightSidebarDemoExtension; + packageProductDependencies = ( + ); + productName = RightSidebarDemoExtension; + productReference = E7000005A1B2C3D4E5F60718 /* RightSidebarDemoExtension.appex */; + productType = "com.apple.product-type.extensionkit-extension"; + }; F1000004A1B2C3D4E5F60718 /* cmuxTests */ = { isa = PBXNativeTarget; buildConfigurationList = F1000010A1B2C3D4E5F60718 /* Build configuration list for PBXNativeTarget "cmuxTests" */; @@ -811,6 +887,7 @@ targets = ( A5001050 /* GhosttyTabs */, D1320AA0D1320AA0D1320AA8 /* CmuxDockTilePlugin */, + E7000010A1B2C3D4E5F60718 /* RightSidebarDemoExtension */, B9000005A1B2C3D4E5F60719 /* cmux-cli */, CB450DF0F0B3839599082C4D /* cmuxUITests */, F1000004A1B2C3D4E5F60718 /* cmuxTests */, @@ -839,6 +916,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E700000FA1B2C3D4E5F60718 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E7000008A1B2C3D4E5F60718 /* Localizable.xcstrings in Resources */, + E7000016A1B2C3D4E5F60718 /* InfoPlist.xcstrings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; EC1BC8BE24F703F6BBEAA1DE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -961,6 +1047,7 @@ FE003101 /* SessionIndexStore.swift in Sources */, FE003102 /* SessionIndexView.swift in Sources */, FE003103 /* RightSidebarPanelView.swift in Sources */, + E7000001A1B2C3D4E5F60718 /* RightSidebarExtensionDemo.swift in Sources */, 36CE99ED050785B5E96B72BB /* AuthCallbackRouter.swift in Sources */, D9FEC58D5BACCF76459F1BBE /* AuthEnvironment.swift in Sources */, 350DAC5EBD38642A3E81471A /* AuthManager.swift in Sources */, @@ -985,6 +1072,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E700000DA1B2C3D4E5F60718 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E7000006A1B2C3D4E5F60718 /* RightSidebarDemoExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; E436EF0BA8EC9E6721A42F79 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1067,6 +1162,11 @@ target = D1320AA0D1320AA0D1320AA8 /* CmuxDockTilePlugin */; targetProxy = D1320AA0D1320AA0D1320AA3 /* PBXContainerItemProxy */; }; + E700000BA1B2C3D4E5F60718 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E7000010A1B2C3D4E5F60718 /* RightSidebarDemoExtension */; + targetProxy = E700000AA1B2C3D4E5F60718 /* PBXContainerItemProxy */; + }; F1000009A1B2C3D4E5F60718 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A5001050 /* GhosttyTabs */; @@ -1158,12 +1258,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Debug"; CMUX_AUTH_CALLBACK_SCHEME = "cmux-dev"; + CMUX_HOST_BUNDLE_IDENTIFIER = com.cmuxterm.app.debug; CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 79; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; + EX_ENABLE_EXTENSION_POINT_GENERATION = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1184,7 +1286,7 @@ "-framework", Carbon, ); - PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app.debug; + PRODUCT_BUNDLE_IDENTIFIER = "$(CMUX_HOST_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "cmux DEV"; SPARKLE_PUBLIC_KEY = "avjcgKibf1FTvhIjLBxhd+0HSpsXU4D0IGlVk8cgqRc="; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1198,12 +1300,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CMUX_AUTH_CALLBACK_SCHEME = cmux; + CMUX_HOST_BUNDLE_IDENTIFIER = com.cmuxterm.app; CODE_SIGN_ENTITLEMENTS = Resources/cmux.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 79; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; + EX_ENABLE_EXTENSION_POINT_GENERATION = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1225,7 +1329,7 @@ "-framework", Carbon, ); - PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app; + PRODUCT_BUNDLE_IDENTIFIER = "$(CMUX_HOST_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = cmux; SPARKLE_PUBLIC_KEY = "avjcgKibf1FTvhIjLBxhd+0HSpsXU4D0IGlVk8cgqRc="; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1234,6 +1338,69 @@ }; name = Release; }; + E7000012A1B2C3D4E5F60718 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CMUX_HOST_BUNDLE_IDENTIFIER = com.cmuxterm.app.debug; + CMUX_RIGHT_SIDEBAR_EXTENSION_POINT_IDENTIFIER = com.cmuxterm.app.debug.extkit.right-sidebar-panel; + CODE_SIGN_ENTITLEMENTS = "RightSidebarDemoExtension/RightSidebarDemoExtension-Debug.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 79; + DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + EX_ENABLE_EXTENSION_POINT_GENERATION = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = RightSidebarDemoExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 0.63.2; + PRODUCT_BUNDLE_IDENTIFIER = "$(CMUX_HOST_BUNDLE_IDENTIFIER).right-sidebar-demo"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = appex; + }; + name = Debug; + }; + E7000013A1B2C3D4E5F60718 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CMUX_HOST_BUNDLE_IDENTIFIER = com.cmuxterm.app; + CMUX_RIGHT_SIDEBAR_EXTENSION_POINT_IDENTIFIER = com.cmuxterm.app.right-sidebar-panel; + CODE_SIGN_ENTITLEMENTS = RightSidebarDemoExtension/RightSidebarDemoExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 79; + DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + EX_ENABLE_EXTENSION_POINT_GENERATION = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = RightSidebarDemoExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 0.63.2; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = "$(CMUX_HOST_BUNDLE_IDENTIFIER).right-sidebar-demo"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) CMUX_RELEASE_EXTENSION_POINT"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = appex; + }; + name = Release; + }; B9000008A1B2C3D4E5F60719 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1424,6 +1591,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + E7000014A1B2C3D4E5F60718 /* Build configuration list for PBXNativeTarget "RightSidebarDemoExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E7000012A1B2C3D4E5F60718 /* Debug */, + E7000013A1B2C3D4E5F60718 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F1000010A1B2C3D4E5F60718 /* Build configuration list for PBXNativeTarget "cmuxTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index b1d6c2e865d9..cad6ea6fe7c9 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -91057,6 +91057,1431 @@ } } } + }, + "rightSidebar.demoExtension.reload": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rebuild and reload to change this panel" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このパネルを変更するには再ビルドしてリロード" + } + } + } + }, + "rightSidebar.demoExtension.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rendered by ExtensionKit" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKitでレンダリング" + } + } + } + }, + "rightSidebar.demoExtension.swift": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SwiftUI view from a bundled extension" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "バンドルされた拡張機能のSwiftUIビュー" + } + } + } + }, + "rightSidebar.demoExtension.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Demo extension" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "デモ拡張機能" + } + } + } + }, + "rightSidebar.demoExtension.xpc": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scene-specific XPC connection accepted" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "シーン固有のXPC接続を受け入れました" + } + } + } + }, + "rightSidebar.extensionDemo.copyPrompt": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy extension prompt" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張機能プロンプトをコピー" + } + } + } + }, + "rightSidebar.extensionDemo.copyPromptBody": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build a cmux right sidebar ExtensionKit demo extension in Swift.\n\nRequirements:\n- Use ExtensionFoundation and ExtensionKit.\n- Bind to app extension point com.cmuxterm.app.debug.extkit.right-sidebar-panel.\n- Provide a PrimitiveAppExtensionScene with scene id cmux-right-sidebar-demo.\n- Render a compact SwiftUI panel that fits a narrow right sidebar.\n- Return AppExtensionSceneConfiguration(body) from the AppExtension configuration and do not install a custom NSXPCConnection acceptor for the scene.\n- Add the Info.plist and Xcode project settings needed for an ExtensionKit .appex, including EXAppExtensionAttributes.EXExtensionPointIdentifier = com.cmuxterm.app.debug.extkit.right-sidebar-panel.\n- Keep all UI strings localized." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Swiftでcmux右サイドバー用のExtensionKitデモ拡張機能を作成してください。\n\n要件:\n- ExtensionFoundationとExtensionKitを使用する。\n- アプリ拡張ポイントcom.cmuxterm.app.debug.extkit.right-sidebar-panelにバインドする。\n- シーンID cmux-right-sidebar-demoのPrimitiveAppExtensionSceneを用意する。\n- 狭い右サイドバーに収まるコンパクトなSwiftUIパネルを描画する。\n- AppExtensionのconfigurationからAppExtensionSceneConfiguration(body)を返し、シーン用のカスタムNSXPCConnectionアクセプターを追加しない。\n- EXAppExtensionAttributes.EXExtensionPointIdentifier = com.cmuxterm.app.debug.extkit.right-sidebar-panelを含め、ExtensionKit .appexに必要なInfo.plistとXcodeプロジェクト設定を追加する。\n- すべてのUI文字列をローカライズする。" + } + } + } + }, + "rightSidebar.extensionDemo.copyPromptCopied": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Starter prompt copied." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "スタータープロンプトをコピーしました。" + } + } + } + }, + "rightSidebar.extensionDemo.copyPromptHint": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy a starter prompt and paste it into your AI agent to generate a sidebar extension." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "スタータープロンプトをコピーしてAIエージェントに貼り付けると、サイドバー拡張機能を生成できます。" + } + } + } + }, + "rightSidebar.extensionDemo.deactivated": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension scene disconnected." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張シーンが切断されました。" + } + } + } + }, + "rightSidebar.extensionDemo.deactivatedWithError": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension scene disconnected:" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張シーンが切断されました:" + } + } + } + }, + "rightSidebar.extensionDemo.emptyStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No demo extension found." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "デモ拡張機能が見つかりません。" + } + } + } + }, + "rightSidebar.extensionDemo.errorStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension discovery failed." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張機能の検出に失敗しました。" + } + } + } + }, + "rightSidebar.extensionDemo.extensionPicker": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張機能" + } + } + } + }, + "rightSidebar.extensionDemo.foundStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Demo extension discovered." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "デモ拡張機能が見つかりました。" + } + } + } + }, + "rightSidebar.extensionDemo.loading": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for demo extensions..." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "デモ拡張機能を検索中..." + } + } + } + }, + "rightSidebar.extensionDemo.noExtensions": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No ExtensionKit demo extension is registered yet." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKitデモ拡張機能はまだ登録されていません。" + } + } + } + }, + "rightSidebar.extensionDemo.placeholder": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Waiting for the extension scene..." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張シーンを待機中..." + } + } + } + }, + "rightSidebar.extensionDemo.promptAuditTitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Full prompt" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "プロンプト全文" + } + } + } + }, + "rightSidebar.extensionDemo.promptInfo": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "About this prompt" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このプロンプトについて" + } + } + } + }, + "rightSidebar.extensionDemo.promptInfoBody": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This prompt is copied only when you press Copy. It asks your AI agent to generate a Swift ExtensionKit sidebar extension that binds to cmux's right sidebar extension point." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このプロンプトはコピーを押したときだけコピーされます。cmuxの右サイドバー拡張ポイントにバインドするSwift ExtensionKitサイドバー拡張機能をAIエージェントに生成させる内容です。" + } + } + } + }, + "rightSidebar.extensionDemo.promptInfoTitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension prompt" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張機能プロンプト" + } + } + } + }, + "rightSidebar.extensionDemo.ready": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "準備完了" + } + } + } + }, + "rightSidebar.extensionDemo.registrationSettlingStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Bundled demo extension is installed. Waiting for macOS registration." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "バンドル済みのデモ拡張機能はインストールされています。macOSの登録を待機しています。" + } + } + } + }, + "rightSidebar.extensionDemo.refresh": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Refresh extensions" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張機能を更新" + } + } + } + }, + "rightSidebar.extensionDemo.requiresMacOS26": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKit sidebar demos require macOS 26." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKitサイドバーデモにはmacOS 26が必要です。" + } + } + } + }, + "rightSidebar.extensionDemo.settlingStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension discovery is settling. Keeping the last extension." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張機能の検出を安定化しています。最後の拡張機能を維持します。" + } + } + } + }, + "rightSidebar.extensionDemo.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKit Demo" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKitデモ" + } + } + } + }, + "rightSidebar.extensionDemo.unavailableStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension is registered but not available yet." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張機能は登録されていますが、まだ利用できません。" + } + } + } + }, + "rightSidebar.extensionDemo.waitingForBundledDemo": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Bundled demo extension is installed. Waiting for macOS to finish registration." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "バンドル済みのデモ拡張機能はインストールされています。macOSの登録完了を待機しています。" + } + } + } + }, + "rightSidebar.extensionDemo.xpcConnected": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension scene activated." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張シーンが有効になりました。" + } + } + } + }, + "rightSidebar.extensionDemo.xpcFailed": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extension scene activated, but XPC failed." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拡張シーンは有効になりましたが、XPCに失敗しました。" + } + } + } + }, + "rightSidebar.mode.extensionDemo": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKit" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKit" + } + } + } + }, + "sampleExtension.accent": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Accent" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アクセント" + } + } + } + }, + "sampleExtension.actionCopied": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Snapshot copied" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "スナップショットをコピーしました" + } + } + } + }, + "sampleExtension.actionRan": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Run button clicked" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "実行ボタンをクリックしました" + } + } + } + }, + "sampleExtension.actionReady": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "準備完了" + } + } + } + }, + "sampleExtension.actionReset": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Controls reset" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コントロールをリセットしました" + } + } + } + }, + "sampleExtension.advanced": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Advanced" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "詳細" + } + } + } + }, + "sampleExtension.advancedOptions": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Advanced options" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "詳細オプション" + } + } + } + }, + "sampleExtension.batchSize": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Batch size" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "バッチサイズ" + } + } + } + }, + "sampleExtension.compactLayout": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Compact layout" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コンパクト表示" + } + } + } + }, + "sampleExtension.confidence": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Confidence" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "信頼度" + } + } + } + }, + "sampleExtension.copy": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コピー" + } + } + } + }, + "sampleExtension.count": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Count" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "回数" + } + } + } + }, + "sampleExtension.defaultTitle": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Draft launch plan" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ローンチ計画の下書き" + } + } + } + }, + "sampleExtension.dueDate": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Due date" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "期限" + } + } + } + }, + "sampleExtension.externalApp": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installed from a separate app bundle" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "別のアプリバンドルからインストール" + } + } + } + }, + "sampleExtension.formControls": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Form controls" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "フォームコントロール" + } + } + } + }, + "sampleExtension.hosted": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hosted by cmux through ExtensionKit" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ExtensionKit経由でcmuxがホスト" + } + } + } + }, + "sampleExtension.live": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Live" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ライブ" + } + } + } + }, + "sampleExtension.livePreview": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Live preview" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ライブプレビュー" + } + } + } + }, + "sampleExtension.liveStatus": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Live status" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ライブ状態" + } + } + } + }, + "sampleExtension.mode": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mode" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "モード" + } + } + } + }, + "sampleExtension.mode.review": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Review" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "レビュー" + } + } + } + }, + "sampleExtension.mode.run": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Run" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "実行" + } + } + } + }, + "sampleExtension.mode.watch": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Watch" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "監視" + } + } + } + }, + "sampleExtension.model": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Model" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "モデル" + } + } + } + }, + "sampleExtension.model.balanced": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Balanced" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "バランス" + } + } + } + }, + "sampleExtension.model.deep": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deep" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "深く" + } + } + } + }, + "sampleExtension.model.fast": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fast" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "高速" + } + } + } + }, + "sampleExtension.notesPlaceholder": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Write sidebar notes..." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイドバーのメモを書く..." + } + } + } + }, + "sampleExtension.notifications": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Notifications" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "通知" + } + } + } + }, + "sampleExtension.pinned": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pinned" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "固定" + } + } + } + }, + "sampleExtension.priority": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Priority" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "優先度" + } + } + } + }, + "sampleExtension.priority.high": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "High" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "高" + } + } + } + }, + "sampleExtension.priority.low": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Low" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "低" + } + } + } + }, + "sampleExtension.priority.normal": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Normal" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "通常" + } + } + } + }, + "sampleExtension.quickActions": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Quick actions" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クイックアクション" + } + } + } + }, + "sampleExtension.readiness": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Readiness" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "準備度" + } + } + } + }, + "sampleExtension.reset": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "リセット" + } + } + } + }, + "sampleExtension.run": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Run" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "実行" + } + } + } + }, + "sampleExtension.scope": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scope" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "スコープ" + } + } + } + }, + "sampleExtension.scope.project": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Project" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "プロジェクト" + } + } + } + }, + "sampleExtension.scope.session": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Session" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "セッション" + } + } + } + }, + "sampleExtension.scope.workspace": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Workspace" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークスペース" + } + } + } + }, + "sampleExtension.secretField": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Secret" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "シークレット" + } + } + } + }, + "sampleExtension.subtitle": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "External ExtensionKit app" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "外部ExtensionKitアプリ" + } + } + } + }, + "sampleExtension.tab.actions": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Actions" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アクション" + } + } + } + }, + "sampleExtension.tab.inputs": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Inputs" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "入力" + } + } + } + }, + "sampleExtension.tab.settings": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "設定" + } + } + } + }, + "sampleExtension.tabs": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Tabs" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "タブ" + } + } + } + }, + "sampleExtension.textInputs": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Text inputs" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テキスト入力" + } + } + } + }, + "sampleExtension.title": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "BYO controls" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "BYOコントロール" + } + } + } + }, + "sampleExtension.titleField": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Title" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "タイトル" + } + } + } + }, + "sampleExtension.toggles": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toggles" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "トグル" + } + } + } + }, + "sampleExtension.xpc": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scene XPC connection accepted" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "シーンXPC接続を受け入れました" + } + } + } } } -} \ No newline at end of file +} diff --git a/Resources/RightSidebarExtensionPoints.xcappextensionpoints b/Resources/RightSidebarExtensionPoints.xcappextensionpoints new file mode 100644 index 000000000000..04ff322206bd --- /dev/null +++ b/Resources/RightSidebarExtensionPoints.xcappextensionpoints @@ -0,0 +1,27 @@ + + + + + com.cmuxterm.app.debug.extkit.right-sidebar-panel + + EXExtensionPointIsPublic + + EXPresentsUserInterface + + + com.cmuxterm.app.right-sidebar-panel + + EXExtensionPointIsPublic + + EXPresentsUserInterface + + + com.cmuxterm.right-sidebar-panel + + EXExtensionPointIsPublic + + EXPresentsUserInterface + + + + diff --git a/RightSidebarDemoExtension/Info.plist b/RightSidebarDemoExtension/Info.plist new file mode 100644 index 000000000000..e6f4db2fb399 --- /dev/null +++ b/RightSidebarDemoExtension/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + cmux Right Sidebar Demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + EXAppExtensionAttributes + + EXExtensionPointIdentifier + $(CMUX_RIGHT_SIDEBAR_EXTENSION_POINT_IDENTIFIER) + + + diff --git a/RightSidebarDemoExtension/InfoPlist.xcstrings b/RightSidebarDemoExtension/InfoPlist.xcstrings new file mode 100644 index 000000000000..e3e4f1b1f681 --- /dev/null +++ b/RightSidebarDemoExtension/InfoPlist.xcstrings @@ -0,0 +1,23 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "cmux Right Sidebar Demo" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "cmux Right Sidebar Demo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "cmux右サイドバーデモ" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/RightSidebarDemoExtension/RightSidebarDemoExtension-Debug.entitlements b/RightSidebarDemoExtension/RightSidebarDemoExtension-Debug.entitlements new file mode 100644 index 000000000000..6c5bad12b557 --- /dev/null +++ b/RightSidebarDemoExtension/RightSidebarDemoExtension-Debug.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.get-task-allow + + + diff --git a/RightSidebarDemoExtension/RightSidebarDemoExtension.entitlements b/RightSidebarDemoExtension/RightSidebarDemoExtension.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/RightSidebarDemoExtension/RightSidebarDemoExtension.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/RightSidebarDemoExtension/RightSidebarDemoExtension.swift b/RightSidebarDemoExtension/RightSidebarDemoExtension.swift new file mode 100644 index 000000000000..b70d592ffb51 --- /dev/null +++ b/RightSidebarDemoExtension/RightSidebarDemoExtension.swift @@ -0,0 +1,497 @@ +#if compiler(>=6.3) && canImport(ExtensionFoundation) && canImport(ExtensionKit) +import ExtensionFoundation +import ExtensionKit +import Foundation +import AppKit +import SwiftUI + +private enum DemoExtensionConstants { + static let sceneID = "cmux-right-sidebar-demo" +} + +protocol CmuxRightSidebarExtension: AppExtension { + associatedtype Body: CmuxRightSidebarScene + var body: Body { get } +} + +protocol CmuxRightSidebarScene: AppExtensionScene {} + +struct CmuxRightSidebarPanelScene: CmuxRightSidebarScene { + private let content: () -> Content + + init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + var body: some AppExtensionScene { + PrimitiveAppExtensionScene(id: DemoExtensionConstants.sceneID) { + content() + } onConnection: { connection in + connection.resume() + return true + } + } +} + +extension CmuxRightSidebarExtension { + var configuration: AppExtensionSceneConfiguration { + AppExtensionSceneConfiguration(self.body) + } +} + +@main +final class RightSidebarDemoExtension: CmuxRightSidebarExtension { + required init() {} + + @AppExtensionPoint.Bind + var boundExtensionPoint: AppExtensionPoint { + #if CMUX_RELEASE_EXTENSION_POINT + AppExtensionPoint.Identifier(host: "com.cmuxterm.app", name: "right-sidebar-panel") + #else + AppExtensionPoint.Identifier(host: "com.cmuxterm.app.debug.extkit", name: "right-sidebar-panel") + #endif + } + + var body: some CmuxRightSidebarScene { + CmuxRightSidebarPanelScene { + RightSidebarDemoExtensionView() + } + } +} + +private struct RightSidebarDemoExtensionView: View { + @State private var selectedTab = 0 + @State private var selectedMode = 0 + @State private var selectedModel = 0 + @State private var selectedPriority = 1 + @State private var selectedScope = 0 + @State private var title: String + @State private var secret = "" + @State private var notes = "" + @State private var isPinned = true + @State private var notificationsEnabled = true + @State private var compactLayout = false + @State private var livePreview = true + @State private var confidence = 0.62 + @State private var batchSize = 4 + @State private var dueDate = Date() + @State private var accentColor = Color.green + @State private var actionCount = 0 + @State private var lastAction: String + + init() { + _title = State(initialValue: String( + localized: "sampleExtension.defaultTitle", + defaultValue: "Draft launch plan" + )) + _lastAction = State(initialValue: String( + localized: "sampleExtension.actionReady", + defaultValue: "Ready" + )) + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .center, spacing: 9) { + Image(systemName: "slider.horizontal.3") + .font(.system(size: 24, weight: .semibold)) + .foregroundStyle(accentColor) + + VStack(alignment: .leading, spacing: 2) { + Text(String(localized: "sampleExtension.title", defaultValue: "BYO controls")) + .font(.system(size: 14, weight: .semibold)) + Text(String(localized: "rightSidebar.demoExtension.subtitle", defaultValue: "Rendered by ExtensionKit")) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + + Spacer(minLength: 0) + } + + Picker( + String(localized: "sampleExtension.tabs", defaultValue: "Tabs"), + selection: $selectedTab + ) { + Text(String(localized: "sampleExtension.tab.actions", defaultValue: "Actions")).tag(0) + Text(String(localized: "sampleExtension.tab.inputs", defaultValue: "Inputs")).tag(1) + Text(String(localized: "sampleExtension.tab.settings", defaultValue: "Settings")).tag(2) + } + .labelsHidden() + .pickerStyle(.segmented) + + ScrollView { + VStack(alignment: .leading, spacing: 10) { + switch selectedTab { + case 0: + actionsTab + case 1: + inputsTab + default: + settingsTab + } + } + .padding(.bottom, 4) + } + + footer + } + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(Color(nsColor: .controlBackgroundColor)) + } + + private var actionsTab: some View { + VStack(alignment: .leading, spacing: 10) { + section( + title: String(localized: "sampleExtension.quickActions", defaultValue: "Quick actions"), + systemImage: "bolt" + ) { + Picker( + String(localized: "sampleExtension.mode", defaultValue: "Mode"), + selection: $selectedMode + ) { + Text(String(localized: "sampleExtension.mode.run", defaultValue: "Run")).tag(0) + Text(String(localized: "sampleExtension.mode.review", defaultValue: "Review")).tag(1) + Text(String(localized: "sampleExtension.mode.watch", defaultValue: "Watch")).tag(2) + } + .pickerStyle(.segmented) + + HStack(spacing: 8) { + Button { + actionCount += 1 + lastAction = String( + localized: "sampleExtension.actionRan", + defaultValue: "Run button clicked" + ) + } label: { + Label( + String(localized: "sampleExtension.run", defaultValue: "Run"), + systemImage: "play.fill" + ) + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + + Button { + copySnapshot() + } label: { + Label( + String(localized: "sampleExtension.copy", defaultValue: "Copy"), + systemImage: "doc.on.doc" + ) + } + .controlSize(.small) + + Button { + resetControls() + } label: { + Image(systemName: "arrow.counterclockwise") + } + .buttonStyle(.borderless) + .controlSize(.small) + .help(String(localized: "sampleExtension.reset", defaultValue: "Reset")) + .accessibilityLabel(String(localized: "sampleExtension.reset", defaultValue: "Reset")) + } + + Toggle(isOn: $isPinned) { + Label( + String(localized: "sampleExtension.pinned", defaultValue: "Pinned"), + systemImage: "pin" + ) + } + .toggleStyle(.checkbox) + + HStack(spacing: 8) { + Text(String(localized: "sampleExtension.count", defaultValue: "Count")) + Spacer(minLength: 0) + Text("\(actionCount)") + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(.secondary) + } + .font(.system(size: 11)) + } + + section( + title: String(localized: "sampleExtension.liveStatus", defaultValue: "Live status"), + systemImage: "waveform.path.ecg" + ) { + ProgressView(value: confidence) + .accessibilityLabel(String(localized: "sampleExtension.confidence", defaultValue: "Confidence")) + .accessibilityValue(confidence.formatted(.percent.precision(.fractionLength(0)))) + Text(lastAction) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .lineLimit(2) + } + } + } + + private var inputsTab: some View { + VStack(alignment: .leading, spacing: 10) { + section( + title: String(localized: "sampleExtension.textInputs", defaultValue: "Text inputs"), + systemImage: "text.cursor" + ) { + TextField( + String(localized: "sampleExtension.titleField", defaultValue: "Title"), + text: $title + ) + SecureField( + String(localized: "sampleExtension.secretField", defaultValue: "Secret"), + text: $secret + ) + + ZStack(alignment: .topLeading) { + TextEditor(text: $notes) + .font(.system(size: 11)) + .frame(minHeight: 76) + .scrollContentBackground(.hidden) + .accessibilityLabel(String( + localized: "sampleExtension.notesPlaceholder", + defaultValue: "Write sidebar notes..." + )) + + if notes.isEmpty { + Text(String(localized: "sampleExtension.notesPlaceholder", defaultValue: "Write sidebar notes...")) + .font(.system(size: 11)) + .foregroundStyle(.tertiary) + .padding(.horizontal, 5) + .padding(.vertical, 7) + .allowsHitTesting(false) + } + } + .background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 6, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 6, style: .continuous) + .stroke(Color.primary.opacity(0.08), lineWidth: 1) + ) + } + + section( + title: String(localized: "sampleExtension.formControls", defaultValue: "Form controls"), + systemImage: "list.bullet.rectangle" + ) { + Picker( + String(localized: "sampleExtension.model", defaultValue: "Model"), + selection: $selectedModel + ) { + Text(String(localized: "sampleExtension.model.fast", defaultValue: "Fast")).tag(0) + Text(String(localized: "sampleExtension.model.balanced", defaultValue: "Balanced")).tag(1) + Text(String(localized: "sampleExtension.model.deep", defaultValue: "Deep")).tag(2) + } + + Picker( + String(localized: "sampleExtension.priority", defaultValue: "Priority"), + selection: $selectedPriority + ) { + Text(String(localized: "sampleExtension.priority.low", defaultValue: "Low")).tag(0) + Text(String(localized: "sampleExtension.priority.normal", defaultValue: "Normal")).tag(1) + Text(String(localized: "sampleExtension.priority.high", defaultValue: "High")).tag(2) + } + + Stepper(value: $batchSize, in: 1...12) { + Text(String( + localized: "sampleExtension.batchSize", + defaultValue: "Batch size" + ) + ": \(batchSize)") + } + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(String(localized: "sampleExtension.confidence", defaultValue: "Confidence")) + Spacer(minLength: 0) + Text(confidence.formatted(.percent.precision(.fractionLength(0)))) + .foregroundStyle(.secondary) + } + .font(.system(size: 11)) + + Slider(value: $confidence, in: 0...1) + .accessibilityLabel(String(localized: "sampleExtension.confidence", defaultValue: "Confidence")) + .accessibilityValue(confidence.formatted(.percent.precision(.fractionLength(0)))) + } + + DatePicker( + String(localized: "sampleExtension.dueDate", defaultValue: "Due date"), + selection: $dueDate, + displayedComponents: [.date] + ) + + ColorPicker( + String(localized: "sampleExtension.accent", defaultValue: "Accent"), + selection: $accentColor + ) + } + } + } + + private var settingsTab: some View { + VStack(alignment: .leading, spacing: 10) { + section( + title: String(localized: "sampleExtension.toggles", defaultValue: "Toggles"), + systemImage: "switch.2" + ) { + Toggle( + String(localized: "sampleExtension.notifications", defaultValue: "Notifications"), + isOn: $notificationsEnabled + ) + Toggle( + String(localized: "sampleExtension.compactLayout", defaultValue: "Compact layout"), + isOn: $compactLayout + ) + Toggle( + String(localized: "sampleExtension.livePreview", defaultValue: "Live preview"), + isOn: $livePreview + ) + } + + section( + title: String(localized: "sampleExtension.advanced", defaultValue: "Advanced"), + systemImage: "gearshape.2" + ) { + DisclosureGroup(String(localized: "sampleExtension.advancedOptions", defaultValue: "Advanced options")) { + VStack(alignment: .leading, spacing: 8) { + Picker(String(localized: "sampleExtension.scope", defaultValue: "Scope"), selection: $selectedScope) { + Text(String(localized: "sampleExtension.scope.session", defaultValue: "Session")).tag(0) + Text(String(localized: "sampleExtension.scope.project", defaultValue: "Project")).tag(1) + Text(String(localized: "sampleExtension.scope.workspace", defaultValue: "Workspace")).tag(2) + } + + Gauge(value: confidence) { + Text(String(localized: "sampleExtension.readiness", defaultValue: "Readiness")) + } + .gaugeStyle(.accessoryLinear) + + Text(ProcessInfo.processInfo.processName) + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(.tertiary) + .lineLimit(1) + } + .padding(.top, 6) + } + } + } + } + + private var footer: some View { + TimelineView(.periodic(from: .now, by: 1)) { timeline in + HStack(spacing: 6) { + Circle() + .fill(livePreview ? accentColor : Color.secondary) + .frame(width: 7, height: 7) + Text(String(localized: "sampleExtension.live", defaultValue: "Live")) + .font(.system(size: 10, weight: .medium)) + Spacer(minLength: 0) + Text(timeline.date, style: .time) + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(.secondary) + } + } + } + + private func section( + title: String, + systemImage: String, + @ViewBuilder content: () -> Content + ) -> some View { + VStack(alignment: .leading, spacing: 8) { + Label(title, systemImage: systemImage) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + + content() + .font(.system(size: 11)) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.primary.opacity(0.045), in: RoundedRectangle(cornerRadius: 7, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 7, style: .continuous) + .stroke(Color.primary.opacity(0.08), lineWidth: 1) + ) + } + + private func copySnapshot() { + let snapshot = [ + String(localized: "sampleExtension.titleField", defaultValue: "Title") + ": \(title)", + String(localized: "sampleExtension.mode", defaultValue: "Mode") + ": \(modeTitle)", + String(localized: "sampleExtension.model", defaultValue: "Model") + ": \(modelTitle)", + String(localized: "sampleExtension.priority", defaultValue: "Priority") + ": \(priorityTitle)", + String(localized: "sampleExtension.batchSize", defaultValue: "Batch size") + ": \(batchSize)" + ].joined(separator: "\n") + + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(snapshot, forType: .string) + lastAction = String( + localized: "sampleExtension.actionCopied", + defaultValue: "Snapshot copied" + ) + } + + private var modeTitle: String { + switch selectedMode { + case 0: + String(localized: "sampleExtension.mode.run", defaultValue: "Run") + case 1: + String(localized: "sampleExtension.mode.review", defaultValue: "Review") + case 2: + String(localized: "sampleExtension.mode.watch", defaultValue: "Watch") + default: + "\(selectedMode)" + } + } + + private var modelTitle: String { + switch selectedModel { + case 0: + String(localized: "sampleExtension.model.fast", defaultValue: "Fast") + case 1: + String(localized: "sampleExtension.model.balanced", defaultValue: "Balanced") + case 2: + String(localized: "sampleExtension.model.deep", defaultValue: "Deep") + default: + "\(selectedModel)" + } + } + + private var priorityTitle: String { + switch selectedPriority { + case 0: + String(localized: "sampleExtension.priority.low", defaultValue: "Low") + case 1: + String(localized: "sampleExtension.priority.normal", defaultValue: "Normal") + case 2: + String(localized: "sampleExtension.priority.high", defaultValue: "High") + default: + "\(selectedPriority)" + } + } + + private func resetControls() { + selectedTab = 0 + selectedMode = 0 + selectedModel = 0 + selectedPriority = 1 + selectedScope = 0 + title = String(localized: "sampleExtension.defaultTitle", defaultValue: "Draft launch plan") + secret = "" + notes = "" + isPinned = true + notificationsEnabled = true + compactLayout = false + livePreview = true + confidence = 0.62 + batchSize = 4 + dueDate = Date() + accentColor = .green + actionCount = 0 + lastAction = String(localized: "sampleExtension.actionReset", defaultValue: "Controls reset") + } +} +#else +import Foundation + +@main +enum RightSidebarDemoExtensionFallback { + static func main() {} +} +#endif diff --git a/Samples/RightSidebarBYOExtension/README.md b/Samples/RightSidebarBYOExtension/README.md new file mode 100644 index 000000000000..860141c99c98 --- /dev/null +++ b/Samples/RightSidebarBYOExtension/README.md @@ -0,0 +1,35 @@ +# cmux Right Sidebar BYO Extension Sample + +This sample is a separate macOS app that carries a cmux right sidebar ExtensionKit extension. + +The app installs like any other macOS app. Its embedded `.appex` declares the cmux extension point: + +```text +com.cmuxterm.app.debug.extkit.right-sidebar-panel +``` + +That is the current dogfood extension point. Production builds also declare `com.cmuxterm.app.right-sidebar-panel`, and the sample can switch to that identifier when the feature ships. + +The extension exposes the scene id that the current cmux host demo loads: + +```text +cmux-right-sidebar-demo +``` + +Build and register it: + +```bash +Samples/RightSidebarBYOExtension/build-and-register.sh +``` + +The script defaults to the current Mac architecture. To cross-build manually, set `TARGET_TRIPLE`, for example `TARGET_TRIPLE=x86_64-apple-macos26.0`. + +Then open the tagged cmux build, switch the right sidebar to `ExtensionKit`, click refresh, and choose `cmux BYO Sidebar Sample`. + +The resulting bundle is written to: + +```text +~/Library/Application Support/cmux/ExtensionSamples/RightSidebarBYOExtension/cmux BYO Sidebar Sample.app +``` + +During development the script uses `pluginkit -a` to register the `.appex` directly. A production version would be distributed as a signed app whose containing bundle installs the extension through normal LaunchServices and PlugInKit registration. diff --git a/Samples/RightSidebarBYOExtension/Resources/Localizable.xcstrings b/Samples/RightSidebarBYOExtension/Resources/Localizable.xcstrings new file mode 100644 index 000000000000..696e2305f1e9 --- /dev/null +++ b/Samples/RightSidebarBYOExtension/Resources/Localizable.xcstrings @@ -0,0 +1,950 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "sampleContainer.stepInstall" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Build and register this app with the included script." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "同梱のスクリプトでこのアプリをビルドして登録します。" + } + } + } + }, + "sampleContainer.stepRefresh" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open cmux, choose the ExtensionKit sidebar, then refresh." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "cmuxを開き、ExtensionKitサイドバーを選んで更新します。" + } + } + } + }, + "sampleContainer.stepSelect" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick cmux BYO Sidebar Sample from the extension menu." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "拡張機能メニューからcmux BYO Sidebar Sampleを選びます。" + } + } + } + }, + "sampleContainer.subtitle" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This app carries a cmux right sidebar extension." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このアプリにはcmux右サイドバー拡張機能が含まれています。" + } + } + } + }, + "sampleContainer.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "cmux BYO Sidebar Sample" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "cmux BYO Sidebar Sample" + } + } + } + }, + "sampleExtension.accent" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accent" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクセント" + } + } + } + }, + "sampleExtension.actionCopied" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Snapshot copied" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スナップショットをコピーしました" + } + } + } + }, + "sampleExtension.actionRan" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Run button clicked" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "実行ボタンをクリックしました" + } + } + } + }, + "sampleExtension.actionReady" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ready" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "準備完了" + } + } + } + }, + "sampleExtension.actionReset" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Controls reset" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コントロールをリセットしました" + } + } + } + }, + "sampleExtension.advanced" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advanced" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "詳細" + } + } + } + }, + "sampleExtension.advancedOptions" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advanced options" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "詳細オプション" + } + } + } + }, + "sampleExtension.batchSize" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batch size" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バッチサイズ" + } + } + } + }, + "sampleExtension.compactLayout" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compact layout" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コンパクト表示" + } + } + } + }, + "sampleExtension.confidence" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confidence" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "信頼度" + } + } + } + }, + "sampleExtension.copy" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピー" + } + } + } + }, + "sampleExtension.count" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Count" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "回数" + } + } + } + }, + "sampleExtension.defaultTitle" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Draft launch plan" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローンチ計画の下書き" + } + } + } + }, + "sampleExtension.dueDate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Due date" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "期限" + } + } + } + }, + "sampleExtension.externalApp" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installed from a separate app bundle" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "別のアプリバンドルからインストール" + } + } + } + }, + "sampleExtension.formControls" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Form controls" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フォームコントロール" + } + } + } + }, + "sampleExtension.hosted" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hosted by cmux through ExtensionKit" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ExtensionKit経由でcmuxがホスト" + } + } + } + }, + "sampleExtension.live" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Live" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライブ" + } + } + } + }, + "sampleExtension.livePreview" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Live preview" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライブプレビュー" + } + } + } + }, + "sampleExtension.liveStatus" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Live status" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライブ状態" + } + } + } + }, + "sampleExtension.mode" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mode" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モード" + } + } + } + }, + "sampleExtension.mode.review" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Review" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レビュー" + } + } + } + }, + "sampleExtension.mode.run" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Run" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "実行" + } + } + } + }, + "sampleExtension.mode.watch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Watch" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視" + } + } + } + }, + "sampleExtension.model" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Model" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モデル" + } + } + } + }, + "sampleExtension.model.balanced" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Balanced" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バランス" + } + } + } + }, + "sampleExtension.model.deep" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deep" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "深く" + } + } + } + }, + "sampleExtension.model.fast" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fast" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高速" + } + } + } + }, + "sampleExtension.notesPlaceholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Write sidebar notes..." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サイドバーのメモを書く..." + } + } + } + }, + "sampleExtension.notifications" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notifications" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知" + } + } + } + }, + "sampleExtension.pinned" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pinned" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "固定" + } + } + } + }, + "sampleExtension.priority" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Priority" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "優先度" + } + } + } + }, + "sampleExtension.priority.high" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "High" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高" + } + } + } + }, + "sampleExtension.priority.low" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Low" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "低" + } + } + } + }, + "sampleExtension.priority.normal" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Normal" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通常" + } + } + } + }, + "sampleExtension.quickActions" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quick actions" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クイックアクション" + } + } + } + }, + "sampleExtension.readiness" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Readiness" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "準備度" + } + } + } + }, + "sampleExtension.reset" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リセット" + } + } + } + }, + "sampleExtension.run" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Run" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "実行" + } + } + } + }, + "sampleExtension.scope" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scope" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スコープ" + } + } + } + }, + "sampleExtension.scope.project" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Project" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロジェクト" + } + } + } + }, + "sampleExtension.scope.session" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッション" + } + } + } + }, + "sampleExtension.scope.workspace" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Workspace" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペース" + } + } + } + }, + "sampleExtension.secretField" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シークレット" + } + } + } + }, + "sampleExtension.subtitle" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "External ExtensionKit app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部ExtensionKitアプリ" + } + } + } + }, + "sampleExtension.tab.actions" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actions" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクション" + } + } + } + }, + "sampleExtension.tab.inputs" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inputs" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "入力" + } + } + } + }, + "sampleExtension.tab.settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + } + } + }, + "sampleExtension.tabs" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タブ" + } + } + } + }, + "sampleExtension.textInputs" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text inputs" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキスト入力" + } + } + } + }, + "sampleExtension.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "BYO controls" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BYOコントロール" + } + } + } + }, + "sampleExtension.titleField" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Title" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイトル" + } + } + } + }, + "sampleExtension.toggles" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toggles" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トグル" + } + } + } + }, + "sampleExtension.xpc" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scene XPC connection accepted" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シーンXPC接続を受け入れました" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/Samples/RightSidebarBYOExtension/Sources/ContainerApp/RightSidebarSampleContainerApp.swift b/Samples/RightSidebarBYOExtension/Sources/ContainerApp/RightSidebarSampleContainerApp.swift new file mode 100644 index 000000000000..73ac13e56aaa --- /dev/null +++ b/Samples/RightSidebarBYOExtension/Sources/ContainerApp/RightSidebarSampleContainerApp.swift @@ -0,0 +1,62 @@ +import SwiftUI + +@main +struct RightSidebarSampleContainerApp: App { + var body: some Scene { + WindowGroup { + RightSidebarSampleContainerView() + .frame(width: 420, height: 230) + } + .windowResizability(.contentSize) + } +} + +private struct RightSidebarSampleContainerView: View { + var body: some View { + VStack(alignment: .leading, spacing: 14) { + HStack(spacing: 10) { + Image(systemName: "puzzlepiece.extension") + .font(.system(size: 28, weight: .semibold)) + .foregroundStyle(.green) + + VStack(alignment: .leading, spacing: 3) { + Text(String(localized: "sampleContainer.title", defaultValue: "cmux BYO Sidebar Sample")) + .font(.system(size: 18, weight: .semibold)) + Text(String(localized: "sampleContainer.subtitle", defaultValue: "This app carries a cmux right sidebar extension.")) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + } + + Divider() + + VStack(alignment: .leading, spacing: 7) { + Label( + String( + localized: "sampleContainer.stepInstall", + defaultValue: "Build and register this app with the included script." + ), + systemImage: "hammer" + ) + Label( + String( + localized: "sampleContainer.stepRefresh", + defaultValue: "Open cmux, choose the ExtensionKit sidebar, then refresh." + ), + systemImage: "arrow.clockwise" + ) + Label( + String( + localized: "sampleContainer.stepSelect", + defaultValue: "Pick cmux BYO Sidebar Sample from the extension menu." + ), + systemImage: "cursorarrow.click" + ) + } + .font(.system(size: 12)) + + Spacer(minLength: 0) + } + .padding(18) + } +} diff --git a/Samples/RightSidebarBYOExtension/Sources/Extension/RightSidebarSampleExtension.swift b/Samples/RightSidebarBYOExtension/Sources/Extension/RightSidebarSampleExtension.swift new file mode 100644 index 000000000000..a83655771a16 --- /dev/null +++ b/Samples/RightSidebarBYOExtension/Sources/Extension/RightSidebarSampleExtension.swift @@ -0,0 +1,538 @@ +import ExtensionFoundation +import ExtensionKit +import Foundation +import AppKit +import SwiftUI + +private enum SampleExtensionConstants { + static let sceneID = "cmux-right-sidebar-demo" +} + +protocol CmuxRightSidebarExtension: AppExtension { + associatedtype Body: CmuxRightSidebarScene + var body: Body { get } +} + +protocol CmuxRightSidebarScene: AppExtensionScene {} + +struct CmuxRightSidebarPanelScene: CmuxRightSidebarScene { + private let content: () -> Content + + init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + var body: some AppExtensionScene { + PrimitiveAppExtensionScene(id: SampleExtensionConstants.sceneID) { + content() + } onConnection: { connection in + connection.resume() + return true + } + } +} + +extension CmuxRightSidebarExtension { + var configuration: AppExtensionSceneConfiguration { + AppExtensionSceneConfiguration(self.body) + } +} + +@main +final class RightSidebarSampleExtension: CmuxRightSidebarExtension { + required init() {} + + @AppExtensionPoint.Bind + var boundExtensionPoint: AppExtensionPoint { + AppExtensionPoint.Identifier(host: "com.cmuxterm.app.debug.extkit", name: "right-sidebar-panel") + } + + var body: some CmuxRightSidebarScene { + CmuxRightSidebarPanelScene { + BYOSidebarSampleView() + } + } +} + +private struct BYOSidebarSampleView: View { + @State private var selectedTab = SampleTab.actions + @State private var selectedMode = SampleMode.run + @State private var selectedModel = SampleModel.fast + @State private var selectedPriority = SamplePriority.normal + @State private var selectedScope = SampleScope.session + @State private var title: String + @State private var secret = "" + @State private var notes = "" + @State private var isPinned = true + @State private var notificationsEnabled = true + @State private var compactLayout = false + @State private var livePreview = true + @State private var confidence = 0.62 + @State private var batchSize = 4 + @State private var dueDate = Date() + @State private var accentColor = Color.green + @State private var actionCount = 0 + @State private var lastAction: String + + init() { + _title = State(initialValue: String( + localized: "sampleExtension.defaultTitle", + defaultValue: "Draft launch plan" + )) + _lastAction = State(initialValue: String( + localized: "sampleExtension.actionReady", + defaultValue: "Ready" + )) + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + header + + Picker( + String(localized: "sampleExtension.tabs", defaultValue: "Tabs"), + selection: $selectedTab + ) { + ForEach(SampleTab.allCases) { tab in + Text(tab.title).tag(tab) + } + } + .labelsHidden() + .pickerStyle(.segmented) + + ScrollView { + VStack(alignment: .leading, spacing: 10) { + switch selectedTab { + case .actions: + actionsTab + case .inputs: + inputsTab + case .settings: + settingsTab + } + } + .padding(.bottom, 4) + } + + footer + } + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(Color(nsColor: .controlBackgroundColor)) + } + + private var header: some View { + HStack(alignment: .center, spacing: 9) { + Image(systemName: "slider.horizontal.3") + .font(.system(size: 24, weight: .semibold)) + .foregroundStyle(accentColor) + + VStack(alignment: .leading, spacing: 2) { + Text(String(localized: "sampleExtension.title", defaultValue: "BYO controls")) + .font(.system(size: 14, weight: .semibold)) + Text(String(localized: "sampleExtension.subtitle", defaultValue: "External ExtensionKit app")) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + + Spacer(minLength: 0) + } + } + + private var actionsTab: some View { + VStack(alignment: .leading, spacing: 10) { + section( + title: String(localized: "sampleExtension.quickActions", defaultValue: "Quick actions"), + systemImage: "bolt" + ) { + Picker( + String(localized: "sampleExtension.mode", defaultValue: "Mode"), + selection: $selectedMode + ) { + ForEach(SampleMode.allCases) { mode in + Text(mode.title).tag(mode) + } + } + .pickerStyle(.segmented) + + HStack(spacing: 8) { + Button { + actionCount += 1 + lastAction = String( + localized: "sampleExtension.actionRan", + defaultValue: "Run button clicked" + ) + } label: { + Label( + String(localized: "sampleExtension.run", defaultValue: "Run"), + systemImage: "play.fill" + ) + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + + Button { + copySnapshot() + } label: { + Label( + String(localized: "sampleExtension.copy", defaultValue: "Copy"), + systemImage: "doc.on.doc" + ) + } + .controlSize(.small) + + Button { + resetControls() + } label: { + Image(systemName: "arrow.counterclockwise") + } + .buttonStyle(.borderless) + .controlSize(.small) + .help(String(localized: "sampleExtension.reset", defaultValue: "Reset")) + } + + Toggle(isOn: $isPinned) { + Label( + String(localized: "sampleExtension.pinned", defaultValue: "Pinned"), + systemImage: "pin" + ) + } + .toggleStyle(.checkbox) + + HStack(spacing: 8) { + Text(String(localized: "sampleExtension.count", defaultValue: "Count")) + Spacer(minLength: 0) + Text("\(actionCount)") + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(.secondary) + } + .font(.system(size: 11)) + } + + section( + title: String(localized: "sampleExtension.liveStatus", defaultValue: "Live status"), + systemImage: "waveform.path.ecg" + ) { + ProgressView(value: confidence) + Text(lastAction) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .lineLimit(2) + } + } + } + + private var inputsTab: some View { + VStack(alignment: .leading, spacing: 10) { + section( + title: String(localized: "sampleExtension.textInputs", defaultValue: "Text inputs"), + systemImage: "text.cursor" + ) { + TextField( + String(localized: "sampleExtension.titleField", defaultValue: "Title"), + text: $title + ) + SecureField( + String(localized: "sampleExtension.secretField", defaultValue: "Secret"), + text: $secret + ) + + ZStack(alignment: .topLeading) { + TextEditor(text: $notes) + .font(.system(size: 11)) + .frame(minHeight: 76) + .scrollContentBackground(.hidden) + + if notes.isEmpty { + Text(String(localized: "sampleExtension.notesPlaceholder", defaultValue: "Write sidebar notes...")) + .font(.system(size: 11)) + .foregroundStyle(.tertiary) + .padding(.horizontal, 5) + .padding(.vertical, 7) + .allowsHitTesting(false) + } + } + .background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 6, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 6, style: .continuous) + .stroke(Color.primary.opacity(0.08), lineWidth: 1) + ) + } + + section( + title: String(localized: "sampleExtension.formControls", defaultValue: "Form controls"), + systemImage: "list.bullet.rectangle" + ) { + Picker( + String(localized: "sampleExtension.model", defaultValue: "Model"), + selection: $selectedModel + ) { + ForEach(SampleModel.allCases) { model in + Text(model.title).tag(model) + } + } + + Picker( + String(localized: "sampleExtension.priority", defaultValue: "Priority"), + selection: $selectedPriority + ) { + ForEach(SamplePriority.allCases) { priority in + Text(priority.title).tag(priority) + } + } + + Stepper(value: $batchSize, in: 1...12) { + Text(String( + localized: "sampleExtension.batchSize", + defaultValue: "Batch size" + ) + ": \(batchSize)") + } + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(String(localized: "sampleExtension.confidence", defaultValue: "Confidence")) + Spacer(minLength: 0) + Text(confidence.formatted(.percent.precision(.fractionLength(0)))) + .foregroundStyle(.secondary) + } + .font(.system(size: 11)) + + Slider(value: $confidence, in: 0...1) + } + + DatePicker( + String(localized: "sampleExtension.dueDate", defaultValue: "Due date"), + selection: $dueDate, + displayedComponents: [.date] + ) + + ColorPicker( + String(localized: "sampleExtension.accent", defaultValue: "Accent"), + selection: $accentColor + ) + } + } + } + + private var settingsTab: some View { + VStack(alignment: .leading, spacing: 10) { + section( + title: String(localized: "sampleExtension.toggles", defaultValue: "Toggles"), + systemImage: "switch.2" + ) { + Toggle( + String(localized: "sampleExtension.notifications", defaultValue: "Notifications"), + isOn: $notificationsEnabled + ) + Toggle( + String(localized: "sampleExtension.compactLayout", defaultValue: "Compact layout"), + isOn: $compactLayout + ) + Toggle( + String(localized: "sampleExtension.livePreview", defaultValue: "Live preview"), + isOn: $livePreview + ) + } + + section( + title: String(localized: "sampleExtension.advanced", defaultValue: "Advanced"), + systemImage: "gearshape.2" + ) { + DisclosureGroup(String(localized: "sampleExtension.advancedOptions", defaultValue: "Advanced options")) { + VStack(alignment: .leading, spacing: 8) { + Picker( + String(localized: "sampleExtension.scope", defaultValue: "Scope"), + selection: $selectedScope + ) { + ForEach(SampleScope.allCases) { scope in + Text(scope.title).tag(scope) + } + } + + Gauge(value: confidence) { + Text(String(localized: "sampleExtension.readiness", defaultValue: "Readiness")) + } + .gaugeStyle(.accessoryLinear) + + Text(ProcessInfo.processInfo.processName) + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(.tertiary) + .lineLimit(1) + } + .padding(.top, 6) + } + } + } + } + + private var footer: some View { + TimelineView(.periodic(from: .now, by: 1)) { timeline in + HStack(spacing: 6) { + Circle() + .fill(livePreview ? accentColor : Color.secondary) + .frame(width: 7, height: 7) + Text(String(localized: "sampleExtension.live", defaultValue: "Live")) + .font(.system(size: 10, weight: .medium)) + Spacer(minLength: 0) + Text(timeline.date, style: .time) + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(.secondary) + } + } + } + + private func section( + title: String, + systemImage: String, + @ViewBuilder content: () -> Content + ) -> some View { + VStack(alignment: .leading, spacing: 8) { + Label(title, systemImage: systemImage) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + + content() + .font(.system(size: 11)) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.primary.opacity(0.045), in: RoundedRectangle(cornerRadius: 7, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 7, style: .continuous) + .stroke(Color.primary.opacity(0.08), lineWidth: 1) + ) + } + + private func copySnapshot() { + let snapshot = [ + String(localized: "sampleExtension.titleField", defaultValue: "Title") + ": \(title)", + String(localized: "sampleExtension.mode", defaultValue: "Mode") + ": \(selectedMode.title)", + String(localized: "sampleExtension.model", defaultValue: "Model") + ": \(selectedModel.title)", + String(localized: "sampleExtension.priority", defaultValue: "Priority") + ": \(selectedPriority.title)", + String(localized: "sampleExtension.batchSize", defaultValue: "Batch size") + ": \(batchSize)" + ].joined(separator: "\n") + + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(snapshot, forType: .string) + lastAction = String( + localized: "sampleExtension.actionCopied", + defaultValue: "Snapshot copied" + ) + } + + private func resetControls() { + selectedTab = .actions + selectedMode = .run + selectedModel = .fast + selectedPriority = .normal + selectedScope = .session + title = String(localized: "sampleExtension.defaultTitle", defaultValue: "Draft launch plan") + secret = "" + notes = "" + isPinned = true + notificationsEnabled = true + compactLayout = false + livePreview = true + confidence = 0.62 + batchSize = 4 + dueDate = Date() + accentColor = .green + actionCount = 0 + lastAction = String(localized: "sampleExtension.actionReset", defaultValue: "Controls reset") + } +} + +private enum SampleTab: String, CaseIterable, Identifiable { + case actions + case inputs + case settings + + var id: String { rawValue } + + var title: String { + switch self { + case .actions: + String(localized: "sampleExtension.tab.actions", defaultValue: "Actions") + case .inputs: + String(localized: "sampleExtension.tab.inputs", defaultValue: "Inputs") + case .settings: + String(localized: "sampleExtension.tab.settings", defaultValue: "Settings") + } + } +} + +private enum SampleMode: String, CaseIterable, Identifiable { + case run + case review + case watch + + var id: String { rawValue } + + var title: String { + switch self { + case .run: + String(localized: "sampleExtension.mode.run", defaultValue: "Run") + case .review: + String(localized: "sampleExtension.mode.review", defaultValue: "Review") + case .watch: + String(localized: "sampleExtension.mode.watch", defaultValue: "Watch") + } + } +} + +private enum SampleModel: String, CaseIterable, Identifiable { + case fast + case balanced + case deep + + var id: String { rawValue } + + var title: String { + switch self { + case .fast: + String(localized: "sampleExtension.model.fast", defaultValue: "Fast") + case .balanced: + String(localized: "sampleExtension.model.balanced", defaultValue: "Balanced") + case .deep: + String(localized: "sampleExtension.model.deep", defaultValue: "Deep") + } + } +} + +private enum SamplePriority: String, CaseIterable, Identifiable { + case low + case normal + case high + + var id: String { rawValue } + + var title: String { + switch self { + case .low: + String(localized: "sampleExtension.priority.low", defaultValue: "Low") + case .normal: + String(localized: "sampleExtension.priority.normal", defaultValue: "Normal") + case .high: + String(localized: "sampleExtension.priority.high", defaultValue: "High") + } + } +} + +private enum SampleScope: String, CaseIterable, Identifiable { + case session + case project + case workspace + + var id: String { rawValue } + + var title: String { + switch self { + case .session: + String(localized: "sampleExtension.scope.session", defaultValue: "Session") + case .project: + String(localized: "sampleExtension.scope.project", defaultValue: "Project") + case .workspace: + String(localized: "sampleExtension.scope.workspace", defaultValue: "Workspace") + } + } +} diff --git a/Samples/RightSidebarBYOExtension/build-and-register.sh b/Samples/RightSidebarBYOExtension/build-and-register.sh new file mode 100755 index 000000000000..6bb53385405b --- /dev/null +++ b/Samples/RightSidebarBYOExtension/build-and-register.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_ROOT="${BUILD_ROOT:-$HOME/Library/Application Support/cmux/ExtensionSamples/RightSidebarBYOExtension}" +APP_NAME="cmux BYO Sidebar Sample" +EXT_NAME="CmuxBYOSidebarSampleExtension" +APP_BUNDLE_ID="${APP_BUNDLE_ID:-com.cmuxterm.samples.byo-sidebar}" +EXT_BUNDLE_ID="${EXT_BUNDLE_ID:-${APP_BUNDLE_ID}.extension}" +EXTENSION_POINT_ID="com.cmuxterm.app.debug.extkit.right-sidebar-panel" +SCENE_ID="cmux-right-sidebar-demo" +SDK_PATH="$(xcrun --sdk macosx --show-sdk-path)" +TARGET_TRIPLE="${TARGET_TRIPLE:-}" +if [ -z "$TARGET_TRIPLE" ]; then + HOST_ARCH="$(uname -m)" + case "$HOST_ARCH" in + arm64) + TARGET_TRIPLE="arm64-apple-macos26.0" + ;; + x86_64) + TARGET_TRIPLE="x86_64-apple-macos26.0" + ;; + *) + echo "Unsupported host architecture: $HOST_ARCH. Set TARGET_TRIPLE explicitly." >&2 + exit 1 + ;; + esac +fi +APP_PATH="${BUILD_ROOT}/${APP_NAME}.app" +EXT_PATH="${APP_PATH}/Contents/Extensions/${EXT_NAME}.appex" + +if [ -e "/tmp/cmux-byo-sidebar-sample/cmux BYO Sidebar Sample.app/Contents/Extensions/CmuxBYOSidebarSampleExtension.appex" ]; then + pluginkit -r "/tmp/cmux-byo-sidebar-sample/cmux BYO Sidebar Sample.app/Contents/Extensions/CmuxBYOSidebarSampleExtension.appex" 2>/dev/null || true +fi +if [ -e "/tmp/cmux-byo-sidebar-sample/cmux BYO Sidebar Sample.app" ]; then + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ + -u -f "/tmp/cmux-byo-sidebar-sample/cmux BYO Sidebar Sample.app" 2>/dev/null || true +fi +if [ -e "$EXT_PATH" ]; then + pluginkit -r "$EXT_PATH" 2>/dev/null || true +fi +if [ -e "$APP_PATH" ]; then + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ + -u -f "$APP_PATH" 2>/dev/null || true +fi +rm -rf "$BUILD_ROOT" +mkdir -p \ + "$APP_PATH/Contents/MacOS" \ + "$APP_PATH/Contents/Resources" \ + "$APP_PATH/Contents/Extensions" \ + "$EXT_PATH/Contents/MacOS" \ + "$EXT_PATH/Contents/Resources" + +xcrun swiftc \ + -sdk "$SDK_PATH" \ + -target "$TARGET_TRIPLE" \ + -parse-as-library \ + "$ROOT/Sources/ContainerApp/RightSidebarSampleContainerApp.swift" \ + -o "$APP_PATH/Contents/MacOS/$APP_NAME" + +xcrun swiftc \ + -sdk "$SDK_PATH" \ + -target "$TARGET_TRIPLE" \ + -parse-as-library \ + "$ROOT/Sources/Extension/RightSidebarSampleExtension.swift" \ + -o "$EXT_PATH/Contents/MacOS/$EXT_NAME" + +cat > "$APP_PATH/Contents/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${APP_NAME} + CFBundleExecutable + ${APP_NAME} + CFBundleIdentifier + ${APP_BUNDLE_ID} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${APP_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 26.0 + NSPrincipalClass + NSApplication + + +PLIST + +cat > "$EXT_PATH/Contents/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${APP_NAME} + CFBundleExecutable + ${EXT_NAME} + CFBundleIdentifier + ${EXT_BUNDLE_ID} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${EXT_NAME} + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + EXAppExtensionAttributes + + EXExtensionPointIdentifier + ${EXTENSION_POINT_ID} + + LSMinimumSystemVersion + 26.0 + + +PLIST + +cat > "$BUILD_ROOT/Extension.entitlements" < + + + + com.apple.security.app-sandbox + + com.apple.security.get-task-allow + + + +PLIST + +xcrun xcstringstool installloc "$ROOT/Resources" --output-directory "$APP_PATH/Contents/Resources" --languages "en ja" +xcrun xcstringstool installloc "$ROOT/Resources" --output-directory "$EXT_PATH/Contents/Resources" --languages "en ja" + +codesign --force --sign - --timestamp=none --entitlements "$BUILD_ROOT/Extension.entitlements" "$EXT_PATH" +codesign --force --sign - --timestamp=none "$APP_PATH" + +/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "$APP_PATH" +pluginkit -a "$EXT_PATH" +pluginkit -e use -i "$EXT_BUNDLE_ID" -p "$EXTENSION_POINT_ID" >/dev/null 2>&1 || true + +echo "Sample app:" +echo " $APP_PATH" +echo +echo "Sample extension:" +echo " $EXT_PATH" +echo +echo "Extension point:" +echo " $EXTENSION_POINT_ID" +echo +echo "Scene id:" +echo " $SCENE_ID" +echo +pluginkit -m -A -D -v -p "$EXTENSION_POINT_ID" diff --git a/Sources/App/MenuBarExtraController.swift b/Sources/App/MenuBarExtraController.swift index fe6d6ef7d540..5a83790f57ba 100644 --- a/Sources/App/MenuBarExtraController.swift +++ b/Sources/App/MenuBarExtraController.swift @@ -241,7 +241,6 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate { onQuitApp() } } - private final class NotificationMenuItemPayload: NSObject { let notification: TerminalNotification @@ -661,5 +660,3 @@ enum MenuBarIconRenderer { (text as NSString).draw(in: textRect, withAttributes: attrs) } } - - diff --git a/Sources/App/ShortcutRoutingSupport.swift b/Sources/App/ShortcutRoutingSupport.swift index 8bf882135229..fd657b1d3ff7 100644 --- a/Sources/App/ShortcutRoutingSupport.swift +++ b/Sources/App/ShortcutRoutingSupport.swift @@ -570,6 +570,62 @@ func cmuxFieldEditorOwnerView(_ editor: NSTextView) -> NSView? { return editor.superview } +let cmuxRightSidebarExtensionHostContainerIdentifier = + NSUserInterfaceItemIdentifier("cmux.rightSidebar.extensionHost.container") + +func cmuxRightSidebarExtensionHostOwnsResponder( + _ responder: NSResponder?, + in window: NSWindow? = nil +) -> Bool { + guard let responder else { return false } + + let ownerView: NSView? + if let textView = responder as? NSTextView, textView.isFieldEditor { + ownerView = cmuxFieldEditorOwnerView(textView) ?? textView + } else { + ownerView = responder as? NSView + } + + guard let ownerView else { return false } + if let window, ownerView.window !== window { return false } + return cmuxRightSidebarExtensionHostAncestor(for: ownerView) != nil +} + +func cmuxRightSidebarExtensionHostContains(windowPoint: NSPoint, in window: NSWindow) -> Bool { + guard let contentView = window.contentView else { return false } + return cmuxRightSidebarExtensionHost(atWindowPoint: windowPoint, in: contentView) != nil +} + +private func cmuxRightSidebarExtensionHostAncestor(for view: NSView) -> NSView? { + var current: NSView? = view + while let candidate = current { + if candidate.identifier == cmuxRightSidebarExtensionHostContainerIdentifier { + return candidate + } + current = candidate.superview + } + return nil +} + +private func cmuxRightSidebarExtensionHost(atWindowPoint windowPoint: NSPoint, in view: NSView) -> NSView? { + guard !view.isHidden else { return nil } + + let localPoint = view.convert(windowPoint, from: nil) + guard view.bounds.contains(localPoint) else { return nil } + + if view.identifier == cmuxRightSidebarExtensionHostContainerIdentifier { + return view + } + + for subview in view.subviews.reversed() { + if let host = cmuxRightSidebarExtensionHost(atWindowPoint: windowPoint, in: subview) { + return host + } + } + + return nil +} + private func cmuxOwningGhosttyView(for view: NSView) -> GhosttyNSView? { if let ghosttyView = view as? GhosttyNSView { return ghosttyView diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index d392cb58abea..4d8039cff4bc 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4133,6 +4133,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent if window.firstResponder is NSText { return } + if cmuxRightSidebarExtensionHostOwnsResponder(window.firstResponder, in: window) { + return + } guard let context = contextForMainWindow(window) ?? contextForMainTerminalWindow(window), let workspace = context.tabManager.selectedWorkspace, let panelId = workspace.focusedPanelId, diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index 19a9d7dee0b1..1bb0415f3278 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -516,6 +516,23 @@ final class WindowBrowserHostView: NSView { dividerHit: dividerHit, hitView: nil ) +#endif + return nil + } + if let window, + cmuxRightSidebarExtensionHostContains( + windowPoint: convert(point, to: nil), + in: window + ) { +#if DEBUG + debugLogPointerRouting( + stage: "hitTest.extensionSidebarPass", + point: point, + titlebarPassThrough: false, + sidebarPassThrough: false, + dividerHit: dividerHit, + hitView: nil + ) #endif return nil } diff --git a/Sources/RightSidebarExtensionDemo.swift b/Sources/RightSidebarExtensionDemo.swift new file mode 100644 index 000000000000..459041fdddf6 --- /dev/null +++ b/Sources/RightSidebarExtensionDemo.swift @@ -0,0 +1,883 @@ +#if compiler(>=6.3) && canImport(ExtensionFoundation) && canImport(ExtensionKit) +import AppKit +import ExtensionFoundation +import ExtensionKit +import Observation +import SwiftUI + +enum RightSidebarExtensionPoint { + static let identifier = "com.cmuxterm.app.debug.extkit.right-sidebar-panel" + static let productionIdentifier = "com.cmuxterm.app.right-sidebar-panel" + static let legacyIdentifier = "com.cmuxterm.right-sidebar-panel" + static let matchingDiscoveryIdentifiers = [ + identifier, + productionIdentifier, + legacyIdentifier, + ] + static let sceneID = "cmux-right-sidebar-demo" +} + +@available(macOS 26.0, *) +extension AppExtensionPoint { + @Definition + static var cmuxRightSidebarPanel: AppExtensionPoint { + Name("right-sidebar-panel") + UserInterface(true) + Scope(restriction: .none) + } +} + +@MainActor +final class RightSidebarExtensionDemoStore: ObservableObject { + @Published var identities: [AppExtensionIdentity] = [] + @Published var selectedIdentityID: AppExtensionIdentity.ID? + @Published var statusMessage: String = String( + localized: "rightSidebar.extensionDemo.ready", + defaultValue: "Ready" + ) + @Published var isLoading: Bool = false + @Published var isBundledDemoExtensionPresent: Bool = RightSidebarExtensionDemoStore.bundledDemoExtensionExists() + @Published var registryGeneration: Int = 0 + + private var monitor: Any? + private var reloadGeneration = 0 + + var selectedIdentity: AppExtensionIdentity? { + guard let selectedIdentityID else { + return identities.first + } + + return identities.first { $0.id == selectedIdentityID } ?? identities.first + } + + func reload() { + guard !isLoading else { return } + + reloadGeneration += 1 + let generation = reloadGeneration + isLoading = true + isBundledDemoExtensionPresent = Self.bundledDemoExtensionExists() + statusMessage = String( + localized: "rightSidebar.extensionDemo.loading", + defaultValue: "Looking for demo extensions..." + ) + + Task { + await loadIdentities(generation: generation) + } + } + + private func loadIdentities(generation: Int) async { + do { + if #available(macOS 26.0, *) { + let monitor = AppExtensionPoint.Monitor() + try await monitor.addAppExtensionPoint(.cmuxRightSidebarPanel) + self.monitor = monitor + observeMonitor(monitor) + guard generation == reloadGeneration else { return } + let monitorState = monitor.state + let matchingIdentities = (try? await loadMatchingIdentities()) ?? [] + guard generation == reloadGeneration else { return } + if matchingIdentities.isEmpty { + applyMonitorState(monitorState) + } else { + applyDiscoveredIdentities( + matchingIdentities, + disabledCount: monitorState.disabledCount, + unapprovedCount: monitorState.unapprovedCount + ) + } + } else { + self.monitor = nil + let discoveredIdentities = try await loadMatchingIdentities() + guard generation == reloadGeneration else { return } + applyDiscoveredIdentities(discoveredIdentities) + } + } catch { + guard generation == reloadGeneration else { return } + monitor = nil + identities = [] + selectedIdentityID = nil + statusMessage = String( + localized: "rightSidebar.extensionDemo.errorStatus", + defaultValue: "Extension discovery failed." + ) + " \(error.localizedDescription)" + } + + isLoading = false + } + + @available(macOS 26.0, *) + private func applyMonitorState(_ state: AppExtensionPoint.Monitor.State) { + applyDiscoveredIdentities( + state.identities, + disabledCount: state.disabledCount, + unapprovedCount: state.unapprovedCount + ) + } + + private func applyDiscoveredIdentities( + _ discoveredIdentities: [AppExtensionIdentity], + disabledCount: Int = 0, + unapprovedCount: Int = 0 + ) { + isBundledDemoExtensionPresent = Self.bundledDemoExtensionExists() + let previousIdentityIDs = identities.map(\.id) + let sortedIdentities = uniqueIdentities(discoveredIdentities).sorted { + $0.localizedName.localizedCaseInsensitiveCompare($1.localizedName) == .orderedAscending + } + let currentIdentityIDs = sortedIdentities.map(\.id) + if currentIdentityIDs != previousIdentityIDs { + registryGeneration += 1 + } + + let unavailableCount = disabledCount + unapprovedCount + if sortedIdentities.isEmpty, !identities.isEmpty { + let bundledIdentifier = RightSidebarExtensionDemoStore.bundledDemoExtensionBundleIdentifier() + let bundledIdentities = identities.filter { identity in + bundledIdentifier.map { $0 == identity.bundleIdentifier } == true && + RightSidebarExtensionDemoStore.bundledDemoExtensionExists() + } + if !bundledIdentities.isEmpty { + identities = bundledIdentities + if selectedIdentityID.map({ id in !bundledIdentities.contains { $0.id == id } }) ?? true { + selectedIdentityID = bundledIdentities.first?.id + } + registryGeneration += 1 + statusMessage = String( + localized: "rightSidebar.extensionDemo.settlingStatus", + defaultValue: "Extension discovery is settling. Keeping the last extension." + ) + return + } + } + + identities = sortedIdentities + if selectedIdentityID.map({ id in !sortedIdentities.contains { $0.id == id } }) ?? true { + selectedIdentityID = sortedIdentities.first?.id + } + + if sortedIdentities.isEmpty { + if unavailableCount > 0 { + statusMessage = String( + localized: "rightSidebar.extensionDemo.unavailableStatus", + defaultValue: "Extension is registered but not available yet." + ) + } else if isBundledDemoExtensionPresent { + statusMessage = String( + localized: "rightSidebar.extensionDemo.registrationSettlingStatus", + defaultValue: "Bundled demo extension is installed. Waiting for macOS registration." + ) + } else { + statusMessage = String( + localized: "rightSidebar.extensionDemo.emptyStatus", + defaultValue: "No demo extension found." + ) + } + } else { + statusMessage = String( + localized: "rightSidebar.extensionDemo.foundStatus", + defaultValue: "Demo extension discovered." + ) + } + } + + private func uniqueIdentities(_ discoveredIdentities: [AppExtensionIdentity]) -> [AppExtensionIdentity] { + var seen = Set() + return discoveredIdentities.filter { identity in + seen.insert(identity.id).inserted + } + } + + private static func bundledDemoExtensionExists() -> Bool { + FileManager.default.fileExists(atPath: bundledDemoExtensionURL.path) + } + + private static func bundledDemoExtensionBundleIdentifier() -> String? { + Bundle(url: bundledDemoExtensionURL)?.bundleIdentifier + } + + private static var bundledDemoExtensionURL: URL { + let extensionURL = Bundle.main.bundleURL + .appendingPathComponent("Contents", isDirectory: true) + .appendingPathComponent("Extensions", isDirectory: true) + .appendingPathComponent("RightSidebarDemoExtension.appex", isDirectory: true) + return extensionURL + } + + @available(macOS 26.0, *) + private func observeMonitor(_ monitor: AppExtensionPoint.Monitor) { + _ = withObservationTracking { + monitor.state + } onChange: { [weak self, weak monitor] in + Task { @MainActor in + guard let self, let monitor else { return } + guard (self.monitor as AnyObject?) === monitor else { return } + self.applyMonitorState(monitor.state) + self.observeMonitor(monitor) + } + } + } + + @available(macOS, introduced: 13.0, deprecated: 26.0, message: "Use AppExtensionPoint.Monitor") + private func loadMatchingIdentities() async throws -> [AppExtensionIdentity] { + var discoveredIdentities: [AppExtensionIdentity] = [] + for identifier in RightSidebarExtensionPoint.matchingDiscoveryIdentifiers { + let identities = try AppExtensionIdentity.matching(appExtensionPointIDs: identifier) + var iterator = identities.makeAsyncIterator() + discoveredIdentities.append(contentsOf: await iterator.next() ?? []) + } + return uniqueIdentities(discoveredIdentities) + } +} + +struct RightSidebarExtensionDemoPanelView: View { + @StateObject private var store = RightSidebarExtensionDemoStore() + @State private var isPromptInfoPresented = false + + var body: some View { + VStack(spacing: 0) { + header + Divider() + content + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .onAppear { + store.reload() + } + } + + private var header: some View { + HStack(spacing: 8) { + Label { + Text(String(localized: "rightSidebar.extensionDemo.title", defaultValue: "ExtensionKit Demo")) + } icon: { + Image(systemName: "puzzlepiece.extension") + } + .font(.system(size: 12, weight: .semibold)) + + Spacer(minLength: 0) + + if store.isLoading { + ProgressView() + .controlSize(.small) + .scaleEffect(0.65) + } + + Button { + store.reload() + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 11, weight: .medium)) + } + .buttonStyle(.plain) + .help(String(localized: "rightSidebar.extensionDemo.refresh", defaultValue: "Refresh extensions")) + } + .padding(.horizontal, 10) + .padding(.vertical, 7) + } + + @ViewBuilder + private var content: some View { + if #available(macOS 26.0, *) { + extensionContent + } else { + VStack(spacing: 8) { + Image(systemName: "puzzlepiece.extension") + .font(.system(size: 22)) + .foregroundStyle(.secondary) + Text(String( + localized: "rightSidebar.extensionDemo.requiresMacOS26", + defaultValue: "ExtensionKit sidebar demos require macOS 26." + )) + .font(.system(size: 12)) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + } + .padding(18) + } + } + + private var extensionContent: some View { + VStack(alignment: .leading, spacing: 10) { + Text(store.statusMessage) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .lineLimit(3) + .frame(maxWidth: .infinity, alignment: .leading) + + if store.identities.isEmpty { + Spacer(minLength: 0) + if store.isBundledDemoExtensionPresent { + VStack(spacing: 9) { + ProgressView() + .controlSize(.small) + Text(String( + localized: "rightSidebar.extensionDemo.waitingForBundledDemo", + defaultValue: "Bundled demo extension is installed. Waiting for macOS to finish registration." + )) + .font(.system(size: 12)) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 6) + .frame(maxWidth: .infinity) + } else { + VStack(spacing: 9) { + Image(systemName: "exclamationmark.magnifyingglass") + .font(.system(size: 21)) + .foregroundStyle(.secondary) + Text(String( + localized: "rightSidebar.extensionDemo.noExtensions", + defaultValue: "No ExtensionKit demo extension is registered yet." + )) + .font(.system(size: 12)) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + Text(String( + localized: "rightSidebar.extensionDemo.copyPromptHint", + defaultValue: "Copy a starter prompt and paste it into your AI agent to generate a sidebar extension." + )) + .font(.system(size: 11)) + .multilineTextAlignment(.center) + .foregroundStyle(.tertiary) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 8) { + Button { + copyStarterPrompt() + } label: { + Label( + String(localized: "rightSidebar.extensionDemo.copyPrompt", defaultValue: "Copy extension prompt"), + systemImage: "doc.on.clipboard" + ) + } + .buttonStyle(.bordered) + .controlSize(.small) + + Button { + isPromptInfoPresented.toggle() + } label: { + Image(systemName: "info.circle") + .font(.system(size: 12, weight: .medium)) + } + .buttonStyle(.plain) + .controlSize(.small) + .help(String( + localized: "rightSidebar.extensionDemo.promptInfo", + defaultValue: "About this prompt" + )) + .popover(isPresented: $isPromptInfoPresented, arrowEdge: .trailing) { + ExtensionPromptInfoPopover( + prompt: Self.starterPrompt, + copyAction: copyStarterPrompt + ) + } + } + } + .padding(.horizontal, 6) + .frame(maxWidth: .infinity) + } + Spacer(minLength: 0) + } else { + Picker( + String(localized: "rightSidebar.extensionDemo.extensionPicker", defaultValue: "Extension"), + selection: $store.selectedIdentityID + ) { + ForEach(store.identities, id: \.id) { identity in + Text(identity.localizedName) + .tag(Optional(identity.id)) + } + } + .labelsHidden() + .pickerStyle(.menu) + + if let identity = store.selectedIdentity { + ExtensionKitSidebarHostView( + identity: identity, + registryGeneration: store.registryGeneration, + statusMessage: $store.statusMessage + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 6, style: .continuous) + .stroke(Color.primary.opacity(0.08), lineWidth: 1) + ) + + extensionIdentityFooter(identity) + } + } + } + .padding(10) + } + + private func extensionIdentityFooter(_ identity: AppExtensionIdentity) -> some View { + VStack(alignment: .leading, spacing: 2) { + Text(identity.bundleIdentifier) + .lineLimit(1) + Text(identity.extensionPointIdentifier) + .lineLimit(1) + } + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(.tertiary) + .help("\(identity.bundleIdentifier)\n\(identity.extensionPointIdentifier)") + } + + private func copyStarterPrompt() { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(Self.starterPrompt, forType: .string) + store.statusMessage = String( + localized: "rightSidebar.extensionDemo.copyPromptCopied", + defaultValue: "Starter prompt copied." + ) + } + + private static var starterPrompt: String { + String( + localized: "rightSidebar.extensionDemo.copyPromptBody", + defaultValue: """ + Build a cmux right sidebar ExtensionKit demo extension in Swift. + + Requirements: + - Use ExtensionFoundation and ExtensionKit. + - Bind to app extension point com.cmuxterm.app.debug.extkit.right-sidebar-panel. + - Provide a PrimitiveAppExtensionScene with scene id cmux-right-sidebar-demo. + - Render a compact SwiftUI panel that fits a narrow right sidebar. + - Return AppExtensionSceneConfiguration(body) from the AppExtension configuration and do not install a custom NSXPCConnection acceptor for the scene. + - Add the Info.plist and Xcode project settings needed for an ExtensionKit .appex, including EXAppExtensionAttributes.EXExtensionPointIdentifier = com.cmuxterm.app.debug.extkit.right-sidebar-panel. + - Keep all UI strings localized. + """ + ) + } +} + +private struct ExtensionPromptInfoPopover: View { + let prompt: String + let copyAction: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Label { + Text(String(localized: "rightSidebar.extensionDemo.promptInfoTitle", defaultValue: "Extension prompt")) + } icon: { + Image(systemName: "info.circle") + } + .font(.system(size: 12, weight: .semibold)) + + Text(String( + localized: "rightSidebar.extensionDemo.promptInfoBody", + defaultValue: "This prompt is copied only when you press Copy. It asks your AI agent to generate a Swift ExtensionKit sidebar extension that binds to cmux's right sidebar extension point." + )) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + Divider() + + Text(String(localized: "rightSidebar.extensionDemo.promptAuditTitle", defaultValue: "Full prompt")) + .font(.system(size: 11, weight: .semibold)) + + ScrollView { + Text(prompt) + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + } + .frame(width: 340, height: 170) + .background(Color.primary.opacity(0.045), in: RoundedRectangle(cornerRadius: 6, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 6, style: .continuous) + .stroke(Color.primary.opacity(0.08), lineWidth: 1) + ) + + Button { + copyAction() + } label: { + Label( + String(localized: "rightSidebar.extensionDemo.copyPrompt", defaultValue: "Copy extension prompt"), + systemImage: "doc.on.clipboard" + ) + } + .controlSize(.small) + } + .padding(12) + .frame(width: 368) + } +} + +struct ExtensionKitSidebarHostView: NSViewControllerRepresentable { + let identity: AppExtensionIdentity + let registryGeneration: Int + @Binding var statusMessage: String + + func makeCoordinator() -> Coordinator { + Coordinator(statusMessage: $statusMessage) + } + + func makeNSViewController(context: Context) -> RightSidebarExtensionHostContainerController { + let controller = RightSidebarExtensionHostContainerController() + controller.configure( + delegate: context.coordinator, + placeholderView: NSHostingView(rootView: ExtensionKitHostPlaceholderView()), + configuration: configuration, + registryGeneration: registryGeneration + ) + return controller + } + + func updateNSViewController(_ controller: RightSidebarExtensionHostContainerController, context: Context) { + context.coordinator.statusMessage = $statusMessage + controller.hostViewController.delegate = context.coordinator + + if controller.hostViewController.configuration?.appExtension != identity || + controller.hostViewController.configuration?.sceneID != RightSidebarExtensionPoint.sceneID || + controller.registryGeneration != registryGeneration { + controller.apply(configuration: configuration, registryGeneration: registryGeneration) + } + } + + private var configuration: EXHostViewController.Configuration { + EXHostViewController.Configuration( + appExtension: identity, + sceneID: RightSidebarExtensionPoint.sceneID + ) + } + + final class Coordinator: NSObject, EXHostViewControllerDelegate { + var statusMessage: Binding + + init(statusMessage: Binding) { + self.statusMessage = statusMessage + } + + func hostViewControllerDidActivate(_ viewController: EXHostViewController) { + reconnectAttempts = 0 + statusMessage.wrappedValue = String( + localized: "rightSidebar.extensionDemo.xpcConnected", + defaultValue: "Extension scene activated." + ) + } + + func hostViewControllerWillDeactivate(_ viewController: EXHostViewController, error: Error?) { + if let error { + statusMessage.wrappedValue = String( + localized: "rightSidebar.extensionDemo.deactivatedWithError", + defaultValue: "Extension scene disconnected:" + ) + " \(error.localizedDescription)" + retryAfterDisconnect(viewController) + } else { + statusMessage.wrappedValue = String( + localized: "rightSidebar.extensionDemo.deactivated", + defaultValue: "Extension scene disconnected." + ) + } + } + + private var reconnectAttempts = 0 + + private func retryAfterDisconnect(_ viewController: EXHostViewController) { + guard reconnectAttempts < 2, + let configuration = viewController.configuration else { + return + } + reconnectAttempts += 1 + viewController.configuration = nil + Task { @MainActor [weak viewController] in + viewController?.configuration = configuration + } + } + } +} + +final class RightSidebarExtensionHostContainerController: NSViewController { + let hostViewController = EXHostViewController() + private(set) var registryGeneration: Int = -1 + + override func loadView() { + let containerView = RightSidebarExtensionInteractionView() + containerView.identifier = cmuxRightSidebarExtensionHostContainerIdentifier + view = containerView + } + + override func viewDidLoad() { + super.viewDidLoad() + + addChild(hostViewController) + + let hostedView = hostViewController.view + hostedView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostedView) + if let interactionView = view as? RightSidebarExtensionInteractionView { + interactionView.hostedContentView = hostedView + } + + NSLayoutConstraint.activate([ + hostedView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostedView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostedView.topAnchor.constraint(equalTo: view.topAnchor), + hostedView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + func configure( + delegate: EXHostViewControllerDelegate, + placeholderView: NSView, + configuration: EXHostViewController.Configuration, + registryGeneration: Int + ) { + hostViewController.delegate = delegate + hostViewController.placeholderView = placeholderView + apply(configuration: configuration, registryGeneration: registryGeneration) + } + + func apply( + configuration: EXHostViewController.Configuration, + registryGeneration: Int + ) { + self.registryGeneration = registryGeneration + hostViewController.configuration = configuration + } +} + +private final class RightSidebarExtensionInteractionView: NSView { + weak var hostedContentView: NSView? + private weak var lastPointerTarget: NSView? + private var keyForwardingDepth = 0 + + override var isOpaque: Bool { false } + override var acceptsFirstResponder: Bool { true } + + override func hitTest(_ point: NSPoint) -> NSView? { + let target = super.hitTest(point) + if let target, Self.shouldClaimFocus(for: NSApp.currentEvent) { + claimKeyboardFocus(startingAt: target) + } + return target + } + + override func mouseDown(with event: NSEvent) { + claimKeyboardFocus(startingAt: self) + super.mouseDown(with: event) + } + + override func rightMouseDown(with event: NSEvent) { + claimKeyboardFocus(startingAt: self) + super.rightMouseDown(with: event) + } + + override func otherMouseDown(with event: NSEvent) { + claimKeyboardFocus(startingAt: self) + super.otherMouseDown(with: event) + } + + override func keyDown(with event: NSEvent) { + guard forwardKeyDown(event) else { + super.keyDown(with: event) + return + } + } + + override func flagsChanged(with event: NSEvent) { + guard forwardFlagsChanged(event) else { + super.flagsChanged(with: event) + return + } + } + + override func performKeyEquivalent(with event: NSEvent) -> Bool { + if keyForwardingDepth == 0, + let target = preferredKeyTarget(), + target !== self { + keyForwardingDepth += 1 + defer { keyForwardingDepth = max(0, keyForwardingDepth - 1) } + if target.performKeyEquivalent(with: event) { + return true + } + } + return super.performKeyEquivalent(with: event) + } + + private func forwardKeyDown(_ event: NSEvent) -> Bool { + guard keyForwardingDepth == 0, + let target = preferredKeyTarget(), + target !== self else { + return false + } + keyForwardingDepth += 1 + defer { keyForwardingDepth = max(0, keyForwardingDepth - 1) } + target.keyDown(with: event) + return true + } + + private func forwardFlagsChanged(_ event: NSEvent) -> Bool { + guard keyForwardingDepth == 0, + let target = preferredKeyTarget(), + target !== self else { + return false + } + keyForwardingDepth += 1 + defer { keyForwardingDepth = max(0, keyForwardingDepth - 1) } + target.flagsChanged(with: event) + return true + } + + private func preferredKeyTarget() -> NSView? { + guard window?.firstResponder === self else { return nil } + if let lastPointerTarget, + lastPointerTarget !== self, + lastPointerTarget.window === window, + lastPointerTarget.isDescendant(of: self) { + return lastPointerTarget + } + return hostedContentView + } + + private static func shouldClaimFocus(for event: NSEvent?) -> Bool { + switch event?.type { + case .leftMouseDown, .rightMouseDown, .otherMouseDown: + return true + default: + return false + } + } + + private func claimKeyboardFocus(startingAt target: NSView) { + guard target === self || target.isDescendant(of: self) else { return } + lastPointerTarget = target + + guard let window else { return } + if let focusTarget = firstFocusableTarget(startingAt: target), + window.makeFirstResponder(focusTarget) { + return + } + _ = window.makeFirstResponder(self) + } + + private func firstFocusableTarget(startingAt target: NSView) -> NSView? { + var current: NSView? = target + while let candidate = current { + if candidate.acceptsFirstResponder { + return candidate + } + if candidate === self { + break + } + current = candidate.superview + } + return nil + } +} + +private struct ExtensionKitHostPlaceholderView: View { + var body: some View { + VStack(spacing: 8) { + ProgressView() + .controlSize(.small) + Text(String( + localized: "rightSidebar.extensionDemo.placeholder", + defaultValue: "Waiting for the extension scene..." + )) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(14) + } +} +#else +import AppKit +import SwiftUI + +enum RightSidebarExtensionPoint { + static let identifier = "com.cmuxterm.app.debug.extkit.right-sidebar-panel" + static let legacyIdentifier = "com.cmuxterm.right-sidebar-panel" + static let sceneID = "cmux-right-sidebar-demo" +} + +struct RightSidebarExtensionDemoPanelView: View { + @State private var statusMessage = String( + localized: "rightSidebar.extensionDemo.requiresMacOS26", + defaultValue: "ExtensionKit sidebar demos require macOS 26." + ) + + var body: some View { + VStack(spacing: 0) { + header + Divider() + VStack(spacing: 9) { + Image(systemName: "puzzlepiece.extension") + .font(.system(size: 22)) + .foregroundStyle(.secondary) + Text(statusMessage) + .font(.system(size: 12)) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + Text(String( + localized: "rightSidebar.extensionDemo.copyPromptHint", + defaultValue: "Copy a starter prompt and paste it into your AI agent to generate a sidebar extension." + )) + .font(.system(size: 11)) + .multilineTextAlignment(.center) + .foregroundStyle(.tertiary) + .fixedSize(horizontal: false, vertical: true) + Button { + copyStarterPrompt() + } label: { + Label( + String(localized: "rightSidebar.extensionDemo.copyPrompt", defaultValue: "Copy extension prompt"), + systemImage: "doc.on.clipboard" + ) + } + .controlSize(.small) + } + .padding(18) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + private var header: some View { + HStack(spacing: 8) { + Label { + Text(String(localized: "rightSidebar.extensionDemo.title", defaultValue: "ExtensionKit Demo")) + } icon: { + Image(systemName: "puzzlepiece.extension") + } + .font(.system(size: 12, weight: .semibold)) + + Spacer(minLength: 0) + } + .padding(.horizontal, 10) + .padding(.vertical, 7) + } + + private func copyStarterPrompt() { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(Self.starterPrompt, forType: .string) + statusMessage = String( + localized: "rightSidebar.extensionDemo.copyPromptCopied", + defaultValue: "Starter prompt copied." + ) + } + + private static var starterPrompt: String { + String( + localized: "rightSidebar.extensionDemo.copyPromptBody", + defaultValue: """ + Build a cmux right sidebar ExtensionKit demo extension in Swift. + + Requirements: + - Use ExtensionFoundation and ExtensionKit. + - Bind to app extension point com.cmuxterm.app.debug.extkit.right-sidebar-panel. + - Provide a PrimitiveAppExtensionScene with scene id cmux-right-sidebar-demo. + - Render a compact SwiftUI panel that fits a narrow right sidebar. + - Return AppExtensionSceneConfiguration(body) from the AppExtension configuration and do not install a custom NSXPCConnection acceptor for the scene. + - Add the Info.plist and Xcode project settings needed for an ExtensionKit .appex, including EXAppExtensionAttributes.EXExtensionPointIdentifier = com.cmuxterm.app.debug.extkit.right-sidebar-panel. + - Keep all UI strings localized. + """ + ) + } +} +#endif diff --git a/Sources/RightSidebarPanelView.swift b/Sources/RightSidebarPanelView.swift index 568f2e6840e5..829b8b8a8bc7 100644 --- a/Sources/RightSidebarPanelView.swift +++ b/Sources/RightSidebarPanelView.swift @@ -5,11 +5,13 @@ import SwiftUI enum RightSidebarMode: String, CaseIterable { case files case sessions + case extensionDemo var label: String { switch self { case .files: return String(localized: "rightSidebar.mode.files", defaultValue: "Files") case .sessions: return String(localized: "rightSidebar.mode.sessions", defaultValue: "Sessions") + case .extensionDemo: return String(localized: "rightSidebar.mode.extensionDemo", defaultValue: "ExtensionKit") } } @@ -17,6 +19,7 @@ enum RightSidebarMode: String, CaseIterable { switch self { case .files: return "folder" case .sessions: return "bubble.left.and.text.bubble.right" + case .extensionDemo: return "puzzlepiece.extension" } } } @@ -73,6 +76,8 @@ struct RightSidebarPanelView: View { .onAppear { sessionIndexStore.setCurrentDirectoryIfChanged(sessionIndexDirectory) } + case .extensionDemo: + RightSidebarExtensionDemoPanelView() } } diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index 2bdc2398fd0e..fcd9226e14ec 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -151,6 +151,15 @@ final class WindowTerminalHostView: NSView { return nil } + if let window, + cmuxRightSidebarExtensionHostContains( + windowPoint: convert(point, to: nil), + in: window + ) { + clearActiveDividerCursor(restoreArrow: true) + return nil + } + // Compute divider hit once and reuse for both cursor update and pass-through. if let kind = splitDividerCursorKind(at: point) { activeDividerCursorKind = kind diff --git a/scripts/reload.sh b/scripts/reload.sh index 961d84174445..d45d27228e30 100755 --- a/scripts/reload.sh +++ b/scripts/reload.sh @@ -313,11 +313,13 @@ XCODEBUILD_ARGS=( if [[ -n "$DERIVED_DATA" ]]; then XCODEBUILD_ARGS+=(-derivedDataPath "$DERIVED_DATA") fi +XCODEBUILD_ARGS+=( + CMUX_HOST_BUNDLE_IDENTIFIER="$BUNDLE_ID" +) if [[ -z "$TAG" ]]; then XCODEBUILD_ARGS+=( INFOPLIST_KEY_CFBundleName="$APP_NAME" INFOPLIST_KEY_CFBundleDisplayName="$APP_NAME" - PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" ) fi # Forward CMUX_SKIP_ZIG_BUILD to xcodebuild run script phases (e.g. macOS @@ -523,6 +525,48 @@ if ! /usr/bin/codesign --force --sign - --timestamp=none --generate-entitlement- exit 1 fi fi +register_extensionkit_extensions() { + if [[ ! -d "$APP_PATH/Contents/Extensions" ]]; then + return + fi + + elect_extensionkit_extension_if_needed() { + local extension_bundle="$1" + local info_plist="$extension_bundle/Contents/Info.plist" + local extension_bundle_id + local extension_point_id + + extension_bundle_id="$(plutil -extract CFBundleIdentifier raw -o - "$info_plist" 2>/dev/null || true)" + extension_point_id="$(plutil -extract EXAppExtensionAttributes.EXExtensionPointIdentifier raw -o - "$info_plist" 2>/dev/null || true)" + + if [[ -z "$extension_bundle_id" || -z "$extension_point_id" ]]; then + return + fi + + if ! pluginkit -m -A -D -v -p "$extension_point_id" 2>/dev/null | grep -q '^[[:space:]]*+'; then + pluginkit -e use -i "$extension_bundle_id" -p "$extension_point_id" >/dev/null 2>&1 || true + fi + } + + if [[ -n "${TAG:-}" ]]; then + UNTAGGED_APP_PATH="$(dirname "$APP_PATH")/${BASE_APP_NAME}.app" + if [[ -d "$UNTAGGED_APP_PATH/Contents/Extensions" && "$UNTAGGED_APP_PATH" != "$APP_PATH" ]]; then + while IFS= read -r -d '' EXTENSION_BUNDLE; do + pluginkit -r "$EXTENSION_BUNDLE" >/dev/null 2>&1 || true + done < <(find "$UNTAGGED_APP_PATH/Contents/Extensions" -maxdepth 1 -name "*.appex" -print0) + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ + -u -f "$UNTAGGED_APP_PATH" >/dev/null 2>&1 || true + fi + fi + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ + -f "$APP_PATH" >/dev/null 2>&1 || true + while IFS= read -r -d '' EXTENSION_BUNDLE; do + if ! pluginkit -a "$EXTENSION_BUNDLE" >/dev/null 2>&1; then + echo "warning: failed to register ExtensionKit extension: $EXTENSION_BUNDLE" >&2 + fi + elect_extensionkit_extension_if_needed "$EXTENSION_BUNDLE" + done < <(find "$APP_PATH/Contents/Extensions" -maxdepth 1 -name "*.appex" -print0) +} CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux" if [[ -x "$CLI_PATH" ]]; then echo "$CLI_PATH" > /tmp/cmux-last-cli-path || true @@ -539,6 +583,8 @@ if [[ -n "$TAG" ]]; then sleep 0.3 fi +register_extensionkit_extensions + if [[ "$LAUNCH" -eq 1 ]]; then if [[ -z "$TAG" ]]; then # Non-tag mode: kill any running instance (across any DerivedData path) to avoid socket conflicts.