Repository: intitni/CopilotForXcode
Branch: main
Commit: a98f1f2b0435
Files: 530
Total size: 4.3 MB
Directory structure:
gitextract_zonbntg8/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yaml
│ │ ├── feature_reqeust.yaml
│ │ ├── help_wanted.yml
│ │ └── z_bug_report_beta.yaml
│ └── workflows/
│ └── close_inactive_issues.yml
├── .gitignore
├── .gitmodules
├── .swiftformat
├── ChatPlugins/
│ ├── .gitignore
│ ├── .swiftpm/
│ │ └── xcode/
│ │ └── xcshareddata/
│ │ └── xcschemes/
│ │ └── ChatPlugins.xcscheme
│ ├── Package.swift
│ ├── Sources/
│ │ ├── ShortcutChatPlugin/
│ │ │ └── ShortcutChatPlugin.swift
│ │ └── TerminalChatPlugin/
│ │ └── TerminalChatPlugin.swift
│ └── Tests/
│ └── ChatPluginsTests/
│ └── ChatPluginsTests.swift
├── CommunicationBridge/
│ ├── ServiceDelegate.swift
│ └── main.swift
├── Config.debug.xcconfig
├── Config.xcconfig
├── Copilot for Xcode/
│ ├── App.swift
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── BackgroundColor.colorset/
│ │ │ └── Contents.json
│ │ ├── BackgroundColorTop.colorset/
│ │ │ └── Contents.json
│ │ ├── ButtonBackgroundColorDefault.colorset/
│ │ │ └── Contents.json
│ │ ├── ButtonBackgroundColorPressed.colorset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Copilot_for_Xcode.entitlements
│ └── Preview Content/
│ └── Preview Assets.xcassets/
│ └── Contents.json
├── Copilot for Xcode.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm/
│ │ └── Package.resolved
│ └── xcshareddata/
│ └── xcschemes/
│ ├── CommunicationBridge.xcscheme
│ ├── Copilot for Xcode Debug.xcscheme
│ ├── Copilot for Xcode.xcscheme
│ ├── EditorExtension.xcscheme
│ ├── ExtensionService.xcscheme
│ └── SandboxedClientTester.xcscheme
├── Copilot for Xcode.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm/
│ └── Package.resolved
├── Copilot-for-Xcode-Info.plist
├── Core/
│ ├── .gitignore
│ ├── .swiftpm/
│ │ └── xcode/
│ │ └── xcshareddata/
│ │ └── xcschemes/
│ │ ├── Client.xcscheme
│ │ ├── HostApp.xcscheme
│ │ ├── Service.xcscheme
│ │ └── SuggestionInjector.xcscheme
│ ├── Package.resolved
│ ├── Package.swift
│ ├── README.md
│ ├── Sources/
│ │ ├── ChatContextCollectors/
│ │ │ ├── SystemInfoChatContextCollector/
│ │ │ │ └── SystemInfoChatContextCollector.swift
│ │ │ └── WebChatContextCollector/
│ │ │ ├── QueryWebsiteFunction.swift
│ │ │ ├── SearchFunction.swift
│ │ │ └── WebChatContextCollector.swift
│ │ ├── ChatGPTChatTab/
│ │ │ ├── Chat.swift
│ │ │ ├── ChatContextMenu.swift
│ │ │ ├── ChatGPTChatTab.swift
│ │ │ ├── ChatPanel.swift
│ │ │ ├── CodeBlockHighlighter.swift
│ │ │ ├── Styles.swift
│ │ │ └── Views/
│ │ │ ├── BotMessage.swift
│ │ │ ├── FunctionCallMarkdownTheme.swift
│ │ │ ├── FunctionMessage.swift
│ │ │ ├── InstructionMarkdownTheme.swift
│ │ │ ├── Instructions.swift
│ │ │ ├── ThemedMarkdownText.swift
│ │ │ └── UserMessage.swift
│ │ ├── ChatService/
│ │ │ ├── AllContextCollector.swift
│ │ │ ├── AllPlugins.swift
│ │ │ ├── ChatFunctionProvider.swift
│ │ │ ├── ChatPluginController.swift
│ │ │ ├── ChatService.swift
│ │ │ ├── ContextAwareAutoManagedChatGPTMemory.swift
│ │ │ └── DynamicContextController.swift
│ │ ├── Client/
│ │ │ └── XPCService.swift
│ │ ├── FileChangeChecker/
│ │ │ └── FileChangeChecker.swift
│ │ ├── HostApp/
│ │ │ ├── AccountSettings/
│ │ │ │ ├── APIKeyManagement/
│ │ │ │ │ ├── APIKeyManagementView.swift
│ │ │ │ │ ├── APIKeyManangement.swift
│ │ │ │ │ ├── APIKeyPicker.swift
│ │ │ │ │ ├── APIKeySelection.swift
│ │ │ │ │ └── APIKeySubmission.swift
│ │ │ │ ├── ChatModelManagement/
│ │ │ │ │ ├── ChatModelEdit.swift
│ │ │ │ │ ├── ChatModelEditView.swift
│ │ │ │ │ ├── ChatModelManagement.swift
│ │ │ │ │ └── ChatModelManagementView.swift
│ │ │ │ ├── CodeiumView.swift
│ │ │ │ ├── CustomHeaderSettingsView.swift
│ │ │ │ ├── EmbeddingModel.swift
│ │ │ │ ├── EmbeddingModelManagement/
│ │ │ │ │ ├── EmbeddingModelEdit.swift
│ │ │ │ │ ├── EmbeddingModelEditView.swift
│ │ │ │ │ ├── EmbeddingModelManagement.swift
│ │ │ │ │ └── EmbeddingModelManagementView.swift
│ │ │ │ ├── GitHubCopilotModelPicker.swift
│ │ │ │ ├── GitHubCopilotView.swift
│ │ │ │ ├── OtherSuggestionServicesView.swift
│ │ │ │ ├── SharedModelManagement/
│ │ │ │ │ ├── AIModelManagementVIew.swift
│ │ │ │ │ ├── BaseURLPicker.swift
│ │ │ │ │ └── BaseURLSelection.swift
│ │ │ │ └── WebSearchView.swift
│ │ │ ├── CustomCommandSettings/
│ │ │ │ ├── CustomCommand.swift
│ │ │ │ ├── CustomCommandView.swift
│ │ │ │ ├── EditCustomCommand.swift
│ │ │ │ └── EditCustomCommandView.swift
│ │ │ ├── DebugView.swift
│ │ │ ├── FeatureSettings/
│ │ │ │ ├── Chat/
│ │ │ │ │ ├── ChatSettingsGeneralSectionView.swift
│ │ │ │ │ └── ChatSettingsView.swift
│ │ │ │ ├── PromptToCodeSettingsView.swift
│ │ │ │ ├── Suggestion/
│ │ │ │ │ ├── SuggestionFeatureDisabledLanguageListView.swift
│ │ │ │ │ ├── SuggestionFeatureEnabledProjectListView.swift
│ │ │ │ │ ├── SuggestionSettingsGeneralSectionView.swift
│ │ │ │ │ └── SuggestionSettingsView.swift
│ │ │ │ ├── TerminalSettingsView.swift
│ │ │ │ └── XcodeSettingsView.swift
│ │ │ ├── FeatureSettingsView.swift
│ │ │ ├── General.swift
│ │ │ ├── GeneralView.swift
│ │ │ ├── HandleToast.swift
│ │ │ ├── HostApp.swift
│ │ │ ├── IsPreview.swift
│ │ │ ├── LaunchAgentManager.swift
│ │ │ ├── ServiceView.swift
│ │ │ ├── SharedComponents/
│ │ │ │ ├── CodeHighlightThemePicker.swift
│ │ │ │ └── EditableText.swift
│ │ │ ├── SidebarTabView.swift
│ │ │ └── TabContainer.swift
│ │ ├── KeyBindingManager/
│ │ │ ├── KeyBindingManager.swift
│ │ │ └── TabToAcceptSuggestion.swift
│ │ ├── LaunchAgentManager/
│ │ │ └── LaunchAgentManager.swift
│ │ ├── LegacyChatPlugin/
│ │ │ ├── AskChatGPT.swift
│ │ │ ├── CallAIFunction.swift
│ │ │ ├── LegacyChatPlugin.swift
│ │ │ ├── TerminalChatPlugin.swift
│ │ │ └── Translate.swift
│ │ ├── PlusFeatureFlag/
│ │ │ └── PlusFeatureFlag.swift
│ │ ├── PromptToCodeService/
│ │ │ ├── OpenAIPromptToCodeService.swift
│ │ │ ├── PreviewPromptToCodeService.swift
│ │ │ └── PromptToCodeServiceType.swift
│ │ ├── Service/
│ │ │ ├── DependencyUpdater.swift
│ │ │ ├── GUI/
│ │ │ │ ├── ChatTabFactory.swift
│ │ │ │ ├── GraphicalUserInterfaceController.swift
│ │ │ │ └── WidgetDataSource.swift
│ │ │ ├── GlobalShortcutManager.swift
│ │ │ ├── Helpers.swift
│ │ │ ├── RealtimeSuggestionController.swift
│ │ │ ├── ScheduledCleaner.swift
│ │ │ ├── Service.swift
│ │ │ ├── SuggestionCommandHandler/
│ │ │ │ ├── PseudoCommandHandler.swift
│ │ │ │ ├── SuggestionCommandHandler.swift
│ │ │ │ └── WindowBaseCommandHandler.swift
│ │ │ ├── SuggestionPresenter/
│ │ │ │ └── PresentInWindowSuggestionPresenter.swift
│ │ │ ├── WorkspaceExtension/
│ │ │ │ └── Workspace+Cleanup.swift
│ │ │ └── XPCService.swift
│ │ ├── ServiceUpdateMigration/
│ │ │ ├── MigrateTo135.swift
│ │ │ ├── MigrateTo240.swift
│ │ │ └── ServiceUpdateMigrator.swift
│ │ ├── SuggestionService/
│ │ │ └── SuggestionService.swift
│ │ ├── SuggestionWidget/
│ │ │ ├── ChatPanelWindow.swift
│ │ │ ├── ChatWindowView.swift
│ │ │ ├── FeatureReducers/
│ │ │ │ ├── ChatPanel.swift
│ │ │ │ ├── CircularWidget.swift
│ │ │ │ ├── PromptToCodeGroup.swift
│ │ │ │ ├── PromptToCodePanel.swift
│ │ │ │ ├── SharedPanel.swift
│ │ │ │ ├── SuggestionPanel.swift
│ │ │ │ ├── ToastPanel.swift
│ │ │ │ ├── Widget.swift
│ │ │ │ └── WidgetPanel.swift
│ │ │ ├── ModuleDependency.swift
│ │ │ ├── PromptToCodePanelGroupView.swift
│ │ │ ├── SharedPanelView.swift
│ │ │ ├── Styles.swift
│ │ │ ├── SuggestionPanelContent/
│ │ │ │ ├── CodeBlockSuggestionPanelView.swift
│ │ │ │ ├── ErrorPanelView.swift
│ │ │ │ ├── PromptToCodePanelView.swift
│ │ │ │ └── ToastPanelView.swift
│ │ │ ├── SuggestionPanelView.swift
│ │ │ ├── SuggestionWidgetController.swift
│ │ │ ├── SuggestionWidgetDataSource.swift
│ │ │ ├── TextCursorTracker.swift
│ │ │ ├── WidgetPositionStrategy.swift
│ │ │ ├── WidgetView.swift
│ │ │ └── WidgetWindowsController.swift
│ │ ├── UpdateChecker/
│ │ │ └── UpdateChecker.swift
│ │ ├── UserDefaultsObserver/
│ │ │ └── UserDefaultsObserver.swift
│ │ └── XcodeThemeController/
│ │ ├── HighlightJSThemeTemplate.swift
│ │ ├── HighlightrThemeManager.swift
│ │ ├── PreferenceKey+Theme.swift
│ │ ├── XcodeThemeController.swift
│ │ └── XcodeThemeParser.swift
│ └── Tests/
│ ├── ChatServiceTests/
│ │ └── ParseScopesTests.swift
│ ├── KeyBindingManagerTests/
│ │ └── TabToAcceptSuggestionTests.swift
│ ├── PromptToCodeServiceTests/
│ │ └── ExtractCodeFromChatGPTTests.swift
│ ├── ServiceTests/
│ │ ├── Environment.swift
│ │ ├── ExtractSelectedCodeTests.swift
│ │ └── FilespaceSuggestionInvalidationTests.swift
│ ├── ServiceUpdateMigrationTests/
│ │ └── MigrateTo240Tests.swift
│ └── SuggestionWidgetTests/
│ └── File.swift
├── DEVELOPMENT.md
├── EditorExtension/
│ ├── AcceptPromptToCodeCommand.swift
│ ├── AcceptSuggestionCommand.swift
│ ├── CloseIdleTabsCommand.swift
│ ├── CustomCommand.swift
│ ├── EditorExtension.entitlements
│ ├── GetSuggestionsCommand.swift
│ ├── Helpers.swift
│ ├── Info.plist
│ ├── NextSuggestionCommand.swift
│ ├── OpenChat.swift
│ ├── PrefetchSuggestionsCommand.swift
│ ├── PreviousSuggestionCommand.swift
│ ├── PromptToCodeCommand.swift
│ ├── RealtimeSuggestionCommand.swift
│ ├── RejectSuggestionCommand.swift
│ ├── SeparatorCommand.swift
│ ├── SourceEditorExtension.swift
│ └── ToggleRealtimeSuggestionsCommand.swift
├── ExtensionPoint.appextensionpoint
├── ExtensionService/
│ ├── AppDelegate+Menu.swift
│ ├── AppDelegate.swift
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ └── MenuBarIcon.imageset/
│ │ └── Contents.json
│ ├── ExtensionService.entitlements
│ ├── Info.plist
│ ├── Main.storyboard
│ ├── ServiceDelegate.swift
│ └── XPCController.swift
├── Helper/
│ ├── ReloadLaunchAgent.swift
│ └── main.swift
├── LICENSE
├── OverlayWindow/
│ ├── .gitignore
│ ├── Package.swift
│ ├── Sources/
│ │ └── OverlayWindow/
│ │ ├── IDEWorkspaceWindowOverlayWindowController.swift
│ │ ├── OverlayPanel.swift
│ │ └── OverlayWindowController.swift
│ └── Tests/
│ └── OverlayWindowTests/
│ └── WindowTests.swift
├── Playground.playground/
│ ├── Pages/
│ │ ├── RetrievalQAChain.xcplaygroundpage/
│ │ │ ├── Contents.swift
│ │ │ └── timeline.xctimeline
│ │ └── WebScrapper.xcplaygroundpage/
│ │ ├── Contents.swift
│ │ └── timeline.xctimeline
│ └── contents.xcplayground
├── README.md
├── SandboxedClientTester/
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── Info.plist
│ ├── Preview Content/
│ │ └── Preview Assets.xcassets/
│ │ └── Contents.json
│ ├── SandboxedClientTester.entitlements
│ └── SandboxedClientTesterApp.swift
├── TestPlan.xctestplan
├── Tool/
│ ├── .gitignore
│ ├── .swiftpm/
│ │ └── xcode/
│ │ └── xcshareddata/
│ │ └── xcschemes/
│ │ └── SuggestionModel.xcscheme
│ ├── Package.resolved
│ ├── Package.swift
│ ├── README.md
│ ├── Sources/
│ │ ├── AIModel/
│ │ │ ├── ChatModel.swift
│ │ │ └── EmbeddingModel.swift
│ │ ├── ASTParser/
│ │ │ ├── ASTParser.swift
│ │ │ ├── ASTTreeVisitor.swift
│ │ │ ├── DumpSyntaxTree.swift
│ │ │ └── TreeCursor.swift
│ │ ├── AXExtension/
│ │ │ ├── AXUIElement.swift
│ │ │ └── AXUIElementPrivateAPI.swift
│ │ ├── AXNotificationStream/
│ │ │ └── AXNotificationStream.swift
│ │ ├── ActiveApplicationMonitor/
│ │ │ └── ActiveApplicationMonitor.swift
│ │ ├── AppActivator/
│ │ │ └── AppActivator.swift
│ │ ├── AsyncPassthroughSubject/
│ │ │ └── AsyncPassthroughSubject.swift
│ │ ├── BuiltinExtension/
│ │ │ ├── BuiltinExtension.swift
│ │ │ ├── BuiltinExtensionManager.swift
│ │ │ ├── BuiltinExtensionSuggestionServiceProvider.swift
│ │ │ └── BuiltinExtensionWorkspacePlugin.swift
│ │ ├── ChatBasic/
│ │ │ ├── ChatAgent.swift
│ │ │ ├── ChatGPTFunction.swift
│ │ │ ├── ChatMessage.swift
│ │ │ ├── ChatPlugin.swift
│ │ │ └── JSONSchema.swift
│ │ ├── ChatContextCollector/
│ │ │ └── ChatContextCollector.swift
│ │ ├── ChatContextCollectors/
│ │ │ └── ActiveDocumentChatContextCollector/
│ │ │ ├── ActiveDocumentChatContextCollector.swift
│ │ │ ├── Functions/
│ │ │ │ └── GetCodeCodeAroundLineFunction.swift
│ │ │ ├── LegacyActiveDocumentChatContextCollector.swift
│ │ │ └── ReadableCursorRange.swift
│ │ ├── ChatTab/
│ │ │ ├── ChatTab.swift
│ │ │ ├── ChatTabItem.swift
│ │ │ └── ChatTabPool.swift
│ │ ├── CodeDiff/
│ │ │ └── CodeDiff.swift
│ │ ├── CodeiumService/
│ │ │ ├── ChatTab/
│ │ │ │ ├── CodeiumChatBrowser.swift
│ │ │ │ ├── CodeiumChatTab.swift
│ │ │ │ ├── CodeiumChatTabItem.swift
│ │ │ │ ├── CodeiumChatView.swift
│ │ │ │ └── CodeiumWebView.swift
│ │ │ ├── CodeiumExtension.swift
│ │ │ ├── CodeiumWorkspacePlugin.swift
│ │ │ ├── LanguageServer/
│ │ │ │ ├── CodeiumInstallationManager.swift
│ │ │ │ ├── CodeiumLanguageServer.swift
│ │ │ │ ├── CodeiumModels.swift
│ │ │ │ ├── CodeiumRequest.swift
│ │ │ │ ├── CodeiumSupportedLanguage.swift
│ │ │ │ └── OpendDocumentPool.swift
│ │ │ └── Services/
│ │ │ ├── CodeiumAuthService.swift
│ │ │ ├── CodeiumService.swift
│ │ │ └── CodeiumSuggestionService.swift
│ │ ├── CommandHandler/
│ │ │ └── CommandHandler.swift
│ │ ├── Configs/
│ │ │ └── Configurations.swift
│ │ ├── CustomAsyncAlgorithms/
│ │ │ └── TimedDebounce.swift
│ │ ├── CustomCommandTemplateProcessor/
│ │ │ └── CustomCommandTemplateProcessor.swift
│ │ ├── DebounceFunction/
│ │ │ ├── DebounceFunction.swift
│ │ │ └── ThrottleFunction.swift
│ │ ├── FileSystem/
│ │ │ ├── ByteString.swift
│ │ │ ├── FileInfo.swift
│ │ │ ├── FileSystem.swift
│ │ │ ├── Lock.swift
│ │ │ ├── Misc.swift
│ │ │ ├── Path.swift
│ │ │ ├── PathShim.swift
│ │ │ └── WritableByteStream.swift
│ │ ├── FocusedCodeFinder/
│ │ │ ├── ActiveDocumentContext.swift
│ │ │ ├── FocusedCodeFinder.swift
│ │ │ ├── KnownLanguageFocusedCodeFinder.swift
│ │ │ ├── ObjectiveC/
│ │ │ │ ├── ObjectiveCCodeFinder.swift
│ │ │ │ ├── ObjectiveCScopeHierarchySyntaxVisitor.swift
│ │ │ │ └── ObjectiveCSyntax.swift
│ │ │ ├── Swift/
│ │ │ │ ├── SwiftFocusedCodeFinder.swift
│ │ │ │ └── SwiftScopeHierarchySyntaxVisitor.swift
│ │ │ └── UnknownLanguageFocusCodeFinder.swift
│ │ ├── GitHubCopilotService/
│ │ │ ├── GitHubCopilotExtension.swift
│ │ │ ├── GitHubCopilotWorkspacePlugin.swift
│ │ │ ├── LanguageServer/
│ │ │ │ ├── CopilotLocalProcessServer.swift
│ │ │ │ ├── CustomStdioTransport.swift
│ │ │ │ ├── GitHubCopilotAccountStatus.swift
│ │ │ │ ├── GitHubCopilotInstallationManager.swift
│ │ │ │ ├── GitHubCopilotRequest.swift
│ │ │ │ └── GitHubCopilotService.swift
│ │ │ ├── Resources/
│ │ │ │ └── load-self-signed-cert-1.34.0.js
│ │ │ └── Services/
│ │ │ ├── GitHubCopilotChatService.swift
│ │ │ └── GitHubCopilotSuggestionService.swift
│ │ ├── GitIgnoreCheck/
│ │ │ └── GitIgnoreCheck.swift
│ │ ├── JoinJSON/
│ │ │ └── JoinJSON.swift
│ │ ├── Keychain/
│ │ │ └── Keychain.swift
│ │ ├── LangChain/
│ │ │ ├── Agent.swift
│ │ │ ├── AgentExecutor.swift
│ │ │ ├── AgentTool.swift
│ │ │ ├── Agents/
│ │ │ │ └── ChatAgent.swift
│ │ │ ├── Callback.swift
│ │ │ ├── Chain.swift
│ │ │ ├── Chains/
│ │ │ │ ├── CombineAnswersChain.swift
│ │ │ │ ├── LLMChain.swift
│ │ │ │ ├── QAInformationRetrievalChain.swift
│ │ │ │ ├── RefineDocumentChain.swift
│ │ │ │ ├── RelevantInformationExtractionChain.swift
│ │ │ │ └── StructuredOutputChatModelChain.swift
│ │ │ ├── ChatModel/
│ │ │ │ ├── ChatModel.swift
│ │ │ │ └── OpenAIChat.swift
│ │ │ ├── DocumentLoader/
│ │ │ │ ├── DocumentLoader.swift
│ │ │ │ ├── TextLoader.swift
│ │ │ │ └── WebLoader.swift
│ │ │ ├── DocumentTransformer/
│ │ │ │ ├── DocumentTransformer.swift
│ │ │ │ ├── RecursiveCharacterTextSplitter.swift
│ │ │ │ ├── TextSplitter.swift
│ │ │ │ └── TextSplitterSeparatorSet.swift
│ │ │ ├── Embedding/
│ │ │ │ ├── Embedding.swift
│ │ │ │ └── OpenAIEmbedding.swift
│ │ │ └── VectorStore/
│ │ │ ├── TemporaryUSearch.swift
│ │ │ └── VectorStore.swift
│ │ ├── Logger/
│ │ │ └── Logger.swift
│ │ ├── ModificationBasic/
│ │ │ ├── ExplanationThenCodeStreamParser.swift
│ │ │ ├── ModificationAgent.swift
│ │ │ └── ModificationState.swift
│ │ ├── ObjectiveCExceptionHandling/
│ │ │ ├── ObjectiveCExceptionHandling.m
│ │ │ └── include/
│ │ │ └── ObjectiveCExceptionHandling.h
│ │ ├── OpenAIService/
│ │ │ ├── APIs/
│ │ │ │ ├── BuiltinExtensionChatCompletionsService.swift
│ │ │ │ ├── ChatCompletionsAPIBuilder.swift
│ │ │ │ ├── ChatCompletionsAPIDefinition.swift
│ │ │ │ ├── ClaudeChatCompletionsService.swift
│ │ │ │ ├── EmbeddingAPIDefinitions.swift
│ │ │ │ ├── GitHubCopilotChatCompletionsService.swift
│ │ │ │ ├── GitHubCopilotEmbeddingService.swift
│ │ │ │ ├── GoogleAIChatCompletionsService.swift
│ │ │ │ ├── OlamaChatCompletionsService.swift
│ │ │ │ ├── OllamaEmbeddingService.swift
│ │ │ │ ├── OpenAIChatCompletionsService.swift
│ │ │ │ ├── OpenAIEmbeddingService.swift
│ │ │ │ ├── OpenAIResponsesRawService.swift
│ │ │ │ └── ResponseStream.swift
│ │ │ ├── ChatGPTService.swift
│ │ │ ├── Configuration/
│ │ │ │ ├── ChatGPTConfiguration.swift
│ │ │ │ ├── EmbeddingConfiguration.swift
│ │ │ │ ├── UserPreferenceChatGPTConfiguration.swift
│ │ │ │ └── UserPreferenceEmbeddingConfiguration.swift
│ │ │ ├── Debug/
│ │ │ │ └── Debug.swift
│ │ │ ├── EmbeddingService.swift
│ │ │ ├── FucntionCall/
│ │ │ │ └── ChatGPTFuntionProvider.swift
│ │ │ ├── HeaderValueParser.swift
│ │ │ ├── LegacyChatGPTService.swift
│ │ │ ├── Memory/
│ │ │ │ ├── AutoManagedChatGPTMemory.swift
│ │ │ │ ├── AutoManagedChatGPTMemoryStrategy/
│ │ │ │ │ ├── AutoManagedChatGPTMemoryGoogleAIStrategy.swift
│ │ │ │ │ └── AutoManagedChatGPTMemoryOpenAIStrategy.swift
│ │ │ │ ├── ChatGPTMemory.swift
│ │ │ │ ├── ConversationChatGPTMemory.swift
│ │ │ │ ├── EmptyChatGPTMemory.swift
│ │ │ │ └── TemplateChatGPTMemory.swift
│ │ │ └── Models.swift
│ │ ├── Preferences/
│ │ │ ├── AppStorage.swift
│ │ │ ├── Keys.swift
│ │ │ ├── Types/
│ │ │ │ ├── ChatFeatureProvider.swift
│ │ │ │ ├── ChatGPTModel.swift
│ │ │ │ ├── CustomCommand.swift
│ │ │ │ ├── EmbeddingFeatureProvider.swift
│ │ │ │ ├── GoogleGenerativeChatModel.swift
│ │ │ │ ├── Locale.swift
│ │ │ │ ├── NodeRunner.swift
│ │ │ │ ├── OpenAIEmbeddingModel.swift
│ │ │ │ ├── OpenChatMode.swift
│ │ │ │ ├── PresentationMode.swift
│ │ │ │ ├── PromptToCodeFeatureProvider.swift
│ │ │ │ ├── StorableColors.swift
│ │ │ │ ├── StorableFont.swift
│ │ │ │ ├── SuggestionFeatureProvider.swift
│ │ │ │ ├── SuggestionWidgetPositionMode.swift
│ │ │ │ └── WidgetColorScheme.swift
│ │ │ └── UserDefaults.swift
│ │ ├── PromptToCodeCustomization/
│ │ │ └── PromptToCodeCustomization.swift
│ │ ├── RAGChatAgent/
│ │ │ ├── RAGChatAgent.swift
│ │ │ ├── RAGChatAgentCapability.swift
│ │ │ └── RAGChatAgentConfiguration.swift
│ │ ├── SharedUIComponents/
│ │ │ ├── AsyncCodeBlock.swift
│ │ │ ├── AsyncDiffCodeBlock.swift
│ │ │ ├── CodeBlock.swift
│ │ │ ├── CopyButton.swift
│ │ │ ├── CustomScrollView.swift
│ │ │ ├── CustomTextEditor.swift
│ │ │ ├── DynamicHeightTextInFormWorkaround.swift
│ │ │ ├── FontPicker.swift
│ │ │ ├── ModifierFlagsMonitor.swift
│ │ │ ├── SettingsDivider.swift
│ │ │ ├── SubSection.swift
│ │ │ ├── SyntaxHighlighting.swift
│ │ │ ├── TabContainer.swift
│ │ │ ├── View+Modify.swift
│ │ │ └── XcodeStyleFrame.swift
│ │ ├── SuggestionBasic/
│ │ │ ├── CodeSuggestion.swift
│ │ │ ├── EditorInformation.swift
│ │ │ ├── ExportedFromLSP.swift
│ │ │ ├── LanguageIdentifierFromFilePath.swift
│ │ │ ├── Modification.swift
│ │ │ └── String+LineEnding.swift
│ │ ├── SuggestionInjector/
│ │ │ └── SuggestionInjector.swift
│ │ ├── SuggestionProvider/
│ │ │ ├── PostProcessingSuggestionServiceMiddleware.swift
│ │ │ ├── String+Extension.swift
│ │ │ ├── SuggestionProvider.swift
│ │ │ ├── SuggestionServiceEventHandler.swift
│ │ │ └── SuggestionServiceMiddleware.swift
│ │ ├── Terminal/
│ │ │ └── Terminal.swift
│ │ ├── Toast/
│ │ │ └── Toast.swift
│ │ ├── TokenEncoder/
│ │ │ ├── CharacterTokenCounter.swift
│ │ │ ├── GoogleAITokenCounter.swift
│ │ │ ├── Resources/
│ │ │ │ └── cl100k_base.tiktoken
│ │ │ ├── TiktokenCl100kBaseTokenEncoder.swift
│ │ │ └── Tokenizer.swift
│ │ ├── USearchIndex/
│ │ │ └── UsearchIndex.swift
│ │ ├── UserDefaultsObserver/
│ │ │ └── UserDefaultsObserver.swift
│ │ ├── WebScrapper/
│ │ │ └── WebScrapper.swift
│ │ ├── WebSearchService/
│ │ │ ├── SearchServices/
│ │ │ │ ├── AppleDocumentationSearchService.swift
│ │ │ │ ├── BingSearchService.swift
│ │ │ │ ├── HeadlessBrowserSearchService.swift
│ │ │ │ └── SerpAPISearchService.swift
│ │ │ └── WebSearchService.swift
│ │ ├── Workspace/
│ │ │ ├── FileSaveWatcher.swift
│ │ │ ├── Filespace.swift
│ │ │ ├── OpenedFileRocoverableStorage.swift
│ │ │ ├── Workspace.swift
│ │ │ └── WorkspacePool.swift
│ │ ├── WorkspaceSuggestionService/
│ │ │ ├── Filespace+SuggestionService.swift
│ │ │ ├── SuggestionWorkspacePlugin.swift
│ │ │ └── Workspace+SuggestionService.swift
│ │ ├── XPCShared/
│ │ │ ├── CommunicationBridgeXPCServiceProtocol.swift
│ │ │ ├── Models.swift
│ │ │ ├── XPCCommunicationBridge.swift
│ │ │ ├── XPCExtensionService.swift
│ │ │ ├── XPCService.swift
│ │ │ └── XPCServiceProtocol.swift
│ │ └── XcodeInspector/
│ │ ├── AppInstanceInspector.swift
│ │ ├── Apps/
│ │ │ └── XcodeAppInstanceInspector.swift
│ │ ├── Helpers.swift
│ │ ├── SourceEditor.swift
│ │ ├── XcodeInspector+TriggerCommand.swift
│ │ ├── XcodeInspector.swift
│ │ └── XcodeWindowInspector.swift
│ └── Tests/
│ ├── ASTParserTests/
│ │ └── CursorDeepFirstSearchTests.swift
│ ├── ActiveDocumentChatContextCollectorTests/
│ │ └── File.swift
│ ├── CodeDiffTests/
│ │ └── CodeDiffTests.swift
│ ├── FocusedCodeFinderTests/
│ │ ├── ObjectiveCFocusedCodeFinderTests.swift
│ │ ├── SwiftFocusedCodeFinderTests.swift
│ │ └── UnknownLanguageFocusedCodeFinderTests.swift
│ ├── GitHubCopilotServiceTests/
│ │ ├── FetchSuggestionsTests.swift
│ │ └── FileExtensionToLanguageIdentifierTests.swift
│ ├── JoinJSONTests/
│ │ └── JoinJSONTests.swift
│ ├── KeychainTests/
│ │ └── KeychainTests.swift
│ ├── LangChainTests/
│ │ ├── ChatAgentTests.swift
│ │ ├── TextSplitterTests/
│ │ │ ├── RecursiveCharacterTextSplitterTests.swift
│ │ │ ├── TextChunkTests.swift
│ │ │ └── TextSplitterTests.swift
│ │ └── VectorStoreTests/
│ │ ├── EmbeddingDataForTests.swift
│ │ └── TemporaryUSearchTests.swift
│ ├── ModificationBasicTests/
│ │ └── ExplanationThenCodeStreamParserTests.swift
│ ├── OpenAIServiceTests/
│ │ ├── AutoManagedChatGPTMemoryRetrievedContentTests.swift
│ │ ├── ChatGPTServiceTests.swift
│ │ ├── GoogleAIChatCompletionsAPITests.swift
│ │ └── LimitMessagesTests.swift
│ ├── SharedUIComponentsTests/
│ │ └── ConvertToCodeLinesTests.swift
│ ├── SuggestionBasicTests/
│ │ ├── BreakLinePerformanceTests.swift
│ │ ├── LineAnnotationParsingTests.swift
│ │ ├── ModificationTests.swift
│ │ └── TextExtrationFromCodeTests.swift
│ ├── SuggestionInjectorTests/
│ │ ├── AcceptSuggestionTests.swift
│ │ ├── ProposeSuggestionTests.swift
│ │ └── RejectSuggestionTests.swift
│ ├── SuggestionProviderTests/
│ │ └── PostProcessingSuggestionServiceMiddlewareTests.swift
│ ├── TokenEncoderTests/
│ │ └── TiktokenCl100kBaseTokenEncoderTests.swift
│ ├── WebSearchServiceTests/
│ │ └── HeadlessBrowserSearchServiceTests.swift
│ └── XcodeInspectorTests/
│ ├── EditorRangeConversionTests.swift
│ ├── SourceEditorCachePerformanceTests.swift
│ └── SourceEditorCacheTests.swift
├── VERSIONS
├── Version.xcconfig
├── appcast.xml
├── bridgeLaunchAgent.plist
└── launchAgent.plist
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
custom: ["https://intii.lemonsqueezy.com", "https://www.buymeacoffee.com/intitni"]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
assignees:
- intitni
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: checkboxes
id: before-reporting
attributes:
label: Before Reporting
description: Before reporting the bug, we suggestion that you first refer to the [FAQ](https://github.com/intitni/CopilotForXcode/wiki/Frequently-Asked-Questions) to check if it may address your issue. And search for existing issues to avoid duplication. If you are reporting a bug from a beta build, please use the dedicated template for beta build.
options:
- label: I have checked FAQ, and there is no solution to my issue
required: true
- label: I have searched the existing issues, and there is no existing issue for my issue
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: How to reproduce the bug.
description: If possible, please provide the steps to reproduce the bug and relevant settings in screenshots.
placeholder: "1. *****\n2.*****"
value: "It just happened!"
- type: textarea
id: logs
attributes:
label: Relevant log output
description: If it's a crash, please provide the crash report. You can find it in the Console.app.
render: shell
- type: input
id: mac-version
attributes:
label: macOS version
- type: input
id: xcode-version
attributes:
label: Xcode version
- type: input
id: copilot-for-xcode-version
attributes:
label: Copilot for Xcode version
================================================
FILE: .github/ISSUE_TEMPLATE/feature_reqeust.yaml
================================================
name: Feature Request
description: Request a feature
title: "[Enhancement]: "
labels: ["enhancement"]
assignees:
- intitni
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! But please firstly [post your idea in discussion](https://github.com/intitni/CopilotForXcode/discussions/new?category=ideas) so that we can discuss about it in advance.
- type: checkboxes
id: before-reporting
attributes:
label: Before Requesting
description: Before requesting the feature, we suggestion that you first search for existing issues to avoid duplication.
options:
- label: I have searched the existing issues, and there is no existing issue for my feature request
required: true
- type: textarea
id: what-feature
attributes:
label: What feature do you want?
description: Please describe the feature you want.
placeholder: Tell us what you want!
value: "I want a feature!"
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/help_wanted.yml
================================================
name: Help Wanted
description: Ask for help from the developer and the community
title: "[Help Wanted]: "
labels: ["help wanted"]
body:
- type: checkboxes
id: before-reporting
attributes:
label: Before Reporting
description: Before asking for help, we suggestion that you first refer to the [FAQ](https://github.com/intitni/CopilotForXcode/wiki/Frequently-Asked-Questions) to check if it may address your issue. And search for existing issues to avoid duplication.
options:
- label: I have checked FAQ, and there is no solution to my issue
required: true
- label: I have searched the existing issues, and there is no existing issue for my issue
required: true
- type: textarea
id: what-help
attributes:
label: Describe your issue
description: Please describe the help you want.
placeholder: My issue is...
value: "I want help!"
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/z_bug_report_beta.yaml
================================================
name: Bug Report (Beta)
description: File a bug report
title: "[Bug (Beta)]: "
labels: ["bug", "beta"]
assignees:
- intitni
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: checkboxes
id: before-reporting
attributes:
label: Before Reporting
description: Before reporting the bug, we suggestion that you first refer to the [FAQ](https://github.com/intitni/CopilotForXcode/wiki/Frequently-Asked-Questions) to check if it may address your issue. And search for existing issues to avoid duplication.
options:
- label: I have checked FAQ, and there is no solution to my issue
required: true
- label: I have searched the existing issues, and there is no existing issue for my issue
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: How to reproduce the bug.
description: If possible, please provide the steps to reproduce the bug and relevant settings in screenshots.
placeholder: "1. *****\n2.*****"
value: "It just happened!"
- type: textarea
id: logs
attributes:
label: Relevant log output
description: If it's a crash, please provide the crash report. You can find it in the Console.app.
render: shell
- type: input
id: mac-version
attributes:
label: macOS version
- type: input
id: xcode-version
attributes:
label: Xcode version
- type: input
id: copilot-for-xcode-version
attributes:
label: Copilot for Xcode version
================================================
FILE: .github/workflows/close_inactive_issues.yml
================================================
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
exempt-issue-labels: "low priority, help wanted, planned, investigating, blocked"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# IDE
.idea
# Created by
https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager
# Edit at
https://www.toptal.com/developers/gitignore?templates=xcode,macos,swift,swiftpackagemanager
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Swift ###
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore,
Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting
Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting
Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
# Swift Package Manager
# Add this line if you want to avoid checking in source code from Swift
Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
# Xcode automatically generates this directory with a .xcworkspacedata
file and xcuserdata
# hence it is not needed unless you have added a package configuration
file to your project
# .swiftpm
.build/
# CocoaPods
# We recommend against adding the Pods directory to your .gitignore.
However
# you should judge for yourself, the pros and cons are mentioned at:
#
https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
# Pods/
# Add this line if you want to avoid checking in source code from the
# Carthage
# Add this line if you want to avoid checking in source code from Carthage
dependencies.
# Carthage/Checkouts
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
# fastlane
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are
needed.
# For more information about the recommended setup visit:
#
https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
# After new code Injection tools there's a generated folder
/iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# End of
https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager
Secrets.xcconfig
Python/Python.xcframework
Python/python-stdlib
Python/site-packages/*
!Python/site-packages/requirements.txt
!Python/site-packages/install.sh
Python/VERSIONS
Copilot for Xcode Plus.xcworkspace
PLUS
================================================
FILE: .gitmodules
================================================
================================================
FILE: .swiftformat
================================================
--allman false
--beforemarks
--binarygrouping 4,8
--categorymark "MARK: %c"
--classthreshold 0
--closingparen balanced
--commas always
--conflictmarkers reject
--decimalgrouping 3,6
--elseposition same-line
--enumthreshold 0
--exponentcase lowercase
--exponentgrouping disabled
--fractiongrouping disabled
--fragment false
--funcattributes preserve
--guardelse auto
--header ignore
--hexgrouping 4,8
--hexliteralcase uppercase
--ifdef no-indent
--importgrouping testable-bottom
--indent 4
--indentcase false
--lifecycle
--linebreaks lf
--maxwidth 100
--modifierorder
--nospaceoperators ...,..<
--nowrapoperators
--octalgrouping 4,8
--operatorfunc spaced
--patternlet hoist
--ranges spaced
--self remove
--selfrequired
--semicolons inline
--shortoptionals always
--smarttabs enabled
--stripunusedargs unnamed-only
--structthreshold 0
--tabwidth unspecified
--trailingclosures
--trimwhitespace always
--typeattributes preserve
--varattributes preserve
--voidtype void
--wraparguments before-first
--wrapcollections disabled
--wrapparameters before-first
--xcodeindentation disabled
--yodaswap always
--enable isEmpty
--exclude Pods,**/Generated
================================================
FILE: ChatPlugins/.gitignore
================================================
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
================================================
FILE: ChatPlugins/.swiftpm/xcode/xcshareddata/xcschemes/ChatPlugins.xcscheme
================================================
================================================
FILE: ChatPlugins/Package.swift
================================================
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ChatPlugins",
platforms: [.macOS(.v12)],
products: [
.library(
name: "ChatPlugins",
targets: ["TerminalChatPlugin", "ShortcutChatPlugin"]
),
],
dependencies: [
.package(path: "../Tool"),
],
targets: [
.target(
name: "TerminalChatPlugin",
dependencies: [
.product(name: "Chat", package: "Tool"),
.product(name: "Terminal", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
]
),
.target(
name: "ShortcutChatPlugin",
dependencies: [
.product(name: "Chat", package: "Tool"),
.product(name: "Terminal", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
]
),
]
)
================================================
FILE: ChatPlugins/Sources/ShortcutChatPlugin/ShortcutChatPlugin.swift
================================================
import ChatBasic
import Foundation
import Terminal
public final class ShortcutChatPlugin: ChatPlugin {
public static var id: String { "com.intii.shortcut" }
public static var command: String { "shortcut" }
public static var name: String { "Shortcut" }
public static var description: String { """
Run a shortcut and use message content as input. You need to provide the shortcut name as an argument, for example, `/shortcut(Shortcut Name)`.
""" }
let terminal: TerminalType
init(terminal: TerminalType) {
self.terminal = terminal
}
public init() {
terminal = Terminal()
}
public func sendForTextResponse(_ request: Request) async
-> AsyncThrowingStream
{
let stream = await sendForComplicatedResponse(request)
return .init { continuation in
let task = Task {
do {
for try await response in stream {
switch response {
case let .content(.text(content)):
continuation.yield(content)
default:
break
}
}
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
public func sendForComplicatedResponse(_ request: Request) async
-> AsyncThrowingStream
{
return .init { continuation in
let task = Task {
let id = "\(Self.command)-\(UUID().uuidString)"
guard let shortcutName = request.arguments.first, !shortcutName.isEmpty else {
continuation.yield(.content(.text(
"Please provide the shortcut name in format: `/\(Self.command)(shortcut name)`"
)))
return
}
var input = String(request.text).trimmingCharacters(in: .whitespacesAndNewlines)
if input.isEmpty {
// if no input detected, use the previous message as input
input = request.history.last?.content ?? ""
}
do {
continuation.yield(.startAction(
id: "run",
task: "Run shortcut `\(shortcutName)`"
))
let env = ProcessInfo.processInfo.environment
let shell = env["SHELL"] ?? "/bin/bash"
let temporaryURL = FileManager.default.temporaryDirectory
let temporaryInputFileURL = temporaryURL
.appendingPathComponent("\(id)-input.txt")
let temporaryOutputFileURL = temporaryURL
.appendingPathComponent("\(id)-output")
try input.write(to: temporaryInputFileURL, atomically: true, encoding: .utf8)
let command = """
shortcuts run "\(shortcutName)" \
-i "\(temporaryInputFileURL.path)" \
-o "\(temporaryOutputFileURL.path)"
"""
continuation.yield(.startAction(
id: "run",
task: "Run shortcut \(shortcutName)"
))
do {
let result = try await terminal.runCommand(
shell,
arguments: ["-i", "-l", "-c", command],
currentDirectoryURL: nil,
environment: [:]
)
continuation.yield(.finishAction(id: "run", result: .success(result)))
} catch {
continuation.yield(.finishAction(
id: "run",
result: .failure(error.localizedDescription)
))
throw error
}
await Task.yield()
try Task.checkCancellation()
if FileManager.default.fileExists(atPath: temporaryOutputFileURL.path) {
let data = try Data(contentsOf: temporaryOutputFileURL)
if let text = String(data: data, encoding: .utf8) {
var response = text
if response.isEmpty {
response = "Finished"
}
continuation.yield(.content(.text(response)))
} else {
let content = """
[View File](\(temporaryOutputFileURL))
"""
continuation.yield(.content(.text(content)))
}
} else {
continuation.yield(.content(.text("Finished")))
}
} catch {
continuation.yield(.content(.text(error.localizedDescription)))
}
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
}
================================================
FILE: ChatPlugins/Sources/TerminalChatPlugin/TerminalChatPlugin.swift
================================================
import ChatBasic
import Foundation
import Terminal
import XcodeInspector
public final class TerminalChatPlugin: ChatPlugin {
public static var id: String { "com.intii.terminal" }
public static var command: String { "shell" }
public static var name: String { "Shell" }
public static var description: String { """
Run the command in the message from shell.
You can use environment variable `$FILE_PATH` and `$PROJECT_ROOT` to access the current file path and project root.
""" }
let terminal: TerminalType
init(terminal: TerminalType) {
self.terminal = terminal
}
public init() {
terminal = Terminal()
}
public func getTextContent(from request: Request) async
-> AsyncStream
{
return .init { continuation in
let task = Task {
do {
let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL
let projectURL = XcodeInspector.shared.realtimeActiveProjectURL
var environment = [String: String]()
if let fileURL {
environment["FILE_PATH"] = fileURL.path
}
if let projectURL {
environment["PROJECT_ROOT"] = projectURL.path
}
try Task.checkCancellation()
let env = ProcessInfo.processInfo.environment
let shell = env["SHELL"] ?? "/bin/bash"
let output = terminal.streamCommand(
shell,
arguments: ["-i", "-l", "-c", request.text],
currentDirectoryURL: projectURL,
environment: environment
)
var accumulatedOutput = ""
for try await content in output {
try Task.checkCancellation()
accumulatedOutput += content
continuation.yield(accumulatedOutput)
}
} catch let error as Terminal.TerminationError {
let errorMessage = "\n\n[error: \(error.reason)]"
continuation.yield(errorMessage)
} catch {
let errorMessage = "\n\n[error: \(error.localizedDescription)]"
continuation.yield(errorMessage)
}
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
Task {
await self.terminal.terminate()
}
}
}
}
public func sendForTextResponse(_ request: Request) async
-> AsyncThrowingStream
{
let stream = await getTextContent(from: request)
return .init { continuation in
let task = Task {
continuation.yield("Executing command: `\(request.text)`\n\n")
continuation.yield("```console\n")
for await text in stream {
try Task.checkCancellation()
continuation.yield(text)
}
continuation.yield("\n```\n")
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
public func formatContent(_ content: Response.Content) -> Response.Content {
switch content {
case let .text(content):
return .text("""
```console
\(content)
```
""")
}
}
public func sendForComplicatedResponse(_ request: Request) async
-> AsyncThrowingStream
{
return .init { continuation in
let task = Task {
var updateTime = Date()
continuation.yield(.startAction(id: "run", task: "Run `\(request.text)`"))
let textStream = await getTextContent(from: request)
var previousOutput = ""
continuation.yield(.finishAction(
id: "run",
result: .success("Executed.")
))
for await accumulatedOutput in textStream {
try Task.checkCancellation()
let newContent = accumulatedOutput.dropFirst(previousOutput.count)
previousOutput = accumulatedOutput
if !newContent.isEmpty {
if Date().timeIntervalSince(updateTime) > 60 * 2 {
continuation.yield(.startNewMessage)
continuation.yield(.startAction(
id: "run",
task: "Continue `\(request.text)`"
))
continuation.yield(.finishAction(
id: "run",
result: .success("Executed.")
))
continuation.yield(.content(.text("[continue]\n")))
updateTime = Date()
}
continuation.yield(.content(.text(String(newContent))))
}
}
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
}
================================================
FILE: ChatPlugins/Tests/ChatPluginsTests/ChatPluginsTests.swift
================================================
import Testing
@testable import ChatPlugins
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}
================================================
FILE: CommunicationBridge/ServiceDelegate.swift
================================================
import AppKit
import Foundation
import Logger
import XPCShared
class ServiceDelegate: NSObject, NSXPCListenerDelegate {
func listener(
_: NSXPCListener,
shouldAcceptNewConnection newConnection: NSXPCConnection
) -> Bool {
newConnection.exportedInterface = NSXPCInterface(
with: CommunicationBridgeXPCServiceProtocol.self
)
let exportedObject = XPCService()
newConnection.exportedObject = exportedObject
newConnection.resume()
Logger.communicationBridge.info("Accepted new connection.")
return true
}
}
class XPCService: CommunicationBridgeXPCServiceProtocol {
static let eventHandler = EventHandler()
func launchExtensionServiceIfNeeded(
withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void
) {
Task {
await Self.eventHandler.launchExtensionServiceIfNeeded(withReply: reply)
}
}
func quit(withReply reply: @escaping () -> Void) {
Task {
await Self.eventHandler.quit(withReply: reply)
}
}
func updateServiceEndpoint(
endpoint: NSXPCListenerEndpoint,
withReply reply: @escaping () -> Void
) {
Task {
await Self.eventHandler.updateServiceEndpoint(endpoint: endpoint, withReply: reply)
}
}
}
actor EventHandler {
var endpoint: NSXPCListenerEndpoint?
let launcher = ExtensionServiceLauncher()
var exitTask: Task?
init() {
Task { await rescheduleExitTask() }
}
func launchExtensionServiceIfNeeded(
withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void
) async {
rescheduleExitTask()
#if DEBUG
if let endpoint, !(await testXPCListenerEndpoint(endpoint)) {
self.endpoint = nil
}
reply(endpoint)
#else
if await launcher.isApplicationValid {
Logger.communicationBridge.info("Service app is still valid")
reply(endpoint)
} else {
endpoint = nil
await launcher.launch()
reply(nil)
}
#endif
}
func quit(withReply reply: () -> Void) {
Logger.communicationBridge.info("Exiting service.")
listener.invalidate()
exit(0)
}
func updateServiceEndpoint(endpoint: NSXPCListenerEndpoint, withReply reply: () -> Void) {
rescheduleExitTask()
self.endpoint = endpoint
reply()
}
/// The bridge will kill itself when it's not used for a period.
/// It's fine that the bridge is killed because it will be launched again when needed.
private func rescheduleExitTask() {
exitTask?.cancel()
exitTask = Task {
#if DEBUG
try await Task.sleep(nanoseconds: 60_000_000_000)
Logger.communicationBridge.info("Exit will be called in release build.")
#else
try await Task.sleep(nanoseconds: 1_800_000_000_000)
Logger.communicationBridge.info("Exiting service.")
listener.invalidate()
exit(0)
#endif
}
}
}
actor ExtensionServiceLauncher {
let appIdentifier = bundleIdentifierBase.appending(".ExtensionService")
let appURL = Bundle.main.bundleURL.appendingPathComponent(
"CopilotForXcodeExtensionService.app"
)
var isLaunching: Bool = false
var application: NSRunningApplication?
var isApplicationValid: Bool {
guard let application else { return false }
if application.isTerminated { return false }
let identifier = application.processIdentifier
if let application = NSWorkspace.shared.runningApplications.first(where: {
$0.processIdentifier == identifier
}) {
Logger.communicationBridge.info(
"Service app found: \(application.processIdentifier) \(String(describing: application.bundleIdentifier))"
)
return true
}
return false
}
func launch() {
guard !isLaunching else { return }
isLaunching = true
Logger.communicationBridge.info("Launching extension service app.")
NSWorkspace.shared.openApplication(
at: appURL,
configuration: {
let configuration = NSWorkspace.OpenConfiguration()
configuration.createsNewApplicationInstance = false
configuration.addsToRecentItems = false
configuration.activates = false
return configuration
}()
) { app, error in
if let error = error {
Logger.communicationBridge.error(
"Failed to launch extension service app: \(error)"
)
} else {
Logger.communicationBridge.info(
"Finished launching extension service app."
)
}
self.application = app
self.isLaunching = false
}
}
}
================================================
FILE: CommunicationBridge/main.swift
================================================
import AppKit
import Foundation
class AppDelegate: NSObject, NSApplicationDelegate {}
let bundleIdentifierBase = Bundle(url: Bundle.main.bundleURL.appendingPathComponent(
"CopilotForXcodeExtensionService.app"
))?.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as? String ?? "com.intii.CopilotForXcode"
let serviceIdentifier = bundleIdentifierBase + ".CommunicationBridge"
let appDelegate = AppDelegate()
let delegate = ServiceDelegate()
let listener = NSXPCListener(machServiceName: serviceIdentifier)
listener.delegate = delegate
listener.resume()
let app = NSApplication.shared
app.delegate = appDelegate
app.run()
================================================
FILE: Config.debug.xcconfig
================================================
#include "Version.xcconfig"
SLASH = /
HOST_APP_NAME = Copilot for Xcode Dev
BUNDLE_IDENTIFIER_BASE = dev.com.intii.CopilotForXcode
SPARKLE_FEED_URL = http:$(SLASH)$(SLASH)127.0.0.1:9433/appcast.xml
SPARKLE_PUBLIC_KEY = WDzm5GHnc6c8kjeJEgX5GuGiPpW6Lc/ovGjLnrrZvPY=
APPLICATION_SUPPORT_FOLDER = dev.com.intii.CopilotForXcode
EXTENSION_BUNDLE_NAME = Copilot Dev
EXTENSION_BUNDLE_DISPLAY_NAME = Copilot Dev
EXTENSION_SERVICE_NAME = CopilotForXcodeExtensionService
// see also target Configs
================================================
FILE: Config.xcconfig
================================================
#include "Version.xcconfig"
SLASH = /
HOST_APP_NAME = Copilot for Xcode
BUNDLE_IDENTIFIER_BASE = com.intii.CopilotForXcode
SPARKLE_FEED_URL = https:$(SLASH)$(SLASH)copilotforxcode.intii.com/appcast.xml
SPARKLE_PUBLIC_KEY = WDzm5GHnc6c8kjeJEgX5GuGiPpW6Lc/ovGjLnrrZvPY=
APPLICATION_SUPPORT_FOLDER = com.intii.CopilotForXcode
EXTENSION_BUNDLE_NAME = Copilot
EXTENSION_BUNDLE_DISPLAY_NAME = Copilot
EXTENSION_SERVICE_NAME = CopilotForXcodeExtensionService
// see also target Configs
================================================
FILE: Copilot for Xcode/App.swift
================================================
import Client
import HostApp
import LaunchAgentManager
import SwiftUI
import UpdateChecker
import XPCShared
struct VisualEffect: NSViewRepresentable {
func makeNSView(context: Self.Context) -> NSView { return NSVisualEffectView() }
func updateNSView(_ nsView: NSView, context: Context) {}
}
class TheUpdateCheckerDelegate: UpdateCheckerDelegate {
func prepareForRelaunch(finish: @escaping () -> Void) {
Task {
let service = try? getService()
try? await service?.quitService()
finish()
}
}
}
let updateCheckerDelegate = TheUpdateCheckerDelegate()
@main
struct CopilotForXcodeApp: App {
var body: some Scene {
WindowGroup {
TabContainer()
.frame(minWidth: 800, minHeight: 600)
.background(VisualEffect().ignoresSafeArea())
.onAppear {
UserDefaults.setupDefaultSettings()
}
.environment(
\.updateChecker,
{
let checker = UpdateChecker(
hostBundle: Bundle.main,
shouldAutomaticallyCheckForUpdate: false
)
checker.updateCheckerDelegate = updateCheckerDelegate
return checker
}()
)
}
}
}
var isPreview: Bool { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" }
================================================
FILE: Copilot for Xcode/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"filename" : "app-icon@16w.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "app-icon@32w.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "app-icon@32w.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "app-icon@64w.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "app-icon@128w.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "app-icon@256w.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "app-icon@256w.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "app-icon@512w.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "app-icon@512w.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "app-icon.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode/Assets.xcassets/BackgroundColor.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "40",
"green" : "23",
"red" : "25"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode/Assets.xcassets/BackgroundColorTop.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "54",
"green" : "25",
"red" : "30"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorDefault.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x46",
"green" : "0x24",
"red" : "0x2C"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorPressed.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.275",
"green" : "0.141",
"red" : "0.290"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode/Copilot_for_Xcode.entitlements
================================================
com.apple.security.app-sandbox
com.apple.security.application-groups
$(TeamIdentifierPrefix)group.$(BUNDLE_IDENTIFIER_BASE)
com.apple.security.automation.apple-events
com.apple.security.files.user-selected.read-only
keychain-access-groups
$(AppIdentifierPrefix)$(BUNDLE_IDENTIFIER_BASE).Shared
================================================
FILE: Copilot for Xcode/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Copilot for Xcode.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */; };
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */; };
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */; };
C80FFB962A95F58200704A25 /* AcceptPromptToCodeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */; };
C81291D72994FE6900196E12 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C81291D52994FE6900196E12 /* Main.storyboard */; };
C814588F2939EFDC00135263 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C814588E2939EFDC00135263 /* Cocoa.framework */; };
C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458932939EFDC00135263 /* SourceEditorExtension.swift */; };
C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */; };
C814589B2939EFDC00135263 /* Copilot.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C814588C2939EFDC00135263 /* Copilot.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
C8189B1A2938972F00C9DCDA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8189B192938972F00C9DCDA /* App.swift */; };
C8189B1E2938973000C9DCDA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B1D2938973000C9DCDA /* Assets.xcassets */; };
C8189B212938973000C9DCDA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B202938973000C9DCDA /* Preview Assets.xcassets */; };
C8216B73298036EC00AD38C7 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8216B72298036EC00AD38C7 /* main.swift */; };
C8216B782980370100AD38C7 /* ReloadLaunchAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */; };
C8216B7D2980374300AD38C7 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C8216B7C2980374300AD38C7 /* ArgumentParser */; };
C8216B802980378300AD38C7 /* Helper in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C8216B70298036EC00AD38C7 /* Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C828B27F2B1F7B4F00E7612A /* ExtensionPoint.appextensionpoint in Copy Extension Point */ = {isa = PBXBuildFile; fileRef = C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */; };
C8520301293C4D9000460097 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8520300293C4D9000460097 /* Helpers.swift */; };
C861A6A329E5503F005C41A3 /* PromptToCodeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */; };
C861E6112994F6070056CB02 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861E6102994F6070056CB02 /* AppDelegate.swift */; };
C861E6152994F6080056CB02 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C861E6142994F6080056CB02 /* Assets.xcassets */; };
C861E61E2994F6150056CB02 /* Service in Frameworks */ = {isa = PBXBuildFile; productRef = C861E61D2994F6150056CB02 /* Service */; };
C861E6202994F63A0056CB02 /* ServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861E61F2994F6390056CB02 /* ServiceDelegate.swift */; };
C86612F82A06AF74009197D9 /* HostApp in Frameworks */ = {isa = PBXBuildFile; productRef = C86612F72A06AF74009197D9 /* HostApp */; };
C8738B662BE4D4B900609E7F /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B652BE4D4B900609E7F /* main.swift */; };
C8738B6B2BE4D56F00609E7F /* ServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */; };
C8738B6F2BE4F7A600609E7F /* XPCShared in Frameworks */ = {isa = PBXBuildFile; productRef = C8738B6E2BE4F7A600609E7F /* XPCShared */; };
C8738B712BE4F8B700609E7F /* XPCController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B702BE4F8B700609E7F /* XPCController.swift */; };
C8738B7B2BE5363800609E7F /* SandboxedClientTesterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B7A2BE5363800609E7F /* SandboxedClientTesterApp.swift */; };
C8738B7D2BE5363800609E7F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B7C2BE5363800609E7F /* ContentView.swift */; };
C8738B7F2BE5363900609E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8738B7E2BE5363900609E7F /* Assets.xcassets */; };
C8738B822BE5363900609E7F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8738B812BE5363900609E7F /* Preview Assets.xcassets */; };
C8738B882BE5365000609E7F /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C8738B872BE5365000609E7F /* Client */; };
C8738B8A2BE540D000609E7F /* bridgeLaunchAgent.plist in Copy Launch Agent */ = {isa = PBXBuildFile; fileRef = C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */; };
C8738B8B2BE540DD00609E7F /* CommunicationBridge in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C8738B632BE4D4B900609E7F /* CommunicationBridge */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C8758E7029F04BFF00D29C1C /* CustomCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */; };
C8758E7229F04CF100D29C1C /* SeparatorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */; };
C87B03A5293B261200C77EAE /* AcceptSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */; };
C87B03A7293B261900C77EAE /* RejectSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */; };
C87B03A9293B262600C77EAE /* NextSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */; };
C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */; };
C87B03AC293B2CF300C77EAE /* XcodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; };
C87B03AD293B2CF300C77EAE /* XcodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C882175C294187EF00A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C882175B294187EF00A22FD3 /* Client */; };
C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */; };
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
C8DCF00029CE11D500FDDDD7 /* OpenChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */; };
C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
C81291AF2994F92700196E12 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C8189B0E2938972F00C9DCDA /* Project object */;
proxyType = 1;
remoteGlobalIDString = C861E60D2994F6070056CB02;
remoteInfo = ExtensionService;
};
C81458992939EFDC00135263 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C8189B0E2938972F00C9DCDA /* Project object */;
proxyType = 1;
remoteGlobalIDString = C814588B2939EFDC00135263;
remoteInfo = EditorExtension;
};
C8216B7E2980377E00AD38C7 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C8189B0E2938972F00C9DCDA /* Project object */;
proxyType = 1;
remoteGlobalIDString = C8216B6F298036EC00AD38C7;
remoteInfo = Helper;
};
C8738B8C2BE540F900609E7F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C8189B0E2938972F00C9DCDA /* Project object */;
proxyType = 1;
remoteGlobalIDString = C8738B622BE4D4B900609E7F;
remoteInfo = CommunicationBridge;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
C814589F2939EFDC00135263 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
C814589B2939EFDC00135263 /* Copilot.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
C8216B6E298036EC00AD38C7 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
C828B27E2B1F7B3C00E7612A /* Copy Extension Point */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(EXTENSIONS_FOLDER_PATH)";
dstSubfolderSpec = 16;
files = (
C828B27F2B1F7B4F00E7612A /* ExtensionPoint.appextensionpoint in Copy Extension Point */,
);
name = "Copy Extension Point";
runOnlyForDeploymentPostprocessing = 0;
};
C8520306293CF0EF00460097 /* Embed XPCService */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = ../Applications;
dstSubfolderSpec = 6;
files = (
C8738B8B2BE540DD00609E7F /* CommunicationBridge in Embed XPCService */,
C8216B802980378300AD38C7 /* Helper in Embed XPCService */,
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */,
);
name = "Embed XPCService";
runOnlyForDeploymentPostprocessing = 0;
};
C8738B612BE4D4B900609E7F /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 12;
dstPath = "";
dstSubfolderSpec = 16;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C87B03AE293B2CF300C77EAE /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
C87B03AD293B2CF300C77EAE /* XcodeKit.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
C8C8B60829AFA32800034BEE /* Embed Service */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
dstSubfolderSpec = 16;
files = (
);
name = "Embed Service";
runOnlyForDeploymentPostprocessing = 0;
};
C8F1032A2A7A38D200D28F4F /* Copy Launch Agent */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 12;
dstPath = Contents/Library/LaunchAgents;
dstSubfolderSpec = 1;
files = (
C8738B8A2BE540D000609E7F /* bridgeLaunchAgent.plist in Copy Launch Agent */,
);
name = "Copy Launch Agent";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRealtimeSuggestionsCommand.swift; sourceTree = ""; };
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealtimeSuggestionCommand.swift; sourceTree = ""; };
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchSuggestionsCommand.swift; sourceTree = ""; };
C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptPromptToCodeCommand.swift; sourceTree = ""; };
C81291D52994FE6900196E12 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; };
C81291D92994FE7900196E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
C814588C2939EFDC00135263 /* Copilot.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Copilot.appex; sourceTree = BUILT_PRODUCTS_DIR; };
C814588E2939EFDC00135263 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
C81458902939EFDC00135263 /* XcodeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XcodeKit.framework; path = Library/Frameworks/XcodeKit.framework; sourceTree = DEVELOPER_DIR; };
C81458932939EFDC00135263 /* SourceEditorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceEditorExtension.swift; sourceTree = ""; };
C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSuggestionsCommand.swift; sourceTree = ""; };
C81458972939EFDC00135263 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
C81458982939EFDC00135263 /* EditorExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EditorExtension.entitlements; sourceTree = ""; };
C81458AD293A009600135263 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
C81458AE293A009800135263 /* Config.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.debug.xcconfig; sourceTree = ""; };
C8189B162938972F00C9DCDA /* Copilot for Xcode Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Copilot for Xcode Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; };
C8189B192938972F00C9DCDA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
C8189B1D2938973000C9DCDA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
C8189B202938973000C9DCDA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
C8189B222938973000C9DCDA /* Copilot_for_Xcode.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Copilot_for_Xcode.entitlements; sourceTree = ""; };
C8189B282938979000C9DCDA /* Core */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Core; sourceTree = ""; };
C81D181E2A1B509B006C1B70 /* Tool */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Tool; sourceTree = ""; };
C81E867D296FE4420026E908 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; };
C8216B70298036EC00AD38C7 /* Helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = Helper; sourceTree = BUILT_PRODUCTS_DIR; };
C8216B72298036EC00AD38C7 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadLaunchAgent.swift; sourceTree = ""; };
C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = ExtensionPoint.appextensionpoint; sourceTree = ""; };
C82E38492A1F025F00D4EADF /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
C84FD9D72CC671C600BE5093 /* ChatPlugins */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ChatPlugins; sourceTree = ""; };
C8520300293C4D9000460097 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; };
C8520308293D805800460097 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptToCodeCommand.swift; sourceTree = ""; };
C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CopilotForXcodeExtensionService.app; sourceTree = BUILT_PRODUCTS_DIR; };
C861E6102994F6070056CB02 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
C861E6142994F6080056CB02 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
C861E6192994F6080056CB02 /* ExtensionService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ExtensionService.entitlements; sourceTree = ""; };
C861E61F2994F6390056CB02 /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = ""; };
C8738B632BE4D4B900609E7F /* CommunicationBridge */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = CommunicationBridge; sourceTree = BUILT_PRODUCTS_DIR; };
C8738B652BE4D4B900609E7F /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = ""; };
C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bridgeLaunchAgent.plist; sourceTree = ""; };
C8738B702BE4F8B700609E7F /* XPCController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCController.swift; sourceTree = ""; };
C8738B782BE5363800609E7F /* SandboxedClientTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SandboxedClientTester.app; sourceTree = BUILT_PRODUCTS_DIR; };
C8738B7A2BE5363800609E7F /* SandboxedClientTesterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxedClientTesterApp.swift; sourceTree = ""; };
C8738B7C2BE5363800609E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
C8738B7E2BE5363900609E7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
C8738B812BE5363900609E7F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
C8738B832BE5363900609E7F /* SandboxedClientTester.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SandboxedClientTester.entitlements; sourceTree = ""; };
C8738B892BE5379E00609E7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCommand.swift; sourceTree = ""; };
C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorCommand.swift; sourceTree = ""; };
C87B03A3293B24AB00C77EAE /* Copilot-for-Xcode-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Copilot-for-Xcode-Info.plist"; sourceTree = SOURCE_ROOT; };
C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptSuggestionCommand.swift; sourceTree = ""; };
C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RejectSuggestionCommand.swift; sourceTree = ""; };
C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextSuggestionCommand.swift; sourceTree = ""; };
C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviousSuggestionCommand.swift; sourceTree = ""; };
C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = ""; };
C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = ""; };
C8BE64922EB9B42E00EDB2D7 /* OverlayWindow */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = OverlayWindow; sourceTree = ""; };
C8CD828229B88006008D044D /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; };
C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenChat.swift; sourceTree = ""; };
C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseIdleTabsCommand.swift; sourceTree = ""; };
C8F103292A7A365000D28F4F /* launchAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = launchAgent.plist; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
C81458892939EFDC00135263 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C87B03AC293B2CF300C77EAE /* XcodeKit.framework in Frameworks */,
C814588F2939EFDC00135263 /* Cocoa.framework in Frameworks */,
C882175C294187EF00A22FD3 /* Client in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8189B132938972F00C9DCDA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C86612F82A06AF74009197D9 /* HostApp in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8216B6D298036EC00AD38C7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C8216B7D2980374300AD38C7 /* ArgumentParser in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C861E60B2994F6070056CB02 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C861E61E2994F6150056CB02 /* Service in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8738B602BE4D4B900609E7F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C8738B6F2BE4F7A600609E7F /* XPCShared in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8738B752BE5363800609E7F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C8738B882BE5365000609E7F /* Client in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
C814588D2939EFDC00135263 /* Frameworks */ = {
isa = PBXGroup;
children = (
C814588E2939EFDC00135263 /* Cocoa.framework */,
C81458902939EFDC00135263 /* XcodeKit.framework */,
);
name = Frameworks;
sourceTree = "";
};
C81458922939EFDC00135263 /* EditorExtension */ = {
isa = PBXGroup;
children = (
C81458932939EFDC00135263 /* SourceEditorExtension.swift */,
C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */,
C8520300293C4D9000460097 /* Helpers.swift */,
C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */,
C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */,
C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */,
C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */,
C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */,
C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */,
C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */,
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */,
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */,
C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */,
C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */,
C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */,
C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */,
C81458972939EFDC00135263 /* Info.plist */,
C81458982939EFDC00135263 /* EditorExtension.entitlements */,
);
path = EditorExtension;
sourceTree = "";
};
C8189B0D2938972F00C9DCDA = {
isa = PBXGroup;
children = (
C887BC832965D96000931567 /* DEVELOPMENT.md */,
C8520308293D805800460097 /* README.md */,
C82E38492A1F025F00D4EADF /* LICENSE */,
C8F103292A7A365000D28F4F /* launchAgent.plist */,
C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */,
C81E867D296FE4420026E908 /* Version.xcconfig */,
C81458AD293A009600135263 /* Config.xcconfig */,
C81458AE293A009800135263 /* Config.debug.xcconfig */,
C8CD828229B88006008D044D /* TestPlan.xctestplan */,
C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */,
C8BE64922EB9B42E00EDB2D7 /* OverlayWindow */,
C84FD9D72CC671C600BE5093 /* ChatPlugins */,
C81D181E2A1B509B006C1B70 /* Tool */,
C8189B282938979000C9DCDA /* Core */,
C8189B182938972F00C9DCDA /* Copilot for Xcode */,
C81458922939EFDC00135263 /* EditorExtension */,
C8216B71298036EC00AD38C7 /* Helper */,
C861E60F2994F6070056CB02 /* ExtensionService */,
C8738B642BE4D4B900609E7F /* CommunicationBridge */,
C8738B792BE5363800609E7F /* SandboxedClientTester */,
C814588D2939EFDC00135263 /* Frameworks */,
C8189B172938972F00C9DCDA /* Products */,
);
sourceTree = "";
};
C8189B172938972F00C9DCDA /* Products */ = {
isa = PBXGroup;
children = (
C8189B162938972F00C9DCDA /* Copilot for Xcode Dev.app */,
C814588C2939EFDC00135263 /* Copilot.appex */,
C8216B70298036EC00AD38C7 /* Helper */,
C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */,
C8738B632BE4D4B900609E7F /* CommunicationBridge */,
C8738B782BE5363800609E7F /* SandboxedClientTester.app */,
);
name = Products;
sourceTree = "";
};
C8189B182938972F00C9DCDA /* Copilot for Xcode */ = {
isa = PBXGroup;
children = (
C87B03A3293B24AB00C77EAE /* Copilot-for-Xcode-Info.plist */,
C8189B192938972F00C9DCDA /* App.swift */,
C8189B1D2938973000C9DCDA /* Assets.xcassets */,
C8189B222938973000C9DCDA /* Copilot_for_Xcode.entitlements */,
C8189B1F2938973000C9DCDA /* Preview Content */,
);
path = "Copilot for Xcode";
sourceTree = "";
};
C8189B1F2938973000C9DCDA /* Preview Content */ = {
isa = PBXGroup;
children = (
C8189B202938973000C9DCDA /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "";
};
C8216B71298036EC00AD38C7 /* Helper */ = {
isa = PBXGroup;
children = (
C8216B72298036EC00AD38C7 /* main.swift */,
C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */,
);
path = Helper;
sourceTree = "";
};
C861E60F2994F6070056CB02 /* ExtensionService */ = {
isa = PBXGroup;
children = (
C81291D92994FE7900196E12 /* Info.plist */,
C861E61F2994F6390056CB02 /* ServiceDelegate.swift */,
C861E6102994F6070056CB02 /* AppDelegate.swift */,
C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */,
C8738B702BE4F8B700609E7F /* XPCController.swift */,
C81291D52994FE6900196E12 /* Main.storyboard */,
C861E6142994F6080056CB02 /* Assets.xcassets */,
C861E6192994F6080056CB02 /* ExtensionService.entitlements */,
);
path = ExtensionService;
sourceTree = "";
};
C8738B642BE4D4B900609E7F /* CommunicationBridge */ = {
isa = PBXGroup;
children = (
C8738B652BE4D4B900609E7F /* main.swift */,
C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */,
);
path = CommunicationBridge;
sourceTree = "";
};
C8738B792BE5363800609E7F /* SandboxedClientTester */ = {
isa = PBXGroup;
children = (
C8738B892BE5379E00609E7F /* Info.plist */,
C8738B7A2BE5363800609E7F /* SandboxedClientTesterApp.swift */,
C8738B7C2BE5363800609E7F /* ContentView.swift */,
C8738B7E2BE5363900609E7F /* Assets.xcassets */,
C8738B832BE5363900609E7F /* SandboxedClientTester.entitlements */,
C8738B802BE5363900609E7F /* Preview Content */,
);
path = SandboxedClientTester;
sourceTree = "";
};
C8738B802BE5363900609E7F /* Preview Content */ = {
isa = PBXGroup;
children = (
C8738B812BE5363900609E7F /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
C814588B2939EFDC00135263 /* EditorExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = C814589C2939EFDC00135263 /* Build configuration list for PBXNativeTarget "EditorExtension" */;
buildPhases = (
C81458882939EFDC00135263 /* Sources */,
C81458892939EFDC00135263 /* Frameworks */,
C814588A2939EFDC00135263 /* Resources */,
C87B03AE293B2CF300C77EAE /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = EditorExtension;
packageProductDependencies = (
C882175B294187EF00A22FD3 /* Client */,
);
productName = EditorExtension;
productReference = C814588C2939EFDC00135263 /* Copilot.appex */;
productType = "com.apple.product-type.xcode-extension";
};
C8189B152938972F00C9DCDA /* Copilot for Xcode */ = {
isa = PBXNativeTarget;
buildConfigurationList = C8189B252938973000C9DCDA /* Build configuration list for PBXNativeTarget "Copilot for Xcode" */;
buildPhases = (
C8189B122938972F00C9DCDA /* Sources */,
C8189B132938972F00C9DCDA /* Frameworks */,
C8189B142938972F00C9DCDA /* Resources */,
C814589F2939EFDC00135263 /* Embed Foundation Extensions */,
C8520306293CF0EF00460097 /* Embed XPCService */,
C8C8B60829AFA32800034BEE /* Embed Service */,
C8F1032A2A7A38D200D28F4F /* Copy Launch Agent */,
);
buildRules = (
);
dependencies = (
C8738B8D2BE540F900609E7F /* PBXTargetDependency */,
C81291B02994F92700196E12 /* PBXTargetDependency */,
C8216B7F2980377E00AD38C7 /* PBXTargetDependency */,
C814589A2939EFDC00135263 /* PBXTargetDependency */,
);
name = "Copilot for Xcode";
packageProductDependencies = (
C86612F72A06AF74009197D9 /* HostApp */,
);
productName = "Copilot for Xcode";
productReference = C8189B162938972F00C9DCDA /* Copilot for Xcode Dev.app */;
productType = "com.apple.product-type.application";
};
C8216B6F298036EC00AD38C7 /* Helper */ = {
isa = PBXNativeTarget;
buildConfigurationList = C8216B74298036EC00AD38C7 /* Build configuration list for PBXNativeTarget "Helper" */;
buildPhases = (
C8216B6C298036EC00AD38C7 /* Sources */,
C8216B6D298036EC00AD38C7 /* Frameworks */,
C8216B6E298036EC00AD38C7 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = Helper;
packageProductDependencies = (
C8216B7C2980374300AD38C7 /* ArgumentParser */,
);
productName = Helper;
productReference = C8216B70298036EC00AD38C7 /* Helper */;
productType = "com.apple.product-type.tool";
};
C861E60D2994F6070056CB02 /* ExtensionService */ = {
isa = PBXNativeTarget;
buildConfigurationList = C861E61A2994F6080056CB02 /* Build configuration list for PBXNativeTarget "ExtensionService" */;
buildPhases = (
C861E60A2994F6070056CB02 /* Sources */,
C861E60B2994F6070056CB02 /* Frameworks */,
C861E60C2994F6070056CB02 /* Resources */,
C828B27E2B1F7B3C00E7612A /* Copy Extension Point */,
);
buildRules = (
);
dependencies = (
);
name = ExtensionService;
packageProductDependencies = (
C861E61D2994F6150056CB02 /* Service */,
);
productName = ExtensionService;
productReference = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */;
productType = "com.apple.product-type.application";
};
C8738B622BE4D4B900609E7F /* CommunicationBridge */ = {
isa = PBXNativeTarget;
buildConfigurationList = C8738B672BE4D4B900609E7F /* Build configuration list for PBXNativeTarget "CommunicationBridge" */;
buildPhases = (
C8738B5F2BE4D4B900609E7F /* Sources */,
C8738B602BE4D4B900609E7F /* Frameworks */,
C8738B612BE4D4B900609E7F /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = CommunicationBridge;
packageProductDependencies = (
C8738B6E2BE4F7A600609E7F /* XPCShared */,
);
productName = CommunicationBridge;
productReference = C8738B632BE4D4B900609E7F /* CommunicationBridge */;
productType = "com.apple.product-type.tool";
};
C8738B772BE5363800609E7F /* SandboxedClientTester */ = {
isa = PBXNativeTarget;
buildConfigurationList = C8738B842BE5363900609E7F /* Build configuration list for PBXNativeTarget "SandboxedClientTester" */;
buildPhases = (
C8738B742BE5363800609E7F /* Sources */,
C8738B752BE5363800609E7F /* Frameworks */,
C8738B762BE5363800609E7F /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SandboxedClientTester;
packageProductDependencies = (
C8738B872BE5365000609E7F /* Client */,
);
productName = SandboxedClientTester;
productReference = C8738B782BE5363800609E7F /* SandboxedClientTester.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
C8189B0E2938972F00C9DCDA /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1520;
LastUpgradeCheck = 1410;
TargetAttributes = {
C814588B2939EFDC00135263 = {
CreatedOnToolsVersion = 14.1;
};
C8189B152938972F00C9DCDA = {
CreatedOnToolsVersion = 14.1;
};
C8216B6F298036EC00AD38C7 = {
CreatedOnToolsVersion = 14.1;
};
C861E60D2994F6070056CB02 = {
CreatedOnToolsVersion = 14.2;
};
C8738B622BE4D4B900609E7F = {
CreatedOnToolsVersion = 15.2;
};
C8738B772BE5363800609E7F = {
CreatedOnToolsVersion = 15.2;
};
};
};
buildConfigurationList = C8189B112938972F00C9DCDA /* Build configuration list for PBXProject "Copilot for Xcode" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = C8189B0D2938972F00C9DCDA;
packageReferences = (
C8216B792980373800AD38C7 /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
C80C91742A588DD800B5EADA /* XCRemoteSwiftPackageReference "usearch" */,
);
productRefGroup = C8189B172938972F00C9DCDA /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
C8189B152938972F00C9DCDA /* Copilot for Xcode */,
C814588B2939EFDC00135263 /* EditorExtension */,
C8216B6F298036EC00AD38C7 /* Helper */,
C861E60D2994F6070056CB02 /* ExtensionService */,
C8738B622BE4D4B900609E7F /* CommunicationBridge */,
C8738B772BE5363800609E7F /* SandboxedClientTester */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
C814588A2939EFDC00135263 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C8189B142938972F00C9DCDA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C8189B212938973000C9DCDA /* Preview Assets.xcassets in Resources */,
C8189B1E2938973000C9DCDA /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C861E60C2994F6070056CB02 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C861E6152994F6080056CB02 /* Assets.xcassets in Resources */,
C81291D72994FE6900196E12 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8738B762BE5363800609E7F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C8738B822BE5363900609E7F /* Preview Assets.xcassets in Resources */,
C8738B7F2BE5363900609E7F /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C81458882939EFDC00135263 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C8DCF00029CE11D500FDDDD7 /* OpenChat.swift in Sources */,
C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */,
C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */,
C8758E7029F04BFF00D29C1C /* CustomCommand.swift in Sources */,
C8758E7229F04CF100D29C1C /* SeparatorCommand.swift in Sources */,
C861A6A329E5503F005C41A3 /* PromptToCodeCommand.swift in Sources */,
C8520301293C4D9000460097 /* Helpers.swift in Sources */,
C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */,
C80FFB962A95F58200704A25 /* AcceptPromptToCodeCommand.swift in Sources */,
C87B03A5293B261200C77EAE /* AcceptSuggestionCommand.swift in Sources */,
C87B03A9293B262600C77EAE /* NextSuggestionCommand.swift in Sources */,
C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */,
C87B03A7293B261900C77EAE /* RejectSuggestionCommand.swift in Sources */,
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */,
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */,
C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8189B122938972F00C9DCDA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C8189B1A2938972F00C9DCDA /* App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8216B6C298036EC00AD38C7 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C8216B73298036EC00AD38C7 /* main.swift in Sources */,
C8216B782980370100AD38C7 /* ReloadLaunchAgent.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C861E60A2994F6070056CB02 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */,
C8738B712BE4F8B700609E7F /* XPCController.swift in Sources */,
C861E6202994F63A0056CB02 /* ServiceDelegate.swift in Sources */,
C861E6112994F6070056CB02 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8738B5F2BE4D4B900609E7F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C8738B6B2BE4D56F00609E7F /* ServiceDelegate.swift in Sources */,
C8738B662BE4D4B900609E7F /* main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C8738B742BE5363800609E7F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C8738B7D2BE5363800609E7F /* ContentView.swift in Sources */,
C8738B7B2BE5363800609E7F /* SandboxedClientTesterApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
C81291B02994F92700196E12 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C861E60D2994F6070056CB02 /* ExtensionService */;
targetProxy = C81291AF2994F92700196E12 /* PBXContainerItemProxy */;
};
C814589A2939EFDC00135263 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C814588B2939EFDC00135263 /* EditorExtension */;
targetProxy = C81458992939EFDC00135263 /* PBXContainerItemProxy */;
};
C8216B7F2980377E00AD38C7 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C8216B6F298036EC00AD38C7 /* Helper */;
targetProxy = C8216B7E2980377E00AD38C7 /* PBXContainerItemProxy */;
};
C8738B8D2BE540F900609E7F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C8738B622BE4D4B900609E7F /* CommunicationBridge */;
targetProxy = C8738B8C2BE540F900609E7F /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
C814589D2939EFDC00135263 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = EditorExtension/EditorExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = "$(APP_BUILD)";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = EditorExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "$(EXTENSION_BUNDLE_NAME)";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "$(APP_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).EditorExtension";
PRODUCT_NAME = Copilot;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C814589E2939EFDC00135263 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = EditorExtension/EditorExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = "$(APP_BUILD)";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = EditorExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "$(EXTENSION_BUNDLE_NAME)";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "$(APP_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).EditorExtension";
PRODUCT_NAME = Copilot;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
C8189B232938973000C9DCDA /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C81458AE293A009800135263 /* Config.debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
C8189B242938973000C9DCDA /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C81458AD293A009600135263 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
C8189B262938973000C9DCDA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Copilot for Xcode/Copilot_for_Xcode.entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = "$(APP_BUILD)";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Copilot for Xcode/Preview Content\"";
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Copilot-for-Xcode-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "$(APP_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE)";
PRODUCT_MODULE_NAME = Copilot_for_Xcode;
PRODUCT_NAME = "$(HOST_APP_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C8189B272938973000C9DCDA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Copilot for Xcode/Copilot_for_Xcode.entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = "$(APP_BUILD)";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Copilot for Xcode/Preview Content\"";
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Copilot-for-Xcode-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "$(APP_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE)";
PRODUCT_NAME = "$(HOST_APP_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
C8216B75298036EC00AD38C7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C8216B76298036EC00AD38C7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
C861E61B2994F6080056CB02 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ExtensionService/ExtensionService.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = "$(APP_BUILD)";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ExtensionService/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "$(APP_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).ExtensionService";
PRODUCT_NAME = "$(EXTENSION_SERVICE_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C861E61C2994F6080056CB02 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ExtensionService/ExtensionService.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = "$(APP_BUILD)";
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ExtensionService/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "$(APP_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).ExtensionService";
PRODUCT_NAME = "$(EXTENSION_SERVICE_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
C8738B682BE4D4B900609E7F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).CommunicationBridge";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C8738B692BE4D4B900609E7F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).CommunicationBridge";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
C8738B852BE5363900609E7F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SandboxedClientTester/SandboxedClientTester.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SandboxedClientTester/Preview Content\"";
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SandboxedClientTester/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.intii.CopilotForXcode.SandboxedClientTester;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C8738B862BE5363900609E7F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SandboxedClientTester/SandboxedClientTester.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SandboxedClientTester/Preview Content\"";
DEVELOPMENT_TEAM = 5YKZ4Y3DAW;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SandboxedClientTester/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.intii.CopilotForXcode.SandboxedClientTester;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C814589C2939EFDC00135263 /* Build configuration list for PBXNativeTarget "EditorExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C814589D2939EFDC00135263 /* Debug */,
C814589E2939EFDC00135263 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C8189B112938972F00C9DCDA /* Build configuration list for PBXProject "Copilot for Xcode" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C8189B232938973000C9DCDA /* Debug */,
C8189B242938973000C9DCDA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C8189B252938973000C9DCDA /* Build configuration list for PBXNativeTarget "Copilot for Xcode" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C8189B262938973000C9DCDA /* Debug */,
C8189B272938973000C9DCDA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C8216B74298036EC00AD38C7 /* Build configuration list for PBXNativeTarget "Helper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C8216B75298036EC00AD38C7 /* Debug */,
C8216B76298036EC00AD38C7 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C861E61A2994F6080056CB02 /* Build configuration list for PBXNativeTarget "ExtensionService" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C861E61B2994F6080056CB02 /* Debug */,
C861E61C2994F6080056CB02 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C8738B672BE4D4B900609E7F /* Build configuration list for PBXNativeTarget "CommunicationBridge" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C8738B682BE4D4B900609E7F /* Debug */,
C8738B692BE4D4B900609E7F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C8738B842BE5363900609E7F /* Build configuration list for PBXNativeTarget "SandboxedClientTester" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C8738B852BE5363900609E7F /* Debug */,
C8738B862BE5363900609E7F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
C80C91742A588DD800B5EADA /* XCRemoteSwiftPackageReference "usearch" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/unum-cloud/usearch";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.19.1;
};
};
C8216B792980373800AD38C7 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-argument-parser.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
C8216B7C2980374300AD38C7 /* ArgumentParser */ = {
isa = XCSwiftPackageProductDependency;
package = C8216B792980373800AD38C7 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
productName = ArgumentParser;
};
C861E61D2994F6150056CB02 /* Service */ = {
isa = XCSwiftPackageProductDependency;
productName = Service;
};
C86612F72A06AF74009197D9 /* HostApp */ = {
isa = XCSwiftPackageProductDependency;
productName = HostApp;
};
C8738B6E2BE4F7A600609E7F /* XPCShared */ = {
isa = XCSwiftPackageProductDependency;
productName = XPCShared;
};
C8738B872BE5365000609E7F /* Client */ = {
isa = XCSwiftPackageProductDependency;
productName = Client;
};
C882175B294187EF00A22FD3 /* Client */ = {
isa = XCSwiftPackageProductDependency;
productName = Client;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = C8189B0E2938972F00C9DCDA /* Project object */;
}
================================================
FILE: Copilot for Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
================================================
{
"pins" : [
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "0625932976b3ae23949f6b816d13bd97f3b40b7c",
"version" : "0.10.0"
}
},
{
"identity" : "fseventswrapper",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Frizlab/FSEventsWrapper",
"state" : {
"revision" : "e0c59a2ce2775e5f6642da6d19207445f10112d0",
"version" : "1.0.2"
}
},
{
"identity" : "glob",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Bouke/Glob",
"state" : {
"revision" : "deda6e163d2ff2a8d7e138e2c3326dbd71157faf",
"version" : "1.0.5"
}
},
{
"identity" : "highlightr",
"kind" : "remoteSourceControl",
"location" : "https://github.com/raspu/Highlightr",
"state" : {
"revision" : "93199b9e434f04bda956a613af8f571933f9f037",
"version" : "2.1.2"
}
},
{
"identity" : "jsonrpc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/JSONRPC",
"state" : {
"revision" : "5da978702aece6ba5c7879b0d253c180d61e4ef3",
"version" : "0.6.0"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "languageclient",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/LanguageClient",
"state" : {
"revision" : "f0198ee0a102d266078f7d9c28f086f2989f988a",
"version" : "0.3.1"
}
},
{
"identity" : "languageserverprotocol",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/LanguageServerProtocol",
"state" : {
"revision" : "6e97f943dc024307c5524a80bd33cdbd1cc621de",
"version" : "0.8.0"
}
},
{
"identity" : "operationplus",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/OperationPlus",
"state" : {
"revision" : "1340f95dce3e93d742497d88db18f8676f4badf4",
"version" : "1.6.0"
}
},
{
"identity" : "processenv",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/ProcessEnv",
"state" : {
"revision" : "29487b6581bb785c372c611c943541ef4309d051",
"version" : "0.3.1"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "631846cc829f0f0cae327df9bafe5a32b7ddadce",
"version" : "2.4.0"
}
},
{
"identity" : "splash",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JohnSundell/Splash",
"state" : {
"branch" : "master",
"revision" : "2e3f17c2d09689c8bf175c4a84ff7f2ad3353301"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a",
"version" : "1.2.2"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms",
"state" : {
"revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a",
"version" : "0.1.0"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984",
"version" : "0.14.1"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "f9acfa1a45f4483fe0f2c434a74e6f68f865d12d",
"version" : "0.3.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
"version" : "1.0.4"
}
},
{
"identity" : "swift-composable-architecture",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
"state" : {
"revision" : "89f80fe2400d21a853abc9556a060a2fa50eb2cb",
"version" : "0.55.0"
}
},
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump",
"state" : {
"revision" : "3a35f7892e7cf6ba28a78cd46a703c0be4e0c6dc",
"version" : "0.11.0"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "de1a984a71e51f6e488e98ce3652035563eb8acb",
"version" : "0.5.1"
}
},
{
"identity" : "swift-identified-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-identified-collections",
"state" : {
"revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29",
"version" : "0.8.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9",
"version" : "2.1.0"
}
},
{
"identity" : "swift-parsing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-parsing",
"state" : {
"revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70",
"version" : "0.12.1"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "0e96a20ffd37a515c5c963952d4335c89bed50a6",
"version" : "2.6.0"
}
},
{
"identity" : "swiftui-navigation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swiftui-navigation",
"state" : {
"revision" : "2aa885e719087ee19df251c08a5980ad3e787f12",
"version" : "0.8.0"
}
},
{
"identity" : "tiktoken",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/Tiktoken",
"state" : {
"branch" : "main",
"revision" : "6a2ac324c6daec167ca95268d5a487e6de6a1cea"
}
},
{
"identity" : "usearch",
"kind" : "remoteSourceControl",
"location" : "https://github.com/unum-cloud/usearch",
"state" : {
"revision" : "f2ab884d50902c3ad63f07a3a20bc34008a17449",
"version" : "0.19.3"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "4af50b38daf0037cfbab15514a241224c3f62f98",
"version" : "0.8.5"
}
}
],
"version" : 2
}
================================================
FILE: Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/CommunicationBridge.xcscheme
================================================
================================================
FILE: Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/Copilot for Xcode Debug.xcscheme
================================================
================================================
FILE: Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/Copilot for Xcode.xcscheme
================================================
================================================
FILE: Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/EditorExtension.xcscheme
================================================
================================================
FILE: Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme
================================================
================================================
FILE: Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/SandboxedClientTester.xcscheme
================================================
================================================
FILE: Copilot for Xcode.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: Copilot for Xcode.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved
================================================
{
"pins" : [
{
"identity" : "aexml",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tadija/AEXML.git",
"state" : {
"revision" : "db806756c989760b35108146381535aec231092b",
"version" : "4.7.0"
}
},
{
"identity" : "cgeventoverride",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/CGEventOverride",
"state" : {
"revision" : "571d36d63e68fac30e4a350600cd186697936f74",
"version" : "1.2.3"
}
},
{
"identity" : "codablewrappers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/GottaGetSwifty/CodableWrappers",
"state" : {
"revision" : "4eb46a4c656333e8514db8aad204445741de7d40",
"version" : "2.0.7"
}
},
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
"version" : "1.0.3"
}
},
{
"identity" : "copilotforxcodekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/CopilotForXcodeKit",
"state" : {
"branch" : "feature/custom-chat-tab",
"revision" : "63915ee1f8aba5375bc0f0166c8645fe81fe5b88"
}
},
{
"identity" : "fseventswrapper",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Frizlab/FSEventsWrapper",
"state" : {
"revision" : "e0c59a2ce2775e5f6642da6d19207445f10112d0",
"version" : "1.0.2"
}
},
{
"identity" : "generative-ai-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/generative-ai-swift",
"state" : {
"branch" : "support-setting-base-url",
"revision" : "12d7b30b566a64cc0dd628130bfb99a07368fea7"
}
},
{
"identity" : "glob",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Bouke/Glob",
"state" : {
"revision" : "deda6e163d2ff2a8d7e138e2c3326dbd71157faf",
"version" : "1.0.5"
}
},
{
"identity" : "highlightr",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/Highlightr",
"state" : {
"branch" : "master",
"revision" : "81d8c8b3733939bf5d9e52cd6318f944cc033bd2"
}
},
{
"identity" : "indexstore-db",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/indexstore-db.git",
"state" : {
"branch" : "release/6.1",
"revision" : "54212fce1aecb199070808bdb265e7f17e396015"
}
},
{
"identity" : "jsonrpc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/JSONRPC",
"state" : {
"revision" : "5da978702aece6ba5c7879b0d253c180d61e4ef3",
"version" : "0.6.0"
}
},
{
"identity" : "keyboardshortcuts",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/KeyboardShortcuts",
"state" : {
"branch" : "main",
"revision" : "65fb410b0c6d3ed96623b460bab31ffce5f48b4d"
}
},
{
"identity" : "languageclient",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/LanguageClient",
"state" : {
"revision" : "f0198ee0a102d266078f7d9c28f086f2989f988a",
"version" : "0.3.1"
}
},
{
"identity" : "languageserverprotocol",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/LanguageServerProtocol",
"state" : {
"revision" : "6e97f943dc024307c5524a80bd33cdbd1cc621de",
"version" : "0.8.0"
}
},
{
"identity" : "messagepacker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hirotakan/MessagePacker.git",
"state" : {
"revision" : "4d8346c6bc579347e4df0429493760691c5aeca2",
"version" : "0.4.7"
}
},
{
"identity" : "networkimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
"version" : "6.0.1"
}
},
{
"identity" : "operationplus",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/OperationPlus",
"state" : {
"revision" : "1340f95dce3e93d742497d88db18f8676f4badf4",
"version" : "1.6.0"
}
},
{
"identity" : "pathkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kylef/PathKit.git",
"state" : {
"revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
"version" : "1.0.1"
}
},
{
"identity" : "processenv",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/ProcessEnv",
"state" : {
"revision" : "29487b6581bb785c372c611c943541ef4309d051",
"version" : "0.3.1"
}
},
{
"identity" : "sourcekitten",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/SourceKitten",
"state" : {
"revision" : "eb6656ed26bdef967ad8d07c27e2eab34dc582f2",
"version" : "0.37.0"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99",
"version" : "2.7.0"
}
},
{
"identity" : "spectre",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kylef/Spectre.git",
"state" : {
"revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7",
"version" : "0.10.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a",
"version" : "1.2.2"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms",
"state" : {
"revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
"version" : "1.0.4"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25",
"version" : "1.7.0"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
"version" : "1.0.6"
}
},
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark",
"state" : {
"revision" : "b022b08312decdc46585e0b3440d97f6f22ef703",
"version" : "0.6.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
"identity" : "swift-composable-architecture",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
"state" : {
"revision" : "69247baf7be2fd6f5820192caef0082d01849cd0",
"version" : "1.16.1"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8",
"version" : "1.3.1"
}
},
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump",
"state" : {
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
"version" : "1.3.3"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596",
"version" : "1.9.2"
}
},
{
"identity" : "swift-identified-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-identified-collections",
"state" : {
"revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597",
"version" : "1.1.1"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
},
{
"identity" : "swift-navigation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-navigation",
"state" : {
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
"version" : "2.3.0"
}
},
{
"identity" : "swift-parsing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-parsing",
"state" : {
"revision" : "3432cb81164dd3d69a75d0d63205be5fbae2c34b",
"version" : "0.14.1"
}
},
{
"identity" : "swift-perception",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-perception",
"state" : {
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
"version" : "1.6.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6",
"version" : "2.6.1"
}
},
{
"identity" : "swiftterm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/migueldeicaza/SwiftTerm",
"state" : {
"revision" : "e2b431dbf73f775fb4807a33e4572ffd3dc6933a",
"version" : "1.2.5"
}
},
{
"identity" : "swifttreesitter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/SwiftTreeSitter.git",
"state" : {
"branch" : "main",
"revision" : "fd499bfafcccfae12a1a579dc922d8418025a35d"
}
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/swiftui-introspect",
"state" : {
"revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336",
"version" : "1.3.0"
}
},
{
"identity" : "swxmlhash",
"kind" : "remoteSourceControl",
"location" : "https://github.com/drmohundro/SWXMLHash.git",
"state" : {
"revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f",
"version" : "7.0.2"
}
},
{
"identity" : "tiktoken",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/Tiktoken",
"state" : {
"branch" : "main",
"revision" : "6a2ac324c6daec167ca95268d5a487e6de6a1cea"
}
},
{
"identity" : "tree-sitter-objc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/lukepistrol/tree-sitter-objc",
"state" : {
"branch" : "feature/spm",
"revision" : "1b54ef0b5efddddf393b45e173788499cc572048"
}
},
{
"identity" : "usearch",
"kind" : "remoteSourceControl",
"location" : "https://github.com/unum-cloud/usearch",
"state" : {
"revision" : "f2ab884d50902c3ad63f07a3a20bc34008a17449",
"version" : "0.19.3"
}
},
{
"identity" : "xcodeproj",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/XcodeProj.git",
"state" : {
"revision" : "b1caa062d4aaab3e3d2bed5fe0ac5f8ce9bf84f4",
"version" : "8.27.7"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
"version" : "1.5.2"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b",
"version" : "5.3.1"
}
}
],
"version" : 2
}
================================================
FILE: Copilot-for-Xcode-Info.plist
================================================
APPLICATION_SUPPORT_FOLDER
$(APPLICATION_SUPPORT_FOLDER)
APP_ID_PREFIX
$(AppIdentifierPrefix)
BUNDLE_IDENTIFIER_BASE
$(BUNDLE_IDENTIFIER_BASE)
EXTENSION_BUNDLE_NAME
$(EXTENSION_BUNDLE_NAME)
HOST_APP_NAME
$(HOST_APP_NAME)
NSAppTransportSecurity
NSAllowsArbitraryLoads
SUEnableJavaScript
YES
SUFeedURL
$(SPARKLE_FEED_URL)
SUPublicEDKey
$(SPARKLE_PUBLIC_KEY)
TEAM_ID_PREFIX
$(TeamIdentifierPrefix)
================================================
FILE: Core/.gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
================================================
FILE: Core/.swiftpm/xcode/xcshareddata/xcschemes/Client.xcscheme
================================================
================================================
FILE: Core/.swiftpm/xcode/xcshareddata/xcschemes/HostApp.xcscheme
================================================
================================================
FILE: Core/.swiftpm/xcode/xcshareddata/xcschemes/Service.xcscheme
================================================
================================================
FILE: Core/.swiftpm/xcode/xcshareddata/xcschemes/SuggestionInjector.xcscheme
================================================
================================================
FILE: Core/Package.resolved
================================================
{
"pins" : [
{
"identity" : "codablewrappers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/GottaGetSwifty/CodableWrappers",
"state" : {
"revision" : "4eb46a4c656333e8514db8aad204445741de7d40",
"version" : "2.0.7"
}
},
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a",
"version" : "0.11.0"
}
},
{
"identity" : "fseventswrapper",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Frizlab/FSEventsWrapper",
"state" : {
"revision" : "e0c59a2ce2775e5f6642da6d19207445f10112d0",
"version" : "1.0.2"
}
},
{
"identity" : "glob",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Bouke/Glob",
"state" : {
"revision" : "deda6e163d2ff2a8d7e138e2c3326dbd71157faf",
"version" : "1.0.5"
}
},
{
"identity" : "highlightr",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/Highlightr",
"state" : {
"branch" : "bump-highlight-js-version",
"revision" : "4ffbb1b0b721378263297cafea6f2838044eb1eb"
}
},
{
"identity" : "jsonrpc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/JSONRPC",
"state" : {
"revision" : "5da978702aece6ba5c7879b0d253c180d61e4ef3",
"version" : "0.6.0"
}
},
{
"identity" : "languageclient",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/LanguageClient",
"state" : {
"revision" : "f0198ee0a102d266078f7d9c28f086f2989f988a",
"version" : "0.3.1"
}
},
{
"identity" : "languageserverprotocol",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/LanguageServerProtocol",
"state" : {
"revision" : "6e97f943dc024307c5524a80bd33cdbd1cc621de",
"version" : "0.8.0"
}
},
{
"identity" : "operationplus",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/OperationPlus",
"state" : {
"revision" : "1340f95dce3e93d742497d88db18f8676f4badf4",
"version" : "1.6.0"
}
},
{
"identity" : "processenv",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/ProcessEnv",
"state" : {
"revision" : "29487b6581bb785c372c611c943541ef4309d051",
"version" : "0.3.1"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "631846cc829f0f0cae327df9bafe5a32b7ddadce",
"version" : "2.4.0"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms",
"state" : {
"revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a",
"version" : "0.1.0"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984",
"version" : "0.14.1"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d",
"version" : "0.4.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
"version" : "1.0.4"
}
},
{
"identity" : "swift-composable-architecture",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
"state" : {
"revision" : "9f4202ab5b8422aa90f0ed983bf7652c3af7abf0",
"version" : "0.59.0"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "479750bd98fac2e813fffcf2af0728b5b0085795",
"version" : "0.1.1"
}
},
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump",
"state" : {
"revision" : "4a87bb75be70c983a9548597e8783236feb3401e",
"version" : "0.11.1"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee",
"version" : "0.6.0"
}
},
{
"identity" : "swift-identified-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-identified-collections",
"state" : {
"revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29",
"version" : "0.8.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9",
"version" : "2.1.0"
}
},
{
"identity" : "swift-parsing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-parsing",
"state" : {
"revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70",
"version" : "0.12.1"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"branch" : "main",
"revision" : "e149b01cfd3e96240e102729697e2095c19157ef"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6",
"version" : "2.6.1"
}
},
{
"identity" : "swifttreesitter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/SwiftTreeSitter",
"state" : {
"revision" : "a9b1335d5151b62b11f07599bd07d07dc5965de3",
"version" : "0.7.2"
}
},
{
"identity" : "swiftui-navigation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swiftui-navigation",
"state" : {
"revision" : "2aa885e719087ee19df251c08a5980ad3e787f12",
"version" : "0.8.0"
}
},
{
"identity" : "tiktoken",
"kind" : "remoteSourceControl",
"location" : "https://github.com/intitni/Tiktoken",
"state" : {
"branch" : "main",
"revision" : "6a2ac324c6daec167ca95268d5a487e6de6a1cea"
}
},
{
"identity" : "tree-sitter-objc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/lukepistrol/tree-sitter-objc",
"state" : {
"branch" : "feature/spm",
"revision" : "1b54ef0b5efddddf393b45e173788499cc572048"
}
},
{
"identity" : "tree-sitter-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/alex-pinkus/tree-sitter-swift",
"state" : {
"branch" : "with-generated-files",
"revision" : "eda05af7ac41adb4eb19c346883c0fa32fe3bdd8"
}
},
{
"identity" : "usearch",
"kind" : "remoteSourceControl",
"location" : "https://github.com/unum-cloud/usearch",
"state" : {
"revision" : "33c53288b44ccb55de77776820676132a6e4c42a",
"version" : "0.23.0"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865",
"version" : "0.9.0"
}
}
],
"version" : 2
}
================================================
FILE: Core/Package.swift
================================================
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import Foundation
import PackageDescription
// MARK: - Package
let package = Package(
name: "Core",
platforms: [.macOS(.v13)],
products: [
.library(
name: "Service",
targets: [
"Service",
"FileChangeChecker",
"LaunchAgentManager",
"UpdateChecker",
]
),
.library(
name: "Client",
targets: [
"Client",
]
),
.library(
name: "HostApp",
targets: [
"HostApp",
"Client",
"LaunchAgentManager",
"UpdateChecker",
]
),
],
dependencies: [
.package(path: "../Tool"),
.package(path: "../ChatPlugins"),
.package(path: "../OverlayWindow"),
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.4"),
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
.package(
url: "https://github.com/pointfreeco/swift-composable-architecture",
exact: "1.16.1"
),
// quick hack to support custom UserDefaults
// https://github.com/sindresorhus/KeyboardShortcuts
.package(url: "https://github.com/intitni/KeyboardShortcuts", branch: "main"),
.package(url: "https://github.com/intitni/CGEventOverride", from: "1.2.1"),
.package(url: "https://github.com/intitni/Highlightr", branch: "master"),
].pro,
targets: [
// MARK: - Main
.target(
name: "Client",
dependencies: [
.product(name: "XPCShared", package: "Tool"),
.product(name: "SuggestionProvider", package: "Tool"),
.product(name: "SuggestionBasic", package: "Tool"),
.product(name: "Logger", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
].proCore([
"LicenseManagement",
])
),
.target(
name: "Service",
dependencies: [
"SuggestionWidget",
"SuggestionService",
"ChatService",
"PromptToCodeService",
"ServiceUpdateMigration",
"ChatGPTChatTab",
"PlusFeatureFlag",
"KeyBindingManager",
"XcodeThemeController",
.product(name: "XPCShared", package: "Tool"),
.product(name: "SuggestionProvider", package: "Tool"),
.product(name: "Workspace", package: "Tool"),
.product(name: "WorkspaceSuggestionService", package: "Tool"),
.product(name: "UserDefaultsObserver", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
.product(name: "SuggestionBasic", package: "Tool"),
.product(name: "PromptToCode", package: "Tool"),
.product(name: "ChatTab", package: "Tool"),
.product(name: "Logger", package: "Tool"),
.product(name: "OpenAIService", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
.product(name: "CommandHandler", package: "Tool"),
.product(name: "OverlayWindow", package: "OverlayWindow"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"),
.product(name: "CustomCommandTemplateProcessor", package: "Tool"),
].pro([
"ProService",
])
),
.testTarget(
name: "ServiceTests",
dependencies: [
"Service",
"Client",
.product(name: "XPCShared", package: "Tool"),
.product(name: "SuggestionProvider", package: "Tool"),
.product(name: "SuggestionBasic", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
]
),
// MARK: - Host App
.target(
name: "HostApp",
dependencies: [
"Client",
"LaunchAgentManager",
"PlusFeatureFlag",
.product(name: "SuggestionProvider", package: "Tool"),
.product(name: "Toast", package: "Tool"),
.product(name: "SharedUIComponents", package: "Tool"),
.product(name: "SuggestionBasic", package: "Tool"),
.product(name: "WebSearchService", package: "Tool"),
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
.product(name: "OpenAIService", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"),
].pro([
"ProHostApp",
])
),
// MARK: - Suggestion Service
.target(
name: "SuggestionService",
dependencies: [
.product(name: "UserDefaultsObserver", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
.product(name: "SuggestionBasic", package: "Tool"),
.product(name: "SuggestionProvider", package: "Tool"),
].pro([
"ProExtension",
])
),
// MARK: - Prompt To Code
.target(
name: "PromptToCodeService",
dependencies: [
.product(name: "PromptToCode", package: "Tool"),
.product(name: "FocusedCodeFinder", package: "Tool"),
.product(name: "SuggestionBasic", package: "Tool"),
.product(name: "OpenAIService", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
].pro([
"ProService",
])
),
.testTarget(name: "PromptToCodeServiceTests", dependencies: ["PromptToCodeService"]),
// MARK: - Chat
.target(
name: "ChatService",
dependencies: [
"LegacyChatPlugin",
// context collectors
"WebChatContextCollector",
"SystemInfoChatContextCollector",
.product(name: "ChatContextCollector", package: "Tool"),
.product(name: "PromptToCode", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
.product(name: "OpenAIService", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
.product(name: "CustomCommandTemplateProcessor", package: "Tool"),
.product(name: "ChatPlugins", package: "ChatPlugins"),
.product(name: "Parsing", package: "swift-parsing"),
].pro([
"ProService",
])
),
.testTarget(name: "ChatServiceTests", dependencies: ["ChatService"]),
.target(
name: "LegacyChatPlugin",
dependencies: [
.product(name: "AppMonitoring", package: "Tool"),
.product(name: "OpenAIService", package: "Tool"),
.product(name: "Terminal", package: "Tool"),
]
),
.target(
name: "ChatGPTChatTab",
dependencies: [
"ChatService",
.product(name: "SharedUIComponents", package: "Tool"),
.product(name: "OpenAIService", package: "Tool"),
.product(name: "Logger", package: "Tool"),
.product(name: "ChatTab", package: "Tool"),
.product(name: "Terminal", package: "Tool"),
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
),
// MARK: - UI
.target(
name: "SuggestionWidget",
dependencies: [
"PromptToCodeService",
"ChatGPTChatTab",
.product(name: "PromptToCode", package: "Tool"),
.product(name: "Toast", package: "Tool"),
.product(name: "UserDefaultsObserver", package: "Tool"),
.product(name: "SharedUIComponents", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
.product(name: "ChatTab", package: "Tool"),
.product(name: "Logger", package: "Tool"),
.product(name: "CustomAsyncAlgorithms", package: "Tool"),
.product(name: "CodeDiff", package: "Tool"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
),
.testTarget(name: "SuggestionWidgetTests", dependencies: ["SuggestionWidget"]),
// MARK: - Helpers
.target(name: "FileChangeChecker"),
.target(name: "LaunchAgentManager"),
.target(
name: "UpdateChecker",
dependencies: [
"Sparkle",
.product(name: "Preferences", package: "Tool"),
.product(name: "Logger", package: "Tool"),
]
),
.target(
name: "ServiceUpdateMigration",
dependencies: [
.product(name: "SuggestionProvider", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
.product(name: "Keychain", package: "Tool"),
]
),
.testTarget(
name: "ServiceUpdateMigrationTests",
dependencies: [
"ServiceUpdateMigration",
]
),
.target(
name: "PlusFeatureFlag",
dependencies: [
].pro([
"LicenseManagement",
])
),
// MAKR: - Chat Context Collector
.target(
name: "WebChatContextCollector",
dependencies: [
.product(name: "ChatContextCollector", package: "Tool"),
.product(name: "LangChain", package: "Tool"),
.product(name: "OpenAIService", package: "Tool"),
.product(name: "ExternalServices", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
],
path: "Sources/ChatContextCollectors/WebChatContextCollector"
),
.target(
name: "SystemInfoChatContextCollector",
dependencies: [
.product(name: "ChatContextCollector", package: "Tool"),
.product(name: "OpenAIService", package: "Tool"),
],
path: "Sources/ChatContextCollectors/SystemInfoChatContextCollector"
),
// MARK: Key Binding
.target(
name: "KeyBindingManager",
dependencies: [
.product(name: "CommandHandler", package: "Tool"),
.product(name: "Workspace", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
.product(name: "Logger", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
.product(name: "UserDefaultsObserver", package: "Tool"),
.product(name: "CGEventOverride", package: "CGEventOverride"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
),
.testTarget(
name: "KeyBindingManagerTests",
dependencies: ["KeyBindingManager"]
),
// MARK: Theming
.target(
name: "XcodeThemeController",
dependencies: [
.product(name: "Preferences", package: "Tool"),
.product(name: "AppMonitoring", package: "Tool"),
.product(name: "Highlightr", package: "Highlightr"),
]
),
]
)
extension [Target.Dependency] {
func pro(_ targetNames: [String]) -> [Target.Dependency] {
if isProIncluded {
return self + targetNames.map { Target.Dependency.product(name: $0, package: "Pro") }
}
return self
}
func proCore(_ targetNames: [String]) -> [Target.Dependency] {
if isProIncluded {
return self + targetNames
.map { Target.Dependency.product(name: $0, package: "ProCore") }
}
return self
}
}
extension [Package.Dependency] {
var pro: [Package.Dependency] {
if isProIncluded {
return self + [.package(path: "../../Pro"), .package(path: "../../Pro/ProCore")]
}
return self
}
}
var isProIncluded: Bool {
func isProIncluded(file: StaticString = #file) -> Bool {
let filePath = "\(file)"
let fileURL = URL(fileURLWithPath: filePath)
let rootURL = fileURL
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let confURL = rootURL.appendingPathComponent("PLUS")
return FileManager.default.fileExists(atPath: confURL.path)
}
return isProIncluded()
}
================================================
FILE: Core/README.md
================================================
# Core
A description of this package.
================================================
FILE: Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift
================================================
import ChatContextCollector
import Foundation
import OpenAIService
public final class SystemInfoChatContextCollector: ChatContextCollector {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, yyyy-MM-dd HH:mm:ssZ"
return formatter
}()
public init() {}
public func generateContext(
history: [ChatMessage],
scopes: Set,
content: String,
configuration: ChatGPTConfiguration
) -> ChatContext {
return .init(
systemPrompt: """
## System Info
Current Time: \(
Self.dateFormatter.string(from: Date())
) (You can use it to calculate time in another time zone)
""",
retrievedContent: [],
functions: []
)
}
}
================================================
FILE: Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift
================================================
import ChatBasic
import Foundation
import LangChain
import OpenAIService
import Preferences
struct QueryWebsiteFunction: ChatGPTFunction {
struct Arguments: Codable {
var query: String
var urls: [String]
}
struct Result: ChatGPTFunctionResult {
var answers: [String]
var botReadableContent: String {
return answers.joined(separator: "\n")
}
var userReadableContent: ChatGPTFunctionResultUserReadableContent {
.text(botReadableContent)
}
}
var name: String {
"queryWebsite"
}
var description: String {
"Useful for when you need to answer a question using information from a website."
}
var argumentSchema: JSONSchemaValue {
return [
.type: "object",
.properties: [
"query": [
.type: "string",
.description: "things you want to know about the website",
],
"urls": [
.type: "array",
.description: "urls of the website, you can use urls appearing in the conversation",
.items: [
.type: "string",
],
],
],
.required: ["query", "urls"],
]
}
func prepare(reportProgress: @escaping (String) async -> Void) async {
await reportProgress("Reading..")
}
func call(
arguments: Arguments,
reportProgress: @escaping (String) async -> Void
) async throws -> Result {
do {
let configuration = UserPreferenceEmbeddingConfiguration()
let embedding = OpenAIEmbedding(configuration: configuration)
let dimensions = configuration.dimensions
let modelName = configuration.model?.name ?? "model"
let result = try await withThrowingTaskGroup(of: String.self) { group in
for urlString in arguments.urls {
let storeIdentifier = "\(urlString)-\(modelName)-\(dimensions)"
guard let url = URL(string: urlString) else { continue }
group.addTask {
// 1. grab the website content
await reportProgress("Loading \(url)..")
if let database = await TemporaryUSearch.view(
identifier: storeIdentifier,
dimensions: dimensions
) {
await reportProgress("Getting relevant information..")
let qa = QAInformationRetrievalChain(
vectorStore: database,
embedding: embedding
)
return try await qa.call(.init(arguments.query)).information
}
let loader = WebLoader(urls: [url])
let documents = try await loader.load()
await reportProgress("Processing \(url)..")
// 2. split the content
let splitter = RecursiveCharacterTextSplitter(
chunkSize: 1500,
chunkOverlap: 100
)
let splitDocuments = try await splitter.transformDocuments(documents)
// 3. embedding and store in db
await reportProgress("Embedding \(url)..")
let embeddedDocuments = try await embedding.embed(documents: splitDocuments)
let database = TemporaryUSearch(
identifier: storeIdentifier,
dimensions: dimensions
)
try await database.set(embeddedDocuments)
// 4. generate answer
await reportProgress("Getting relevant information..")
let qa = QAInformationRetrievalChain(
vectorStore: database,
embedding: embedding
)
let result = try await qa.call(.init(arguments.query))
return result.information
}
}
var all = [String]()
for try await result in group {
all.append(result)
}
await reportProgress("""
Finish reading websites.
\(
arguments.urls
.map { "- [\($0)](\($0))" }
.joined(separator: "\n")
)
""")
return all
}
return .init(answers: result)
} catch {
await reportProgress("Failed reading websites.")
throw error
}
}
}
================================================
FILE: Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift
================================================
import ChatBasic
import Foundation
import OpenAIService
import Preferences
import WebSearchService
struct SearchFunction: ChatGPTFunction {
static let dateFormatter = {
let it = DateFormatter()
it.dateFormat = "yyyy-MM-dd"
return it
}()
struct Arguments: Codable {
var query: String
var freshness: String?
}
struct Result: ChatGPTFunctionResult {
var result: WebSearchResult
var botReadableContent: String {
result.webPages.enumerated().map {
let (index, page) = $0
return """
\(index + 1). \(page.title) \(page.urlString)
\(page.snippet)
"""
}.joined(separator: "\n")
}
var userReadableContent: ChatGPTFunctionResultUserReadableContent {
.text(botReadableContent)
}
}
let maxTokens: Int
var name: String {
"searchWeb"
}
var description: String {
"Useful for when you need to answer questions about latest information."
}
var argumentSchema: JSONSchemaValue {
let today = Self.dateFormatter.string(from: Date())
return [
.type: "object",
.properties: [
"query": [
.type: "string",
.description: "the search query",
],
"freshness": [
.type: "string",
.description: .string(
"limit the search result to a specific range, use only when I ask the question about current events. Today is \(today). Format: yyyy-MM-dd..yyyy-MM-dd"
),
.examples: ["1919-10-20..1988-10-20"],
],
],
.required: ["query"],
]
}
func prepare(reportProgress: @escaping ReportProgress) async {
await reportProgress("Searching..")
}
func call(
arguments: Arguments,
reportProgress: @escaping ReportProgress
) async throws -> Result {
await reportProgress("Searching \(arguments.query)")
do {
let search = WebSearchService(provider: .userPreferred)
let result = try await search.search(query: arguments.query)
await reportProgress("""
Finish searching \(arguments.query)
\(
result.webPages
.map { "- [\($0.title)](\($0.urlString))" }
.joined(separator: "\n")
)
""")
return .init(result: result)
} catch {
await reportProgress("Failed searching: \(error.localizedDescription)")
throw error
}
}
}
================================================
FILE: Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift
================================================
import ChatBasic
import ChatContextCollector
import Foundation
import OpenAIService
public final class WebChatContextCollector: ChatContextCollector {
var recentLinks = [String]()
public init() {}
public func generateContext(
history: [ChatMessage],
scopes: Set,
content: String,
configuration: ChatGPTConfiguration
) -> ChatContext {
guard scopes.contains(.web) else { return .empty }
let links = Self.detectLinks(from: history) + Self.detectLinks(from: content)
let functions: [(any ChatGPTFunction)?] = [
SearchFunction(maxTokens: configuration.maxTokens),
// allow this function only when there is a link in the memory.
links.isEmpty ? nil : QueryWebsiteFunction(),
]
return .init(
systemPrompt: "You prefer to answer questions with latest content on the internet.",
retrievedContent: [],
functions: functions.compactMap { $0 }
)
}
}
extension WebChatContextCollector {
static func detectLinks(from messages: [ChatMessage]) -> [String] {
return messages.lazy
.compactMap {
$0.content ?? $0.toolCalls?.map(\.function.arguments).joined(separator: " ") ?? ""
}
.map(detectLinks(from:))
.flatMap { $0 }
}
static func detectLinks(from content: String) -> [String] {
var links = [String]()
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector?.matches(
in: content,
options: [],
range: NSRange(content.startIndex..., in: content)
)
for match in matches ?? [] {
guard let range = Range(match.range, in: content) else { continue }
let url = content[range]
links.append(String(url))
}
return links
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Chat.swift
================================================
import AppKit
import ChatBasic
import ChatService
import ComposableArchitecture
import Foundation
import MarkdownUI
import OpenAIService
import Preferences
import Terminal
public struct DisplayedChatMessage: Equatable {
public enum Role: Equatable {
case user
case assistant
case tool
case ignored
}
public struct Reference: Equatable {
public typealias Kind = ChatMessage.Reference.Kind
public var title: String
public var subtitle: String
public var uri: String
public var startLine: Int?
public var kind: Kind
public init(
title: String,
subtitle: String,
uri: String,
startLine: Int?,
kind: Kind
) {
self.title = title
self.subtitle = subtitle
self.uri = uri
self.startLine = startLine
self.kind = kind
}
}
public var id: String
public var role: Role
public var text: String
public var markdownContent: MarkdownContent
public var references: [Reference] = []
public init(id: String, role: Role, text: String, references: [Reference]) {
self.id = id
self.role = role
self.text = text
markdownContent = .init(text)
self.references = references
}
}
private var isPreview: Bool {
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
@Reducer
struct Chat {
public typealias MessageID = String
@ObservableState
struct State: Equatable {
var title: String = "Chat"
var typedMessage = ""
var history: [DisplayedChatMessage] = []
var isReceivingMessage = false
var chatMenu = ChatMenu.State()
var focusedField: Field?
var isEnabled = true
var isPinnedToBottom = true
enum Field: String, Hashable {
case textField
}
}
enum Action: Equatable, BindableAction {
case binding(BindingAction)
case appear
case refresh
case setIsEnabled(Bool)
case sendButtonTapped
case returnButtonTapped
case stopRespondingButtonTapped
case clearButtonTap
case deleteMessageButtonTapped(MessageID)
case resendMessageButtonTapped(MessageID)
case setAsExtraPromptButtonTapped(MessageID)
case manuallyScrolledUp
case scrollToBottomButtonTapped
case focusOnTextField
case referenceClicked(DisplayedChatMessage.Reference)
case observeChatService
case observeHistoryChange
case observeIsReceivingMessageChange
case observeSystemPromptChange
case observeExtraSystemPromptChange
case observeDefaultScopesChange
case historyChanged
case isReceivingMessageChanged
case systemPromptChanged
case extraSystemPromptChanged
case defaultScopesChanged
case chatMenu(ChatMenu.Action)
}
let service: ChatService
let id = UUID()
enum CancelID: Hashable {
case observeHistoryChange(UUID)
case observeIsReceivingMessageChange(UUID)
case observeSystemPromptChange(UUID)
case observeExtraSystemPromptChange(UUID)
case observeDefaultScopesChange(UUID)
case sendMessage(UUID)
}
var body: some ReducerOf {
BindingReducer()
Scope(state: \.chatMenu, action: \.chatMenu) {
ChatMenu(service: service)
}
Reduce { state, action in
switch action {
case .appear:
return .run { send in
if isPreview { return }
await send(.observeChatService)
await send(.historyChanged)
await send(.isReceivingMessageChanged)
await send(.systemPromptChanged)
await send(.extraSystemPromptChanged)
await send(.focusOnTextField)
await send(.refresh)
}
case .refresh:
return .run { send in
await send(.chatMenu(.refresh))
}
case let .setIsEnabled(isEnabled):
state.isEnabled = isEnabled
return .none
case .sendButtonTapped:
guard !state.typedMessage.isEmpty else { return .none }
let message = state.typedMessage
state.typedMessage = ""
return .run { _ in
try await service.send(content: message)
}.cancellable(id: CancelID.sendMessage(id))
case .returnButtonTapped:
state.typedMessage += "\n"
return .none
case .stopRespondingButtonTapped:
return .merge(
.run { _ in
await service.stopReceivingMessage()
},
.cancel(id: CancelID.sendMessage(id))
)
case .clearButtonTap:
return .run { _ in
await service.clearHistory()
}
case let .deleteMessageButtonTapped(id):
return .run { _ in
await service.deleteMessage(id: id)
}
case let .resendMessageButtonTapped(id):
return .run { _ in
try await service.resendMessage(id: id)
}
case let .setAsExtraPromptButtonTapped(id):
return .run { _ in
await service.setMessageAsExtraPrompt(id: id)
}
case let .referenceClicked(reference):
let fileURL = URL(fileURLWithPath: reference.uri)
return .run { _ in
if FileManager.default.fileExists(atPath: fileURL.path) {
let terminal = Terminal()
do {
_ = try await terminal.runCommand(
"/bin/bash",
arguments: [
"-c",
"xed -l \(reference.startLine ?? 0) ${TARGET_FILE}",
],
environment: ["TARGET_FILE": reference.uri]
)
} catch {
print(error)
}
} else if let url = URL(string: reference.uri), url.scheme != nil {
NSWorkspace.shared.open(url)
}
}
case .manuallyScrolledUp:
state.isPinnedToBottom = false
return .none
case .scrollToBottomButtonTapped:
state.isPinnedToBottom = true
return .none
case .focusOnTextField:
state.focusedField = .textField
return .none
case .observeChatService:
return .run { send in
await send(.observeHistoryChange)
await send(.observeIsReceivingMessageChange)
await send(.observeSystemPromptChange)
await send(.observeExtraSystemPromptChange)
await send(.observeDefaultScopesChange)
}
case .observeHistoryChange:
return .run { send in
let stream = AsyncStream { continuation in
let cancellable = service.$chatHistory.sink { _ in
continuation.yield()
}
continuation.onTermination = { _ in
cancellable.cancel()
}
}
let debouncedHistoryChange = TimedDebounceFunction(duration: 0.2) {
await send(.historyChanged)
}
for await _ in stream {
await debouncedHistoryChange()
}
}.cancellable(id: CancelID.observeHistoryChange(id), cancelInFlight: true)
case .observeIsReceivingMessageChange:
return .run { send in
let stream = AsyncStream { continuation in
let cancellable = service.$isReceivingMessage
.sink { _ in
continuation.yield()
}
continuation.onTermination = { _ in
cancellable.cancel()
}
}
for await _ in stream {
await send(.isReceivingMessageChanged)
}
}.cancellable(
id: CancelID.observeIsReceivingMessageChange(id),
cancelInFlight: true
)
case .observeSystemPromptChange:
return .run { send in
let stream = AsyncStream { continuation in
let cancellable = service.$systemPrompt.sink { _ in
continuation.yield()
}
continuation.onTermination = { _ in
cancellable.cancel()
}
}
for await _ in stream {
await send(.systemPromptChanged)
}
}.cancellable(id: CancelID.observeSystemPromptChange(id), cancelInFlight: true)
case .observeExtraSystemPromptChange:
return .run { send in
let stream = AsyncStream { continuation in
let cancellable = service.$extraSystemPrompt
.sink { _ in
continuation.yield()
}
continuation.onTermination = { _ in
cancellable.cancel()
}
}
for await _ in stream {
await send(.extraSystemPromptChanged)
}
}.cancellable(id: CancelID.observeExtraSystemPromptChange(id), cancelInFlight: true)
case .observeDefaultScopesChange:
return .run { send in
let stream = AsyncStream { continuation in
let cancellable = service.$defaultScopes
.sink { _ in
continuation.yield()
}
continuation.onTermination = { _ in
cancellable.cancel()
}
}
for await _ in stream {
await send(.defaultScopesChanged)
}
}.cancellable(id: CancelID.observeDefaultScopesChange(id), cancelInFlight: true)
case .historyChanged:
state.history = service.chatHistory.flatMap { message in
var all = [DisplayedChatMessage]()
all.append(.init(
id: message.id,
role: {
switch message.role {
case .system: return .ignored
case .user: return .user
case .assistant:
if let text = message.summary ?? message.content,
!text.isEmpty
{
return .assistant
}
return .ignored
}
}(),
text: message.summary ?? message.content ?? "",
references: message.references.map(convertReference)
))
for call in message.toolCalls ?? [] {
all.append(.init(
id: message.id + call.id,
role: .tool,
text: call.response.summary ?? call.response.content,
references: []
))
}
return all
}
state.title = {
let defaultTitle = "Chat"
guard let lastMessageText = state.history
.filter({ $0.role == .assistant || $0.role == .user })
.last?
.text else { return defaultTitle }
if lastMessageText.isEmpty { return defaultTitle }
let trimmed = lastMessageText
.trimmingCharacters(in: .punctuationCharacters)
.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.starts(with: "```") {
return "Code Block"
} else {
return trimmed
}
}()
return .none
case .isReceivingMessageChanged:
state.isReceivingMessage = service.isReceivingMessage
if service.isReceivingMessage {
state.isPinnedToBottom = true
}
return .none
case .systemPromptChanged:
state.chatMenu.systemPrompt = service.systemPrompt
return .none
case .extraSystemPromptChanged:
state.chatMenu.extraSystemPrompt = service.extraSystemPrompt
return .none
case .defaultScopesChanged:
state.chatMenu.defaultScopes = service.defaultScopes
return .none
case .binding:
return .none
case .chatMenu:
return .none
}
}
}
}
@Reducer
struct ChatMenu {
@ObservableState
struct State: Equatable {
var systemPrompt: String = ""
var extraSystemPrompt: String = ""
var temperatureOverride: Double? = nil
var chatModelIdOverride: String? = nil
var defaultScopes: Set = []
}
enum Action: Equatable {
case appear
case refresh
case resetPromptButtonTapped
case temperatureOverrideSelected(Double?)
case chatModelIdOverrideSelected(String?)
case customCommandButtonTapped(CustomCommand)
case resetDefaultScopesButtonTapped
case toggleScope(ChatService.Scope)
}
let service: ChatService
var body: some ReducerOf {
Reduce { state, action in
switch action {
case .appear:
return .run {
await $0(.refresh)
}
case .refresh:
state.temperatureOverride = service.configuration.overriding.temperature
state.chatModelIdOverride = service.configuration.overriding.modelId
return .none
case .resetPromptButtonTapped:
return .run { _ in
await service.resetPrompt()
}
case let .temperatureOverrideSelected(temperature):
state.temperatureOverride = temperature
return .run { _ in
service.configuration.overriding.temperature = temperature
}
case let .chatModelIdOverrideSelected(chatModelId):
state.chatModelIdOverride = chatModelId
return .run { _ in
service.configuration.overriding.modelId = chatModelId
}
case let .customCommandButtonTapped(command):
return .run { _ in
try await service.handleCustomCommand(command)
}
case .resetDefaultScopesButtonTapped:
return .run { _ in
service.resetDefaultScopes()
}
case let .toggleScope(scope):
return .run { _ in
service.defaultScopes.formSymmetricDifference([scope])
}
}
}
}
}
private actor TimedDebounceFunction {
let duration: TimeInterval
let block: () async -> Void
var task: Task?
var lastFireTime: Date = .init(timeIntervalSince1970: 0)
init(duration: TimeInterval, block: @escaping () async -> Void) {
self.duration = duration
self.block = block
}
func callAsFunction() async {
task?.cancel()
if lastFireTime.timeIntervalSinceNow < -duration {
await fire()
task = nil
} else {
task = Task.detached { [weak self, duration] in
try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
await self?.fire()
}
}
}
func fire() async {
lastFireTime = Date()
await block()
}
}
private func convertReference(
_ reference: ChatMessage.Reference
) -> DisplayedChatMessage.Reference {
.init(
title: reference.title,
subtitle: {
switch reference.kind {
case let .symbol(_, uri, _, _):
return uri
case let .webpage(uri):
return uri
case let .textFile(uri):
return uri
case let .other(kind):
return kind
case .text:
return reference.content
case .error:
return reference.content
}
}(),
uri: {
switch reference.kind {
case let .symbol(_, uri, _, _):
return uri
case let .webpage(uri):
return uri
case let .textFile(uri):
return uri
case .other:
return ""
case .text:
return ""
case .error:
return ""
}
}(),
startLine: {
switch reference.kind {
case let .symbol(_, _, startLine, _):
return startLine
default:
return nil
}
}(),
kind: reference.kind
)
}
================================================
FILE: Core/Sources/ChatGPTChatTab/ChatContextMenu.swift
================================================
import AppKit
import ChatService
import ComposableArchitecture
import SharedUIComponents
import SwiftUI
struct ChatTabItemView: View {
let chat: StoreOf
var body: some View {
WithPerceptionTracking {
Text(chat.title)
}
}
}
struct ChatContextMenu: View {
let store: StoreOf
@AppStorage(\.customCommands) var customCommands
@AppStorage(\.chatModels) var chatModels
@AppStorage(\.defaultChatFeatureChatModelId) var defaultChatModelId
@AppStorage(\.chatGPTTemperature) var defaultTemperature
var body: some View {
WithPerceptionTracking {
currentSystemPrompt
.onAppear { store.send(.appear) }
currentExtraSystemPrompt
resetPrompt
Divider()
chatModel
temperature
defaultScopes
Divider()
customCommandMenu
}
}
@ViewBuilder
var currentSystemPrompt: some View {
Text("System Prompt:")
Text({
var text = store.systemPrompt
if text.isEmpty { text = "N/A" }
if text.count > 30 { text = String(text.prefix(30)) + "..." }
return text
}() as String)
}
@ViewBuilder
var currentExtraSystemPrompt: some View {
Text("Extra Prompt:")
Text({
var text = store.extraSystemPrompt
if text.isEmpty { text = "N/A" }
if text.count > 30 { text = String(text.prefix(30)) + "..." }
return text
}() as String)
}
var resetPrompt: some View {
Button("Reset System Prompt") {
store.send(.resetPromptButtonTapped)
}
}
@ViewBuilder
var chatModel: some View {
let allModels = chatModels + [.init(
id: "com.github.copilot",
name: "GitHub Copilot Language Server",
format: .openAI,
info: .init()
)]
Menu("Chat Model") {
Button(action: {
store.send(.chatModelIdOverrideSelected(nil))
}) {
HStack {
if let defaultModel = allModels
.first(where: { $0.id == defaultChatModelId })
{
Text("Default (\(defaultModel.name))")
if store.chatModelIdOverride == nil {
Image(systemName: "checkmark")
}
} else {
Text("No Model Available")
}
}
}
if let id = store.chatModelIdOverride, !allModels.map(\.id).contains(id) {
Button(action: {
store.send(.chatModelIdOverrideSelected(nil))
}) {
HStack {
Text("Default (Selected Model Not Found)")
Image(systemName: "checkmark")
}
}
}
Divider()
ForEach(allModels, id: \.id) { model in
Button(action: {
store.send(.chatModelIdOverrideSelected(model.id))
}) {
HStack {
Text(model.name)
if model.id == store.chatModelIdOverride {
Image(systemName: "checkmark")
}
}
}
}
}
}
@ViewBuilder
var temperature: some View {
Menu("Temperature") {
Button(action: {
store.send(.temperatureOverrideSelected(nil))
}) {
HStack {
Text(
"Default (\(defaultTemperature.formatted(.number.precision(.fractionLength(1)))))"
)
if store.temperatureOverride == nil {
Image(systemName: "checkmark")
}
}
}
Divider()
ForEach(Array(stride(from: 0.0, through: 2.0, by: 0.1)), id: \.self) { value in
Button(action: {
store.send(.temperatureOverrideSelected(value))
}) {
HStack {
Text("\(value.formatted(.number.precision(.fractionLength(1))))")
if value == store.temperatureOverride {
Image(systemName: "checkmark")
}
}
}
}
}
}
@ViewBuilder
var defaultScopes: some View {
Menu("Default Scopes") {
Button(action: {
store.send(.resetDefaultScopesButtonTapped)
}) {
Text("Reset Default Scopes")
}
Divider()
ForEach(ChatService.Scope.allCases, id: \.rawValue) { value in
Button(action: {
store.send(.toggleScope(value))
}) {
HStack {
Text("@" + value.rawValue)
if store.defaultScopes.contains(value) {
Image(systemName: "checkmark")
}
}
}
}
}
}
var customCommandMenu: some View {
Menu("Custom Commands") {
ForEach(
customCommands.filter {
switch $0.feature {
case .chatWithSelection, .customChat: return true
case .promptToCode: return false
case .singleRoundDialog: return false
}
},
id: \.name
) { command in
Button(action: {
store.send(.customCommandButtonTapped(command))
}) {
Text(command.name)
}
}
}
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift
================================================
import ChatContextCollector
import ChatService
import ChatTab
import CodableWrappers
import Combine
import ComposableArchitecture
import DebounceFunction
import Foundation
import OpenAIService
import Preferences
import SwiftUI
/// A chat tab that provides a context aware chat bot, powered by ChatGPT.
public class ChatGPTChatTab: ChatTab {
public static var name: String { "Chat" }
public let service: ChatService
let chat: StoreOf
private var cancellable = Set()
private var observer = NSObject()
private let updateContentDebounce = DebounceRunner(duration: 0.5)
struct RestorableState: Codable {
var history: [OpenAIService.ChatMessage]
var configuration: OverridingChatGPTConfiguration.Overriding
var systemPrompt: String
var extraSystemPrompt: String
var defaultScopes: Set?
}
struct Builder: ChatTabBuilder {
var title: String
var customCommand: CustomCommand?
var afterBuild: (ChatGPTChatTab) async -> Void = { _ in }
func build(store: StoreOf) async -> (any ChatTab)? {
let tab = await ChatGPTChatTab(store: store)
if let customCommand {
try? await tab.service.handleCustomCommand(customCommand)
}
await afterBuild(tab)
return tab
}
}
public func buildView() -> any View {
ChatPanel(chat: chat)
}
public func buildTabItem() -> any View {
ChatTabItemView(chat: chat)
}
public func buildIcon() -> any View {
WithPerceptionTracking {
if self.chat.isReceivingMessage {
Image(systemName: "ellipsis.message")
} else {
Image(systemName: "message")
}
}
}
public func buildMenu() -> any View {
ChatContextMenu(store: chat.scope(state: \.chatMenu, action: \.chatMenu))
}
public func restorableState() async -> Data {
let state = RestorableState(
history: await service.memory.history,
configuration: service.configuration.overriding,
systemPrompt: service.systemPrompt,
extraSystemPrompt: service.extraSystemPrompt,
defaultScopes: service.defaultScopes
)
return (try? JSONEncoder().encode(state)) ?? Data()
}
public static func restore(from data: Data) async throws -> any ChatTabBuilder {
let state = try JSONDecoder().decode(RestorableState.self, from: data)
let builder = Builder(title: "Chat") { @MainActor tab in
tab.service.configuration.overriding = state.configuration
tab.service.mutateSystemPrompt(state.systemPrompt)
tab.service.mutateExtraSystemPrompt(state.extraSystemPrompt)
if let scopes = state.defaultScopes {
tab.service.defaultScopes = scopes
}
await tab.service.memory.mutateHistory { history in
history = state.history
}
tab.chat.send(.refresh)
}
return builder
}
public static func chatBuilders() -> [ChatTabBuilder] {
let customCommands = UserDefaults.shared.value(for: \.customCommands).compactMap {
command in
if case .customChat = command.feature {
return Builder(title: command.name, customCommand: command)
}
return nil
}
return [Builder(title: "Legacy Chat", customCommand: nil)] + customCommands
}
public static func defaultBuilder() -> ChatTabBuilder {
Builder(title: "Legacy Chat", customCommand: nil)
}
@MainActor
public init(service: ChatService = .init(), store: StoreOf) {
self.service = service
chat = .init(initialState: .init(), reducer: { Chat(service: service) })
super.init(store: store)
}
public func start() {
observer = .init()
cancellable = []
chatTabStore.send(.updateTitle("Chat"))
service.$systemPrompt.removeDuplicates().sink { [weak self] _ in
Task { @MainActor [weak self] in
self?.chatTabStore.send(.tabContentUpdated)
}
}.store(in: &cancellable)
service.$extraSystemPrompt.removeDuplicates().sink { [weak self] _ in
Task { @MainActor [weak self] in
self?.chatTabStore.send(.tabContentUpdated)
}
}.store(in: &cancellable)
Task { @MainActor in
var lastTrigger = -1
observer.observe { [weak self] in
guard let self else { return }
let trigger = chatTabStore.focusTrigger
guard lastTrigger != trigger else { return }
lastTrigger = trigger
Task { @MainActor [weak self] in
self?.chat.send(.focusOnTextField)
}
}
}
Task { @MainActor in
var lastTitle = ""
observer.observe { [weak self] in
guard let self else { return }
let title = self.chatTabStore.state.title
guard lastTitle != title else { return }
lastTitle = title
Task { @MainActor [weak self] in
self?.chatTabStore.send(.updateTitle(title))
}
}
}
Task { @MainActor in
observer.observe { [weak self] in
guard let self else { return }
_ = chat.history
_ = chat.title
_ = chat.isReceivingMessage
Task {
await self.updateContentDebounce.debounce { @MainActor [weak self] in
self?.chatTabStore.send(.tabContentUpdated)
}
}
}
}
}
public func handleCustomCommand(_ customCommand: CustomCommand) -> Bool {
Task {
if service.isReceivingMessage {
await service.stopReceivingMessage()
}
try? await service.handleCustomCommand(customCommand)
}
return true
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/ChatPanel.swift
================================================
import AppKit
import Combine
import ComposableArchitecture
import MarkdownUI
import OpenAIService
import SharedUIComponents
import SwiftUI
private let r: Double = 8
public struct ChatPanel: View {
let chat: StoreOf
@Namespace var inputAreaNamespace
public var body: some View {
VStack(spacing: 0) {
ChatPanelMessages(chat: chat)
Divider()
ChatPanelInputArea(chat: chat)
}
.background(Color(nsColor: .windowBackgroundColor))
.onAppear { chat.send(.appear) }
}
}
private struct ScrollViewOffsetPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat.zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
private struct ListHeightPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat.zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
struct ChatPanelMessages: View {
let chat: StoreOf
@State var cancellable = Set()
@State var isScrollToBottomButtonDisplayed = true
@Namespace var bottomID
@Namespace var topID
@Namespace var scrollSpace
@State var scrollOffset: Double = 0
@State var listHeight: Double = 0
@State var didScrollToBottomOnAppearOnce = false
@State var isBottomHidden = true
@Environment(\.isEnabled) var isEnabled
var body: some View {
WithPerceptionTracking {
ScrollViewReader { proxy in
GeometryReader { listGeo in
List {
Group {
Spacer(minLength: 12)
.id(topID)
Instruction(chat: chat)
ChatHistory(chat: chat)
.listItemTint(.clear)
ExtraSpacingInResponding(chat: chat)
Spacer(minLength: 12)
.id(bottomID)
.onAppear {
isBottomHidden = false
if !didScrollToBottomOnAppearOnce {
proxy.scrollTo(bottomID, anchor: .bottom)
didScrollToBottomOnAppearOnce = true
}
}
.onDisappear {
isBottomHidden = true
}
.background(GeometryReader { geo in
let offset = geo.frame(in: .named(scrollSpace)).minY
Color.clear.preference(
key: ScrollViewOffsetPreferenceKey.self,
value: offset
)
})
}
.modify { view in
if #available(macOS 13.0, *) {
view
.listRowSeparator(.hidden)
.listSectionSeparator(.hidden)
} else {
view
}
}
}
.listStyle(.plain)
.listRowBackground(EmptyView())
.modify { view in
if #available(macOS 13.0, *) {
view.scrollContentBackground(.hidden)
} else {
view
}
}
.coordinateSpace(name: scrollSpace)
.preference(
key: ListHeightPreferenceKey.self,
value: listGeo.size.height
)
.onPreferenceChange(ListHeightPreferenceKey.self) { value in
listHeight = value
updatePinningState()
}
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
scrollOffset = value
updatePinningState()
}
.overlay(alignment: .bottom) {
StopRespondingButton(chat: chat)
}
.overlay(alignment: .bottomTrailing) {
scrollToBottomButton(proxy: proxy)
}
.background {
PinToBottomHandler(chat: chat, isBottomHidden: isBottomHidden) {
proxy.scrollTo(bottomID, anchor: .bottom)
}
}
.onAppear {
proxy.scrollTo(bottomID, anchor: .bottom)
}
.task {
proxy.scrollTo(bottomID, anchor: .bottom)
}
}
}
.onAppear {
trackScrollWheel()
}
.onDisappear {
cancellable.forEach { $0.cancel() }
cancellable = []
}
.onChange(of: isEnabled) { isEnabled in
chat.send(.setIsEnabled(isEnabled))
}
}
}
func trackScrollWheel() {
NSApplication.shared.publisher(for: \.currentEvent)
.receive(on: DispatchQueue.main)
.filter { [chat] in
guard chat.withState(\.isEnabled) else { return false }
return $0?.type == .scrollWheel
}
.compactMap { $0 }
.sink { event in
guard chat.withState(\.isPinnedToBottom) else { return }
let delta = event.deltaY
let scrollUp = delta > 0
if scrollUp {
chat.send(.manuallyScrolledUp)
}
}
.store(in: &cancellable)
}
@MainActor
func updatePinningState() {
// where does the 32 come from?
withAnimation(.linear(duration: 0.1)) {
isScrollToBottomButtonDisplayed = scrollOffset > listHeight + 32 + 20
|| scrollOffset <= 0
}
}
@ViewBuilder
func scrollToBottomButton(proxy: ScrollViewProxy) -> some View {
Button(action: {
chat.send(.scrollToBottomButtonTapped)
withAnimation(.easeInOut(duration: 0.1)) {
proxy.scrollTo(bottomID, anchor: .bottom)
}
}) {
Image(systemName: "arrow.down")
.padding(4)
.background {
Circle()
.fill(.thickMaterial)
.shadow(color: .black.opacity(0.2), radius: 2)
}
.overlay {
Circle().stroke(Color(nsColor: .separatorColor), lineWidth: 1)
}
.foregroundStyle(.secondary)
.padding(4)
}
.keyboardShortcut(.downArrow, modifiers: [.command])
.opacity(isScrollToBottomButtonDisplayed ? 1 : 0)
.buttonStyle(.plain)
}
struct ExtraSpacingInResponding: View {
let chat: StoreOf
var body: some View {
WithPerceptionTracking {
if chat.isReceivingMessage {
Spacer(minLength: 12)
}
}
}
}
struct PinToBottomHandler: View {
let chat: StoreOf
let isBottomHidden: Bool
let scrollToBottom: () -> Void
@State var isInitialLoad = true
var body: some View {
WithPerceptionTracking {
EmptyView()
.onChange(of: chat.isReceivingMessage) { isReceiving in
if isReceiving {
Task {
await Task.yield()
withAnimation(.easeInOut(duration: 0.1)) {
scrollToBottom()
}
}
}
}
.onChange(of: chat.history.last) { _ in
if chat.withState(\.isPinnedToBottom) || isInitialLoad {
if isInitialLoad {
isInitialLoad = false
}
Task {
await Task.yield()
withAnimation(.easeInOut(duration: 0.1)) {
scrollToBottom()
}
}
}
}
.onChange(of: isBottomHidden) { value in
// This is important to prevent it from jumping to the top!
if value, chat.withState(\.isPinnedToBottom) {
scrollToBottom()
}
}
}
}
}
}
struct ChatHistory: View {
let chat: StoreOf
var body: some View {
WithPerceptionTracking {
ForEach(chat.history, id: \.id) { message in
WithPerceptionTracking {
ChatHistoryItem(chat: chat, message: message).id(message.id)
}
}
}
}
}
struct ChatHistoryItem: View {
let chat: StoreOf
let message: DisplayedChatMessage
var body: some View {
WithPerceptionTracking {
let text = message.text
let markdownContent = message.markdownContent
switch message.role {
case .user:
UserMessage(
id: message.id,
text: text,
markdownContent: markdownContent,
chat: chat
)
.listRowInsets(EdgeInsets(
top: 0,
leading: -8,
bottom: 0,
trailing: -8
))
.padding(.vertical, 4)
case .assistant:
BotMessage(
id: message.id,
text: text,
markdownContent: markdownContent,
references: message.references,
chat: chat
)
.listRowInsets(EdgeInsets(
top: 0,
leading: -8,
bottom: 0,
trailing: -8
))
.padding(.vertical, 4)
case .tool:
FunctionMessage(id: message.id, text: text)
case .ignored:
EmptyView()
}
}
}
}
private struct StopRespondingButton: View {
let chat: StoreOf
var body: some View {
WithPerceptionTracking {
if chat.isReceivingMessage {
Button(action: {
chat.send(.stopRespondingButtonTapped)
}) {
HStack(spacing: 4) {
Image(systemName: "stop.fill")
Text("Stop Responding")
}
.padding(8)
.background(
.regularMaterial,
in: RoundedRectangle(cornerRadius: r, style: .continuous)
)
.overlay {
RoundedRectangle(cornerRadius: r, style: .continuous)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
}
}
.buttonStyle(.borderless)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.bottom, 8)
.opacity(chat.isReceivingMessage ? 1 : 0)
.disabled(!chat.isReceivingMessage)
.transformEffect(.init(
translationX: 0,
y: chat.isReceivingMessage ? 0 : 20
))
}
}
}
}
struct ChatPanelInputArea: View {
let chat: StoreOf
@FocusState var focusedField: Chat.State.Field?
var body: some View {
HStack {
clearButton
InputAreaTextEditor(chat: chat, focusedField: $focusedField)
}
.padding(8)
.background(.ultraThickMaterial)
}
@MainActor
var clearButton: some View {
Button(action: {
chat.send(.clearButtonTap)
}) {
Group {
if #available(macOS 13.0, *) {
Image(systemName: "eraser.line.dashed.fill")
} else {
Image(systemName: "trash.fill")
}
}
.padding(6)
.background {
Circle().fill(Color(nsColor: .controlBackgroundColor))
}
.overlay {
Circle().stroke(Color(nsColor: .controlColor), lineWidth: 1)
}
}
.buttonStyle(.plain)
}
struct InputAreaTextEditor: View {
@Perception.Bindable var chat: StoreOf
var focusedField: FocusState.Binding
var body: some View {
WithPerceptionTracking {
HStack(spacing: 0) {
AutoresizingCustomTextEditor(
text: $chat.typedMessage,
font: .systemFont(ofSize: 14),
isEditable: true,
maxHeight: 400,
onSubmit: { chat.send(.sendButtonTapped) },
completions: chatAutoCompletion
)
.focused(focusedField, equals: .textField)
.bind($chat.focusedField, to: focusedField)
.padding(8)
.fixedSize(horizontal: false, vertical: true)
Button(action: {
chat.send(.sendButtonTapped)
}) {
Image(systemName: "paperplane.fill")
.padding(8)
}
.buttonStyle(.plain)
.disabled(chat.isReceivingMessage)
.keyboardShortcut(KeyEquivalent.return, modifiers: [])
}
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(Color(nsColor: .controlBackgroundColor))
}
.overlay {
RoundedRectangle(cornerRadius: 6)
.stroke(Color(nsColor: .controlColor), lineWidth: 1)
}
.background {
Button(action: {
chat.send(.returnButtonTapped)
}) {
EmptyView()
}
.keyboardShortcut(KeyEquivalent.return, modifiers: [.shift])
Button(action: {
focusedField.wrappedValue = .textField
}) {
EmptyView()
}
.keyboardShortcut("l", modifiers: [.command])
}
}
}
func chatAutoCompletion(text: String, proposed: [String], range: NSRange) -> [String] {
guard text.count == 1 else { return [] }
let plugins = [String]() // chat.pluginIdentifiers.map { "/\($0)" }
let availableFeatures = plugins + [
"/exit",
"@code",
"@sense",
"@project",
"@web",
]
let result: [String] = availableFeatures
.filter { $0.hasPrefix(text) && $0 != text }
.compactMap {
guard let index = $0.index(
$0.startIndex,
offsetBy: range.location,
limitedBy: $0.endIndex
) else { return nil }
return String($0[index...])
}
return result
}
}
}
// MARK: - Previews
struct ChatPanel_Preview: PreviewProvider {
static let history: [DisplayedChatMessage] = [
.init(
id: "1",
role: .user,
text: "**Hello**",
references: []
),
.init(
id: "2",
role: .assistant,
text: """
```swift
func foo() {}
```
**Hey**! What can I do for you?**Hey**! What can I do for you?**Hey**! What can I do for you?**Hey**! What can I do for you?
""",
references: [
.init(
title: "Hello Hello Hello Hello",
subtitle: "Hi Hi Hi Hi",
uri: "https://google.com",
startLine: nil,
kind: .symbol(.class, uri: "https://google.com", startLine: nil, endLine: nil)
),
]
),
.init(
id: "7",
role: .ignored,
text: "Ignored",
references: []
),
.init(
id: "6",
role: .tool,
text: """
Searching for something...
- abc
- [def](https://1.com)
> hello
> hi
""",
references: []
),
.init(
id: "5",
role: .assistant,
text: "Yooo",
references: []
),
.init(
id: "4",
role: .user,
text: "Yeeeehh",
references: []
),
.init(
id: "3",
role: .user,
text: #"""
Please buy me a coffee!
| Coffee | Milk |
|--------|------|
| Espresso | No |
| Latte | Yes |
```swift
func foo() {}
```
```objectivec
- (void)bar {}
```
"""#,
references: []
),
]
static var previews: some View {
ChatPanel(chat: .init(
initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: true),
reducer: { Chat(service: .init()) }
))
.frame(width: 450, height: 1200)
.colorScheme(.dark)
}
}
struct ChatPanel_EmptyChat_Preview: PreviewProvider {
static var previews: some View {
ChatPanel(chat: .init(
initialState: .init(history: [DisplayedChatMessage](), isReceivingMessage: false),
reducer: { Chat(service: .init()) }
))
.padding()
.frame(width: 450, height: 600)
.colorScheme(.dark)
}
}
struct ChatPanel_InputText_Preview: PreviewProvider {
static var previews: some View {
ChatPanel(chat: .init(
initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: false),
reducer: { Chat(service: .init()) }
))
.padding()
.frame(width: 450, height: 600)
.colorScheme(.dark)
}
}
struct ChatPanel_InputMultilineText_Preview: PreviewProvider {
static var previews: some View {
ChatPanel(
chat: .init(
initialState: .init(
typedMessage: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce turpis dolor, malesuada quis fringilla sit amet, placerat at nunc. Suspendisse orci tortor, tempor nec blandit a, malesuada vel tellus. Nunc sed leo ligula. Ut at ligula eget turpis pharetra tristique. Integer luctus leo non elit rhoncus fermentum.",
history: ChatPanel_Preview.history,
isReceivingMessage: false
),
reducer: { Chat(service: .init()) }
)
)
.padding()
.frame(width: 450, height: 600)
.colorScheme(.dark)
}
}
struct ChatPanel_Light_Preview: PreviewProvider {
static var previews: some View {
ChatPanel(chat: .init(
initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: true),
reducer: { Chat(service: .init()) }
))
.padding()
.frame(width: 450, height: 600)
.colorScheme(.light)
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/CodeBlockHighlighter.swift
================================================
import Combine
import ComposableArchitecture
import DebounceFunction
import Foundation
import MarkdownUI
import Perception
import SharedUIComponents
import SwiftUI
/// Use this instead of the built in ``CodeBlockView`` to highlight code blocks asynchronously,
/// so that the UI doesn't freeze when rendering large code blocks.
struct AsyncCodeBlockView: View {
@Perceptible
class Storage {
static let queue = DispatchQueue(
label: "chat-code-block-highlight",
qos: .userInteractive,
attributes: .concurrent
)
var highlighted: AttributedString?
@PerceptionIgnored var debounceFunction: DebounceFunction?
@PerceptionIgnored private var highlightTask: Task?
init() {
debounceFunction = .init(duration: 0.5, block: { [weak self] view in
self?.highlight(for: view)
})
}
func highlight(debounce: Bool, for view: AsyncCodeBlockView) {
if debounce {
Task { await debounceFunction?(view) }
} else {
highlight(for: view)
}
}
func highlight(for view: AsyncCodeBlockView) {
highlightTask?.cancel()
let content = view.content
let language = view.fenceInfo ?? ""
let brightMode = view.colorScheme != .dark
let font = CodeHighlighting.SendableFont(font: view.font)
highlightTask = Task {
let string = await withUnsafeContinuation { continuation in
Self.queue.async {
let content = CodeHighlighting.highlightedCodeBlock(
code: content,
language: language,
scenario: "chat",
brightMode: brightMode,
font:font
)
continuation.resume(returning: AttributedString(content))
}
}
try Task.checkCancellation()
await MainActor.run {
self.highlighted = string
}
}
}
}
let fenceInfo: String?
let content: String
let font: NSFont
@Environment(\.colorScheme) var colorScheme
@State var storage = Storage()
@AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme
@AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight
@AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight
@AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark
@AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark
init(fenceInfo: String?, content: String, font: NSFont) {
self.fenceInfo = fenceInfo
self.content = content.hasSuffix("\n") ? String(content.dropLast()) : content
self.font = font
}
var body: some View {
WithPerceptionTracking {
Group {
if let highlighted = storage.highlighted {
Text(highlighted)
} else {
Text(content).font(.init(font))
}
}
.onAppear {
storage.highlight(debounce: false, for: self)
}
.onChange(of: colorScheme) { _ in
storage.highlight(debounce: false, for: self)
}
.onChange(of: syncCodeHighlightTheme) { _ in
storage.highlight(debounce: true, for: self)
}
.onChange(of: codeForegroundColorLight) { _ in
storage.highlight(debounce: true, for: self)
}
.onChange(of: codeBackgroundColorLight) { _ in
storage.highlight(debounce: true, for: self)
}
.onChange(of: codeForegroundColorDark) { _ in
storage.highlight(debounce: true, for: self)
}
.onChange(of: codeBackgroundColorDark) { _ in
storage.highlight(debounce: true, for: self)
}
}
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Styles.swift
================================================
import AppKit
import MarkdownUI
import SharedUIComponents
import SwiftUI
extension Color {
static var contentBackground: Color {
Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
if appearance.isDarkMode {
return #colorLiteral(red: 0.1580096483, green: 0.1730263829, blue: 0.2026666105, alpha: 1)
}
return #colorLiteral(red: 0.9896564803, green: 0.9896564803, blue: 0.9896564803, alpha: 1)
}))
}
static var userChatContentBackground: Color {
Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
if appearance.isDarkMode {
return #colorLiteral(red: 0.2284317913, green: 0.2145925438, blue: 0.3214019983, alpha: 1)
}
return #colorLiteral(red: 0.9458052187, green: 0.9311983998, blue: 0.9906365955, alpha: 1)
}))
}
}
extension NSAppearance {
var isDarkMode: Bool {
if bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
return true
} else {
return false
}
}
}
extension View {
var messageBubbleCornerRadius: Double { 8 }
func codeBlockLabelStyle() -> some View {
relativeLineSpacing(.em(0.225))
.markdownTextStyle {
FontFamilyVariant(.monospaced)
FontSize(.em(0.85))
}
.padding(16)
.padding(.top, 14)
}
func codeBlockStyle(
_ configuration: CodeBlockConfiguration,
backgroundColor: Color,
labelColor: Color
) -> some View {
background(backgroundColor)
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay(alignment: .top) {
HStack(alignment: .center) {
Text(configuration.language ?? "code")
.foregroundStyle(labelColor)
.font(.callout.bold())
.padding(.leading, 8)
.lineLimit(1)
Spacer()
CopyButton {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(configuration.content, forType: .string)
}
}
}
.overlay {
RoundedRectangle(cornerRadius: 6).stroke(Color.primary.opacity(0.05), lineWidth: 1)
}
.markdownMargin(top: 4, bottom: 16)
}
}
final class VerticalScrollingFixHostingView: NSHostingView where Content: View {
override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool {
return axis == .vertical
}
}
struct VerticalScrollingFixViewRepresentable: NSViewRepresentable where Content: View {
let content: Content
func makeNSView(context: Context) -> NSHostingView {
return VerticalScrollingFixHostingView(rootView: content)
}
func updateNSView(_ nsView: NSHostingView, context: Context) {}
}
struct VerticalScrollingFixWrapper: View where Content: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VerticalScrollingFixViewRepresentable(content: self.content())
}
}
extension View {
/// https://stackoverflow.com/questions/64920744/swiftui-nested-scrollviews-problem-on-macos
@ViewBuilder func workaroundForVerticalScrollingBugInMacOS() -> some View {
VerticalScrollingFixWrapper { self }
}
}
struct RoundedCorners: Shape {
var tl: CGFloat = 0.0
var tr: CGFloat = 0.0
var bl: CGFloat = 0.0
var br: CGFloat = 0.0
func path(in rect: CGRect) -> Path {
Path { path in
let w = rect.size.width
let h = rect.size.height
// Make sure we do not exceed the size of the rectangle
let tr = min(min(self.tr, h / 2), w / 2)
let tl = min(min(self.tl, h / 2), w / 2)
let bl = min(min(self.bl, h / 2), w / 2)
let br = min(min(self.br, h / 2), w / 2)
path.move(to: CGPoint(x: w / 2.0, y: 0))
path.addLine(to: CGPoint(x: w - tr, y: 0))
path.addArc(
center: CGPoint(x: w - tr, y: tr),
radius: tr,
startAngle: Angle(degrees: -90),
endAngle: Angle(degrees: 0),
clockwise: false
)
path.addLine(to: CGPoint(x: w, y: h - br))
path.addArc(
center: CGPoint(x: w - br, y: h - br),
radius: br,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 90),
clockwise: false
)
path.addLine(to: CGPoint(x: bl, y: h))
path.addArc(
center: CGPoint(x: bl, y: h - bl),
radius: bl,
startAngle: Angle(degrees: 90),
endAngle: Angle(degrees: 180),
clockwise: false
)
path.addLine(to: CGPoint(x: 0, y: tl))
path.addArc(
center: CGPoint(x: tl, y: tl),
radius: tl,
startAngle: Angle(degrees: 180),
endAngle: Angle(degrees: 270),
clockwise: false
)
path.closeSubpath()
}
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Views/BotMessage.swift
================================================
import ComposableArchitecture
import Foundation
import MarkdownUI
import SharedUIComponents
import SwiftUI
struct BotMessage: View {
var r: Double { messageBubbleCornerRadius }
let id: String
let text: String
let markdownContent: MarkdownContent
let references: [DisplayedChatMessage.Reference]
let chat: StoreOf
@Environment(\.colorScheme) var colorScheme
@State var isReferencesPresented = false
@State var isReferencesHovered = false
var body: some View {
HStack(alignment: .bottom, spacing: 2) {
VStack(alignment: .leading, spacing: 16) {
if !references.isEmpty {
Button(action: {
isReferencesPresented.toggle()
}, label: {
HStack(spacing: 4) {
Image(systemName: "plus.circle")
Text("Used \(references.count) references")
}
.padding(8)
.background {
RoundedRectangle(cornerRadius: r - 4)
.foregroundStyle(Color(isReferencesHovered ? .black : .clear))
}
.overlay {
RoundedRectangle(cornerRadius: r - 4)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
}
.foregroundStyle(.secondary)
})
.buttonStyle(.plain)
.popover(isPresented: $isReferencesPresented, arrowEdge: .trailing) {
ReferenceList(references: references, chat: chat)
}
}
ThemedMarkdownText(markdownContent)
}
.frame(alignment: .trailing)
.padding()
.background {
RoundedCorners(tl: r, tr: r, bl: 0, br: r)
.fill(Color.contentBackground)
}
.overlay {
RoundedCorners(tl: r, tr: r, bl: 0, br: r)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
}
.padding(.leading, 8)
.shadow(color: .black.opacity(0.05), radius: 6)
.contextMenu {
Button("Copy") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(text, forType: .string)
}
Button("Set as Extra System Prompt") {
chat.send(.setAsExtraPromptButtonTapped(id))
}
Divider()
Button("Delete") {
chat.send(.deleteMessageButtonTapped(id))
}
}
CopyButton {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(text, forType: .string)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.trailing, 2)
}
}
struct ReferenceList: View {
let references: [DisplayedChatMessage.Reference]
let chat: StoreOf
var body: some View {
WithPerceptionTracking {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
ForEach(0.. MarkdownUI.Theme {
.gitHub.text {
ForegroundColor(.secondary)
BackgroundColor(Color.clear)
FontSize(fontSize - 1)
}
.list { configuration in
configuration.label
.markdownMargin(top: 4, bottom: 4)
}
.paragraph { configuration in
configuration.label
.markdownMargin(top: 0, bottom: 4)
}
.codeBlock { configuration in
configuration.label
.relativeLineSpacing(.em(0.225))
.markdownTextStyle {
FontFamilyVariant(.monospaced)
FontSize(.em(0.85))
}
.padding(16)
.background(Color(nsColor: .textBackgroundColor).opacity(0.7))
.clipShape(RoundedRectangle(cornerRadius: 6))
.markdownMargin(top: 4, bottom: 4)
}
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Views/FunctionMessage.swift
================================================
import Foundation
import MarkdownUI
import SwiftUI
struct FunctionMessage: View {
let id: String
let text: String
@AppStorage(\.chatFontSize) var chatFontSize
var body: some View {
Markdown(text)
.textSelection(.enabled)
.markdownTheme(.functionCall(fontSize: chatFontSize))
.padding(.vertical, 2)
.padding(.trailing, 2)
}
}
#Preview {
FunctionMessage(id: "1", text: """
Searching for something...
- abc
- [def](https://1.com)
> hello
> hi
""")
.padding()
.fixedSize()
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Views/InstructionMarkdownTheme.swift
================================================
import Foundation
import MarkdownUI
import SwiftUI
extension MarkdownUI.Theme {
static func instruction(fontSize: Double) -> MarkdownUI.Theme {
.gitHub.text {
ForegroundColor(.primary)
BackgroundColor(Color.clear)
FontSize(fontSize)
}
.code {
FontFamilyVariant(.monospaced)
FontSize(.em(0.85))
BackgroundColor(Color.secondary.opacity(0.2))
}
.codeBlock { configuration in
let wrapCode = UserDefaults.shared.value(for: \.wrapCodeInChatCodeBlock)
if wrapCode {
configuration.label
.codeBlockLabelStyle()
.codeBlockStyle(
configuration,
backgroundColor: Color(nsColor: .textBackgroundColor).opacity(0.7),
labelColor: Color.secondary.opacity(0.7)
)
} else {
ScrollView(.horizontal) {
configuration.label
.codeBlockLabelStyle()
}
.workaroundForVerticalScrollingBugInMacOS()
.codeBlockStyle(
configuration,
backgroundColor: Color(nsColor: .textBackgroundColor).opacity(0.7),
labelColor: Color.secondary.opacity(0.7)
)
}
}
.table { configuration in
configuration.label
.fixedSize(horizontal: false, vertical: true)
.markdownTableBorderStyle(.init(
color: .init(nsColor: .separatorColor),
strokeStyle: .init(lineWidth: 1)
))
.markdownTableBackgroundStyle(
.alternatingRows(Color.secondary.opacity(0.1), Color.secondary.opacity(0.2))
)
.markdownMargin(top: 0, bottom: 16)
}
.tableCell { configuration in
configuration.label
.markdownTextStyle {
if configuration.row == 0 {
FontWeight(.semibold)
}
BackgroundColor(nil)
}
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, 6)
.padding(.horizontal, 13)
.relativeLineSpacing(.em(0.25))
}
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Views/Instructions.swift
================================================
import ComposableArchitecture
import Foundation
import MarkdownUI
import SwiftUI
struct Instruction: View {
let chat: StoreOf
var body: some View {
WithPerceptionTracking {
Group {
Markdown(
"""
You can use plugins to perform various tasks.
| Plugin Name | Description |
| --- | --- |
| `/shell` | Runs a command under the project root |
| `/shortcut(name)` | Runs a shortcut from the Shortcuts.app, with the previous message as input |
To use plugins, you can prefix a message with `/pluginName`.
"""
)
.modifier(InstructionModifier())
Markdown(
"""
You can use scopes to give the bot extra abilities.
| Scope Name | Abilities |
| --- | --- |
| `@file` | Read the metadata of the editing file |
| `@code` | Read the code and metadata in the editing file |
| `@sense`| Experimental. Read the relevant code of the focused editor |
| `@project` | Experimental. Access content of the project |
| `@web` (beta) | Search on Bing or query from a web page |
To use scopes, you can prefix a message with `@code`.
You can use shorthand to represent a scope, such as `@c`, and enable multiple scopes with `@c+web`.
"""
)
.modifier(InstructionModifier())
let scopes = chat.chatMenu.defaultScopes
Markdown(
"""
Hello, I am your AI programming assistant. I can identify issues, explain and even improve code.
\({
if scopes.isEmpty {
return "No scope is enabled by default"
} else {
let scopes = scopes.map(\.rawValue).sorted()
.joined(separator: ", ")
return "Default scopes: `\(scopes)`"
}
}())
"""
)
.modifier(InstructionModifier())
}
}
}
struct InstructionModifier: ViewModifier {
@AppStorage(\.chatFontSize) var chatFontSize
func body(content: Content) -> some View {
content
.textSelection(.enabled)
.markdownTheme(.instruction(fontSize: chatFontSize))
.opacity(0.8)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
}
}
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Views/ThemedMarkdownText.swift
================================================
import Foundation
import MarkdownUI
import SwiftUI
struct ThemedMarkdownText: View {
@AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme
@AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight
@AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight
@AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark
@AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark
@AppStorage(\.chatFontSize) var chatFontSize
@AppStorage(\.chatCodeFont) var chatCodeFont
@Environment(\.colorScheme) var colorScheme
let content: MarkdownContent
init(_ text: String) {
content = .init(text)
}
init(_ content: MarkdownContent) {
self.content = content
}
var body: some View {
Markdown(content)
.textSelection(.enabled)
.markdownTheme(.custom(
fontSize: chatFontSize,
codeFont: chatCodeFont.value.nsFont,
codeBlockBackgroundColor: {
if syncCodeHighlightTheme {
if colorScheme == .light, let color = codeBackgroundColorLight.value {
return color.swiftUIColor
} else if let color = codeBackgroundColorDark.value {
return color.swiftUIColor
}
}
return Color(nsColor: .textBackgroundColor).opacity(0.7)
}(),
codeBlockLabelColor: {
if syncCodeHighlightTheme {
if colorScheme == .light,
let color = codeForegroundColorLight.value
{
return color.swiftUIColor.opacity(0.5)
} else if let color = codeForegroundColorDark.value {
return color.swiftUIColor.opacity(0.5)
}
}
return Color.secondary.opacity(0.7)
}()
))
}
}
// MARK: - Theme
extension MarkdownUI.Theme {
static func custom(
fontSize: Double,
codeFont: NSFont,
codeBlockBackgroundColor: Color,
codeBlockLabelColor: Color
) -> MarkdownUI.Theme {
.gitHub.text {
ForegroundColor(.primary)
BackgroundColor(Color.clear)
FontSize(fontSize)
}
.codeBlock { configuration in
let wrapCode = UserDefaults.shared.value(for: \.wrapCodeInChatCodeBlock)
|| [
"plaintext", "text", "markdown", "sh", "console", "bash", "shell", "latex",
"tex"
]
.contains(configuration.language)
if wrapCode {
AsyncCodeBlockView(
fenceInfo: configuration.language,
content: configuration.content,
font: codeFont
)
.codeBlockLabelStyle()
.codeBlockStyle(
configuration,
backgroundColor: codeBlockBackgroundColor,
labelColor: codeBlockLabelColor
)
} else {
ScrollView(.horizontal) {
AsyncCodeBlockView(
fenceInfo: configuration.language,
content: configuration.content,
font: codeFont
)
.codeBlockLabelStyle()
}
.workaroundForVerticalScrollingBugInMacOS()
.codeBlockStyle(
configuration,
backgroundColor: codeBlockBackgroundColor,
labelColor: codeBlockLabelColor
)
}
}
}
}
================================================
FILE: Core/Sources/ChatGPTChatTab/Views/UserMessage.swift
================================================
import ComposableArchitecture
import Foundation
import MarkdownUI
import SwiftUI
struct UserMessage: View {
var r: Double { messageBubbleCornerRadius }
let id: String
let text: String
let markdownContent: MarkdownContent
let chat: StoreOf
@Environment(\.colorScheme) var colorScheme
var body: some View {
ThemedMarkdownText(markdownContent)
.frame(alignment: .leading)
.padding()
.background {
RoundedCorners(tl: r, tr: r, bl: r, br: 0)
.fill(Color.userChatContentBackground)
}
.overlay {
RoundedCorners(tl: r, tr: r, bl: r, br: 0)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
}
.padding(.leading)
.padding(.trailing, 8)
.shadow(color: .black.opacity(0.05), radius: 6)
.frame(maxWidth: .infinity, alignment: .trailing)
.contextMenu {
Button("Copy") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(text, forType: .string)
}
Button("Send Again") {
chat.send(.resendMessageButtonTapped(id))
}
Button("Set as Extra System Prompt") {
chat.send(.setAsExtraPromptButtonTapped(id))
}
Divider()
Button("Delete") {
chat.send(.deleteMessageButtonTapped(id))
}
}
}
}
#Preview {
let text = #"""
Please buy me a coffee!
| Coffee | Milk |
|--------|------|
| Espresso | No |
| Latte | Yes |
```swift
func foo() {}
```
```objectivec
- (void)bar {}
```
"""#
return UserMessage(
id: "A",
text: text,
markdownContent: .init(text),
chat: .init(
initialState: .init(history: [] as [DisplayedChatMessage], isReceivingMessage: false),
reducer: { Chat(service: .init()) }
)
)
.padding()
.fixedSize(horizontal: true, vertical: true)
}
================================================
FILE: Core/Sources/ChatService/AllContextCollector.swift
================================================
import ActiveDocumentChatContextCollector
import ChatContextCollector
import SystemInfoChatContextCollector
import WebChatContextCollector
#if canImport(ProChatContextCollectors)
import ProChatContextCollectors
let allContextCollectors: [any ChatContextCollector] = [
SystemInfoChatContextCollector(),
WebChatContextCollector(),
ProChatContextCollectors(),
]
#else
let allContextCollectors: [any ChatContextCollector] = [
SystemInfoChatContextCollector(),
ActiveDocumentChatContextCollector(),
WebChatContextCollector(),
]
#endif
================================================
FILE: Core/Sources/ChatService/AllPlugins.swift
================================================
import ChatBasic
import Foundation
import OpenAIService
import ShortcutChatPlugin
import TerminalChatPlugin
let allPlugins: [LegacyChatPlugin.Type] = [
LegacyChatPluginWrapper.self,
LegacyChatPluginWrapper.self,
]
protocol LegacyChatPlugin: AnyObject {
static var command: String { get }
var name: String { get }
init(inside chatGPTService: any LegacyChatGPTServiceType, delegate: LegacyChatPluginDelegate)
func send(content: String, originalMessage: String) async
func cancel() async
func stopResponding() async
}
protocol LegacyChatPluginDelegate: AnyObject {
func pluginDidStart(_ plugin: LegacyChatPlugin)
func pluginDidEnd(_ plugin: LegacyChatPlugin)
func pluginDidStartResponding(_ plugin: LegacyChatPlugin)
func pluginDidEndResponding(_ plugin: LegacyChatPlugin)
func shouldStartAnotherPlugin(_ type: LegacyChatPlugin.Type, withContent: String)
}
final class LegacyChatPluginWrapper: LegacyChatPlugin {
static var command: String { Plugin.command }
var name: String { Plugin.name }
let chatGPTService: any LegacyChatGPTServiceType
weak var delegate: LegacyChatPluginDelegate?
var isCancelled = false
required init(
inside chatGPTService: any LegacyChatGPTServiceType,
delegate: any LegacyChatPluginDelegate
) {
self.chatGPTService = chatGPTService
self.delegate = delegate
}
func send(content: String, originalMessage: String) async {
delegate?.pluginDidStart(self)
delegate?.pluginDidStartResponding(self)
let id = "\(Self.command)-\(UUID().uuidString)"
var reply = ChatMessage(id: id, role: .assistant, content: "")
await chatGPTService.memory.mutateHistory { history in
history.append(.init(role: .user, content: originalMessage))
}
let plugin = Plugin()
let stream = await plugin.sendForComplicatedResponse(.init(
text: content,
arguments: [],
history: chatGPTService.memory.history
))
do {
var actions = [(id: String, name: String)]()
var actionResults = [String: String]()
var message = ""
for try await response in stream {
guard !isCancelled else { break }
if Task.isCancelled { break }
switch response {
case .status:
break
case let .content(content):
switch content {
case let .text(token):
message.append(token)
}
case .attachments:
break
case let .startAction(id, task):
actions.append((id: id, name: task))
case let .finishAction(id, result):
actionResults[id] = switch result {
case let .failure(error):
error
case let .success(result):
result
}
case .references:
break
case .startNewMessage:
break
case .reasoning:
break
}
await chatGPTService.memory.mutateHistory { history in
if history.last?.id == id {
history.removeLast()
}
let actionString = actions.map {
"> \($0.name): \(actionResults[$0.id] ?? "...")"
}.joined(separator: "\n>\n")
if message.isEmpty {
reply.content = actionString
} else {
reply.content = """
\(actionString)
\(message)
"""
}
history.append(reply)
}
}
} catch {
await chatGPTService.memory.mutateHistory { history in
if history.last?.id == id {
history.removeLast()
}
reply.content = error.localizedDescription
history.append(reply)
}
}
delegate?.pluginDidEndResponding(self)
delegate?.pluginDidEnd(self)
}
func cancel() async {
isCancelled = true
}
func stopResponding() async {
isCancelled = true
}
}
================================================
FILE: Core/Sources/ChatService/ChatFunctionProvider.swift
================================================
import ChatBasic
import Foundation
import OpenAIService
final class ChatFunctionProvider {
var functions: [any ChatGPTFunction] = []
init() {}
func removeAll() {
functions = []
}
func append(functions others: [any ChatGPTFunction]) {
functions.append(contentsOf: others)
}
}
extension ChatFunctionProvider: ChatGPTFunctionProvider {
var functionCallStrategy: OpenAIService.FunctionCallStrategy? {
nil
}
}
================================================
FILE: Core/Sources/ChatService/ChatPluginController.swift
================================================
import Combine
import Foundation
import LegacyChatPlugin
import OpenAIService
final class ChatPluginController {
let chatGPTService: any LegacyChatGPTServiceType
let plugins: [String: LegacyChatPlugin.Type]
var runningPlugin: LegacyChatPlugin?
weak var chatService: ChatService?
init(chatGPTService: any LegacyChatGPTServiceType, plugins: [LegacyChatPlugin.Type]) {
self.chatGPTService = chatGPTService
var all = [String: LegacyChatPlugin.Type]()
for plugin in plugins {
all[plugin.command.lowercased()] = plugin
}
self.plugins = all
}
convenience init(
chatGPTService: any LegacyChatGPTServiceType,
plugins: LegacyChatPlugin.Type...
) {
self.init(chatGPTService: chatGPTService, plugins: plugins)
}
/// Handle the message in a plugin if required. Return false if no plugin handles the message.
func handleContent(_ content: String) async throws -> Bool {
// look for the prefix of content, see if there is something like /command.
// If there is, then we need to find the plugin that can handle this command.
// If there is no such plugin, then we just send the message to the GPT service.
let regex = try NSRegularExpression(pattern: #"^\/([a-zA-Z0-9]+)"#)
let matches = regex.matches(in: content, range: NSRange(content.startIndex..., in: content))
if let match = matches.first {
let command = String(content[Range(match.range(at: 1), in: content)!]).lowercased()
// handle exit plugin
if command == "exit" {
if let plugin = runningPlugin {
runningPlugin = nil
_ = await chatGPTService.memory.mutateHistory { history in
history.append(.init(
role: .user,
content: "",
summary: "Exit plugin \(plugin.name)."
))
history.append(.init(
role: .system,
content: "",
summary: "Exited plugin \(plugin.name)."
))
}
} else {
_ = await chatGPTService.memory.mutateHistory { history in
history.append(.init(
role: .system,
content: "",
summary: "No plugin running."
))
}
}
return true
}
// pass message to running plugin
if let runningPlugin {
await runningPlugin.send(content: content, originalMessage: content)
return true
}
// pass message to new plugin
if let pluginType = plugins[command] {
let plugin = pluginType.init(inside: chatGPTService, delegate: self)
if #available(macOS 13.0, *) {
await plugin.send(
content: String(
content.dropFirst(command.count + 1)
.trimmingPrefix(while: { $0 == " " })
),
originalMessage: content
)
} else {
await plugin.send(
content: String(content.dropFirst(command.count + 1)),
originalMessage: content
)
}
return true
}
return false
} else if let runningPlugin {
// pass message to running plugin
await runningPlugin.send(content: content, originalMessage: content)
return true
} else {
return false
}
}
func stopResponding() async {
await runningPlugin?.stopResponding()
}
func cancel() async {
await runningPlugin?.cancel()
}
}
// MARK: - ChatPluginDelegate
extension ChatPluginController: LegacyChatPluginDelegate {
public func pluginDidStartResponding(_: LegacyChatPlugin) {
chatService?.isReceivingMessage = true
}
public func pluginDidEndResponding(_: LegacyChatPlugin) {
chatService?.isReceivingMessage = false
}
public func pluginDidStart(_ plugin: LegacyChatPlugin) {
runningPlugin = plugin
}
public func pluginDidEnd(_ plugin: LegacyChatPlugin) {
if runningPlugin === plugin {
runningPlugin = nil
}
}
public func shouldStartAnotherPlugin(
_ type: LegacyChatPlugin.Type,
withContent content: String
) {
let plugin = type.init(inside: chatGPTService, delegate: self)
Task {
await plugin.send(content: content, originalMessage: content)
}
}
}
================================================
FILE: Core/Sources/ChatService/ChatService.swift
================================================
import ChatContextCollector
import LegacyChatPlugin
import Combine
import CustomCommandTemplateProcessor
import Foundation
import OpenAIService
import Preferences
public final class ChatService: ObservableObject {
public typealias Scope = ChatContext.Scope
public let memory: ContextAwareAutoManagedChatGPTMemory
public let configuration: OverridingChatGPTConfiguration
public let chatGPTService: any LegacyChatGPTServiceType
public var allPluginCommands: [String] { allPlugins.map { $0.command } }
@Published public internal(set) var chatHistory: [ChatMessage] = []
@Published public internal(set) var isReceivingMessage = false
@Published public internal(set) var systemPrompt = UserDefaults.shared
.value(for: \.defaultChatSystemPrompt)
@Published public internal(set) var extraSystemPrompt = ""
@Published public var defaultScopes = Set()
let pluginController: ChatPluginController
var cancellable = Set()
init(
memory: ContextAwareAutoManagedChatGPTMemory,
configuration: OverridingChatGPTConfiguration,
chatGPTService: T
) {
self.memory = memory
self.configuration = configuration
self.chatGPTService = chatGPTService
pluginController = ChatPluginController(
chatGPTService: chatGPTService,
plugins: allPlugins
)
pluginController.chatService = self
}
public convenience init() {
let configuration = UserPreferenceChatGPTConfiguration().overriding()
/// Used by context collector
let extraConfiguration = configuration.overriding()
extraConfiguration.textWindowTerminator = {
guard let last = $0.last else { return false }
return last.isNewline || last.isPunctuation
}
let memory = ContextAwareAutoManagedChatGPTMemory(
configuration: extraConfiguration,
functionProvider: ChatFunctionProvider()
)
self.init(
memory: memory,
configuration: configuration,
chatGPTService: LegacyChatGPTService(
memory: memory,
configuration: extraConfiguration,
functionProvider: memory.functionProvider
)
)
resetDefaultScopes()
memory.chatService = self
memory.observeHistoryChange { [weak self] in
Task { [weak self] in
self?.chatHistory = await memory.history
}
}
}
public func resetDefaultScopes() {
var scopes = Set()
if UserDefaults.shared.value(for: \.enableFileScopeByDefaultInChatContext) {
scopes.insert(.file)
}
if UserDefaults.shared.value(for: \.enableCodeScopeByDefaultInChatContext) {
scopes.insert(.code)
}
if UserDefaults.shared.value(for: \.enableProjectScopeByDefaultInChatContext) {
scopes.insert(.project)
}
if UserDefaults.shared.value(for: \.enableSenseScopeByDefaultInChatContext) {
scopes.insert(.sense)
}
if UserDefaults.shared.value(for: \.enableWebScopeByDefaultInChatContext) {
scopes.insert(.web)
}
defaultScopes = scopes
}
public func send(content: String) async throws {
memory.contextController.defaultScopes = defaultScopes
guard !isReceivingMessage else { throw CancellationError() }
let handledInPlugin = try await pluginController.handleContent(content)
if handledInPlugin { return }
isReceivingMessage = true
defer { isReceivingMessage = false }
let stream = try await chatGPTService.send(content: content, summary: nil)
do {
for try await _ in stream {
try Task.checkCancellation()
}
} catch {}
}
public func sendAndWait(content: String) async throws -> String {
try await send(content: content)
if let reply = await memory.history.last(where: { $0.role == .assistant })?.content {
return reply
}
return ""
}
public func stopReceivingMessage() async {
await pluginController.stopResponding()
await chatGPTService.stopReceivingMessage()
isReceivingMessage = false
// if it's stopped before the tool calls finish, remove the message.
await memory.mutateHistory { history in
if history.last?.role == .assistant, history.last?.toolCalls != nil {
history.removeLast()
}
}
}
public func clearHistory() async {
await pluginController.cancel()
await memory.clearHistory()
await chatGPTService.stopReceivingMessage()
isReceivingMessage = false
}
public func resetPrompt() async {
systemPrompt = UserDefaults.shared.value(for: \.defaultChatSystemPrompt)
extraSystemPrompt = ""
}
public func deleteMessage(id: String) async {
await memory.removeMessage(id)
}
public func resendMessage(id: String) async throws {
if let message = (await memory.history).first(where: { $0.id == id }),
let content = message.content
{
try await send(content: content)
}
}
public func setMessageAsExtraPrompt(id: String) async {
if let message = (await memory.history).first(where: { $0.id == id }),
let content = message.content
{
mutateExtraSystemPrompt(content)
await mutateHistory { history in
history.append(.init(
role: .assistant,
content: "",
summary: "System prompt updated."
))
}
}
}
/// Setting it to `nil` to reset the system prompt
public func mutateSystemPrompt(_ newPrompt: String?) {
systemPrompt = newPrompt ?? UserDefaults.shared.value(for: \.defaultChatSystemPrompt)
}
public func mutateExtraSystemPrompt(_ newPrompt: String) {
extraSystemPrompt = newPrompt
}
public func mutateHistory(_ mutator: @escaping (inout [ChatMessage]) -> Void) async {
await memory.mutateHistory(mutator)
}
public func handleCustomCommand(_ command: CustomCommand) async throws {
struct CustomCommandInfo {
var specifiedSystemPrompt: String?
var extraSystemPrompt: String?
var sendingMessageImmediately: String?
var name: String?
}
let info: CustomCommandInfo? = {
switch command.feature {
case let .chatWithSelection(extraSystemPrompt, prompt, useExtraSystemPrompt):
let updatePrompt = useExtraSystemPrompt ?? true
return .init(
extraSystemPrompt: updatePrompt ? extraSystemPrompt : nil,
sendingMessageImmediately: prompt,
name: command.name
)
case let .customChat(systemPrompt, prompt):
memory.contextController.defaultScopes = []
return .init(
specifiedSystemPrompt: systemPrompt,
extraSystemPrompt: "",
sendingMessageImmediately: prompt,
name: command.name
)
case .promptToCode: return nil
case .singleRoundDialog: return nil
}
}()
guard let info else { return }
let templateProcessor = CustomCommandTemplateProcessor()
if let specifiedSystemPrompt = info.specifiedSystemPrompt {
await mutateSystemPrompt(templateProcessor.process(specifiedSystemPrompt))
}
if let extraSystemPrompt = info.extraSystemPrompt {
await mutateExtraSystemPrompt(templateProcessor.process(extraSystemPrompt))
} else {
mutateExtraSystemPrompt("")
}
let customCommandPrefix = {
if let name = info.name { return "[\(name)] " }
return ""
}()
if info.specifiedSystemPrompt != nil || info.extraSystemPrompt != nil {
await mutateHistory { history in
history.append(.init(
role: .assistant,
content: "",
summary: "\(customCommandPrefix)System prompt is updated."
))
}
}
if let sendingMessageImmediately = info.sendingMessageImmediately,
!sendingMessageImmediately.isEmpty
{
try await send(content: templateProcessor.process(sendingMessageImmediately))
}
}
public func handleSingleRoundDialogCommand(
systemPrompt: String?,
overwriteSystemPrompt: Bool,
prompt: String
) async throws -> String {
let templateProcessor = CustomCommandTemplateProcessor()
if let systemPrompt {
if overwriteSystemPrompt {
await mutateSystemPrompt(templateProcessor.process(systemPrompt))
} else {
await mutateExtraSystemPrompt(templateProcessor.process(systemPrompt))
}
}
return try await sendAndWait(content: templateProcessor.process(prompt))
}
public func processMessage(
systemPrompt: String?,
extraSystemPrompt: String?,
prompt: String
) async throws -> String {
let templateProcessor = CustomCommandTemplateProcessor()
if let systemPrompt {
await mutateSystemPrompt(templateProcessor.process(systemPrompt))
}
if let extraSystemPrompt {
await mutateExtraSystemPrompt(templateProcessor.process(extraSystemPrompt))
}
return try await sendAndWait(content: templateProcessor.process(prompt))
}
}
================================================
FILE: Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift
================================================
import Foundation
import OpenAIService
public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory {
private let memory: AutoManagedChatGPTMemory
let contextController: DynamicContextController
let functionProvider: ChatFunctionProvider
weak var chatService: ChatService?
public var history: [ChatMessage] {
get async { await memory.history }
}
func observeHistoryChange(_ observer: @escaping () -> Void) {
memory.observeHistoryChange(observer)
}
init(
configuration: OverridingChatGPTConfiguration,
functionProvider: ChatFunctionProvider
) {
memory = AutoManagedChatGPTMemory(
systemPrompt: "",
configuration: configuration,
functionProvider: functionProvider,
maxNumberOfMessages: UserDefaults.shared.value(for: \.chatGPTMaxMessageCount)
)
contextController = DynamicContextController(
memory: memory,
functionProvider: functionProvider,
configuration: configuration,
contextCollectors: allContextCollectors
)
self.functionProvider = functionProvider
}
public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) async {
await memory.mutateHistory(update)
}
public func generatePrompt() async -> ChatGPTPrompt {
let content = (await memory.history)
.last(where: { $0.role == .user })?.content
try? await contextController.collectContextInformation(
systemPrompt: """
\(chatService?.systemPrompt ?? "")
\(chatService?.extraSystemPrompt ?? "")
""".trimmingCharacters(in: .whitespacesAndNewlines),
content: content ?? ""
)
return await memory.generatePrompt()
}
}
================================================
FILE: Core/Sources/ChatService/DynamicContextController.swift
================================================
import ChatContextCollector
import Foundation
import OpenAIService
import Preferences
import XcodeInspector
final class DynamicContextController {
let contextCollectors: [ChatContextCollector]
let memory: AutoManagedChatGPTMemory
let functionProvider: ChatFunctionProvider
let configuration: OverridingChatGPTConfiguration
var defaultScopes = [] as Set
convenience init(
memory: AutoManagedChatGPTMemory,
functionProvider: ChatFunctionProvider,
configuration: OverridingChatGPTConfiguration,
contextCollectors: ChatContextCollector...
) {
self.init(
memory: memory,
functionProvider: functionProvider,
configuration: configuration,
contextCollectors: contextCollectors
)
}
init(
memory: AutoManagedChatGPTMemory,
functionProvider: ChatFunctionProvider,
configuration: OverridingChatGPTConfiguration,
contextCollectors: [ChatContextCollector]
) {
self.memory = memory
self.functionProvider = functionProvider
self.configuration = configuration
self.contextCollectors = contextCollectors
}
func collectContextInformation(systemPrompt: String, content: String) async throws {
var content = content
var scopes = Self.parseScopes(&content)
scopes.formUnion(defaultScopes)
let overridingChatModelId = {
var ids = [String]()
if scopes.contains(.sense) {
ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForSenseScope))
}
if scopes.contains(.project) {
ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForProjectScope))
}
if scopes.contains(.web) {
ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForWebScope))
}
let chatModels = UserDefaults.shared.value(for: \.chatModels)
let idIndexMap = chatModels.enumerated().reduce(into: [String: Int]()) {
$0[$1.element.id] = $1.offset
}
return ids.filter { !$0.isEmpty }.sorted(by: {
let lhs = idIndexMap[$0] ?? Int.max
let rhs = idIndexMap[$1] ?? Int.max
return lhs < rhs
}).first
}()
configuration.overriding.modelId = overridingChatModelId
functionProvider.removeAll()
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
let oldMessages = await memory.history
let contexts = await withTaskGroup(
of: ChatContext.self
) { [scopes, content, configuration] group in
for collector in contextCollectors {
group.addTask {
await collector.generateContext(
history: oldMessages,
scopes: scopes,
content: content,
configuration: configuration
)
}
}
var contexts = [ChatContext]()
for await context in group {
contexts.append(context)
}
return contexts
}
let contextSystemPrompt = contexts
.map(\.systemPrompt)
.filter { !$0.isEmpty }
.joined(separator: "\n\n")
let retrievedContent = contexts
.flatMap(\.retrievedContent)
.filter { !$0.document.content.isEmpty }
.sorted { $0.priority > $1.priority }
.prefix(15)
let contextualSystemPrompt = """
\(language.isEmpty ? "" : "You must always reply in \(language)")
\(systemPrompt)
""".trimmingCharacters(in: .whitespacesAndNewlines)
await memory.mutateSystemPrompt(contextualSystemPrompt)
await memory.mutateContextSystemPrompt(contextSystemPrompt)
await memory.mutateRetrievedContent(retrievedContent.map(\.document))
functionProvider.append(functions: contexts.flatMap(\.functions))
}
}
extension DynamicContextController {
static func parseScopes(_ prompt: inout String) -> Set {
let parser = MessageScopeParser()
return parser(&prompt)
}
}
================================================
FILE: Core/Sources/Client/XPCService.swift
================================================
import Foundation
import Logger
import os.log
import XPCShared
let shared = XPCExtensionService(logger: .client)
public func getService() throws -> XPCExtensionService {
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
struct RunningInPreview: Error {}
throw RunningInPreview()
}
return shared
}
================================================
FILE: Core/Sources/FileChangeChecker/FileChangeChecker.swift
================================================
import CryptoKit
import Dispatch
import Foundation
/// Check that a file is changed.
public actor FileChangeChecker {
let url: URL
var checksum: Data?
public init(fileURL: URL) async {
url = fileURL
checksum = getChecksum()
}
public func checkIfChanged() -> Bool {
guard let newChecksum = getChecksum() else { return false }
return newChecksum != checksum
}
func getChecksum() -> Data? {
let bufferSize = 16 * 1024
guard let file = try? FileHandle(forReadingFrom: url) else { return nil }
defer { try? file.close() }
var md5 = CryptoKit.Insecure.MD5()
while autoreleasepool(invoking: {
let data = file.readData(ofLength: bufferSize)
if !data.isEmpty {
md5.update(data: data)
return true // Continue
} else {
return false // End of file
}
}) {}
let data = Data(md5.finalize())
return data
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift
================================================
import ComposableArchitecture
import SharedUIComponents
import SwiftUI
struct APIKeyManagementView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
VStack(spacing: 0) {
HStack {
Button(action: {
store.send(.closeButtonClicked)
}) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
.padding()
}
.buttonStyle(.plain)
Text("API Keys")
Spacer()
Button(action: {
store.send(.addButtonClicked)
}) {
Image(systemName: "plus.circle.fill")
.foregroundStyle(.secondary)
.padding()
}
.buttonStyle(.plain)
}
.background(Color(nsColor: .separatorColor))
List {
ForEach(store.availableAPIKeyNames, id: \.self) { name in
WithPerceptionTracking {
HStack {
Text(name)
.contextMenu {
Button("Remove") {
store.send(.deleteButtonClicked(name: name))
}
}
Spacer()
Button(action: {
store.send(.deleteButtonClicked(name: name))
}) {
Image(systemName: "trash.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
}
.modify { view in
if #available(macOS 13.0, *) {
view.listRowSeparator(.hidden).listSectionSeparator(.hidden)
} else {
view
}
}
}
.removeBackground()
.overlay {
if store.availableAPIKeyNames.isEmpty {
Text("""
Empty
Add a new key by clicking the add button
""")
.multilineTextAlignment(.center)
.padding()
}
}
}
.focusable(false)
.frame(width: 300, height: 400)
.background(.thickMaterial)
.onAppear {
store.send(.appear)
}
.sheet(item: $store.scope(
state: \.apiKeySubmission,
action: \.apiKeySubmission
)) { store in
WithPerceptionTracking {
APIKeySubmissionView(store: store)
.frame(minWidth: 400)
}
}
}
}
}
struct APIKeySubmissionView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
ScrollView {
VStack(spacing: 0) {
Form {
TextField("Name", text: $store.name)
SecureField("Key", text: $store.key)
}
.padding()
Divider()
HStack {
Spacer()
Button("Cancel") { store.send(.cancelButtonClicked) }
.keyboardShortcut(.cancelAction)
Button("Save", action: { store.send(.saveButtonClicked) })
.keyboardShortcut(.defaultAction)
}.padding()
}
}
.textFieldStyle(.roundedBorder)
}
}
}
class APIKeyManagementView_Preview: PreviewProvider {
static var previews: some View {
APIKeyManagementView(
store: .init(
initialState: .init(
availableAPIKeyNames: ["test1", "test2"]
),
reducer: { APIKeyManagement() }
)
)
}
}
class APIKeySubmissionView_Preview: PreviewProvider {
static var previews: some View {
APIKeySubmissionView(
store: .init(
initialState: .init(),
reducer: { APIKeySubmission() }
)
)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManangement.swift
================================================
import ComposableArchitecture
import Foundation
@Reducer
struct APIKeyManagement {
@ObservableState
struct State: Equatable {
var availableAPIKeyNames: [String] = []
@Presents var apiKeySubmission: APIKeySubmission.State?
}
enum Action: Equatable {
case appear
case closeButtonClicked
case addButtonClicked
case deleteButtonClicked(name: String)
case refreshAvailableAPIKeyNames
case apiKeySubmission(PresentationAction)
}
@Dependency(\.toast) var toast
@Dependency(\.apiKeyKeychain) var keychain
var body: some ReducerOf {
Reduce { state, action in
switch action {
case .appear:
if isPreview { return .none }
return .run { send in
await send(.refreshAvailableAPIKeyNames)
}
case .closeButtonClicked:
return .none
case .addButtonClicked:
state.apiKeySubmission = .init()
return .none
case let .deleteButtonClicked(name):
do {
try keychain.remove(name)
return .run { send in
await send(.refreshAvailableAPIKeyNames)
}
} catch {
toast(error.localizedDescription, .error)
return .none
}
case .refreshAvailableAPIKeyNames:
do {
let pairs = try keychain.getAll()
state.availableAPIKeyNames = Array(pairs.keys).sorted()
} catch {
toast(error.localizedDescription, .error)
}
return .none
case .apiKeySubmission(.presented(.saveFinished)):
state.apiKeySubmission = nil
return .run { send in
await send(.refreshAvailableAPIKeyNames)
}
case .apiKeySubmission(.presented(.cancelButtonClicked)):
state.apiKeySubmission = nil
return .none
case .apiKeySubmission:
return .none
}
}
.ifLet(\.$apiKeySubmission, action: \.apiKeySubmission) {
APIKeySubmission()
}
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyPicker.swift
================================================
import ComposableArchitecture
import SwiftUI
struct APIKeyPicker: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
HStack {
Picker(
selection: $store.apiKeyName,
content: {
Text("No API Key").tag("")
if store.availableAPIKeyNames.isEmpty {
Text("No API key found, please add a new one →")
}
if !store.availableAPIKeyNames.contains(store.apiKeyName),
!store.apiKeyName.isEmpty
{
Text("Key not found: \(store.apiKeyName)")
.tag(store.apiKeyName)
}
ForEach(store.availableAPIKeyNames, id: \.self) { name in
Text(name).tag(name)
}
},
label: { Text("API Key") }
)
Button(action: { store.send(.manageAPIKeysButtonClicked) }) {
Text(Image(systemName: "key"))
}
}.sheet(isPresented: $store.isAPIKeyManagementPresented) {
WithPerceptionTracking {
APIKeyManagementView(store: store.scope(
state: \.apiKeyManagement,
action: \.apiKeyManagement
))
}
}
.onAppear {
store.send(.appear)
}
}
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeySelection.swift
================================================
import Foundation
import SwiftUI
import ComposableArchitecture
@Reducer
struct APIKeySelection {
@ObservableState
struct State: Equatable {
var apiKeyName: String = ""
var availableAPIKeyNames: [String] {
apiKeyManagement.availableAPIKeyNames
}
var apiKeyManagement: APIKeyManagement.State = .init()
var isAPIKeyManagementPresented: Bool = false
}
enum Action: Equatable, BindableAction {
case appear
case manageAPIKeysButtonClicked
case binding(BindingAction)
case apiKeyManagement(APIKeyManagement.Action)
}
@Dependency(\.toast) var toast
@Dependency(\.apiKeyKeychain) var keychain
var body: some ReducerOf {
BindingReducer()
Scope(state: \.apiKeyManagement, action: \.apiKeyManagement) {
APIKeyManagement()
}
Reduce { state, action in
switch action {
case .appear:
return .run { send in
await send(.apiKeyManagement(.refreshAvailableAPIKeyNames))
}
case .manageAPIKeysButtonClicked:
state.isAPIKeyManagementPresented = true
return .none
case .binding:
return .none
case .apiKeyManagement(.closeButtonClicked):
state.isAPIKeyManagementPresented = false
return .none
case .apiKeyManagement:
return .none
}
}
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeySubmission.swift
================================================
import ComposableArchitecture
import Foundation
@Reducer
struct APIKeySubmission {
@ObservableState
struct State: Equatable {
var name: String = ""
var key: String = ""
}
enum Action: Equatable, BindableAction {
case binding(BindingAction)
case saveButtonClicked
case cancelButtonClicked
case saveFinished
}
@Dependency(\.toast) var toast
@Dependency(\.apiKeyKeychain) var keychain
enum E: Error, LocalizedError {
case nameIsEmpty
case keyIsEmpty
}
var body: some ReducerOf {
BindingReducer()
Reduce { state, action in
switch action {
case .saveButtonClicked:
do {
guard !state.name.isEmpty else { throw E.nameIsEmpty }
guard !state.key.isEmpty else { throw E.keyIsEmpty }
try keychain.update(
state.key,
key: state.name.trimmingCharacters(in: .whitespacesAndNewlines)
)
return .run { send in
await send(.saveFinished)
}
} catch {
toast(error.localizedDescription, .error)
return .none
}
case .cancelButtonClicked:
return .none
case .saveFinished:
return .none
case .binding:
return .none
}
}
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift
================================================
import AIModel
import ComposableArchitecture
import Dependencies
import Keychain
import OpenAIService
import Preferences
import SwiftUI
import Toast
@Reducer
struct ChatModelEdit {
@ObservableState
struct State: Equatable, Identifiable {
var id: String
var name: String
var format: ChatModel.Format
var maxTokens: Int = 4000
var supportsFunctionCalling: Bool = true
var modelName: String = ""
var ollamaKeepAlive: String = ""
var apiVersion: String = ""
var apiKeyName: String { apiKeySelection.apiKeyName }
var baseURL: String { baseURLSelection.baseURL }
var isFullURL: Bool { baseURLSelection.isFullURL }
var availableModelNames: [String] = []
var availableAPIKeys: [String] = []
var isTesting = false
var suggestedMaxTokens: Int?
var apiKeySelection: APIKeySelection.State = .init()
var baseURLSelection: BaseURLSelection.State = .init()
var enforceMessageOrder: Bool = false
var openAIOrganizationID: String = ""
var openAIProjectID: String = ""
var customHeaders: [ChatModel.Info.CustomHeaderInfo.HeaderField] = []
var openAICompatibleSupportsMultipartMessageContent = true
var requiresBeginWithUserMessage = false
var customBody: String = ""
var supportsImages: Bool = true
}
enum Action: Equatable, BindableAction {
case binding(BindingAction)
case appear
case saveButtonClicked
case cancelButtonClicked
case refreshAvailableModelNames
case testButtonClicked
case testSucceeded(String)
case testFailed(String)
case checkSuggestedMaxTokens
case selectModelFormat(ModelFormat)
case apiKeySelection(APIKeySelection.Action)
case baseURLSelection(BaseURLSelection.Action)
}
enum ModelFormat: CaseIterable {
case openAI
case azureOpenAI
case googleAI
case ollama
case claude
case gitHubCopilot
case openAICompatible
case deepSeekOpenAICompatible
case openRouterOpenAICompatible
case grokOpenAICompatible
case mistralOpenAICompatible
init(_ format: ChatModel.Format) {
switch format {
case .openAI:
self = .openAI
case .azureOpenAI:
self = .azureOpenAI
case .googleAI:
self = .googleAI
case .ollama:
self = .ollama
case .claude:
self = .claude
case .openAICompatible:
self = .openAICompatible
case .gitHubCopilot:
self = .gitHubCopilot
}
}
}
var toast: (String, ToastType) -> Void {
@Dependency(\.namespacedToast) var toast
return {
toast($0, $1, "ChatModelEdit")
}
}
@Dependency(\.apiKeyKeychain) var keychain
var body: some ReducerOf {
BindingReducer()
Scope(state: \.apiKeySelection, action: \.apiKeySelection) {
APIKeySelection()
}
Scope(state: \.baseURLSelection, action: \.baseURLSelection) {
BaseURLSelection()
}
Reduce { state, action in
switch action {
case .appear:
return .run { send in
await send(.refreshAvailableModelNames)
await send(.checkSuggestedMaxTokens)
}
case .saveButtonClicked:
return .none
case .cancelButtonClicked:
return .none
case .testButtonClicked:
guard !state.isTesting else { return .none }
state.isTesting = true
let model = ChatModel(state: state)
return .run { send in
do {
let configuration = UserPreferenceChatGPTConfiguration().overriding {
$0.model = model
}
let service = ChatGPTService(configuration: configuration)
let stream = service.send(TemplateChatGPTMemory(
memoryTemplate: .init(messages: [
.init(chatMessage: .init(
role: .system,
content: "You are a bot. Just do what is told."
)),
.init(chatMessage: .init(
role: .assistant,
content: "Hello"
)),
.init(chatMessage: .init(
role: .user,
content: "Respond with \"Test succeeded.\""
)),
.init(chatMessage: .init(
role: .user,
content: "Respond with \"Test succeeded.\""
)),
]),
configuration: configuration,
functionProvider: NoChatGPTFunctionProvider()
))
let streamReply = try await stream.asText()
await send(.testSucceeded(streamReply))
} catch {
await send(.testFailed(error.localizedDescription))
}
}
case let .testSucceeded(message):
state.isTesting = false
toast(message.trimmingCharacters(in: .whitespacesAndNewlines), .info)
return .none
case let .testFailed(message):
state.isTesting = false
toast(message.trimmingCharacters(in: .whitespacesAndNewlines), .error)
return .none
case .refreshAvailableModelNames:
if state.format == .openAI {
state.availableModelNames = ChatGPTModel.allCases.map(\.rawValue)
}
return .none
case .checkSuggestedMaxTokens:
switch state.format {
case .openAI:
if let knownModel = ChatGPTModel(rawValue: state.modelName) {
state.suggestedMaxTokens = knownModel.maxToken
} else {
state.suggestedMaxTokens = nil
}
return .none
case .googleAI:
if let knownModel = GoogleGenerativeAIModel(rawValue: state.modelName) {
state.suggestedMaxTokens = knownModel.maxToken
} else {
state.suggestedMaxTokens = nil
}
return .none
case .claude:
if let knownModel = ClaudeChatCompletionsService
.KnownModel(rawValue: state.modelName)
{
state.suggestedMaxTokens = knownModel.contextWindow
} else {
state.suggestedMaxTokens = nil
}
return .none
case .gitHubCopilot:
if let knownModel = AvailableGitHubCopilotModel(rawValue: state.modelName) {
state.suggestedMaxTokens = knownModel.contextWindow
} else {
state.suggestedMaxTokens = nil
}
return .none
default:
state.suggestedMaxTokens = nil
return .none
}
case let .selectModelFormat(format):
switch format {
case .openAI:
state.format = .openAI
case .azureOpenAI:
state.format = .azureOpenAI
case .googleAI:
state.format = .googleAI
case .ollama:
state.format = .ollama
case .claude:
state.format = .claude
case .gitHubCopilot:
state.format = .gitHubCopilot
case .openAICompatible:
state.format = .openAICompatible
case .deepSeekOpenAICompatible:
state.format = .openAICompatible
state.baseURLSelection.baseURL = "https://api.deepseek.com"
state.baseURLSelection.isFullURL = false
case .openRouterOpenAICompatible:
state.format = .openAICompatible
state.baseURLSelection.baseURL = "https://openrouter.ai"
state.baseURLSelection.isFullURL = false
case .grokOpenAICompatible:
state.format = .openAICompatible
state.baseURLSelection.baseURL = "https://api.x.ai"
state.baseURLSelection.isFullURL = false
case .mistralOpenAICompatible:
state.format = .openAICompatible
state.baseURLSelection.baseURL = "https://api.mistral.ai"
state.baseURLSelection.isFullURL = false
}
return .none
case .apiKeySelection:
return .none
case .baseURLSelection:
return .none
case .binding(\.format):
return .run { send in
await send(.refreshAvailableModelNames)
await send(.checkSuggestedMaxTokens)
}
case .binding(\.modelName):
return .run { send in
await send(.checkSuggestedMaxTokens)
}
case .binding:
return .none
}
}
}
}
extension ChatModel {
init(state: ChatModelEdit.State) {
self.init(
id: state.id,
name: state.name,
format: state.format,
info: .init(
apiKeyName: state.apiKeyName,
baseURL: state.baseURL.trimmingCharacters(in: .whitespacesAndNewlines),
isFullURL: state.isFullURL,
maxTokens: state.maxTokens,
supportsFunctionCalling: {
switch state.format {
case .googleAI, .ollama, .claude:
return false
case .azureOpenAI, .openAI, .openAICompatible, .gitHubCopilot:
return state.supportsFunctionCalling
}
}(),
supportsImage: state.supportsImages,
modelName: state.modelName
.trimmingCharacters(in: .whitespacesAndNewlines),
openAIInfo: .init(
organizationID: state.openAIOrganizationID,
projectID: state.openAIProjectID
),
ollamaInfo: .init(keepAlive: state.ollamaKeepAlive),
googleGenerativeAIInfo: .init(apiVersion: state.apiVersion),
openAICompatibleInfo: .init(
enforceMessageOrder: state.enforceMessageOrder,
supportsMultipartMessageContent: state
.openAICompatibleSupportsMultipartMessageContent,
requiresBeginWithUserMessage: state.requiresBeginWithUserMessage
),
customHeaderInfo: .init(headers: state.customHeaders),
customBodyInfo: .init(jsonBody: state.customBody)
)
)
}
func toState() -> ChatModelEdit.State {
.init(
id: id,
name: name,
format: format,
maxTokens: info.maxTokens,
supportsFunctionCalling: info.supportsFunctionCalling,
modelName: info.modelName,
ollamaKeepAlive: info.ollamaInfo.keepAlive,
apiVersion: info.googleGenerativeAIInfo.apiVersion,
apiKeySelection: .init(
apiKeyName: info.apiKeyName,
apiKeyManagement: .init(availableAPIKeyNames: [info.apiKeyName])
),
baseURLSelection: .init(baseURL: info.baseURL, isFullURL: info.isFullURL),
enforceMessageOrder: info.openAICompatibleInfo.enforceMessageOrder,
openAIOrganizationID: info.openAIInfo.organizationID,
openAIProjectID: info.openAIInfo.projectID,
customHeaders: info.customHeaderInfo.headers,
openAICompatibleSupportsMultipartMessageContent: info.openAICompatibleInfo
.supportsMultipartMessageContent,
requiresBeginWithUserMessage: info.openAICompatibleInfo.requiresBeginWithUserMessage,
customBody: info.customBodyInfo.jsonBody,
supportsImages: info.supportsImage
)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift
================================================
import AIModel
import ComposableArchitecture
import OpenAIService
import Preferences
import SwiftUI
@MainActor
struct ChatModelEditView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
ScrollView {
VStack(spacing: 0) {
Form {
NameTextField(store: store)
FormatPicker(store: store)
switch store.format {
case .openAI:
OpenAIForm(store: store)
case .azureOpenAI:
AzureOpenAIForm(store: store)
case .openAICompatible:
OpenAICompatibleForm(store: store)
case .googleAI:
GoogleAIForm(store: store)
case .ollama:
OllamaForm(store: store)
case .claude:
ClaudeForm(store: store)
case .gitHubCopilot:
GitHubCopilotForm(store: store)
}
}
.padding()
Divider()
HStack {
HStack(spacing: 8) {
Button("Test") {
store.send(.testButtonClicked)
}
.disabled(store.isTesting)
if store.isTesting {
ProgressView()
.controlSize(.small)
}
}
CustomBodyEdit(store: store)
.disabled({
switch store.format {
case .openAI, .openAICompatible, .claude:
return false
default:
return true
}
}())
CustomHeaderEdit(store: store)
.disabled({
switch store.format {
case .openAI, .openAICompatible, .ollama, .gitHubCopilot, .claude:
return false
default:
return true
}
}())
Spacer()
Button("Cancel") {
store.send(.cancelButtonClicked)
}
.keyboardShortcut(.cancelAction)
Button(action: { store.send(.saveButtonClicked) }) {
Text("Save")
}
.keyboardShortcut(.defaultAction)
}
.padding()
}
}
.textFieldStyle(.roundedBorder)
.onAppear {
store.send(.appear)
}
.fixedSize(horizontal: false, vertical: true)
.handleToast(namespace: "ChatModelEdit")
}
}
struct NameTextField: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
TextField("Name", text: $store.name)
}
}
}
struct FormatPicker: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
Picker(
selection: Binding(
get: { .init(store.format) },
set: { store.send(.selectModelFormat($0)) }
),
content: {
ForEach(
ChatModelEdit.ModelFormat.allCases,
id: \.self
) { format in
switch format {
case .openAI:
Text("OpenAI")
case .azureOpenAI:
Text("Azure OpenAI")
case .openAICompatible:
Text("OpenAI Compatible")
case .googleAI:
Text("Google AI")
case .ollama:
Text("Ollama")
case .claude:
Text("Claude")
case .gitHubCopilot:
Text("GitHub Copilot")
case .deepSeekOpenAICompatible:
Text("DeepSeek (OpenAI Compatible)")
case .openRouterOpenAICompatible:
Text("OpenRouter (OpenAI Compatible)")
case .grokOpenAICompatible:
Text("Grok (OpenAI Compatible)")
case .mistralOpenAICompatible:
Text("Mistral (OpenAI Compatible)")
}
}
},
label: { Text("Format") }
)
.pickerStyle(.menu)
}
}
}
struct BaseURLTextField: View {
let store: StoreOf
var title: String = "Base URL"
let prompt: Text?
@ViewBuilder var trailingContent: () -> V
var body: some View {
WithPerceptionTracking {
BaseURLPicker(
title: title,
prompt: prompt,
store: store.scope(
state: \.baseURLSelection,
action: \.baseURLSelection
),
trailingContent: trailingContent
)
}
}
}
struct SupportsFunctionCallingToggle: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
Toggle(
"Supports Function Calling",
isOn: $store.supportsFunctionCalling
)
Text(
"Function calling is required by some features, if this model doesn't support function calling, you should turn it off to avoid undefined behaviors."
)
.foregroundColor(.secondary)
.font(.callout)
.dynamicHeightTextInFormWorkaround()
}
}
}
struct MaxTokensTextField: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
HStack {
let textFieldBinding = Binding(
get: { String(store.maxTokens) },
set: {
if let selectionMaxToken = Int($0) {
$store.maxTokens.wrappedValue = selectionMaxToken
} else {
$store.maxTokens.wrappedValue = 0
}
}
)
TextField(text: textFieldBinding) {
Text("Context Window")
.multilineTextAlignment(.trailing)
}
.overlay(alignment: .trailing) {
Stepper(
value: $store.maxTokens,
in: 0...Int.max,
step: 100
) {
EmptyView()
}
}
.foregroundColor({
guard let max = store.suggestedMaxTokens else {
return .primary
}
if store.maxTokens > max {
return .red
}
return .primary
}() as Color)
if let max = store.suggestedMaxTokens {
Text("Max: \(max)")
}
}
}
}
}
struct ApiKeyNamePicker: View {
let store: StoreOf
var body: some View {
WithPerceptionTracking {
APIKeyPicker(store: store.scope(
state: \.apiKeySelection,
action: \.apiKeySelection
))
}
}
}
struct CustomBodyEdit: View {
@Perception.Bindable var store: StoreOf
@State private var isEditing = false
@Dependency(\.namespacedToast) var toast
var body: some View {
Button("Custom Body") {
isEditing = true
}
.sheet(isPresented: $isEditing) {
WithPerceptionTracking {
VStack {
TextEditor(text: $store.customBody)
.font(Font.system(.body, design: .monospaced))
.padding(4)
.frame(minHeight: 120)
.multilineTextAlignment(.leading)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
)
.handleToast(namespace: "CustomBodyEdit")
Text(
"The custom body will be added to the request body. Please use it to add parameters that are not yet available in the form. It should be a valid JSON object."
)
.foregroundColor(.secondary)
.font(.callout)
.padding(.bottom)
Button(action: {
if store.customBody.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty
{
isEditing = false
return
}
guard let _ = try? JSONSerialization
.jsonObject(with: store.customBody.data(using: .utf8) ?? Data())
else {
toast("Invalid JSON object", .error, "CustomBodyEdit")
return
}
isEditing = false
}) {
Text("Done")
}
.keyboardShortcut(.defaultAction)
}
.padding()
.frame(width: 600, height: 500)
.background(Color(nsColor: .windowBackgroundColor))
}
}
}
}
struct CustomHeaderEdit: View {
@Perception.Bindable var store: StoreOf
@State private var isEditing = false
var body: some View {
Button("Custom Headers") {
isEditing = true
}
.sheet(isPresented: $isEditing) {
WithPerceptionTracking {
CustomHeaderSettingsView(headers: $store.customHeaders)
}
}
}
}
struct OpenAIForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("https://api.openai.com")) {
Text("/v1/chat/completions")
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
.overlay(alignment: .trailing) {
Picker(
"",
selection: $store.modelName,
content: {
if ChatGPTModel(rawValue: store.modelName) == nil {
Text("Custom Model").tag(store.modelName)
}
ForEach(ChatGPTModel.allCases, id: \.self) { model in
Text(model.rawValue).tag(model.rawValue)
}
}
)
.frame(width: 20)
}
MaxTokensTextField(store: store)
SupportsFunctionCallingToggle(store: store)
TextField(text: $store.openAIOrganizationID, prompt: Text("Optional")) {
Text("Organization ID")
}
TextField(text: $store.openAIProjectID, prompt: Text("Optional")) {
Text("Project ID")
}
Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" To get an API key, please visit [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)"
)
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" If you don't have access to GPT-4, you may need to visit [https://platform.openai.com/account/billing/overview](https://platform.openai.com/account/billing/overview) to buy some credits. A ChatGPT Plus subscription is not enough to access GPT-4 through API."
)
}
.padding(.vertical)
}
}
}
struct AzureOpenAIForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("https://xxxx.openai.azure.com")) {
EmptyView()
}
ApiKeyNamePicker(store: store)
TextField("Deployment Name", text: $store.modelName)
MaxTokensTextField(store: store)
SupportsFunctionCallingToggle(store: store)
Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
}
}
}
struct OpenAICompatibleForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
Picker(
selection: $store.baseURLSelection.isFullURL,
content: {
Text("Base URL").tag(false)
Text("Full URL").tag(true)
},
label: { Text("URL") }
)
.pickerStyle(.segmented)
BaseURLTextField(
store: store,
title: "",
prompt: store.isFullURL
? Text("https://api.openai.com/v1/chat/completions")
: Text("https://api.openai.com")
) {
if !store.isFullURL {
Text("/v1/chat/completions")
}
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
MaxTokensTextField(store: store)
SupportsFunctionCallingToggle(store: store)
Toggle(isOn: $store.enforceMessageOrder) {
Text("Enforce message order to be user/assistant alternated")
}
Toggle(isOn: $store.openAICompatibleSupportsMultipartMessageContent) {
Text("Support multi-part message content")
}
Toggle(isOn: $store.requiresBeginWithUserMessage) {
Text("Requires the first message to be from the user")
}
Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
}
}
}
struct GoogleAIForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
BaseURLTextField(
store: store,
prompt: Text("https://generativelanguage.googleapis.com")
) {
Text("/v1")
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
.overlay(alignment: .trailing) {
Picker(
"",
selection: $store.modelName,
content: {
if GoogleGenerativeAIModel(rawValue: store.modelName) == nil {
Text("Custom Model").tag(store.modelName)
}
ForEach(GoogleGenerativeAIModel.allCases, id: \.self) { model in
Text(model.rawValue).tag(model.rawValue)
}
}
)
.frame(width: 20)
}
MaxTokensTextField(store: store)
TextField("API Version", text: $store.apiVersion, prompt: Text("v1"))
Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
}
}
}
struct OllamaForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("http://127.0.0.1:11434")) {
Text("/api/chat")
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
MaxTokensTextField(store: store)
TextField(text: $store.ollamaKeepAlive, prompt: Text("Default Value")) {
Text("Keep Alive")
}
Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" For more details, please visit [https://ollama.com](https://ollama.com)."
)
}
.padding(.vertical)
}
}
}
struct ClaudeForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("https://api.anthropic.com")) {
Text("/v1/messages")
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
.overlay(alignment: .trailing) {
Picker(
"",
selection: $store.modelName,
content: {
if ClaudeChatCompletionsService
.KnownModel(rawValue: store.modelName) == nil
{
Text("Custom Model").tag(store.modelName)
}
ForEach(
ClaudeChatCompletionsService.KnownModel.allCases,
id: \.self
) { model in
Text(model.rawValue).tag(model.rawValue)
}
}
)
.frame(width: 20)
}
MaxTokensTextField(store: store)
Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" For more details, please visit [https://anthropic.com](https://anthropic.com)."
)
}
.padding(.vertical)
}
}
}
struct GitHubCopilotForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
#warning("Todo: use the old picker and update the context window limit.")
GitHubCopilotModelPicker(
title: "Model Name",
hasDefaultModel: false,
gitHubCopilotModelId: $store.modelName
)
MaxTokensTextField(store: store)
SupportsFunctionCallingToggle(store: store)
Toggle(isOn: $store.enforceMessageOrder) {
Text("Enforce message order to be user/assistant alternated")
}
Toggle(isOn: $store.openAICompatibleSupportsMultipartMessageContent) {
Text("Support multi-part message content")
}
Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" Please login in the GitHub Copilot settings to use the model."
)
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" This will call the APIs directly, which may not be allowed by GitHub. But it's used in other popular apps like Zed."
)
}
.dynamicHeightTextInFormWorkaround()
.padding(.vertical)
}
}
}
}
#Preview("OpenAI") {
ChatModelEditView(
store: .init(
initialState: ChatModel(
id: "3",
name: "Test Model 3",
format: .openAI,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
).toState(),
reducer: { ChatModelEdit() }
)
)
}
#Preview("OpenAI Compatible") {
ChatModelEditView(
store: .init(
initialState: ChatModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
isFullURL: false,
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
).toState(),
reducer: { ChatModelEdit() }
)
)
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagement.swift
================================================
import AIModel
import ComposableArchitecture
import Keychain
import Preferences
import SwiftUI
extension ChatModel: ManageableAIModel {
var formatName: String {
switch format {
case .openAI: return "OpenAI"
case .azureOpenAI: return "Azure OpenAI"
case .openAICompatible: return "OpenAI Compatible"
case .googleAI: return "Google Generative AI"
case .ollama: return "Ollama"
case .claude: return "Claude"
case .gitHubCopilot: return "GitHub Copilot"
}
}
@ViewBuilder
var infoDescriptors: some View {
Text(info.modelName)
if !info.baseURL.isEmpty {
Image(systemName: "line.diagonal")
Text(info.baseURL)
}
Image(systemName: "line.diagonal")
Text("\(info.maxTokens) tokens")
Image(systemName: "line.diagonal")
Text(
"function calling \(info.supportsFunctionCalling ? Image(systemName: "checkmark.square") : Image(systemName: "xmark.square"))"
)
}
}
@Reducer
struct ChatModelManagement: AIModelManagement {
typealias Model = ChatModel
@ObservableState
struct State: Equatable, AIModelManagementState {
typealias Model = ChatModel
var models: IdentifiedArrayOf = []
@Presents var editingModel: ChatModelEdit.State?
var selectedModelId: String? { editingModel?.id }
}
enum Action: Equatable, AIModelManagementAction {
typealias Model = ChatModel
case appear
case createModel
case removeModel(id: Model.ID)
case selectModel(id: Model.ID)
case duplicateModel(id: Model.ID)
case moveModel(from: IndexSet, to: Int)
case chatModelItem(PresentationAction)
}
@Dependency(\.toast) var toast
@Dependency(\.userDefaults) var userDefaults
var body: some ReducerOf {
Reduce { state, action in
switch action {
case .appear:
if isPreview { return .none }
state.models = .init(
userDefaults.value(for: \.chatModels),
id: \.id,
uniquingIDsWith: { a, _ in a }
)
return .none
case .createModel:
state.editingModel = .init(
id: UUID().uuidString,
name: "New Model",
format: .openAI
)
return .none
case let .removeModel(id):
state.models.remove(id: id)
persist(state)
return .none
case let .selectModel(id):
guard let model = state.models[id: id] else { return .none }
state.editingModel = model.toState()
return .none
case let .duplicateModel(id):
guard var model = state.models[id: id] else { return .none }
model.id = UUID().uuidString
model.name += " (Copy)"
if let index = state.models.index(id: id) {
state.models.insert(model, at: index + 1)
} else {
state.models.append(model)
}
persist(state)
return .none
case let .moveModel(from, to):
state.models.move(fromOffsets: from, toOffset: to)
persist(state)
return .none
case .chatModelItem(.presented(.saveButtonClicked)):
guard let editingModel = state.editingModel, validateModel(editingModel)
else { return .none }
if let index = state.models
.firstIndex(where: { $0.id == editingModel.id })
{
state.models[index] = .init(state: editingModel)
} else {
state.models.append(.init(state: editingModel))
}
persist(state)
return .run { send in
await send(.chatModelItem(.dismiss))
}
case .chatModelItem(.presented(.cancelButtonClicked)):
return .run { send in
await send(.chatModelItem(.dismiss))
}
case .chatModelItem:
return .none
}
}.ifLet(\.$editingModel, action: \.chatModelItem) {
ChatModelEdit()
}
}
func persist(_ state: State) {
let models = state.models
userDefaults.set(Array(models), for: \.chatModels)
}
func validateModel(_ chatModel: ChatModelEdit.State) -> Bool {
guard !chatModel.name.isEmpty else {
toast("Model name cannot be empty", .error)
return false
}
guard !chatModel.id.isEmpty else {
toast("Model ID cannot be empty", .error)
return false
}
guard !chatModel.modelName.isEmpty else {
toast("Model name cannot be empty", .error)
return false
}
return true
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagementView.swift
================================================
import AIModel
import ComposableArchitecture
import SwiftUI
struct ChatModelManagementView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
AIModelManagementView(store: store)
.sheet(item: $store.scope(
state: \.editingModel,
action: \.chatModelItem
)) { store in
ChatModelEditView(store: store)
.frame(width: 800)
}
}
}
}
// MARK: - Previews
class ChatModelManagementView_Previews: PreviewProvider {
static var previews: some View {
ChatModelManagementView(
store: .init(
initialState: .init(
models: IdentifiedArray(uniqueElements: [
ChatModel(
id: "1",
name: "Test Model",
format: .openAI,
info: .init(
apiKeyName: "key",
baseURL: "google.com",
maxTokens: 3000,
supportsFunctionCalling: true,
modelName: "gpt-3.5-turbo"
)
),
ChatModel(
id: "2",
name: "Test Model 2",
format: .azureOpenAI,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
),
ChatModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
),
]),
editingModel: ChatModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
).toState()
),
reducer: { ChatModelManagement() }
)
)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/CodeiumView.swift
================================================
import CodeiumService
import Foundation
import SharedUIComponents
import SwiftUI
struct CodeiumView: View {
class ViewModel: ObservableObject {
let codeiumAuthService = CodeiumAuthService()
let installationManager = CodeiumInstallationManager()
@Published var isSignedIn: Bool
@Published var installationStatus: CodeiumInstallationManager.InstallationStatus
@Published var installationStep: CodeiumInstallationManager.InstallationStep?
@AppStorage(\.codeiumVerboseLog) var codeiumVerboseLog
@AppStorage(\.codeiumEnterpriseMode) var codeiumEnterpriseMode
@AppStorage(\.codeiumPortalUrl) var codeiumPortalUrl
@AppStorage(\.codeiumApiUrl) var codeiumApiUrl
@AppStorage(\.codeiumIndexEnabled) var indexEnabled
init() {
isSignedIn = codeiumAuthService.isSignedIn
installationStatus = .notInstalled
Task { @MainActor in
installationStatus = await installationManager.checkInstallation()
}
}
init(
isSignedIn: Bool,
installationStatus: CodeiumInstallationManager.InstallationStatus,
installationStep: CodeiumInstallationManager.InstallationStep?
) {
assert(isPreview)
self.isSignedIn = isSignedIn
self.installationStatus = installationStatus
self.installationStep = installationStep
}
func generateAuthURL() -> URL {
if codeiumEnterpriseMode && (codeiumPortalUrl != "") {
return URL(
string: codeiumPortalUrl +
"/profile?response_type=token&redirect_uri=show-auth-token&state=\(UUID().uuidString)&scope=openid%20profile%20email&redirect_parameters_type=query"
)!
}
return URL(
string: "https://www.codeium.com/profile?response_type=token&redirect_uri=show-auth-token&state=\(UUID().uuidString)&scope=openid%20profile%20email&redirect_parameters_type=query"
)!
}
func signIn(token: String) async throws {
try await codeiumAuthService.signIn(token: token)
Task { @MainActor in isSignedIn = true }
}
func signOut() async throws {
try await codeiumAuthService.signOut()
Task { @MainActor in isSignedIn = false }
}
func refreshInstallationStatus() {
Task { @MainActor in
installationStatus = await installationManager.checkInstallation()
}
}
func install() async throws {
defer { refreshInstallationStatus() }
do {
for try await step in installationManager.installLatestVersion() {
Task { @MainActor in
self.installationStep = step
}
}
Task {
try await Task.sleep(nanoseconds: 1_000_000_000)
Task { @MainActor in
self.installationStep = nil
}
}
} catch {
Task { @MainActor in
installationStep = nil
}
throw error
}
}
func uninstall() {
Task {
defer { refreshInstallationStatus() }
try await installationManager.uninstall()
}
}
}
@StateObject var viewModel = ViewModel()
@Environment(\.toast) var toast
@State var isSignInPanelPresented = false
var installButton: some View {
Button(action: {
Task {
do {
try await viewModel.install()
} catch {
toast(error.localizedDescription, .error)
}
}
}) {
Text("Install")
}
.disabled(viewModel.installationStep != nil)
}
var updateButton: some View {
Button(action: {
Task {
do {
try await viewModel.install()
} catch {
toast(error.localizedDescription, .error)
}
}
}) {
Text("Update")
}
.disabled(viewModel.installationStep != nil)
}
var uninstallButton: some View {
Button(action: {
viewModel.uninstall()
}) {
Text("Uninstall")
}
.disabled(viewModel.installationStep != nil)
}
var body: some View {
VStack(alignment: .leading) {
SubSection(title: Text("Codeium Language Server")) {
switch viewModel.installationStatus {
case .notInstalled:
HStack {
Text("Language Server Version: Not Installed")
installButton
}
case let .installed(version):
HStack {
Text("Language Server Version: \(version)")
uninstallButton
}
case let .outdated(current: current, latest: latest, _):
HStack {
Text("Language Server Version: \(current) (Update Available: \(latest))")
uninstallButton
updateButton
}
case let .unsupported(current: current, latest: latest):
HStack {
Text("Language Server Version: \(current) (Supported Version: \(latest))")
uninstallButton
updateButton
}
}
if viewModel.isSignedIn {
Text("Status: Signed In")
Button(action: {
Task {
do {
try await viewModel.signOut()
} catch {
toast(error.localizedDescription, .error)
}
}
}) {
Text("Sign Out")
}
} else {
Text("Status: Not Signed In")
Button(action: {
isSignInPanelPresented = true
}) {
Text("Sign In")
}
}
}
.sheet(isPresented: $isSignInPanelPresented) {
CodeiumSignInView(viewModel: viewModel, isPresented: $isSignInPanelPresented)
}
.onChange(of: viewModel.installationStep) { newValue in
if let step = newValue {
switch step {
case .downloading:
toast("Downloading..", .info)
case .uninstalling:
toast("Uninstalling old version..", .info)
case .decompressing:
toast("Decompressing..", .info)
case .done:
toast("Done!", .info)
}
}
}
SubSection(title: Text("Indexing")) {
Form {
Toggle("Enable Indexing", isOn: $viewModel.indexEnabled)
}
}
SubSection(title: Text("Enterprise")) {
Form {
Toggle("Codeium Enterprise Mode", isOn: $viewModel.codeiumEnterpriseMode)
TextField("Codeium Portal URL", text: $viewModel.codeiumPortalUrl)
TextField("Codeium API URL", text: $viewModel.codeiumApiUrl)
}
}
SettingsDivider("Advanced")
Form {
Toggle("Verbose Log", isOn: $viewModel.codeiumVerboseLog)
}
}
}
}
struct CodeiumSignInView: View {
let viewModel: CodeiumView.ViewModel
@Binding var isPresented: Bool
@Environment(\.openURL) var openURL
@Environment(\.toast) var toast
@State var isGeneratingKey = false
@State var token = ""
var body: some View {
VStack {
Text(
"You will be redirected to codeium.com. Please paste the generated token below and click the \"Sign In\" button."
)
TextEditor(text: $token)
.font(Font.system(.body, design: .monospaced))
.padding(4)
.frame(minHeight: 120)
.multilineTextAlignment(.leading)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
)
HStack {
Spacer()
Button(action: {
isPresented = false
}) {
Text("Cancel")
}
Button(action: {
isGeneratingKey = true
Task {
do {
try await viewModel.signIn(token: token)
isGeneratingKey = false
isPresented = false
} catch {
isGeneratingKey = false
toast(error.localizedDescription, .error)
}
}
}) {
Text(isGeneratingKey ? "Signing In.." : "Sign In")
}
.disabled(isGeneratingKey)
.keyboardShortcut(.defaultAction)
}
}
.padding()
.onAppear {
openURL(viewModel.generateAuthURL())
}
}
}
struct CodeiumView_Previews: PreviewProvider {
class TestViewModel: CodeiumView.ViewModel {
override func generateAuthURL() -> URL {
return URL(string: "about:blank")!
}
override func signIn(token: String) async throws {}
override func signOut() async throws {}
override func refreshInstallationStatus() {}
override func install() async throws {}
override func uninstall() {}
}
static var previews: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
CodeiumView(viewModel: TestViewModel(
isSignedIn: false,
installationStatus: .notInstalled,
installationStep: nil
))
CodeiumView(viewModel: TestViewModel(
isSignedIn: true,
installationStatus: .installed("1.2.9"),
installationStep: nil
))
CodeiumView(viewModel: TestViewModel(
isSignedIn: true,
installationStatus: .outdated(current: "1.2.9", latest: "1.3.0", mandatory: true),
installationStep: .downloading
))
CodeiumView(viewModel: TestViewModel(
isSignedIn: true,
installationStatus: .unsupported(current: "1.5.9", latest: "1.3.0"),
installationStep: .downloading
))
}
.padding(8)
}
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/CustomHeaderSettingsView.swift
================================================
import AIModel
import Foundation
import SwiftUI
struct CustomHeaderSettingsView: View {
@Binding var headers: [ChatModel.Info.CustomHeaderInfo.HeaderField]
@Environment(\.dismiss) var dismiss
@State private var newKey = ""
@State private var newValue = ""
var body: some View {
VStack {
List {
ForEach(headers.indices, id: \.self) { index in
HStack {
TextField("Key", text: Binding(
get: { headers[index].key },
set: { newKey in
headers[index].key = newKey
}
))
TextField("Value", text: Binding(
get: { headers[index].value },
set: { headers[index].value = $0 }
))
Button(action: {
headers.remove(at: index)
}) {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
}
HStack {
TextField("New Key", text: $newKey)
TextField("New Value", text: $newValue)
Button(action: {
if !newKey.isEmpty {
headers.append(ChatModel.Info.CustomHeaderInfo.HeaderField(
key: newKey,
value: newValue
))
newKey = ""
newValue = ""
}
}) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
}
}
}
HStack {
Spacer()
Button("Done") {
dismiss()
}
}.padding()
}
.frame(height: 500)
}
}
#Preview {
struct V: View {
@State var headers: [ChatModel.Info.CustomHeaderInfo.HeaderField] = [
.init(key: "key", value: "value"),
.init(key: "key2", value: "value2"),
]
var body: some View {
CustomHeaderSettingsView(headers: $headers)
}
}
return V()
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/EmbeddingModel.swift
================================================
import SwiftUI
import Keychain
================================================
FILE: Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEdit.swift
================================================
import AIModel
import ComposableArchitecture
import Dependencies
import Keychain
import OpenAIService
import Preferences
import SwiftUI
import Toast
@Reducer
struct EmbeddingModelEdit {
@ObservableState
struct State: Equatable, Identifiable {
var id: String
var name: String
var format: EmbeddingModel.Format
var maxTokens: Int = 8191
var dimensions: Int = 1536
var modelName: String = ""
var ollamaKeepAlive: String = ""
var apiKeyName: String { apiKeySelection.apiKeyName }
var baseURL: String { baseURLSelection.baseURL }
var isFullURL: Bool { baseURLSelection.isFullURL }
var availableModelNames: [String] = []
var availableAPIKeys: [String] = []
var isTesting = false
var suggestedMaxTokens: Int?
var apiKeySelection: APIKeySelection.State = .init()
var baseURLSelection: BaseURLSelection.State = .init()
var customHeaders: [ChatModel.Info.CustomHeaderInfo.HeaderField] = []
}
enum Action: Equatable, BindableAction {
case binding(BindingAction)
case appear
case saveButtonClicked
case cancelButtonClicked
case refreshAvailableModelNames
case testButtonClicked
case testSucceeded(String)
case testFailed(String)
case fixDimensions(Int)
case checkSuggestedMaxTokens
case selectModelFormat(ModelFormat)
case apiKeySelection(APIKeySelection.Action)
case baseURLSelection(BaseURLSelection.Action)
}
enum ModelFormat: CaseIterable {
case openAI
case azureOpenAI
case ollama
case gitHubCopilot
case openAICompatible
case mistralOpenAICompatible
case voyageAIOpenAICompatible
init(_ format: EmbeddingModel.Format) {
switch format {
case .openAI:
self = .openAI
case .azureOpenAI:
self = .azureOpenAI
case .ollama:
self = .ollama
case .openAICompatible:
self = .openAICompatible
case .gitHubCopilot:
self = .gitHubCopilot
}
}
}
var toast: (String, ToastType) -> Void {
@Dependency(\.namespacedToast) var toast
return {
toast($0, $1, "EmbeddingModelEdit")
}
}
@Dependency(\.apiKeyKeychain) var keychain
var body: some ReducerOf {
BindingReducer()
Scope(state: \.apiKeySelection, action: \.apiKeySelection) {
APIKeySelection()
}
Scope(state: \.baseURLSelection, action: \.baseURLSelection) {
BaseURLSelection()
}
Reduce { state, action in
switch action {
case .appear:
return .run { send in
await send(.refreshAvailableModelNames)
await send(.checkSuggestedMaxTokens)
}
case .saveButtonClicked:
return .none
case .cancelButtonClicked:
return .none
case .testButtonClicked:
guard !state.isTesting else { return .none }
state.isTesting = true
let dimensions = state.dimensions
let model = EmbeddingModel(
id: state.id,
name: state.name,
format: state.format,
info: .init(
apiKeyName: state.apiKeyName,
baseURL: state.baseURL,
isFullURL: state.isFullURL,
maxTokens: state.maxTokens,
dimensions: dimensions,
modelName: state.modelName
)
)
return .run { send in
do {
let result = try await EmbeddingService(
configuration: UserPreferenceEmbeddingConfiguration()
.overriding {
$0.model = model
}
).embed(text: "Hello")
if result.data.isEmpty {
await send(.testFailed("No data returned"))
return
}
let actualDimensions = result.data.first?.embedding.count ?? 0
if actualDimensions != dimensions {
await send(
.testFailed("Invalid dimension, should be \(actualDimensions)")
)
await send(.fixDimensions(actualDimensions))
} else {
await send(
.testSucceeded("Succeeded! (Dimensions: \(actualDimensions))")
)
}
} catch {
await send(.testFailed(error.localizedDescription))
}
}
case let .testSucceeded(message):
state.isTesting = false
toast(message, .info)
return .none
case let .testFailed(message):
state.isTesting = false
toast(message, .error)
return .none
case .refreshAvailableModelNames:
if state.format == .openAI {
state.availableModelNames = ChatGPTModel.allCases.map(\.rawValue)
}
return .none
case .checkSuggestedMaxTokens:
guard state.format == .openAI,
let knownModel = OpenAIEmbeddingModel(rawValue: state.modelName)
else {
state.suggestedMaxTokens = nil
return .none
}
state.suggestedMaxTokens = knownModel.maxToken
state.dimensions = knownModel.dimensions
return .none
case let .fixDimensions(value):
state.dimensions = value
return .none
case let .selectModelFormat(format):
switch format {
case .openAI:
state.format = .openAI
case .azureOpenAI:
state.format = .azureOpenAI
case .ollama:
state.format = .ollama
case .openAICompatible:
state.format = .openAICompatible
case .gitHubCopilot:
state.format = .gitHubCopilot
case .mistralOpenAICompatible:
state.format = .openAICompatible
state.baseURLSelection.baseURL = "https://api.mistral.ai"
state.baseURLSelection.isFullURL = false
case .voyageAIOpenAICompatible:
state.format = .openAICompatible
state.baseURLSelection.baseURL = "https://api.voyage.ai"
state.baseURLSelection.isFullURL = false
}
return .none
case .apiKeySelection:
return .none
case .baseURLSelection:
return .none
case .binding(\.format):
return .run { send in
await send(.refreshAvailableModelNames)
await send(.checkSuggestedMaxTokens)
}
case .binding(\.modelName):
return .run { send in
await send(.checkSuggestedMaxTokens)
}
case .binding:
return .none
}
}
}
}
extension EmbeddingModel {
init(state: EmbeddingModelEdit.State) {
self.init(
id: state.id,
name: state.name,
format: state.format,
info: .init(
apiKeyName: state.apiKeyName,
baseURL: state.baseURL.trimmingCharacters(in: .whitespacesAndNewlines),
isFullURL: state.isFullURL,
maxTokens: state.maxTokens,
dimensions: state.dimensions,
modelName: state.modelName.trimmingCharacters(in: .whitespacesAndNewlines),
ollamaInfo: .init(keepAlive: state.ollamaKeepAlive),
customHeaderInfo: .init(headers: state.customHeaders)
)
)
}
func toState() -> EmbeddingModelEdit.State {
.init(
id: id,
name: name,
format: format,
maxTokens: info.maxTokens,
dimensions: info.dimensions,
modelName: info.modelName,
ollamaKeepAlive: info.ollamaInfo.keepAlive,
apiKeySelection: .init(
apiKeyName: info.apiKeyName,
apiKeyManagement: .init(availableAPIKeyNames: [info.apiKeyName])
),
baseURLSelection: .init(
baseURL: info.baseURL,
isFullURL: info.isFullURL
),
customHeaders: info.customHeaderInfo.headers
)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEditView.swift
================================================
import AIModel
import ComposableArchitecture
import Preferences
import SwiftUI
@MainActor
struct EmbeddingModelEditView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
ScrollView {
VStack(spacing: 0) {
Form {
NameTextField(store: store)
FormatPicker(store: store)
switch store.format {
case .openAI:
OpenAIForm(store: store)
case .azureOpenAI:
AzureOpenAIForm(store: store)
case .openAICompatible:
OpenAICompatibleForm(store: store)
case .ollama:
OllamaForm(store: store)
case .gitHubCopilot:
GitHubCopilotForm(store: store)
}
}
.padding()
Divider()
HStack {
HStack(spacing: 8) {
Button("Test") {
store.send(.testButtonClicked)
}
.disabled(store.isTesting)
if store.isTesting {
ProgressView()
.controlSize(.small)
}
}
Spacer()
Button("Cancel") {
store.send(.cancelButtonClicked)
}
.keyboardShortcut(.cancelAction)
Button(action: { store.send(.saveButtonClicked) }) {
Text("Save")
}
.keyboardShortcut(.defaultAction)
}
.padding()
}
}
.textFieldStyle(.roundedBorder)
.onAppear {
store.send(.appear)
}
.fixedSize(horizontal: false, vertical: true)
.handleToast(namespace: "EmbeddingModelEdit")
}
}
struct NameTextField: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
TextField("Name", text: $store.name)
}
}
}
struct FormatPicker: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
Picker(
selection: Binding(
get: { .init(store.format) },
set: { store.send(.selectModelFormat($0)) }
),
content: {
ForEach(
EmbeddingModelEdit.ModelFormat.allCases,
id: \.self
) { format in
switch format {
case .openAI:
Text("OpenAI")
case .azureOpenAI:
Text("Azure OpenAI")
case .ollama:
Text("Ollama")
case .openAICompatible:
Text("OpenAI Compatible")
case .mistralOpenAICompatible:
Text("Mistral (OpenAI Compatible)")
case .voyageAIOpenAICompatible:
Text("Voyage (OpenAI Compatible)")
case .gitHubCopilot:
Text("GitHub Copilot")
}
}
},
label: { Text("Format") }
)
.pickerStyle(.menu)
}
}
}
struct BaseURLTextField: View {
let store: StoreOf
var title: String = "Base URL"
let prompt: Text?
@ViewBuilder var trailingContent: () -> V
var body: some View {
WithPerceptionTracking {
BaseURLPicker(
title: title,
prompt: prompt,
store: store.scope(
state: \.baseURLSelection,
action: \.baseURLSelection
),
trailingContent: trailingContent
)
}
}
}
struct MaxTokensTextField: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
HStack {
let textFieldBinding = Binding(
get: { String(store.maxTokens) },
set: {
if let selectionMaxToken = Int($0) {
$store.maxTokens.wrappedValue = selectionMaxToken
} else {
$store.maxTokens.wrappedValue = 0
}
}
)
TextField(text: textFieldBinding) {
Text("Max Input Tokens")
.multilineTextAlignment(.trailing)
}
.overlay(alignment: .trailing) {
Stepper(
value: $store.maxTokens,
in: 0...Int.max,
step: 100
) {
EmptyView()
}
}
.foregroundColor({
guard let max = store.suggestedMaxTokens else {
return .primary
}
if store.maxTokens > max {
return .red
}
return .primary
}() as Color)
if let max = store.suggestedMaxTokens {
Text("Max: \(max)")
}
}
}
}
}
struct DimensionsTextField: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
HStack {
let textFieldBinding = Binding(
get: { String(store.dimensions) },
set: {
if let selectionDimensions = Int($0) {
$store.dimensions.wrappedValue = selectionDimensions
} else {
$store.dimensions.wrappedValue = 0
}
}
)
TextField(text: textFieldBinding) {
Text("Dimensions")
.multilineTextAlignment(.trailing)
}
.overlay(alignment: .trailing) {
Stepper(
value: $store.dimensions,
in: 0...Int.max,
step: 100
) {
EmptyView()
}
}
.foregroundColor({
if store.dimensions <= 0 {
return .red
}
return .primary
}() as Color)
}
Text("If you are not sure, run test to get the correct value.")
.font(.caption)
.dynamicHeightTextInFormWorkaround()
}
}
}
struct ApiKeyNamePicker: View {
let store: StoreOf
var body: some View {
WithPerceptionTracking {
APIKeyPicker(store: store.scope(
state: \.apiKeySelection,
action: \.apiKeySelection
))
}
}
}
struct OpenAIForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("https://api.openai.com")) {
Text("/v1/embeddings")
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
.overlay(alignment: .trailing) {
Picker(
"",
selection: $store.modelName,
content: {
if OpenAIEmbeddingModel(rawValue: store.modelName) == nil {
Text("Custom Model").tag(store.modelName)
}
ForEach(OpenAIEmbeddingModel.allCases, id: \.self) { model in
Text(model.rawValue).tag(model.rawValue)
}
}
)
.frame(width: 20)
}
MaxTokensTextField(store: store)
DimensionsTextField(store: store)
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" To get an API key, please visit [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)"
)
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" If you don't have access to GPT-4, you may need to visit [https://platform.openai.com/account/billing/overview](https://platform.openai.com/account/billing/overview) to buy some credits. A ChatGPT Plus subscription is not enough to access GPT-4 through API."
)
}
.padding(.vertical)
}
}
}
struct AzureOpenAIForm: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("https://xxxx.openai.azure.com")) {
EmptyView()
}
ApiKeyNamePicker(store: store)
TextField("Deployment Name", text: $store.modelName)
MaxTokensTextField(store: store)
DimensionsTextField(store: store)
}
}
}
struct OpenAICompatibleForm: View {
@Perception.Bindable var store: StoreOf
@State var isEditingCustomHeader = false
var body: some View {
WithPerceptionTracking {
Picker(
selection: $store.baseURLSelection.isFullURL,
content: {
Text("Base URL").tag(false)
Text("Full URL").tag(true)
},
label: { Text("URL") }
)
.pickerStyle(.segmented)
BaseURLTextField(
store: store,
title: "",
prompt: store.isFullURL
? Text("https://api.openai.com/v1/embeddings")
: Text("https://api.openai.com")
) {
if !store.isFullURL {
Text("/v1/embeddings")
}
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
MaxTokensTextField(store: store)
DimensionsTextField(store: store)
Button("Custom Headers") {
isEditingCustomHeader.toggle()
}
}.sheet(isPresented: $isEditingCustomHeader) {
CustomHeaderSettingsView(headers: $store.customHeaders)
}
}
}
struct OllamaForm: View {
@Perception.Bindable var store: StoreOf
@State var isEditingCustomHeader = false
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("http://127.0.0.1:11434")) {
Text("/api/embeddings")
}
ApiKeyNamePicker(store: store)
TextField("Model Name", text: $store.modelName)
MaxTokensTextField(store: store)
DimensionsTextField(store: store)
WithPerceptionTracking {
TextField(text: $store.ollamaKeepAlive, prompt: Text("Default Value")) {
Text("Keep Alive")
}
}
Button("Custom Headers") {
isEditingCustomHeader.toggle()
}
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" For more details, please visit [https://ollama.com](https://ollama.com)."
)
}
.padding(.vertical)
}.sheet(isPresented: $isEditingCustomHeader) {
CustomHeaderSettingsView(headers: $store.customHeaders)
}
}
}
struct GitHubCopilotForm: View {
@Perception.Bindable var store: StoreOf
@State var isEditingCustomHeader = false
var body: some View {
WithPerceptionTracking {
TextField("Model Name", text: $store.modelName)
.overlay(alignment: .trailing) {
Picker(
"",
selection: $store.modelName,
content: {
if OpenAIEmbeddingModel(rawValue: store.modelName) == nil {
Text("Custom Model").tag(store.modelName)
}
ForEach(OpenAIEmbeddingModel.allCases, id: \.self) { model in
Text(model.rawValue).tag(model.rawValue)
}
}
)
.frame(width: 20)
}
MaxTokensTextField(store: store)
DimensionsTextField(store: store)
Button("Custom Headers") {
isEditingCustomHeader.toggle()
}
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" Please login in the GitHub Copilot settings to use the model."
)
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" This will call the APIs directly, which may not be allowed by GitHub. But it's used in other popular apps like Zed."
)
}
.dynamicHeightTextInFormWorkaround()
.padding(.vertical)
}.sheet(isPresented: $isEditingCustomHeader) {
CustomHeaderSettingsView(headers: $store.customHeaders)
}
}
}
}
class EmbeddingModelManagementView_Editing_Previews: PreviewProvider {
static var previews: some View {
EmbeddingModelEditView(
store: .init(
initialState: EmbeddingModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
modelName: "gpt-3.5-turbo"
)
).toState(),
reducer: { EmbeddingModelEdit() }
)
)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelManagement.swift
================================================
import AIModel
import ComposableArchitecture
import Keychain
import Preferences
import SwiftUI
extension EmbeddingModel: ManageableAIModel {
var formatName: String {
switch format {
case .openAI: return "OpenAI"
case .azureOpenAI: return "Azure OpenAI"
case .openAICompatible: return "OpenAI Compatible"
case .ollama: return "Ollama"
case .gitHubCopilot: return "GitHub Copilot"
}
}
@ViewBuilder
var infoDescriptors: some View {
Text(info.modelName)
if !info.baseURL.isEmpty {
Image(systemName: "line.diagonal")
Text(info.baseURL)
}
Image(systemName: "line.diagonal")
Text("\(info.maxTokens) tokens")
}
}
@Reducer
struct EmbeddingModelManagement: AIModelManagement {
typealias Model = EmbeddingModel
@ObservableState
struct State: Equatable, AIModelManagementState {
typealias Model = EmbeddingModel
var models: IdentifiedArrayOf = []
@Presents var editingModel: EmbeddingModelEdit.State?
var selectedModelId: Model.ID? { editingModel?.id }
}
enum Action: Equatable, AIModelManagementAction {
typealias Model = EmbeddingModel
case appear
case createModel
case removeModel(id: Model.ID)
case selectModel(id: Model.ID)
case duplicateModel(id: Model.ID)
case moveModel(from: IndexSet, to: Int)
case embeddingModelItem(PresentationAction)
}
@Dependency(\.toast) var toast
@Dependency(\.userDefaults) var userDefaults
var body: some ReducerOf {
Reduce { state, action in
switch action {
case .appear:
if isPreview { return .none }
state.models = .init(
userDefaults.value(for: \.embeddingModels),
id: \.id,
uniquingIDsWith: { a, _ in a }
)
return .none
case .createModel:
state.editingModel = .init(
id: UUID().uuidString,
name: "New Model",
format: .openAI
)
return .none
case let .removeModel(id):
state.models.remove(id: id)
persist(state)
return .none
case let .selectModel(id):
guard let model = state.models[id: id] else { return .none }
state.editingModel = model.toState()
return .none
case let .duplicateModel(id):
guard var model = state.models[id: id] else { return .none }
model.id = UUID().uuidString
model.name += " (Copy)"
if let index = state.models.index(id: id) {
state.models.insert(model, at: index + 1)
} else {
state.models.append(model)
}
persist(state)
return .none
case let .moveModel(from, to):
state.models.move(fromOffsets: from, toOffset: to)
persist(state)
return .none
case .embeddingModelItem(.presented(.saveButtonClicked)):
guard let editingModel = state.editingModel, validateModel(editingModel)
else { return .none }
if let index = state.models
.firstIndex(where: { $0.id == editingModel.id })
{
state.models[index] = .init(state: editingModel)
} else {
state.models.append(.init(state: editingModel))
}
persist(state)
return .run { send in
await send(.embeddingModelItem(.dismiss))
}
case .embeddingModelItem(.presented(.cancelButtonClicked)):
return .run { send in
await send(.embeddingModelItem(.dismiss))
}
case .embeddingModelItem:
return .none
}
}.ifLet(\.$editingModel, action: \.embeddingModelItem) {
EmbeddingModelEdit()
}
}
func persist(_ state: State) {
let models = state.models
userDefaults.set(Array(models), for: \.embeddingModels)
}
func validateModel(_ chatModel: EmbeddingModelEdit.State) -> Bool {
guard !chatModel.name.isEmpty else {
toast("Model name cannot be empty", .error)
return false
}
guard !chatModel.id.isEmpty else {
toast("Model ID cannot be empty", .error)
return false
}
guard !chatModel.modelName.isEmpty else {
toast("Model name cannot be empty", .error)
return false
}
return true
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelManagementView.swift
================================================
import AIModel
import ComposableArchitecture
import SwiftUI
struct EmbeddingModelManagementView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
AIModelManagementView(store: store)
.sheet(item: $store.scope(
state: \.editingModel,
action: \.embeddingModelItem
)) { store in
EmbeddingModelEditView(store: store)
.frame(width: 800)
}
}
}
}
// MARK: - Previews
class EmbeddingModelManagementView_Previews: PreviewProvider {
static var previews: some View {
EmbeddingModelManagementView(
store: .init(
initialState: .init(
models: IdentifiedArray(uniqueElements: [
EmbeddingModel(
id: "1",
name: "Test Model",
format: .openAI,
info: .init(
apiKeyName: "key",
baseURL: "google.com",
maxTokens: 3000,
modelName: "gpt-3.5-turbo"
)
),
EmbeddingModel(
id: "2",
name: "Test Model 2",
format: .azureOpenAI,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
modelName: "gpt-3.5-turbo"
)
),
EmbeddingModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
modelName: "gpt-3.5-turbo"
)
),
]),
editingModel: EmbeddingModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
modelName: "gpt-3.5-turbo"
)
).toState()
),
reducer: { EmbeddingModelManagement() }
)
)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/GitHubCopilotModelPicker.swift
================================================
import Dependencies
import Foundation
import GitHubCopilotService
import Perception
import SwiftUI
import Toast
public struct GitHubCopilotModelPicker: View {
@Perceptible
final class ViewModel {
var availableModels: [GitHubCopilotLLMModel] = []
@PerceptionIgnored @Dependency(\.toast) var toast
init() {}
func appear() {
reloadAvailableModels()
}
func disappear() {}
func reloadAvailableModels() {
Task { @MainActor in
do {
availableModels = try await GitHubCopilotExtension.fetchLLMModels()
} catch {
toast("Failed to fetch GitHub Copilot models: \(error)", .error)
}
}
}
}
let title: String
let hasDefaultModel: Bool
@Binding var gitHubCopilotModelId: String
@State var viewModel: ViewModel
init(
title: String,
hasDefaultModel: Bool = true,
gitHubCopilotModelId: Binding
) {
self.title = title
_gitHubCopilotModelId = gitHubCopilotModelId
self.hasDefaultModel = hasDefaultModel
viewModel = .init()
}
public var body: some View {
WithPerceptionTracking {
TextField(title, text: $gitHubCopilotModelId)
.overlay(alignment: .trailing) {
Picker(
"",
selection: $gitHubCopilotModelId,
content: {
if hasDefaultModel {
Text("Default").tag("")
}
if !gitHubCopilotModelId.isEmpty,
!viewModel.availableModels.contains(where: {
$0.modelId == gitHubCopilotModelId
})
{
Text(gitHubCopilotModelId).tag(gitHubCopilotModelId)
}
if viewModel.availableModels.isEmpty {
Text({
viewModel.reloadAvailableModels()
return "Loading..."
}()).tag("Loading...")
}
ForEach(viewModel.availableModels) { model in
Text(model.modelId)
.tag(model.modelId)
}
}
)
.frame(width: 20)
}
.onAppear {
viewModel.appear()
}
.onDisappear {
viewModel.disappear()
}
}
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift
================================================
import AppKit
import Client
import GitHubCopilotService
import Preferences
import SharedUIComponents
import SuggestionBasic
import SwiftUI
struct GitHubCopilotView: View {
static var copilotAuthService: GitHubCopilotAuthServiceType?
class Settings: ObservableObject {
@AppStorage(\.nodePath) var nodePath: String
@AppStorage(\.runNodeWith) var runNodeWith
@AppStorage("username") var username: String = ""
@AppStorage(\.gitHubCopilotVerboseLog) var gitHubCopilotVerboseLog
@AppStorage(\.gitHubCopilotProxyHost) var gitHubCopilotProxyHost
@AppStorage(\.gitHubCopilotProxyPort) var gitHubCopilotProxyPort
@AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername
@AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword
@AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL
@AppStorage(\.gitHubCopilotEnterpriseURI) var gitHubCopilotEnterpriseURI
@AppStorage(\.gitHubCopilotPretendIDEToBeVSCode) var pretendIDEToBeVSCode
@AppStorage(\.disableGitHubCopilotSettingsAutoRefreshOnAppear)
var disableGitHubCopilotSettingsAutoRefreshOnAppear
@AppStorage(\.gitHubCopilotLoadKeyChainCertificates)
var gitHubCopilotLoadKeyChainCertificates
@AppStorage(\.gitHubCopilotModelId) var gitHubCopilotModelId
init() {}
}
class ViewModel: ObservableObject {
let installationManager = GitHubCopilotInstallationManager()
@Published var installationStatus: GitHubCopilotInstallationManager.InstallationStatus?
@Published var installationStep: GitHubCopilotInstallationManager.InstallationStep?
init() {}
init(
installationStatus: GitHubCopilotInstallationManager.InstallationStatus,
installationStep: GitHubCopilotInstallationManager.InstallationStep?
) {
assert(isPreview)
self.installationStatus = installationStatus
self.installationStep = installationStep
}
func refreshInstallationStatus() {
Task { @MainActor in
installationStatus = installationManager.checkInstallation()
}
}
func install() async throws {
defer { refreshInstallationStatus() }
do {
for try await step in installationManager.installLatestVersion() {
Task { @MainActor in
self.installationStep = step
}
}
Task {
try await Task.sleep(nanoseconds: 1_000_000_000)
Task { @MainActor in
self.installationStep = nil
}
}
} catch {
Task { @MainActor in
installationStep = nil
}
throw error
}
}
func uninstall() {
Task {
defer { refreshInstallationStatus() }
try await installationManager.uninstall()
Task { @MainActor in
GitHubCopilotView.copilotAuthService = nil
}
}
}
}
@Environment(\.openURL) var openURL
@Environment(\.toast) var toast
@StateObject var settings = Settings()
@StateObject var viewModel = ViewModel()
@State var status: GitHubCopilotAccountStatus?
@State var userCode: String?
@State var version: String?
@State var isRunningAction: Bool = false
@State var isUserCodeCopiedAlertPresented = false
func getGitHubCopilotAuthService() throws -> GitHubCopilotAuthServiceType {
if let service = Self.copilotAuthService { return service }
let service = try GitHubCopilotAuthService()
Self.copilotAuthService = service
return service
}
var installButton: some View {
Button(action: {
Task {
do {
try await viewModel.install()
} catch {
toast(error.localizedDescription, .error)
}
}
}) {
Text("Install")
}
.disabled(viewModel.installationStep != nil)
}
var updateButton: some View {
Button(action: {
Task {
do {
try await viewModel.install()
} catch {
toast(error.localizedDescription, .error)
}
}
}) {
Text("Update")
}
.disabled(viewModel.installationStep != nil)
}
var uninstallButton: some View {
Button(action: {
viewModel.uninstall()
}) {
Text("Uninstall")
}
.disabled(viewModel.installationStep != nil)
}
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 8) {
SubSection(
title: Text("Node Settings"),
description: """
You may have to restart the extension app to apply the changes. To do so, simply close the helper app by clicking on the menu bar icon that looks like a tentacle, it will automatically restart as needed.
"""
) {
Form {
TextField(
text: $settings.nodePath,
prompt: Text(
"node"
)
) {
Text("Path to Node (v22.0+)")
}
Text(
"Provide the path to the executable if it can't be found by the app, shim executable is not supported"
)
.lineLimit(10)
.foregroundColor(.secondary)
.font(.callout)
.dynamicHeightTextInFormWorkaround()
Picker(selection: $settings.runNodeWith) {
ForEach(NodeRunner.allCases, id: \.rawValue) { runner in
switch runner {
case .env:
Text("/usr/bin/env").tag(runner)
case .bash:
Text("/bin/bash -i -l").tag(runner)
case .shell:
Text("$SHELL -i -l").tag(runner)
}
}
} label: {
Text("Run Node with")
}
Group {
switch settings.runNodeWith {
case .env:
Text(
"PATH: `/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`"
)
case .bash:
Text("PATH inherited from bash configurations.")
case .shell:
Text("PATH inherited from $SHELL configurations.")
}
}
.lineLimit(10)
.foregroundColor(.secondary)
.font(.callout)
.dynamicHeightTextInFormWorkaround()
Toggle(isOn: $settings.gitHubCopilotLoadKeyChainCertificates) {
Text("Load certificates in keychain")
}
}
}
SubSection(
title: Text("GitHub Copilot Language Server")
) {
HStack {
switch viewModel.installationStatus {
case .none:
Text("Copilot.Vim Version: Loading..")
case .notInstalled:
Text("Copilot.Vim Version: Not Installed")
installButton
case let .installed(version):
Text("Copilot.Vim Version: \(version)")
uninstallButton
case let .outdated(version, latest, _):
Text("Copilot.Vim Version: \(version) (Update Available: \(latest))")
updateButton
uninstallButton
case let .unsupported(version, latest):
Text("Copilot.Vim Version: \(version) (Supported Version: \(latest))")
updateButton
uninstallButton
}
}
Text("Language Server Version: \(version ?? "Loading..")")
Text("Status: \(status?.description ?? "Loading..")")
HStack(alignment: .center) {
Button("Refresh") {
viewModel.refreshInstallationStatus()
checkStatus()
}
if status == .notSignedIn {
Button("Sign In") { signIn() }
.alert(isPresented: $isUserCodeCopiedAlertPresented) {
Alert(
title: Text(userCode ?? ""),
message: Text(
"The user code is pasted into your clipboard, please paste it in the opened website to login.\nAfter that, click \"Confirm Sign-in\" to finish."
),
dismissButton: .default(Text("OK"))
)
}
Button("Confirm Sign-in") { confirmSignIn() }
}
if status == .ok || status == .alreadySignedIn ||
status == .notAuthorized
{
Button("Sign Out") { signOut() }
}
if isRunningAction {
ActivityIndicatorView()
}
}
.opacity(isRunningAction ? 0.8 : 1)
.disabled(isRunningAction)
Button("Refresh configurations") {
refreshConfiguration()
}
Form {
GitHubCopilotModelPicker(
title: "Chat Model Name",
gitHubCopilotModelId: $settings.gitHubCopilotModelId
)
}
}
SettingsDivider("Advanced")
Form {
Toggle("Verbose log", isOn: $settings.gitHubCopilotVerboseLog)
Toggle("Pretend IDE to be VSCode", isOn: $settings.pretendIDEToBeVSCode)
}
SettingsDivider("Enterprise")
Form {
TextField(
text: $settings.gitHubCopilotEnterpriseURI,
prompt: Text("Leave it blank if non is available.")
) {
Text("Auth provider URL")
}
}
SettingsDivider("Proxy")
Form {
TextField(
text: $settings.gitHubCopilotProxyHost,
prompt: Text("xxx.xxx.xxx.xxx, leave it blank to disable proxy.")
) {
Text("Proxy host")
}
TextField(text: $settings.gitHubCopilotProxyPort, prompt: Text("80")) {
Text("Proxy port")
}
TextField(text: $settings.gitHubCopilotProxyUsername) {
Text("Proxy username")
}
SecureField(text: $settings.gitHubCopilotProxyPassword) {
Text("Proxy password")
}
Toggle("Proxy strict SSL", isOn: $settings.gitHubCopilotUseStrictSSL)
}
}
Spacer()
}.onAppear {
if isPreview { return }
if settings.disableGitHubCopilotSettingsAutoRefreshOnAppear { return }
viewModel.refreshInstallationStatus()
checkStatus()
}.onChange(of: settings.runNodeWith) { _ in
Self.copilotAuthService = nil
}.onChange(of: settings.nodePath) { _ in
Self.copilotAuthService = nil
}.onChange(of: viewModel.installationStep) { newValue in
if let step = newValue {
switch step {
case .downloading:
toast("Downloading..", .info)
case .uninstalling:
toast("Uninstalling old version..", .info)
case .decompressing:
toast("Decompressing..", .info)
case .done:
toast("Done!", .info)
checkStatus()
}
}
}
.textFieldStyle(.roundedBorder)
}
func checkStatus() {
Task {
isRunningAction = true
defer { isRunningAction = false }
do {
let service = try getGitHubCopilotAuthService()
status = try await service.checkStatus()
version = try await service.version()
isRunningAction = false
if status != .ok, status != .notSignedIn {
toast(
"GitHub Copilot status is not \"ok\". Please check if you have a valid GitHub Copilot subscription.",
.error
)
}
} catch {
toast(error.localizedDescription, .error)
}
}
}
func signIn() {
Task {
isRunningAction = true
defer { isRunningAction = false }
do {
let service = try getGitHubCopilotAuthService()
let (uri, userCode) = try await service.signInInitiate()
self.userCode = userCode
guard let url = URL(string: uri) else {
toast("Verification URI is incorrect.", .error)
return
}
let pasteboard = NSPasteboard.general
pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
pasteboard.setString(userCode, forType: NSPasteboard.PasteboardType.string)
toast("Usercode \(userCode) already copied!", .info)
openURL(url)
isUserCodeCopiedAlertPresented = true
} catch {
toast(error.localizedDescription, .error)
}
}
}
func confirmSignIn() {
Task {
isRunningAction = true
defer { isRunningAction = false }
do {
let service = try getGitHubCopilotAuthService()
guard let userCode else {
toast("Usercode is empty.", .error)
return
}
let (username, status) = try await service.signInConfirm(userCode: userCode)
self.settings.username = username
self.status = status
} catch {
toast(error.localizedDescription, .error)
}
}
}
func signOut() {
Task {
isRunningAction = true
defer { isRunningAction = false }
do {
let service = try getGitHubCopilotAuthService()
status = try await service.signOut()
} catch {
toast(error.localizedDescription, .error)
}
}
}
func refreshConfiguration() {
NotificationCenter.default.post(
name: .gitHubCopilotShouldRefreshEditorInformation,
object: nil
)
Task {
let service = try getService()
do {
try await service.postNotification(
name: Notification.Name
.gitHubCopilotShouldRefreshEditorInformation.rawValue
)
} catch {
toast(error.localizedDescription, .error)
}
}
}
}
struct ActivityIndicatorView: NSViewRepresentable {
func makeNSView(context _: Context) -> NSProgressIndicator {
let progressIndicator = NSProgressIndicator()
progressIndicator.style = .spinning
progressIndicator.appearance = NSAppearance(named: .vibrantLight)
progressIndicator.controlSize = .small
progressIndicator.startAnimation(nil)
return progressIndicator
}
func updateNSView(_: NSProgressIndicator, context _: Context) {
// No-op
}
}
struct CopilotView_Previews: PreviewProvider {
static var previews: some View {
VStack(alignment: .leading, spacing: 8) {
GitHubCopilotView(status: .notSignedIn, version: "1.0.0")
GitHubCopilotView(status: .alreadySignedIn, isRunningAction: true)
}
.padding(.all, 8)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/OtherSuggestionServicesView.swift
================================================
import Foundation
import SwiftUI
struct OtherSuggestionServicesView: View {
@Environment(\.openURL) var openURL
var body: some View {
VStack(alignment: .leading) {
Text(
"You can use other locally run services (Tabby, Ollma, etc.) to generate suggestions with the Custom Suggestion Service extension."
)
.lineLimit(nil)
.multilineTextAlignment(.leading)
Button(action: {
if let url = URL(
string: "https://github.com/intitni/CustomSuggestionServiceForCopilotForXcode"
) {
openURL(url)
}
}) {
Text("Get It Now")
}
}
}
}
#Preview {
OtherSuggestionServicesView()
.frame(width: 200)
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/SharedModelManagement/AIModelManagementVIew.swift
================================================
import AIModel
import ComposableArchitecture
import PlusFeatureFlag
import SharedUIComponents
import SwiftUI
protocol AIModelManagementAction {
associatedtype Model: ManageableAIModel
static var appear: Self { get }
static var createModel: Self { get }
static func removeModel(id: Model.ID) -> Self
static func selectModel(id: Model.ID) -> Self
static func duplicateModel(id: Model.ID) -> Self
static func moveModel(from: IndexSet, to: Int) -> Self
}
protocol AIModelManagementState: Equatable {
associatedtype Model: ManageableAIModel
var models: IdentifiedArrayOf { get }
var selectedModelId: Model.ID? { get }
}
protocol AIModelManagement: Reducer where
Action: AIModelManagementAction,
State: AIModelManagementState & ObservableState,
Action.Model == Self.Model,
State.Model == Self.Model
{
associatedtype Model: ManageableAIModel
}
protocol ManageableAIModel: Identifiable {
associatedtype V: View
var name: String { get }
var formatName: String { get }
var infoDescriptors: V { get }
}
struct AIModelManagementView: View
where Management.Model == Model
{
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
VStack(spacing: 0) {
HStack {
Spacer()
if isFeatureAvailable(\.unlimitedChatAndEmbeddingModels) {
Button("Add Model") {
store.send(.createModel)
}
} else {
Text("\(store.models.count) / 2")
.foregroundColor(.secondary)
let disabled = store.models.count >= 2
Button(disabled ? "Add More Model (Plus)" : "Add Model") {
store.send(.createModel)
}.disabled(disabled)
}
}.padding(4)
Divider()
ModelList(store: store)
}
.onAppear {
store.send(.appear)
}
}
}
struct ModelList: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
List {
ForEach(store.models) { model in
WithPerceptionTracking {
let isSelected = store.selectedModelId == model.id
HStack(spacing: 4) {
Image(systemName: "line.3.horizontal")
Button(action: {
store.send(.selectModel(id: model.id))
}) {
Cell(model: model, isSelected: isSelected)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.contextMenu {
Button("Duplicate") {
store.send(.duplicateModel(id: model.id))
}
Button("Remove") {
store.send(.removeModel(id: model.id))
}
}
}
}
}
.onMove(perform: { indices, newOffset in
store.send(.moveModel(from: indices, to: newOffset))
})
.modify { view in
if #available(macOS 13.0, *) {
view.listRowSeparator(.hidden).listSectionSeparator(.hidden)
} else {
view
}
}
}
.removeBackground()
.listStyle(.plain)
.listRowInsets(EdgeInsets())
.overlay {
if store.models.isEmpty {
Text("No model found, please add a new one.")
.foregroundColor(.secondary)
}
}
}
}
}
struct Cell: View {
let model: Model
let isSelected: Bool
@State var isHovered: Bool = false
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(model.formatName)
.foregroundColor(isSelected ? .white : .primary)
.font(.subheadline.bold())
.padding(.vertical, 2)
.padding(.horizontal, 4)
.background {
RoundedRectangle(cornerRadius: 4)
.fill(
isSelected
? .white.opacity(0.2)
: Color.primary.opacity(0.1)
)
}
Text(model.name)
.font(.headline)
}
HStack(spacing: 4) {
model.infoDescriptors
}
.font(.subheadline)
.opacity(0.7)
.padding(.leading, 2)
}
Spacer()
}
.onHover(perform: {
isHovered = $0
})
.padding(.vertical, 8)
.padding(.horizontal, 8)
.background {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill({
switch (isSelected, isHovered) {
case (true, _):
return Color.accentColor
case (_, true):
return Color.primary.opacity(0.1)
case (_, false):
return Color.clear
}
}() as Color)
}
.foregroundColor(isSelected ? .white : .primary)
.animation(.easeInOut(duration: 0.1), value: isSelected)
.animation(.easeInOut(duration: 0.1), value: isHovered)
}
}
}
// MARK: - Previews
class AIModelManagement_Previews: PreviewProvider {
static var previews: some View {
AIModelManagementView(
store: .init(
initialState: .init(
models: IdentifiedArray(uniqueElements: [
ChatModel(
id: "1",
name: "Test Model",
format: .openAI,
info: .init(
apiKeyName: "key",
baseURL: "google.com",
maxTokens: 3000,
supportsFunctionCalling: true,
modelName: "gpt-3.5-turbo"
)
),
ChatModel(
id: "2",
name: "Test Model 2",
format: .azureOpenAI,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
),
ChatModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
),
]),
editingModel: ChatModel(
id: "3",
name: "Test Model 3",
format: .openAICompatible,
info: .init(
apiKeyName: "key",
baseURL: "apple.com",
maxTokens: 3000,
supportsFunctionCalling: false,
modelName: "gpt-3.5-turbo"
)
).toState()
),
reducer: { ChatModelManagement() }
)
)
}
}
class AIModelManagement_Empty_Previews: PreviewProvider {
static var previews: some View {
AIModelManagementView(
store: .init(
initialState: .init(models: [] as IdentifiedArrayOf),
reducer: { ChatModelManagement() }
)
)
}
}
class AIModelManagement_Cell_Previews: PreviewProvider {
static var previews: some View {
AIModelManagementView.Cell(model: ChatModel(
id: "1",
name: "Test Model",
format: .openAI,
info: .init(
apiKeyName: "key",
baseURL: "google.com",
maxTokens: 3000,
supportsFunctionCalling: true,
modelName: "gpt-3.5-turbo"
)
), isSelected: false)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/SharedModelManagement/BaseURLPicker.swift
================================================
import ComposableArchitecture
import SwiftUI
struct BaseURLPicker: View {
let title: String
let prompt: Text?
@Perception.Bindable var store: StoreOf
@ViewBuilder let trailingContent: () -> TrailingContent
var body: some View {
WithPerceptionTracking {
HStack {
TextField(title, text: $store.baseURL, prompt: prompt)
.overlay(alignment: .trailing) {
Picker(
"",
selection: $store.baseURL,
content: {
if !store.availableBaseURLs
.contains(store.baseURL),
!store.baseURL.isEmpty
{
Text("Custom Value").tag(store.baseURL)
}
Text("Empty (Default Value)").tag("")
ForEach(store.availableBaseURLs, id: \.self) { baseURL in
Text(baseURL).tag(baseURL)
}
}
)
.frame(width: 20)
}
trailingContent()
.foregroundStyle(.secondary)
}
.onAppear {
store.send(.appear)
}
}
}
}
extension BaseURLPicker where TrailingContent == EmptyView {
init(
title: String,
prompt: Text? = nil,
store: StoreOf
) {
self.init(
title: title,
prompt: prompt,
store: store,
trailingContent: { EmptyView() }
)
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/SharedModelManagement/BaseURLSelection.swift
================================================
import ComposableArchitecture
import Foundation
import Preferences
import SwiftUI
@Reducer
struct BaseURLSelection {
@ObservableState
struct State: Equatable {
var baseURL: String = ""
var isFullURL: Bool = false
var availableBaseURLs: [String] = []
}
enum Action: Equatable, BindableAction {
case appear
case refreshAvailableBaseURLNames
case binding(BindingAction)
}
@Dependency(\.toast) var toast
@Dependency(\.userDefaults) var userDefaults
var body: some ReducerOf {
BindingReducer()
Reduce { state, action in
switch action {
case .appear:
return .run { send in
await send(.refreshAvailableBaseURLNames)
}
case .refreshAvailableBaseURLNames:
let chatModels = userDefaults.value(for: \.chatModels)
let embeddingModels = userDefaults.value(for: \.embeddingModels)
var allBaseURLs = Set(
chatModels.map(\.info.baseURL)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+ embeddingModels.map(\.info.baseURL)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
)
allBaseURLs.remove("")
state.availableBaseURLs = Array(allBaseURLs).sorted()
return .none
case .binding:
return .none
}
}
}
}
================================================
FILE: Core/Sources/HostApp/AccountSettings/WebSearchView.swift
================================================
import AppKit
import Client
import ComposableArchitecture
import OpenAIService
import Preferences
import SuggestionBasic
import SwiftUI
import WebSearchService
import SharedUIComponents
@Reducer
struct WebSearchSettings {
struct TestResult: Identifiable, Equatable {
let id = UUID()
var duration: TimeInterval
var result: Result?
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}
@ObservableState
struct State: Equatable {
var apiKeySelection: APIKeySelection.State = .init()
var testResult: TestResult?
}
enum Action: BindableAction {
case binding(BindingAction)
case appear
case test
case bringUpTestResult
case updateTestResult(TimeInterval, Result)
case apiKeySelection(APIKeySelection.Action)
}
var body: some ReducerOf {
BindingReducer()
Scope(state: \.apiKeySelection, action: \.apiKeySelection) {
APIKeySelection()
}
Reduce { state, action in
switch action {
case .binding:
return .none
case .appear:
state.testResult = nil
state.apiKeySelection.apiKeyName = UserDefaults.shared.value(for: \.serpAPIKeyName)
return .none
case .test:
return .run { send in
let searchService = WebSearchService(provider: .userPreferred)
await send(.bringUpTestResult)
let start = Date()
do {
let result = try await searchService.search(query: "Swift")
let duration = Date().timeIntervalSince(start)
await send(.updateTestResult(duration, .success(result)))
} catch {
let duration = Date().timeIntervalSince(start)
await send(.updateTestResult(duration, .failure(error)))
}
}
case .bringUpTestResult:
state.testResult = .init(duration: 0)
return .none
case let .updateTestResult(duration, result):
state.testResult?.duration = duration
state.testResult?.result = result
return .none
case let .apiKeySelection(action):
switch action {
case .binding(\APIKeySelection.State.apiKeyName):
UserDefaults.shared.set(state.apiKeySelection.apiKeyName, for: \.serpAPIKeyName)
return .none
default:
return .none
}
}
}
}
}
final class WebSearchViewSettings: ObservableObject {
@AppStorage(\.serpAPIEngine) var serpAPIEngine
@AppStorage(\.headlessBrowserEngine) var headlessBrowserEngine
@AppStorage(\.searchProvider) var searchProvider
init() {}
}
struct WebSearchView: View {
@Perception.Bindable var store: StoreOf
@Environment(\.openURL) var openURL
@StateObject var settings = WebSearchViewSettings()
var body: some View {
WithPerceptionTracking {
ScrollView {
VStack(alignment: .leading) {
Form {
Picker("Search Provider", selection: $settings.searchProvider) {
ForEach(UserDefaultPreferenceKeys.SearchProvider.allCases, id: \.self) {
provider in
switch provider {
case .serpAPI:
Text("Serp API").tag(provider)
case .headlessBrowser:
Text("Headless Browser").tag(provider)
}
}
}
.pickerStyle(.segmented)
}
switch settings.searchProvider {
case .serpAPI:
serpAPIForm()
case .headlessBrowser:
headlessBrowserForm()
}
}
.padding()
}
.safeAreaInset(edge: .bottom) {
VStack(spacing: 0) {
Divider()
HStack {
Button("Test Search") {
store.send(.test)
}
Spacer()
}
.padding()
}
.background(.regularMaterial)
}
.sheet(item: $store.testResult) { testResult in
testResultView(testResult: testResult)
}
.onAppear {
store.send(.appear)
}
}
}
@ViewBuilder
func serpAPIForm() -> some View {
SubSection(
title: Text("Serp API Settings"),
description: """
Use Serp API to do web search. Serp API is more reliable and faster than headless browser. But you need to provide an API key for it.
"""
) {
Picker("Engine", selection: $settings.serpAPIEngine) {
ForEach(
UserDefaultPreferenceKeys.SerpAPIEngine.allCases,
id: \.self
) { engine in
Text(engine.rawValue).tag(engine)
}
}
WithPerceptionTracking {
APIKeyPicker(store: store.scope(
state: \.apiKeySelection,
action: \.apiKeySelection
))
}
}
}
@ViewBuilder
func headlessBrowserForm() -> some View {
SubSection(
title: Text("Headless Browser Settings"),
description: """
The app will open a webview in the background to do web search. This method uses a set of rules to extract information from the web page, if you notice that it stops working, please submit an issue to the developer.
"""
) {
Picker("Engine", selection: $settings.headlessBrowserEngine) {
ForEach(
UserDefaultPreferenceKeys.HeadlessBrowserEngine.allCases,
id: \.self
) { engine in
Text(engine.rawValue).tag(engine)
}
}
}
}
@ViewBuilder
func testResultView(testResult: WebSearchSettings.TestResult) -> some View {
VStack {
Text("Test Result")
.padding(.top)
.font(.headline)
if let result = testResult.result {
switch result {
case let .success(webSearchResult):
VStack(alignment: .leading) {
Text("Success (Completed in \(testResult.duration, specifier: "%.2f")s)")
.foregroundColor(.green)
Text("Found \(webSearchResult.webPages.count) results:")
ScrollView {
ForEach(webSearchResult.webPages, id: \.urlString) { page in
HStack {
VStack(alignment: .leading) {
Text(page.title)
.font(.headline)
Text(page.urlString)
.font(.caption)
.foregroundColor(.blue)
Text(page.snippet)
.padding(.top, 2)
}
Spacer(minLength: 0)
}
.padding(.vertical, 4)
Divider()
}
}
}
.padding()
case let .failure(error):
VStack(alignment: .leading) {
Text("Error (Completed in \(testResult.duration, specifier: "%.2f")s)")
.foregroundColor(.red)
Text(error.localizedDescription)
}
}
} else {
ProgressView().padding()
}
Spacer()
VStack(spacing: 0) {
Divider()
HStack {
Spacer()
Button("Close") {
store.testResult = nil
}
.keyboardShortcut(.cancelAction)
}
.padding()
}
}
.frame(minWidth: 400, minHeight: 300)
}
}
// Helper struct to make TestResult identifiable for sheet presentation
private struct TestResultWrapper: Identifiable {
var id: UUID = .init()
var testResult: WebSearchSettings.TestResult
}
struct WebSearchView_Previews: PreviewProvider {
static var previews: some View {
VStack(alignment: .leading, spacing: 8) {
WebSearchView(store: .init(initialState: .init(), reducer: { WebSearchSettings() }))
}
.frame(height: 800)
.padding(.all, 8)
}
}
================================================
FILE: Core/Sources/HostApp/CustomCommandSettings/CustomCommand.swift
================================================
import ComposableArchitecture
import Foundation
import PlusFeatureFlag
import Preferences
import SwiftUI
import Toast
@Reducer
struct CustomCommandFeature {
@ObservableState
struct State: Equatable {
var editCustomCommand: EditCustomCommand.State?
}
let settings: CustomCommandView.Settings
enum Action: Equatable {
case createNewCommand
case editCommand(CustomCommand)
case editCustomCommand(EditCustomCommand.Action)
case deleteCommand(CustomCommand)
case exportCommand(CustomCommand)
case importCommand(at: URL)
case importCommandClicked
}
@Dependency(\.toast) var toast
var body: some ReducerOf {
Reduce { state, action in
switch action {
case .createNewCommand:
if !isFeatureAvailable(\.unlimitedCustomCommands),
settings.customCommands.count >= 10
{
toast("Upgrade to Plus to add more commands", .info)
return .none
}
state.editCustomCommand = EditCustomCommand.State(nil)
return .none
case let .editCommand(command):
state.editCustomCommand = EditCustomCommand.State(command)
return .none
case .editCustomCommand(.close):
state.editCustomCommand = nil
return .none
case let .deleteCommand(command):
settings.customCommands.removeAll(
where: { $0.id == command.id }
)
if state.editCustomCommand?.commandId == command.id {
state.editCustomCommand = nil
}
return .none
case .editCustomCommand:
return .none
case let .exportCommand(command):
return .run { _ in
do {
let data = try JSONEncoder().encode(command)
let filename = "CustomCommand-\(command.name).json"
let url = await withCheckedContinuation { continuation in
Task { @MainActor in
let panel = NSSavePanel()
panel.canCreateDirectories = true
panel.nameFieldStringValue = filename
let result = await panel.begin()
switch result {
case .OK:
continuation.resume(returning: panel.url)
default:
continuation.resume(returning: nil)
}
}
}
if let url {
try data.write(to: url)
toast("Saved!", .info)
}
} catch {
toast(error.localizedDescription, .error)
}
}
case let .importCommand(url):
if !isFeatureAvailable(\.unlimitedCustomCommands),
settings.customCommands.count >= 10
{
toast("Upgrade to Plus to add more commands", .info)
return .none
}
do {
let data = try Data(contentsOf: url)
var command = try JSONDecoder().decode(CustomCommand.self, from: data)
command.commandId = UUID().uuidString
settings.customCommands.append(command)
toast("Imported custom command \(command.name)!", .info)
} catch {
toast("Failed to import command: \(error.localizedDescription)", .error)
}
return .none
case .importCommandClicked:
return .run { send in
let url = await withCheckedContinuation { continuation in
Task { @MainActor in
let panel = NSOpenPanel()
panel.allowedContentTypes = [.json]
let result = await panel.begin()
if result == .OK {
continuation.resume(returning: panel.url)
} else {
continuation.resume(returning: nil)
}
}
}
if let url {
await send(.importCommand(at: url))
}
}
}
}.ifLet(\.editCustomCommand, action: \.editCustomCommand) {
EditCustomCommand(settings: settings)
}
}
}
================================================
FILE: Core/Sources/HostApp/CustomCommandSettings/CustomCommandView.swift
================================================
import ComposableArchitecture
import MarkdownUI
import PlusFeatureFlag
import Preferences
import SharedUIComponents
import SwiftUI
import Toast
extension List {
@ViewBuilder
func removeBackground() -> some View {
if #available(macOS 13.0, *) {
scrollContentBackground(.hidden)
.listRowBackground(EmptyView())
} else {
background(Color.clear)
.listRowBackground(EmptyView())
}
}
}
let customCommandStore = StoreOf(
initialState: .init(),
reducer: { CustomCommandFeature(settings: .init()) }
)
struct CustomCommandView: View {
final class Settings: ObservableObject {
@AppStorage(\.customCommands) var customCommands
init(customCommands: AppStorage<[CustomCommand]>? = nil) {
if let list = customCommands {
_customCommands = list
}
}
}
var store: StoreOf
@StateObject var settings = Settings()
@Environment(\.toast) var toast
var body: some View {
HStack(spacing: 0) {
LeftPanel(store: store, settings: settings)
Divider()
RightPanel(store: store)
}
}
struct LeftPanel: View {
let store: StoreOf
@ObservedObject var settings: Settings
@Environment(\.toast) var toast
var body: some View {
WithPerceptionTracking {
List {
ForEach(settings.customCommands, id: \.commandId) { command in
CommandButton(store: store, command: command)
}
.onMove(perform: { indices, newOffset in
settings.customCommands.move(fromOffsets: indices, toOffset: newOffset)
})
.modify { view in
if #available(macOS 13.0, *) {
view.listRowSeparator(.hidden).listSectionSeparator(.hidden)
} else {
view
}
}
}
.removeBackground()
.padding(.vertical, 4)
.listStyle(.plain)
.frame(width: 200)
.background(Color.primary.opacity(0.05))
.overlay {
if settings.customCommands.isEmpty {
Text("""
Empty
Add command with "+" button
""")
.multilineTextAlignment(.center)
}
}
.safeAreaInset(edge: .bottom) {
Button(action: {
store.send(.createNewCommand)
}) {
if isFeatureAvailable(\.unlimitedCustomCommands) {
Text(Image(systemName: "plus.circle.fill")) + Text(" New Command")
} else {
Text(Image(systemName: "plus.circle.fill")) +
Text(" New Command (\(settings.customCommands.count)/10)")
}
}
.buttonStyle(.plain)
.padding()
.contextMenu {
Button("Import") {
store.send(.importCommandClicked)
}
}
}
.onDrop(of: [.json], delegate: FileDropDelegate(store: store, toast: toast))
}
}
}
struct FileDropDelegate: DropDelegate {
let store: StoreOf
let toast: (String, ToastType) -> Void
func performDrop(info: DropInfo) -> Bool {
let jsonFiles = info.itemProviders(for: [.json])
for file in jsonFiles {
file
.loadInPlaceFileRepresentation(forTypeIdentifier: "public.json") { url, _, error in
Task { @MainActor in
if let url {
store.send(.importCommand(at: url))
} else if let error {
toast(error.localizedDescription, .error)
}
}
}
}
return !jsonFiles.isEmpty
}
}
struct CommandButton: View {
@Perception.Bindable var store: StoreOf
let command: CustomCommand
var body: some View {
WithPerceptionTracking {
HStack(spacing: 4) {
Image(systemName: "line.3.horizontal")
VStack(alignment: .leading) {
Text(command.name)
.foregroundStyle(.primary)
Group {
switch command.feature {
case .chatWithSelection:
Text("Send Message")
case .customChat:
Text("Custom Chat")
case .promptToCode:
Text("Modification")
case .singleRoundDialog:
Text("Single Round Dialog")
}
}
.font(.caption)
.foregroundStyle(.tertiary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
store.send(.editCommand(command))
}
}
.padding(4)
.background {
RoundedRectangle(cornerRadius: 4)
.fill(
store.editCustomCommand?.commandId == command.id
? Color.primary.opacity(0.05)
: Color.clear
)
}
.contextMenu {
Button("Remove") {
store.send(.deleteCommand(command))
}
Button("Export") {
store.send(.exportCommand(command))
}
}
}
}
}
struct RightPanel: View {
let store: StoreOf
var body: some View {
WithPerceptionTracking {
if let store = store.scope(
state: \.editCustomCommand,
action: \.editCustomCommand
) {
EditCustomCommandView(store: store)
} else {
VStack {
SubSection(title: Text("Send Message")) {
Text(
"This command sends a message to the active chat tab. You can provide additional context as well. The additional context will be removed once a message is sent. If the message provided is empty, you can manually type the message in the chat."
)
}
SubSection(title: Text("Modification")) {
Text(
"This command opens the prompt-to-code panel and executes the provided requirements on the selected code. You can provide additional context through the \"Extra Context\" as well."
)
}
SubSection(title: Text("Custom Chat")) {
Text(
"This command will overwrite the context of the chat. You can use it to switch to different contexts in the chat. If a message is provided, it will be sent to the chat as well."
)
}
SubSection(title: Text("Single Round Dialog")) {
Text(
"This command allows you to send a message to a temporary chat without opening the chat panel. It is particularly useful for one-time commands, such as running a terminal command with `/shell`. For example, you can set the prompt to `/shell open .` to open the project in Finder."
)
}
}
.padding()
}
}
}
}
}
struct CustomCommandTypeDescription: View {
let text: String
var body: some View {
ScrollView {
Markdown(text)
.lineLimit(nil)
.markdownTheme(
.gitHub
.text {
ForegroundColor(.secondary)
BackgroundColor(.clear)
FontSize(14)
}
.heading1 { conf in
VStack(alignment: .leading, spacing: 0) {
conf.label
.relativePadding(.bottom, length: .em(0.3))
.relativeLineSpacing(.em(0.125))
.markdownMargin(top: 24, bottom: 16)
.markdownTextStyle {
FontWeight(.semibold)
FontSize(.em(1.25))
}
Divider()
}
}
)
.padding()
.background(Color.primary.opacity(0.02), in: RoundedRectangle(cornerRadius: 8))
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(style: .init(lineWidth: 1))
.foregroundColor(Color(nsColor: .separatorColor))
}
.padding()
}
}
}
// MARK: - Previews
struct CustomCommandView_Preview: PreviewProvider {
static var previews: some View {
let settings = CustomCommandView.Settings(customCommands: .init(wrappedValue: [
.init(
commandId: "1",
name: "Explain Code",
feature: .chatWithSelection(
extraSystemPrompt: nil,
prompt: "Hello",
useExtraSystemPrompt: false
),
ignoreExistingAttachments: false,
attachments: []
),
.init(
commandId: "2",
name: "Refactor Code",
feature: .promptToCode(
extraSystemPrompt: nil,
prompt: "Refactor",
continuousMode: false,
generateDescription: true
),
ignoreExistingAttachments: false,
attachments: []
),
], "CustomCommandView_Preview"))
return CustomCommandView(
store: .init(
initialState: .init(
editCustomCommand: .init(.init(.init(
commandId: "1",
name: "Explain Code",
feature: .chatWithSelection(
extraSystemPrompt: nil,
prompt: "Hello",
useExtraSystemPrompt: false
),
ignoreExistingAttachments: false,
attachments: [] as [CustomCommand.Attachment]
)))
),
reducer: { CustomCommandFeature(settings: settings) }
),
settings: settings
)
}
}
struct CustomCommandView_NoEditing_Preview: PreviewProvider {
static var previews: some View {
let settings = CustomCommandView.Settings(customCommands: .init(wrappedValue: [
.init(
commandId: "1",
name: "Explain Code",
feature: .chatWithSelection(
extraSystemPrompt: nil,
prompt: "Hello",
useExtraSystemPrompt: false
),
ignoreExistingAttachments: false,
attachments: []
),
.init(
commandId: "2",
name: "Refactor Code",
feature: .promptToCode(
extraSystemPrompt: nil,
prompt: "Refactor",
continuousMode: false,
generateDescription: true
),
ignoreExistingAttachments: false,
attachments: []
),
], "CustomCommandView_Preview"))
return CustomCommandView(
store: .init(
initialState: .init(
editCustomCommand: nil
),
reducer: { CustomCommandFeature(settings: settings) }
),
settings: settings
)
}
}
================================================
FILE: Core/Sources/HostApp/CustomCommandSettings/EditCustomCommand.swift
================================================
import ComposableArchitecture
import Foundation
import Preferences
import SwiftUI
@Reducer
struct EditCustomCommand {
enum CommandType: Int, CaseIterable, Equatable {
case sendMessage
case promptToCode
case customChat
case singleRoundDialog
}
@ObservableState
struct State: Equatable {
var name: String = ""
var commandType: CommandType = .sendMessage
var isNewCommand: Bool = false
let commandId: String
var sendMessage = EditSendMessageCommand.State()
var promptToCode = EditPromptToCodeCommand.State()
var customChat = EditCustomChatCommand.State()
var singleRoundDialog = EditSingleRoundDialogCommand.State()
var attachments = EditCustomCommandAttachment.State()
init(_ command: CustomCommand?) {
isNewCommand = command == nil
commandId = command?.id ?? UUID().uuidString
name = command?.name ?? "New Command"
attachments = .init(
attachments: command?.attachments ?? [],
ignoreExistingAttachments: command?.ignoreExistingAttachments ?? false
)
switch command?.feature {
case let .chatWithSelection(extraSystemPrompt, prompt, useExtraSystemPrompt):
commandType = .sendMessage
sendMessage = .init(
extraSystemPrompt: extraSystemPrompt ?? "",
useExtraSystemPrompt: useExtraSystemPrompt ?? false,
prompt: prompt ?? ""
)
case .none:
commandType = .sendMessage
sendMessage = .init(
extraSystemPrompt: "",
useExtraSystemPrompt: false,
prompt: "Hello"
)
case let .customChat(systemPrompt, prompt):
commandType = .customChat
customChat = .init(
systemPrompt: systemPrompt ?? "",
prompt: prompt ?? ""
)
case let .singleRoundDialog(
systemPrompt,
overwriteSystemPrompt,
prompt,
receiveReplyInNotification
):
commandType = .singleRoundDialog
singleRoundDialog = .init(
systemPrompt: systemPrompt ?? "",
overwriteSystemPrompt: overwriteSystemPrompt ?? false,
prompt: prompt ?? "",
receiveReplyInNotification: receiveReplyInNotification ?? false
)
case let .promptToCode(extraSystemPrompt, prompt, continuousMode, generateDescription):
commandType = .promptToCode
promptToCode = .init(
extraSystemPrompt: extraSystemPrompt ?? "",
prompt: prompt ?? "",
continuousMode: continuousMode ?? false,
generateDescription: generateDescription ?? true
)
}
}
}
enum Action: BindableAction, Equatable {
case saveCommand
case close
case binding(BindingAction)
case sendMessage(EditSendMessageCommand.Action)
case promptToCode(EditPromptToCodeCommand.Action)
case customChat(EditCustomChatCommand.Action)
case singleRoundDialog(EditSingleRoundDialogCommand.Action)
case attachments(EditCustomCommandAttachment.Action)
}
let settings: CustomCommandView.Settings
@Dependency(\.toast) var toast
var body: some ReducerOf {
Scope(state: \.sendMessage, action: \.sendMessage) {
EditSendMessageCommand()
}
Scope(state: \.promptToCode, action: \.promptToCode) {
EditPromptToCodeCommand()
}
Scope(state: \.customChat, action: \.customChat) {
EditCustomChatCommand()
}
Scope(state: \.singleRoundDialog, action: \.singleRoundDialog) {
EditSingleRoundDialogCommand()
}
Scope(state: \.attachments, action: \.attachments) {
EditCustomCommandAttachment()
}
BindingReducer()
Reduce { state, action in
switch action {
case .saveCommand:
guard !state.name.isEmpty else {
toast("Command name cannot be empty.", .error)
return .none
}
let newCommand = CustomCommand(
commandId: state.commandId,
name: state.name,
feature: {
switch state.commandType {
case .sendMessage:
let state = state.sendMessage
return .chatWithSelection(
extraSystemPrompt: state.extraSystemPrompt,
prompt: state.prompt,
useExtraSystemPrompt: state.useExtraSystemPrompt
)
case .promptToCode:
let state = state.promptToCode
return .promptToCode(
extraSystemPrompt: state.extraSystemPrompt,
prompt: state.prompt,
continuousMode: state.continuousMode,
generateDescription: state.generateDescription
)
case .customChat:
let state = state.customChat
return .customChat(
systemPrompt: state.systemPrompt,
prompt: state.prompt
)
case .singleRoundDialog:
let state = state.singleRoundDialog
return .singleRoundDialog(
systemPrompt: state.systemPrompt,
overwriteSystemPrompt: state.overwriteSystemPrompt,
prompt: state.prompt,
receiveReplyInNotification: state.receiveReplyInNotification
)
}
}(),
ignoreExistingAttachments: state.attachments.ignoreExistingAttachments,
attachments: state.attachments.attachments
)
if state.isNewCommand {
settings.customCommands.append(newCommand)
state.isNewCommand = false
toast("The command is created.", .info)
} else {
if let index = settings.customCommands.firstIndex(where: {
$0.id == newCommand.id
}) {
settings.customCommands[index] = newCommand
} else {
settings.customCommands.append(newCommand)
}
toast("The command is updated.", .info)
}
return .none
case .close:
return .none
case .binding:
return .none
case .sendMessage:
return .none
case .promptToCode:
return .none
case .customChat:
return .none
case .singleRoundDialog:
return .none
case .attachments:
return .none
}
}
}
}
@Reducer
struct EditCustomCommandAttachment {
@ObservableState
struct State: Equatable {
var attachments: [CustomCommand.Attachment] = []
var ignoreExistingAttachments: Bool = false
}
enum Action: BindableAction, Equatable {
case binding(BindingAction)
}
var body: some ReducerOf {
BindingReducer()
Reduce { _, action in
switch action {
case .binding:
return .none
}
}
}
}
@Reducer
struct EditSendMessageCommand {
@ObservableState
struct State: Equatable {
var extraSystemPrompt: String = ""
var useExtraSystemPrompt: Bool = false
var prompt: String = ""
}
enum Action: BindableAction, Equatable {
case binding(BindingAction)
}
var body: some ReducerOf {
BindingReducer()
Reduce { _, action in
switch action {
case .binding:
return .none
}
}
}
}
@Reducer
struct EditPromptToCodeCommand {
@ObservableState
struct State: Equatable {
var extraSystemPrompt: String = ""
var prompt: String = ""
var continuousMode: Bool = false
var generateDescription: Bool = false
}
enum Action: BindableAction, Equatable {
case binding(BindingAction)
}
var body: some ReducerOf {
BindingReducer()
}
}
@Reducer
struct EditCustomChatCommand {
@ObservableState
struct State: Equatable {
var systemPrompt: String = ""
var prompt: String = ""
}
enum Action: BindableAction, Equatable {
case binding(BindingAction)
}
var body: some ReducerOf {
BindingReducer()
}
}
@Reducer
struct EditSingleRoundDialogCommand {
@ObservableState
struct State: Equatable {
var systemPrompt: String = ""
var overwriteSystemPrompt: Bool = false
var prompt: String = ""
var receiveReplyInNotification: Bool = false
}
enum Action: BindableAction, Equatable {
case binding(BindingAction)
}
var body: some ReducerOf {
BindingReducer()
}
}
================================================
FILE: Core/Sources/HostApp/CustomCommandSettings/EditCustomCommandView.swift
================================================
import ComposableArchitecture
import MarkdownUI
import Preferences
import SwiftUI
@MainActor
struct EditCustomCommandView: View {
@Environment(\.toast) var toast
@Perception.Bindable var store: StoreOf
init(store: StoreOf) {
self.store = store
}
var body: some View {
ScrollView {
Form {
sharedForm
featureSpecificForm
}.padding()
}.safeAreaInset(edge: .bottom) {
bottomBar
}
}
@ViewBuilder var sharedForm: some View {
WithPerceptionTracking {
TextField("Name", text: $store.name)
Picker("Command Type", selection: $store.commandType) {
ForEach(
EditCustomCommand.CommandType.allCases,
id: \.rawValue
) { commandType in
Text({
switch commandType {
case .sendMessage:
return "Send Message"
case .promptToCode:
return "Modification"
case .customChat:
return "Custom Chat"
case .singleRoundDialog:
return "Single Round Dialog"
}
}() as String).tag(commandType)
}
}
}
}
@ViewBuilder var featureSpecificForm: some View {
WithPerceptionTracking {
switch store.commandType {
case .sendMessage:
EditSendMessageCommandView(
store: store.scope(
state: \.sendMessage,
action: \.sendMessage
),
attachmentStore: store.scope(
state: \.attachments,
action: \.attachments
)
)
case .promptToCode:
EditPromptToCodeCommandView(
store: store.scope(
state: \.promptToCode,
action: \.promptToCode
)
)
case .customChat:
EditCustomChatCommandView(
store: store.scope(
state: \.customChat,
action: \.customChat
),
attachmentStore: store.scope(
state: \.attachments,
action: \.attachments
)
)
case .singleRoundDialog:
EditSingleRoundDialogCommandView(
store: store.scope(
state: \.singleRoundDialog,
action: \.singleRoundDialog
)
)
}
}
}
@ViewBuilder var bottomBar: some View {
WithPerceptionTracking {
VStack {
Divider()
VStack(alignment: .trailing) {
Text(
"After renaming or adding a custom command, please restart Xcode to refresh the menu."
)
.foregroundStyle(.secondary)
HStack {
Spacer()
Button("Close") {
store.send(.close)
}
if store.isNewCommand {
Button("Add") {
store.send(.saveCommand)
}
} else {
Button("Save") {
store.send(.saveCommand)
}
}
}
}
.padding(.horizontal)
}
.padding(.bottom)
.background(.regularMaterial)
}
}
}
struct CustomCommandAttachmentPickerView: View {
@Perception.Bindable var store: StoreOf
@State private var isFileInputPresented = false
@State private var filePath = ""
#if canImport(ProHostApp)
var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading) {
Text("Contexts")
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 8) {
if store.attachments.isEmpty {
Text("No context")
.foregroundStyle(.secondary)
} else {
ForEach(store.attachments, id: \.kind) { attachment in
HStack {
switch attachment.kind {
case let .file(path: path):
HStack {
Text("File:")
Text(path).foregroundStyle(.secondary)
}
default:
Text(attachment.kind.description)
}
Spacer()
Button {
store.attachments.removeAll { $0.kind == attachment.kind }
} label: {
Image(systemName: "trash")
}
.buttonStyle(.plain)
}
}
}
}
.frame(minWidth: 240)
.padding(.vertical, 8)
.padding(.horizontal, 8)
.background {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.separator, lineWidth: 1)
}
Form {
Menu {
ForEach(CustomCommand.Attachment.Kind.allCases.filter { kind in
!store.attachments.contains { $0.kind == kind }
}, id: \.self) { kind in
if kind == .file(path: "") {
Button {
isFileInputPresented = true
} label: {
Text("File...")
}
} else {
Button {
store.attachments.append(.init(kind: kind))
} label: {
Text(kind.description)
}
}
}
} label: {
Label("Add context", systemImage: "plus")
}
Toggle(
"Ignore existing contexts",
isOn: $store.ignoreExistingAttachments
)
}
}
}
.sheet(isPresented: $isFileInputPresented) {
VStack(alignment: .leading, spacing: 16) {
Text("Enter file path:")
.font(.headline)
Text(
"You can enter either an absolute path or a path relative to the project root."
)
.font(.caption)
.foregroundStyle(.secondary)
TextField("File path", text: $filePath)
.textFieldStyle(.roundedBorder)
HStack {
Spacer()
Button("Cancel") {
isFileInputPresented = false
filePath = ""
}
Button("Add") {
store.attachments.append(.init(kind: .file(path: filePath)))
isFileInputPresented = false
filePath = ""
}
.disabled(filePath.isEmpty)
}
}
.padding()
.frame(minWidth: 400)
}
}
}
#else
var body: some View { EmptyView() }
#endif
}
extension CustomCommand.Attachment.Kind {
public static var allCases: [CustomCommand.Attachment.Kind] {
[
.activeDocument,
.debugArea,
.clipboard,
.senseScope,
.projectScope,
.webScope,
.gitStatus,
.gitLog,
.file(path: ""),
]
}
var description: String {
switch self {
case .activeDocument: return "Active Document"
case .debugArea: return "Debug Area"
case .clipboard: return "Clipboard"
case .senseScope: return "Sense Scope"
case .projectScope: return "Project Scope"
case .webScope: return "Web Scope"
case .gitStatus: return "Git Status and Diff"
case .gitLog: return "Git Log"
case .file: return "File"
}
}
}
struct EditSendMessageCommandView: View {
@Perception.Bindable var store: StoreOf
var attachmentStore: StoreOf
var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 4) {
Toggle("Extra Context", isOn: $store.useExtraSystemPrompt)
EditableText(text: $store.extraSystemPrompt)
}
.padding(.vertical, 4)
VStack(alignment: .leading, spacing: 4) {
Text("Send immediately")
EditableText(text: $store.prompt)
}
.padding(.vertical, 4)
CustomCommandAttachmentPickerView(store: attachmentStore)
.padding(.vertical, 4)
}
}
}
struct EditPromptToCodeCommandView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
Toggle("Continuous Mode", isOn: $store.continuousMode)
VStack(alignment: .leading, spacing: 4) {
Text("Extra Context")
EditableText(text: $store.extraSystemPrompt)
}
.padding(.vertical, 4)
VStack(alignment: .leading, spacing: 4) {
Text("Instruction")
EditableText(text: $store.prompt)
}
.padding(.vertical, 4)
}
}
}
struct EditCustomChatCommandView: View {
@Perception.Bindable var store: StoreOf
var attachmentStore: StoreOf
var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 4) {
Text("Topic")
EditableText(text: $store.systemPrompt)
}
.padding(.vertical, 4)
VStack(alignment: .leading, spacing: 4) {
Text("Send immediately")
EditableText(text: $store.prompt)
}
.padding(.vertical, 4)
CustomCommandAttachmentPickerView(store: attachmentStore)
.padding(.vertical, 4)
}
}
}
struct EditSingleRoundDialogCommandView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 4) {
Text("System Prompt")
EditableText(text: $store.systemPrompt)
}
.padding(.vertical, 4)
Picker(selection: $store.overwriteSystemPrompt) {
Text("Append to default system prompt").tag(false)
Text("Overwrite default system prompt").tag(true)
} label: {
Text("Mode")
}
.pickerStyle(.radioGroup)
VStack(alignment: .leading, spacing: 4) {
Text("Prompt")
EditableText(text: $store.prompt)
}
.padding(.vertical, 4)
Toggle("Receive response in notification", isOn: $store.receiveReplyInNotification)
Text(
"You will be prompted to grant the app permission to send notifications for the first time."
)
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
// MARK: - Preview
struct EditCustomCommandView_Preview: PreviewProvider {
static var previews: some View {
EditCustomCommandView(
store: .init(
initialState: .init(.init(
commandId: "4",
name: "Explain Code",
feature: .promptToCode(
extraSystemPrompt: nil,
prompt: "Hello",
continuousMode: false,
generateDescription: true
),
ignoreExistingAttachments: false,
attachments: [] as [CustomCommand.Attachment]
)),
reducer: {
EditCustomCommand(
settings: .init(customCommands: .init(
wrappedValue: [],
"CustomCommandView_Preview"
))
)
}
)
)
.frame(width: 800)
}
}
struct EditSingleRoundDialogCommandView_Preview: PreviewProvider {
static var previews: some View {
EditSingleRoundDialogCommandView(store: .init(
initialState: .init(),
reducer: { EditSingleRoundDialogCommand() }
))
.frame(width: 800, height: 600)
}
}
================================================
FILE: Core/Sources/HostApp/DebugView.swift
================================================
import Preferences
import SwiftUI
final class DebugSettings: ObservableObject {
@AppStorage(\.animationACrashSuggestion) var animationACrashSuggestion
@AppStorage(\.animationBCrashSuggestion) var animationBCrashSuggestion
@AppStorage(\.animationCCrashSuggestion) var animationCCrashSuggestion
@AppStorage(\.preCacheOnFileOpen) var preCacheOnFileOpen
@AppStorage(\.useCustomScrollViewWorkaround) var useCustomScrollViewWorkaround
@AppStorage(\.triggerActionWithAccessibilityAPI) var triggerActionWithAccessibilityAPI
@AppStorage(\.alwaysAcceptSuggestionWithAccessibilityAPI)
var alwaysAcceptSuggestionWithAccessibilityAPI
@AppStorage(\.enableXcodeInspectorDebugMenu) var enableXcodeInspectorDebugMenu
@AppStorage(\.disableFunctionCalling) var disableFunctionCalling
@AppStorage(\.disableGitHubCopilotSettingsAutoRefreshOnAppear)
var disableGitHubCopilotSettingsAutoRefreshOnAppear
@AppStorage(\.useUserDefaultsBaseAPIKeychain) var useUserDefaultsBaseAPIKeychain
@AppStorage(\.disableEnhancedWorkspace) var disableEnhancedWorkspace
@AppStorage(\.disableGitIgnoreCheck) var disableGitIgnoreCheck
@AppStorage(\.disableFileContentManipulationByCheatsheet)
var disableFileContentManipulationByCheatsheet
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer)
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
@AppStorage(\.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted)
var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
@AppStorage(\.observeToAXNotificationWithDefaultMode)
var observeToAXNotificationWithDefaultMode
@AppStorage(\.useCloudflareDomainNameForLicenseCheck)
var useCloudflareDomainNameForLicenseCheck
@AppStorage(\.doNotInstallLaunchAgentAutomatically)
var doNotInstallLaunchAgentAutomatically
init() {}
}
struct DebugSettingsView: View {
@StateObject var settings = DebugSettings()
@Environment(\.updateChecker) var updateChecker
var body: some View {
ScrollView {
Form {
Toggle(isOn: $settings.animationACrashSuggestion) {
Text("Enable animation A")
}
Toggle(isOn: $settings.animationBCrashSuggestion) {
Text("Enable animation B")
}
Toggle(isOn: $settings.animationCCrashSuggestion) {
Text("Enable widget breathing animation")
}
Toggle(isOn: $settings.preCacheOnFileOpen) {
Text("Cache editor information on file open")
}
Toggle(isOn: $settings.useCustomScrollViewWorkaround) {
Text("Use custom scroll view workaround for smooth scrolling")
}
Toggle(isOn: $settings.triggerActionWithAccessibilityAPI) {
Text("Trigger command with Accessibility API")
}
Group {
Toggle(isOn: $settings.alwaysAcceptSuggestionWithAccessibilityAPI) {
Text("Always accept suggestion with Accessibility API")
}
Toggle(isOn: $settings.enableXcodeInspectorDebugMenu) {
Text("Enable Xcode inspector debug menu")
}
Toggle(isOn: $settings.disableFunctionCalling) {
Text("Disable function calling for chat feature")
}
Toggle(isOn: $settings.disableGitHubCopilotSettingsAutoRefreshOnAppear) {
Text("Disable GitHub Copilot settings auto refresh status on appear")
}
Toggle(isOn: $settings.useUserDefaultsBaseAPIKeychain) {
Text("Store API keys in UserDefaults")
}
Toggle(isOn: $settings.disableEnhancedWorkspace) {
Text("Disable enhanced workspace")
}
Toggle(isOn: $settings.disableGitIgnoreCheck) {
Text("Disable git ignore check")
}
Toggle(isOn: $settings.disableFileContentManipulationByCheatsheet) {
Text("Disable file content manipulation by cheatsheet")
}
Group {
Toggle(
isOn: $settings
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
) {
Text(
"Re-activate Xcode Inspector when Accessibility API malfunctioning detected"
)
}
Toggle(
isOn: $settings
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
) {
Text("Trigger malfunctioning detection only with events")
}
Toggle(
isOn: $settings
.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
) {
Text("Toast for the reason of re-activation of Xcode Inspector")
}
}
Button("Reset migration version to 0") {
UserDefaults.shared.set(nil, forKey: "OldMigrationVersion")
}
Button("Reset 0.23.0 migration") {
UserDefaults.shared.set("239", forKey: "OldMigrationVersion")
UserDefaults.shared.set(nil, forKey: "MigrateTo240Finished")
UserDefaults.shared.set(nil, forKey: "ChatModels")
UserDefaults.shared.set(nil, forKey: "EmbeddingModels")
}
Group {
Toggle(
isOn: $settings.observeToAXNotificationWithDefaultMode
) {
Text("Observe to AXNotification with default mode")
}
}
Toggle(
isOn: $settings.useCloudflareDomainNameForLicenseCheck
) {
Text("Use Cloudflare domain name for license check")
}
Toggle(
isOn: $settings.doNotInstallLaunchAgentAutomatically
) {
Text("Don't install launch agent automatically")
}
Button("Reset update cycle") {
updateChecker.resetUpdateCycle()
}
}
}
.frame(maxWidth: .infinity)
.padding()
}
}
}
struct DebugSettingsView_Preview: PreviewProvider {
static var previews: some View {
DebugSettingsView()
}
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift
================================================
import Client
import Preferences
import SharedUIComponents
import SwiftUI
import XPCShared
#if canImport(ProHostApp)
import ProHostApp
#endif
struct ChatSettingsGeneralSectionView: View {
class Settings: ObservableObject {
static let availableLocalizedLocales = Locale.availableLocalizedLocales
@AppStorage(\.chatGPTLanguage) var chatGPTLanguage
@AppStorage(\.chatGPTTemperature) var chatGPTTemperature
@AppStorage(\.chatGPTMaxMessageCount) var chatGPTMaxMessageCount
@AppStorage(\.chatFontSize) var chatFontSize
@AppStorage(\.chatCodeFont) var chatCodeFont
@AppStorage(\.defaultChatFeatureChatModelId) var defaultChatFeatureChatModelId
@AppStorage(\.preferredChatModelIdForUtilities) var utilityChatModelId
@AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt
@AppStorage(\.chatSearchPluginMaxIterations) var chatSearchPluginMaxIterations
@AppStorage(\.defaultChatFeatureEmbeddingModelId) var defaultChatFeatureEmbeddingModelId
@AppStorage(\.chatModels) var chatModels
@AppStorage(\.embeddingModels) var embeddingModels
@AppStorage(\.wrapCodeInChatCodeBlock) var wrapCodeInCodeBlock
@AppStorage(\.chatPanelFloatOnTopOption) var chatPanelFloatOnTopOption
@AppStorage(
\.keepFloatOnTopIfChatPanelAndXcodeOverlaps
) var keepFloatOnTopIfChatPanelAndXcodeOverlaps
@AppStorage(
\.disableFloatOnTopWhenTheChatPanelIsDetached
) var disableFloatOnTopWhenTheChatPanelIsDetached
@AppStorage(\.openChatMode) var openChatMode
@AppStorage(\.openChatInBrowserURL) var openChatInBrowserURL
@AppStorage(\.openChatInBrowserInInAppBrowser) var openChatInBrowserInInAppBrowser
var refreshExtensionExtensionOpenChatHandlerTask: Task?
@MainActor
@Published
var openChatOptions = [OpenChatMode]()
init() {
Task { @MainActor in
refreshExtensionOpenChatHandlers()
}
refreshExtensionExtensionOpenChatHandlerTask = Task { [weak self] in
let sequence = NotificationCenter.default
.notifications(named: NSApplication.didBecomeActiveNotification)
for await _ in sequence {
guard let self else { return }
await MainActor.run {
self.refreshExtensionOpenChatHandlers()
}
}
}
}
@MainActor
func refreshExtensionOpenChatHandlers() {
guard let service = try? getService() else { return }
Task { @MainActor in
let handlers = try await service
.send(requestBody: ExtensionServiceRequests.GetExtensionOpenChatHandlers())
openChatOptions = handlers.map {
if $0.isBuiltIn {
return .builtinExtension(
extensionIdentifier: $0.bundleIdentifier,
id: $0.id,
tabName: $0.tabName
)
} else {
return .externalExtension(
extensionIdentifier: $0.bundleIdentifier,
id: $0.id,
tabName: $0.tabName
)
}
}
}
}
}
@Environment(\.openURL) var openURL
@Environment(\.toast) var toast
@StateObject var settings = Settings()
@State var maxTokenOverLimit = false
var body: some View {
VStack {
openChatSettingsForm
SettingsDivider("Conversation")
chatSettingsForm
SettingsDivider("UI")
uiForm
SettingsDivider("Plugin")
pluginForm
}
}
@ViewBuilder
var openChatSettingsForm: some View {
Form {
Picker(
"Open Chat Mode",
selection: .init(get: {
settings.openChatMode.value
}, set: {
settings.openChatMode = .init($0)
})
) {
Text("Open chat panel").tag(OpenChatMode.chatPanel)
Text("Open web page in browser").tag(OpenChatMode.browser)
ForEach(settings.openChatOptions) { mode in
switch mode {
case let .builtinExtension(_, _, name):
Text("Open \(name) tab").tag(mode)
case let .externalExtension(_, _, name):
Text("Open \(name) tab").tag(mode)
default:
EmptyView()
}
}
}
if settings.openChatMode.value == .browser {
TextField(
"Chat web page URL",
text: $settings.openChatInBrowserURL,
prompt: Text("https://")
)
.textFieldStyle(.roundedBorder)
.disableAutocorrection(true)
.autocorrectionDisabled(true)
#if canImport(ProHostApp)
WithFeatureEnabled(\.browserTab) {
Toggle(
"Open web page in chat panel",
isOn: $settings.openChatInBrowserInInAppBrowser
)
}
#endif
}
}
}
@ViewBuilder
var chatSettingsForm: some View {
Form {
Picker(
"Chat model",
selection: $settings.defaultChatFeatureChatModelId
) {
let allModels = settings.chatModels + [.init(
id: "com.github.copilot",
name: "GitHub Copilot Language Server",
format: .openAI,
info: .init()
)]
if !allModels.contains(where: { $0.id == settings.defaultChatFeatureChatModelId }) {
Text(
(allModels.first?.name).map { "\($0) (Default)" } ?? "No model found"
)
.tag(settings.defaultChatFeatureChatModelId)
}
ForEach(allModels, id: \.id) { chatModel in
Text(chatModel.name).tag(chatModel.id)
}
}
Picker(
"Utility chat model",
selection: $settings.utilityChatModelId
) {
Text("Use the default model").tag("")
if !settings.chatModels.contains(where: { $0.id == settings.utilityChatModelId }),
!settings.utilityChatModelId.isEmpty
{
Text(
(settings.chatModels.first?.name).map { "\($0) (Default)" }
?? "No Model Found"
)
.tag(settings.utilityChatModelId)
}
ForEach(settings.chatModels, id: \.id) { chatModel in
Text(chatModel.name).tag(chatModel.id)
}
}
Picker(
"Embedding model",
selection: $settings.defaultChatFeatureEmbeddingModelId
) {
if !settings.embeddingModels
.contains(where: { $0.id == settings.defaultChatFeatureEmbeddingModelId })
{
Text(
(settings.embeddingModels.first?.name).map { "\($0) (Default)" }
?? "No model found"
)
.tag(settings.defaultChatFeatureEmbeddingModelId)
}
ForEach(settings.embeddingModels, id: \.id) { embeddingModel in
Text(embeddingModel.name).tag(embeddingModel.id)
}
}
if #available(macOS 13.0, *) {
LabeledContent("Reply in language") {
languagePicker
}
} else {
HStack {
Text("Reply in language")
languagePicker
}
}
HStack {
Slider(value: $settings.chatGPTTemperature, in: 0...2, step: 0.1) {
Text("Temperature")
}
Text(
"\(settings.chatGPTTemperature.formatted(.number.precision(.fractionLength(1))))"
)
.font(.body)
.foregroundColor(settings.chatGPTTemperature >= 1 ? .red : .secondary)
.monospacedDigit()
.padding(.vertical, 2)
.padding(.horizontal, 6)
.background(
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(Color.primary.opacity(0.1))
)
}
Picker(
"Memory",
selection: $settings.chatGPTMaxMessageCount
) {
Text("No Limit").tag(0)
Text("3 Messages").tag(3)
Text("5 Messages").tag(5)
Text("7 Messages").tag(7)
Text("9 Messages").tag(9)
Text("11 Messages").tag(11)
Text("21 Messages").tag(21)
Text("31 Messages").tag(31)
Text("41 Messages").tag(41)
Text("51 Messages").tag(51)
Text("71 Messages").tag(71)
Text("91 Messages").tag(91)
Text("111 Messages").tag(111)
Text("151 Messages").tag(151)
Text("201 Messages").tag(201)
}
VStack(alignment: .leading, spacing: 4) {
Text("Additional system prompt")
EditableText(text: $settings.defaultChatSystemPrompt)
.lineLimit(6)
}
.padding(.vertical, 4)
}
}
@ViewBuilder
var uiForm: some View {
Form {
HStack {
TextField(text: .init(get: {
"\(Int(settings.chatFontSize))"
}, set: {
settings.chatFontSize = Double(Int($0) ?? 0)
})) {
Text("Font size of message")
}
.textFieldStyle(.roundedBorder)
Text("pt")
}
FontPicker(font: $settings.chatCodeFont) {
Text("Font of code")
}
Toggle(isOn: $settings.wrapCodeInCodeBlock) {
Text("Wrap text in code block")
}
CodeHighlightThemePicker(scenario: .chat)
Picker("Always-on-top behavior", selection: $settings.chatPanelFloatOnTopOption) {
Text("Always").tag(UserDefaultPreferenceKeys.ChatPanelFloatOnTopOption.alwaysOnTop)
Text("When Xcode is active")
.tag(UserDefaultPreferenceKeys.ChatPanelFloatOnTopOption.onTopWhenXcodeIsActive)
Text("Never").tag(UserDefaultPreferenceKeys.ChatPanelFloatOnTopOption.never)
}
Toggle(isOn: $settings.disableFloatOnTopWhenTheChatPanelIsDetached) {
Text("Disable always-on-top when the chat panel is detached")
}.disabled(settings.chatPanelFloatOnTopOption == .never)
Toggle(isOn: $settings.keepFloatOnTopIfChatPanelAndXcodeOverlaps) {
Text("Keep always-on-top if the chat panel and Xcode overlaps and Xcode is active")
}
.disabled(
!settings.disableFloatOnTopWhenTheChatPanelIsDetached
|| settings.chatPanelFloatOnTopOption == .never
)
}
}
@ViewBuilder
var pluginForm: some View {
Form {
TextField(text: .init(get: {
"\(Int(settings.chatSearchPluginMaxIterations))"
}, set: {
settings.chatSearchPluginMaxIterations = Int($0) ?? 0
})) {
Text("Search plugin max iterations")
}
.textFieldStyle(.roundedBorder)
}
}
var languagePicker: some View {
Menu {
if !settings.chatGPTLanguage.isEmpty,
!Settings.availableLocalizedLocales
.contains(settings.chatGPTLanguage)
{
Button(
settings.chatGPTLanguage,
action: { self.settings.chatGPTLanguage = settings.chatGPTLanguage }
)
}
Button(
"Auto-detected by LLM",
action: { self.settings.chatGPTLanguage = "" }
)
ForEach(
Settings.availableLocalizedLocales,
id: \.self
) { localizedLocales in
Button(
localizedLocales,
action: { self.settings.chatGPTLanguage = localizedLocales }
)
}
} label: {
Text(
settings.chatGPTLanguage.isEmpty
? "Auto-detected by LLM"
: settings.chatGPTLanguage
)
}
}
}
// MARK: - Preview
//
// #Preview {
// ScrollView {
// ChatSettingsView()
// .padding()
// }
// .frame(height: 800)
// .environment(\.overrideFeatureFlag, \.never)
// }
//
================================================
FILE: Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsView.swift
================================================
import Preferences
import SharedUIComponents
import SwiftUI
struct ChatSettingsView: View {
enum Tab {
case general
}
@State var tabSelection: Tab = .general
var body: some View {
VStack(spacing: 0) {
Picker("", selection: $tabSelection) {
Text("General").tag(Tab.general)
}
.pickerStyle(.segmented)
.padding(8)
Divider()
.shadow(radius: 10)
ScrollView {
Group {
switch tabSelection {
case .general:
ChatSettingsGeneralSectionView()
}
}.padding()
}
}
}
}
#Preview {
ChatSettingsView()
.frame(width: 600, height: 500)
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift
================================================
import Preferences
import SharedUIComponents
import SwiftUI
struct PromptToCodeSettingsView: View {
final class Settings: ObservableObject {
@AppStorage(\.hideCommonPrecedingSpacesInPromptToCode)
var hideCommonPrecedingSpaces
@AppStorage(\.promptToCodeCodeFont)
var font
@AppStorage(\.promptToCodeGenerateDescription)
var promptToCodeGenerateDescription
@AppStorage(\.promptToCodeGenerateDescriptionInUserPreferredLanguage)
var promptToCodeGenerateDescriptionInUserPreferredLanguage
@AppStorage(\.promptToCodeChatModelId)
var promptToCodeChatModelId
@AppStorage(\.promptToCodeEmbeddingModelId)
var promptToCodeEmbeddingModelId
@AppStorage(\.wrapCodeInPromptToCode) var wrapCode
@AppStorage(\.chatModels) var chatModels
@AppStorage(\.embeddingModels) var embeddingModels
init() {}
}
@StateObject var settings = Settings()
var body: some View {
VStack(alignment: .center) {
Form {
Picker(
"Chat model",
selection: $settings.promptToCodeChatModelId
) {
Text("Same as chat feature").tag("")
if !settings.chatModels
.contains(where: { $0.id == settings.promptToCodeChatModelId }),
!settings.promptToCodeChatModelId.isEmpty
{
Text(
(settings.chatModels.first?.name).map { "\($0) (Default)" }
?? "No model found"
)
.tag(settings.promptToCodeChatModelId)
}
ForEach(settings.chatModels, id: \.id) { chatModel in
Text(chatModel.name).tag(chatModel.id)
}
}
}
SettingsDivider("UI")
Form {
Toggle(isOn: $settings.hideCommonPrecedingSpaces) {
Text("Hide common preceding spaces")
}
Toggle(isOn: $settings.wrapCode) {
Text("Wrap code")
}
CodeHighlightThemePicker(scenario: .promptToCode)
FontPicker(font: $settings.font) {
Text("Font")
}
}
}
}
}
#Preview {
PromptToCodeSettingsView()
.padding()
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionFeatureDisabledLanguageListView.swift
================================================
import SuggestionBasic
import SwiftUI
import SharedUIComponents
struct SuggestionFeatureDisabledLanguageListView: View {
final class Settings: ObservableObject {
@AppStorage(\.suggestionFeatureDisabledLanguageList)
var suggestionFeatureDisabledLanguageList: [String]
init(suggestionFeatureDisabledLanguageList: AppStorage<[String]>? = nil) {
if let list = suggestionFeatureDisabledLanguageList {
_suggestionFeatureDisabledLanguageList = list
}
}
}
var isOpen: Binding
@State var isAddingNewProject = false
@StateObject var settings = Settings()
var body: some View {
VStack(spacing: 0) {
HStack {
Button(action: {
self.isOpen.wrappedValue = false
}) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
.padding()
}
.buttonStyle(.plain)
Text("Disabled Languages")
Spacer()
}
.background(Color(nsColor: .separatorColor))
List {
ForEach(
settings.suggestionFeatureDisabledLanguageList,
id: \.self
) { language in
HStack {
Text(language.capitalized)
.contextMenu {
Button("Remove") {
settings.suggestionFeatureDisabledLanguageList.removeAll(
where: { $0 == language }
)
}
}
Spacer()
Button(action: {
settings.suggestionFeatureDisabledLanguageList.removeAll(
where: { $0 == language }
)
}) {
Image(systemName: "trash.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
.modify { view in
if #available(macOS 13.0, *) {
view.listRowSeparator(.hidden).listSectionSeparator(.hidden)
} else {
view
}
}
}
.removeBackground()
.overlay {
if settings.suggestionFeatureDisabledLanguageList.isEmpty {
Text("""
Empty
Disable the language of a file by right clicking the indicator widget.
""")
.multilineTextAlignment(.center)
.padding()
}
}
}
.focusable(false)
.frame(width: 300, height: 400)
.background(Color(nsColor: .windowBackgroundColor))
}
}
struct SuggestionFeatureDisabledLanguageListView_Preview: PreviewProvider {
static var previews: some View {
SuggestionFeatureDisabledLanguageListView(
isOpen: .constant(true),
settings: .init(suggestionFeatureDisabledLanguageList: .init(wrappedValue: [
"hello/2",
"hello/3",
"hello/4",
], "SuggestionFeatureDisabledLanguageListView_Preview"))
)
}
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionFeatureEnabledProjectListView.swift
================================================
import SharedUIComponents
import SwiftUI
struct SuggestionFeatureEnabledProjectListView: View {
final class Settings: ObservableObject {
@AppStorage(\.suggestionFeatureEnabledProjectList)
var suggestionFeatureEnabledProjectList: [String]
init(suggestionFeatureEnabledProjectList: AppStorage<[String]>? = nil) {
if let list = suggestionFeatureEnabledProjectList {
_suggestionFeatureEnabledProjectList = list
}
}
}
var isOpen: Binding
@State var isAddingNewProject = false
@StateObject var settings = Settings()
var body: some View {
VStack(spacing: 0) {
HStack {
Button(action: {
self.isOpen.wrappedValue = false
}) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
.padding()
}
.buttonStyle(.plain)
Text("Enabled Projects")
Spacer()
Button(action: {
isAddingNewProject = true
}) {
Image(systemName: "plus.circle.fill")
.foregroundStyle(.secondary)
.padding()
}
.buttonStyle(.plain)
}
.background(Color(nsColor: .separatorColor))
List {
ForEach(
settings.suggestionFeatureEnabledProjectList,
id: \.self
) { project in
HStack {
Text(project)
.contextMenu {
Button("Remove") {
settings.suggestionFeatureEnabledProjectList.removeAll(
where: { $0 == project }
)
}
}
Spacer()
Button(action: {
settings.suggestionFeatureEnabledProjectList.removeAll(
where: { $0 == project }
)
}) {
Image(systemName: "trash.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
.modify { view in
if #available(macOS 13.0, *) {
view.listRowSeparator(.hidden).listSectionSeparator(.hidden)
} else {
view
}
}
}
.removeBackground()
.overlay {
if settings.suggestionFeatureEnabledProjectList.isEmpty {
Text("""
Empty
Add project with "+" button
Or right clicking the indicator widget
""")
.multilineTextAlignment(.center)
}
}
}
.focusable(false)
.frame(width: 300, height: 400)
.background(Color(nsColor: .windowBackgroundColor))
.sheet(isPresented: $isAddingNewProject) {
SuggestionFeatureAddEnabledProjectView(isOpen: $isAddingNewProject, settings: settings)
}
}
}
struct SuggestionFeatureAddEnabledProjectView: View {
var isOpen: Binding
var settings: SuggestionFeatureEnabledProjectListView.Settings
@State var rootPath = ""
var body: some View {
VStack {
Text(
"Enter the root path of the project. Do not use `~` to replace /Users/yourUserName."
)
TextField("Root path", text: $rootPath)
HStack {
Spacer()
Button("Cancel") {
isOpen.wrappedValue = false
}
Button("Add") {
settings.suggestionFeatureEnabledProjectList.append(rootPath)
isOpen.wrappedValue = false
}
}
}
.padding()
.frame(minWidth: 500)
.background(Color(nsColor: .windowBackgroundColor))
}
}
struct SuggestionFeatureEnabledProjectListView_Preview: PreviewProvider {
static var previews: some View {
SuggestionFeatureEnabledProjectListView(
isOpen: .constant(true),
settings: .init(suggestionFeatureEnabledProjectList: .init(wrappedValue: [
"hello/2",
"hello/3",
"hello/4",
], "SuggestionFeatureEnabledProjectListView_Preview"))
)
}
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift
================================================
import Client
import Preferences
import SharedUIComponents
import SwiftUI
import XPCShared
#if canImport(ProHostApp)
import ProHostApp
#endif
struct SuggestionSettingsGeneralSectionView: View {
struct SuggestionFeatureProviderOption: Identifiable, Hashable {
var id: String {
(builtInProvider?.rawValue).map(String.init) ?? bundleIdentifier ?? "n/A"
}
var name: String
var builtInProvider: BuiltInSuggestionFeatureProvider?
var bundleIdentifier: String?
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
init(
name: String,
builtInProvider: BuiltInSuggestionFeatureProvider? = nil,
bundleIdentifier: String? = nil
) {
self.name = name
self.builtInProvider = builtInProvider
self.bundleIdentifier = bundleIdentifier
}
}
final class Settings: ObservableObject {
@AppStorage(\.realtimeSuggestionToggle)
var realtimeSuggestionToggle
@AppStorage(\.realtimeSuggestionDebounce)
var realtimeSuggestionDebounce
@AppStorage(\.suggestionPresentationMode)
var suggestionPresentationMode
@AppStorage(\.disableSuggestionFeatureGlobally)
var disableSuggestionFeatureGlobally
@AppStorage(\.suggestionFeatureEnabledProjectList)
var suggestionFeatureEnabledProjectList
@AppStorage(\.hideCommonPrecedingSpacesInSuggestion)
var hideCommonPrecedingSpacesInSuggestion
@AppStorage(\.suggestionCodeFont)
var font
@AppStorage(\.suggestionFeatureProvider)
var suggestionFeatureProvider
@AppStorage(\.suggestionDisplayCompactMode)
var suggestionDisplayCompactMode
@AppStorage(\.acceptSuggestionWithTab)
var acceptSuggestionWithTab
@AppStorage(\.dismissSuggestionWithEsc)
var dismissSuggestionWithEsc
var refreshExtensionSuggestionFeatureProvidersTask: Task?
@MainActor
@Published
var extensionSuggestionFeatureProviderOptions = [SuggestionFeatureProviderOption]()
init() {
Task { @MainActor in
refreshExtensionSuggestionFeatureProviders()
}
refreshExtensionSuggestionFeatureProvidersTask = Task { [weak self] in
let sequence = NotificationCenter.default
.notifications(named: NSApplication.didBecomeActiveNotification)
for await _ in sequence {
guard let self else { return }
await MainActor.run {
self.refreshExtensionSuggestionFeatureProviders()
}
}
}
}
@MainActor
func refreshExtensionSuggestionFeatureProviders() {
guard let service = try? getService() else { return }
Task { @MainActor in
let services = try await service
.send(requestBody: ExtensionServiceRequests.GetExtensionSuggestionServices())
extensionSuggestionFeatureProviderOptions = services.map {
.init(name: $0.name, bundleIdentifier: $0.bundleIdentifier)
}
}
}
}
@StateObject var settings = Settings()
@State var isSuggestionFeatureEnabledListPickerOpen = false
@State var isSuggestionFeatureDisabledLanguageListViewOpen = false
@State var isTabToAcceptSuggestionModifierViewOpen = false
var body: some View {
Form {
Picker(selection: $settings.suggestionPresentationMode) {
ForEach(PresentationMode.allCases, id: \.rawValue) {
switch $0 {
case .nearbyTextCursor:
Text("Nearby text cursor").tag($0)
case .floatingWidget:
Text("Floating widget").tag($0)
}
}
} label: {
Text("Presentation")
}
Picker(selection: Binding(get: {
switch settings.suggestionFeatureProvider {
case let .builtIn(provider):
return SuggestionFeatureProviderOption(
name: "",
builtInProvider: provider
)
case let .extension(name, identifier):
return SuggestionFeatureProviderOption(
name: name,
bundleIdentifier: identifier
)
}
}, set: { (option: SuggestionFeatureProviderOption) in
if let provider = option.builtInProvider {
settings.suggestionFeatureProvider = .builtIn(provider)
} else {
settings.suggestionFeatureProvider = .extension(
name: option.name,
bundleIdentifier: option.bundleIdentifier ?? ""
)
}
})) {
ForEach(BuiltInSuggestionFeatureProvider.allCases, id: \.rawValue) {
switch $0 {
case .gitHubCopilot:
Text("GitHub Copilot")
.tag(SuggestionFeatureProviderOption(name: "", builtInProvider: $0))
case .codeium:
Text("Codeium")
.tag(SuggestionFeatureProviderOption(name: "", builtInProvider: $0))
}
}
ForEach(settings.extensionSuggestionFeatureProviderOptions, id: \.self) { item in
Text(item.name).tag(item)
}
if case let .extension(name, identifier) = settings.suggestionFeatureProvider {
if !settings.extensionSuggestionFeatureProviderOptions.contains(where: {
$0.bundleIdentifier == identifier
}) {
Text("\(name) (Not found)").tag(
SuggestionFeatureProviderOption(
name: name,
bundleIdentifier: identifier
)
)
}
}
} label: {
Text("Feature provider")
}
Toggle(isOn: $settings.realtimeSuggestionToggle) {
Text("Real-time suggestion")
}
Toggle(isOn: $settings.acceptSuggestionWithTab) {
HStack {
Text("Accept suggestion with Tab")
Button(action: {
isTabToAcceptSuggestionModifierViewOpen = true
}) {
Image(systemName: "gearshape.fill")
}
.buttonStyle(.plain)
}
}.sheet(isPresented: $isTabToAcceptSuggestionModifierViewOpen) {
TabToAcceptSuggestionModifierView()
}
Toggle(isOn: $settings.dismissSuggestionWithEsc) {
Text("Dismiss suggestion with ESC")
}
HStack {
Toggle(isOn: $settings.disableSuggestionFeatureGlobally) {
Text("Disable suggestion feature globally")
}
Button("Exception list") {
isSuggestionFeatureEnabledListPickerOpen = true
}
}.sheet(isPresented: $isSuggestionFeatureEnabledListPickerOpen) {
SuggestionFeatureEnabledProjectListView(
isOpen: $isSuggestionFeatureEnabledListPickerOpen
)
}
HStack {
Button("Disabled language list") {
isSuggestionFeatureDisabledLanguageListViewOpen = true
}
}.sheet(isPresented: $isSuggestionFeatureDisabledLanguageListViewOpen) {
SuggestionFeatureDisabledLanguageListView(
isOpen: $isSuggestionFeatureDisabledLanguageListViewOpen
)
}
HStack {
Slider(value: $settings.realtimeSuggestionDebounce, in: 0.1...2, step: 0.1) {
Text("Real-time suggestion debounce")
}
Text(
"\(settings.realtimeSuggestionDebounce.formatted(.number.precision(.fractionLength(2))))s"
)
.font(.body)
.monospacedDigit()
.padding(.vertical, 2)
.padding(.horizontal, 6)
.background(
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(Color.primary.opacity(0.1))
)
}
}
SettingsDivider("UI")
Form {
Toggle(isOn: $settings.suggestionDisplayCompactMode) {
Text("Hide buttons")
}
Toggle(isOn: $settings.hideCommonPrecedingSpacesInSuggestion) {
Text("Hide common preceding spaces")
}
CodeHighlightThemePicker(scenario: .suggestion)
FontPicker(font: $settings.font) {
Text("Font")
}
}
}
struct TabToAcceptSuggestionModifierView: View {
final class Settings: ObservableObject {
@AppStorage(\.acceptSuggestionWithModifierCommand)
var needCommand
@AppStorage(\.acceptSuggestionWithModifierOption)
var needOption
@AppStorage(\.acceptSuggestionWithModifierShift)
var needShift
@AppStorage(\.acceptSuggestionWithModifierControl)
var needControl
@AppStorage(\.acceptSuggestionWithModifierOnlyForSwift)
var onlyForSwift
@AppStorage(\.acceptSuggestionLineWithModifierControl)
var acceptLineWithControl
}
@StateObject var settings = Settings()
@Environment(\.dismiss) var dismiss
var body: some View {
VStack(spacing: 0) {
Form {
Text("Accept suggestion with modifier")
.font(.headline)
HStack {
Toggle(isOn: $settings.needCommand) {
Text("Command")
}
Toggle(isOn: $settings.needOption) {
Text("Option")
}
Toggle(isOn: $settings.needShift) {
Text("Shift")
}
Toggle(isOn: $settings.needControl) {
Text("Control")
}
}
Toggle(isOn: $settings.onlyForSwift) {
Text("Only require modifiers for Swift")
}
Divider()
Toggle(isOn: $settings.acceptLineWithControl) {
Text("Accept suggestion first line with Control")
}
}
.padding()
Divider()
HStack {
Spacer()
Button(action: { dismiss() }) {
Text("Done")
}
.keyboardShortcut(.defaultAction)
}
.padding()
}
}
}
}
#Preview {
SuggestionSettingsGeneralSectionView()
.padding()
}
#Preview {
SuggestionSettingsGeneralSectionView.TabToAcceptSuggestionModifierView()
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsView.swift
================================================
import Client
import Preferences
import SharedUIComponents
import SwiftUI
import XPCShared
struct SuggestionSettingsView: View {
var tabContainer: ExternalTabContainer {
ExternalTabContainer.tabContainer(for: "SuggestionSettings")
}
enum Tab: Hashable {
case general
case other(String)
}
@State var tabSelection: Tab = .general
var body: some View {
VStack(spacing: 0) {
Picker("", selection: $tabSelection) {
Text("General").tag(Tab.general)
ForEach(tabContainer.tabs, id: \.id) { tab in
Text(tab.title).tag(Tab.other(tab.id))
}
}
.pickerStyle(.segmented)
.padding(8)
Divider()
.shadow(radius: 10)
ScrollView {
Group {
switch tabSelection {
case .general:
SuggestionSettingsGeneralSectionView()
case let .other(id):
tabContainer.tabView(for: id)
}
}.padding()
}
}
}
}
struct SuggestionSettingsView_Previews: PreviewProvider {
static var previews: some View {
SuggestionSettingsView()
.frame(width: 600, height: 500)
}
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/TerminalSettingsView.swift
================================================
import Preferences
import SharedUIComponents
import SwiftUI
#if canImport(ProHostApp)
import ProHostApp
#endif
struct TerminalSettingsView: View {
class Settings: ObservableObject {
@AppStorage(\.terminalFont) var terminalFont
init() {}
}
@StateObject var settings = Settings()
var body: some View {
ScrollView {
Form {
FontPicker(font: $settings.terminalFont) {
Text("Font of code")
}
}
}
}
}
================================================
FILE: Core/Sources/HostApp/FeatureSettings/XcodeSettingsView.swift
================================================
import Foundation
import SharedUIComponents
import SwiftUI
#if canImport(ProHostApp)
import ProHostApp
#endif
struct XcodeSettingsView: View {
var body: some View {
VStack {
#if canImport(ProHostApp)
CloseXcodeIdleTabsSettingsView()
#endif
EmptyView()
}
}
}
================================================
FILE: Core/Sources/HostApp/FeatureSettingsView.swift
================================================
import SwiftUI
import SharedUIComponents
struct FeatureSettingsView: View {
var tabContainer: ExternalTabContainer {
ExternalTabContainer.tabContainer(for: "Features")
}
@State var tag = 0
var body: some View {
SidebarTabView(tag: $tag) {
SuggestionSettingsView()
.sidebarItem(
tag: 0,
title: "Suggestion",
subtitle: "Generate suggestions for your code",
image: "lightbulb"
)
ChatSettingsView()
.sidebarItem(
tag: 1,
title: "Chat",
subtitle: "Chat about your code",
image: "character.bubble"
)
ScrollView {
PromptToCodeSettingsView().padding()
}
.sidebarItem(
tag: 2,
title: "Modification",
subtitle: "Write or modify code with natural language",
image: "paintbrush"
)
ScrollView {
XcodeSettingsView().padding()
}
.sidebarItem(
tag: 3,
title: "Xcode",
subtitle: "Xcode related features",
image: "hammer.circle"
)
ForEach(Array(tabContainer.tabs.enumerated()), id: \.1.id) { index, tab in
ScrollView {
tab.viewBuilder().padding()
}
.sidebarItem(
tag: 4 + index,
title: tab.title,
subtitle: tab.description,
image: tab.image
)
}
}
}
}
struct FeatureSettingsView_Previews: PreviewProvider {
static var previews: some View {
FeatureSettingsView()
.frame(width: 800)
}
}
================================================
FILE: Core/Sources/HostApp/General.swift
================================================
import Client
import ComposableArchitecture
import Foundation
import LaunchAgentManager
import SwiftUI
import XPCShared
@Reducer
struct General {
@ObservableState
struct State: Equatable {
var xpcServiceVersion: String?
var isAccessibilityPermissionGranted: Bool?
var isReloading = false
@Presents var alert: AlertState?
}
enum Action {
case appear
case setupLaunchAgentIfNeeded
case setupLaunchAgentClicked
case removeLaunchAgentClicked
case reloadLaunchAgentClicked
case openExtensionManager
case reloadStatus
case finishReloading(xpcServiceVersion: String, permissionGranted: Bool)
case failedReloading
case alert(PresentationAction)
case setupLaunchAgent
case finishSetupLaunchAgent
case finishRemoveLaunchAgent
case finishReloadLaunchAgent
@CasePathable
enum Alert: Equatable {
case moveToApplications
case moveTo(URL)
case install
}
}
@Dependency(\.toast) var toast
struct ReloadStatusCancellableId: Hashable {}
static var didWarnInstallationPosition: Bool {
get { UserDefaults.standard.bool(forKey: "didWarnInstallationPosition") }
set { UserDefaults.standard.set(newValue, forKey: "didWarnInstallationPosition") }
}
static var bundleIsInApplicationsFolder: Bool {
Bundle.main.bundleURL.path.hasPrefix("/Applications")
}
var body: some ReducerOf {
Reduce { state, action in
switch action {
case .appear:
if Self.bundleIsInApplicationsFolder {
return .run { send in
await send(.setupLaunchAgentIfNeeded)
}
}
if !Self.didWarnInstallationPosition {
Self.didWarnInstallationPosition = true
state.alert = .init {
TextState("Move to Applications Folder?")
} actions: {
ButtonState(action: .moveToApplications) {
TextState("Move")
}
ButtonState(role: .cancel) {
TextState("Not Now")
}
} message: {
TextState(
"To ensure the best experience, please move the app to the Applications folder. If the app is not inside the Applications folder, please set up the launch agent manually by clicking the button."
)
}
}
return .none
case .setupLaunchAgentIfNeeded:
return .run { send in
#if DEBUG
// do not auto install on debug build
#else
do {
try await LaunchAgentManager()
.setupLaunchAgentForTheFirstTimeIfNeeded()
} catch {
toast(error.localizedDescription, .error)
}
#endif
await send(.reloadStatus)
}
case .setupLaunchAgentClicked:
if Self.bundleIsInApplicationsFolder {
return .run { send in
await send(.setupLaunchAgent)
}
}
state.alert = .init {
TextState("Setup Launch Agent")
} actions: {
ButtonState(action: .install) {
TextState("Setup")
}
ButtonState(action: .moveToApplications) {
TextState("Move to Applications Folder")
}
ButtonState(role: .cancel) {
TextState("Cancel")
}
} message: {
TextState(
"It's recommended to move the app into the Applications folder. But you can still keep it in the current folder and install the launch agent to ~/Library/LaunchAgents."
)
}
return .none
case .removeLaunchAgentClicked:
return .run { send in
do {
try await LaunchAgentManager().removeLaunchAgent()
await send(.finishRemoveLaunchAgent)
} catch {
toast(error.localizedDescription, .error)
}
await send(.reloadStatus)
}
case .reloadLaunchAgentClicked:
return .run { send in
do {
try await LaunchAgentManager().reloadLaunchAgent()
await send(.finishReloadLaunchAgent)
} catch {
toast(error.localizedDescription, .error)
}
await send(.reloadStatus)
}
case .setupLaunchAgent:
return .run { send in
do {
try await LaunchAgentManager().setupLaunchAgent()
await send(.finishSetupLaunchAgent)
} catch {
toast(error.localizedDescription, .error)
}
await send(.reloadStatus)
}
case .finishSetupLaunchAgent:
state.alert = .init {
TextState("Launch Agent Installed")
} actions: {
ButtonState {
TextState("OK")
}
} message: {
TextState(
"The launch agent has been installed. Please restart the app."
)
}
return .none
case .finishRemoveLaunchAgent:
state.alert = .init {
TextState("Launch Agent Removed")
} actions: {
ButtonState {
TextState("OK")
}
} message: {
TextState(
"The launch agent has been removed."
)
}
return .none
case .finishReloadLaunchAgent:
state.alert = .init {
TextState("Launch Agent Reloaded")
} actions: {
ButtonState {
TextState("OK")
}
} message: {
TextState(
"The launch agent has been reloaded."
)
}
return .none
case .openExtensionManager:
return .run { send in
let service = try getService()
do {
_ = try await service
.send(requestBody: ExtensionServiceRequests.OpenExtensionManager())
} catch {
toast(error.localizedDescription, .error)
await send(.failedReloading)
}
}
case .reloadStatus:
state.isReloading = true
return .run { send in
let service = try getService()
do {
let isCommunicationReady = try await service.launchIfNeeded()
if isCommunicationReady {
let xpcServiceVersion = try await service.getXPCServiceVersion().version
let isAccessibilityPermissionGranted = try await service
.getXPCServiceAccessibilityPermission()
await send(.finishReloading(
xpcServiceVersion: xpcServiceVersion,
permissionGranted: isAccessibilityPermissionGranted
))
} else {
toast("Launching service app.", .info)
try await Task.sleep(nanoseconds: 5_000_000_000)
await send(.reloadStatus)
}
} catch let error as XPCCommunicationBridgeError {
toast(
"Failed to reach communication bridge. \(error.localizedDescription)",
.error
)
await send(.failedReloading)
} catch {
toast(error.localizedDescription, .error)
await send(.failedReloading)
}
}.cancellable(id: ReloadStatusCancellableId(), cancelInFlight: true)
case let .finishReloading(version, granted):
state.xpcServiceVersion = version
state.isAccessibilityPermissionGranted = granted
state.isReloading = false
return .none
case .failedReloading:
state.isReloading = false
return .none
case let .alert(.presented(action)):
switch action {
case .moveToApplications:
return .run { send in
let appURL = URL(fileURLWithPath: "/Applications")
await send(.alert(.presented(.moveTo(appURL))))
}
case let .moveTo(url):
return .run { _ in
do {
try FileManager.default.moveItem(
at: Bundle.main.bundleURL,
to: url.appendingPathComponent(
Bundle.main.bundleURL.lastPathComponent
)
)
await NSApplication.shared.terminate(nil)
} catch {
toast(error.localizedDescription, .error)
}
}
case .install:
return .run { send in
await send(.setupLaunchAgent)
}
}
case .alert(.dismiss):
state.alert = nil
return .none
}
}
}
}
================================================
FILE: Core/Sources/HostApp/GeneralView.swift
================================================
import Client
import ComposableArchitecture
import KeyboardShortcuts
import LaunchAgentManager
import Preferences
import SharedUIComponents
import SwiftUI
struct GeneralView: View {
let store: StoreOf
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
AppInfoView(store: store)
SettingsDivider()
ExtensionServiceView(store: store)
SettingsDivider()
LaunchAgentView(store: store)
SettingsDivider()
GeneralSettingsView()
}
}
.onAppear {
store.send(.appear)
}
}
}
struct AppInfoView: View {
@State var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
@Environment(\.updateChecker) var updateChecker
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(
Bundle.main
.object(forInfoDictionaryKey: "HOST_APP_NAME") as? String
?? "Copilot for Xcode"
)
.font(.title)
Text(appVersion ?? "")
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
Button(action: {
store.send(.openExtensionManager)
}) {
HStack(spacing: 2) {
Image(systemName: "puzzlepiece.extension.fill")
Text("Extensions")
}
}
Button(action: {
updateChecker.checkForUpdates()
}) {
HStack(spacing: 2) {
Image(systemName: "arrow.up.right.circle.fill")
Text("Check for Updates")
}
}
}
HStack(spacing: 16) {
Link(
destination: URL(string: "https://github.com/intitni/CopilotForXcode")!
) {
HStack(spacing: 2) {
Image(systemName: "link")
Text("GitHub")
}
}
.focusable(false)
.foregroundColor(.accentColor)
Link(destination: URL(string: "https://www.buymeacoffee.com/intitni")!) {
HStack(spacing: 2) {
Image(systemName: "cup.and.saucer.fill")
Text("Buy Me A Coffee")
}
}
.foregroundColor(.accentColor)
.focusable(false)
}
}
.padding()
.alert($store.scope(state: \.alert, action: \.alert))
}
}
}
struct ExtensionServiceView: View {
@Perception.Bindable var store: StoreOf
var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading) {
Text("Extension Service Version: \(store.xpcServiceVersion ?? "Loading..")")
let grantedStatus: String = {
guard let granted = store.isAccessibilityPermissionGranted
else { return "Loading.." }
return granted ? "Granted" : "Not Granted"
}()
Text("Accessibility Permission: \(grantedStatus)")
HStack {
Button(action: { store.send(.reloadStatus) }) {
Text("Refresh")
}.disabled(store.isReloading)
Button(action: {
Task {
let workspace = NSWorkspace.shared
let url = Bundle.main.bundleURL
.appendingPathComponent("Contents")
.appendingPathComponent("Applications")
.appendingPathComponent("CopilotForXcodeExtensionService.app")
workspace.activateFileViewerSelecting([url])
}
}) {
Text("Reveal Extension Service in Finder")
}
Button(action: {
let url = URL(
string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
)!
NSWorkspace.shared.open(url)
}) {
Text("Accessibility Settings")
}
Button(action: {
let url = URL(
string: "x-apple.systempreferences:com.apple.ExtensionsPreferences"
)!
NSWorkspace.shared.open(url)
}) {
Text("Extensions Settings")
}
}
}
}
.padding()
}
}
struct LaunchAgentView: View {
@Perception.Bindable var store: StoreOf
@Environment(\.toast) var toast
var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading) {
HStack {
Button(action: {
store.send(.setupLaunchAgentClicked)
}) {
Text("Setup Launch Agent")
}
Button(action: {
store.send(.removeLaunchAgentClicked)
}) {
Text("Remove Launch Agent")
}
Button(action: {
store.send(.reloadLaunchAgentClicked)
}) {
Text("Reload Launch Agent")
}
}
}
.padding()
}
}
}
struct GeneralSettingsView: View {
class Settings: ObservableObject {
@AppStorage(\.quitXPCServiceOnXcodeAndAppQuit)
var quitXPCServiceOnXcodeAndAppQuit
@AppStorage(\.suggestionWidgetPositionMode)
var suggestionWidgetPositionMode
@AppStorage(\.widgetColorScheme)
var widgetColorScheme
@AppStorage(\.preferWidgetToStayInsideEditorWhenWidthGreaterThan)
var preferWidgetToStayInsideEditorWhenWidthGreaterThan
@AppStorage(\.hideCircularWidget)
var hideCircularWidget
@AppStorage(\.showHideWidgetShortcutGlobally)
var showHideWidgetShortcutGlobally
@AppStorage(\.installBetaBuilds)
var installBetaBuilds
}
@StateObject var settings = Settings()
@Environment(\.updateChecker) var updateChecker
@State var automaticallyCheckForUpdate: Bool?
var body: some View {
Form {
Toggle(isOn: $settings.quitXPCServiceOnXcodeAndAppQuit) {
Text("Quit service when Xcode and host app are terminated")
}
Toggle(isOn: .init(
get: { automaticallyCheckForUpdate ?? updateChecker.automaticallyChecksForUpdates },
set: {
updateChecker.automaticallyChecksForUpdates = $0
automaticallyCheckForUpdate = $0
}
)) {
Text("Automatically Check for Update")
}
Toggle(isOn: $settings.installBetaBuilds) {
Text("Install beta builds")
}
Picker(selection: $settings.suggestionWidgetPositionMode) {
ForEach(SuggestionWidgetPositionMode.allCases, id: \.rawValue) {
switch $0 {
case .fixedToBottom:
Text("Fixed to Bottom").tag($0)
case .alignToTextCursor:
Text("Follow Text Cursor").tag($0)
}
}
} label: {
Text("Widget position")
}
Picker(selection: $settings.widgetColorScheme) {
ForEach(WidgetColorScheme.allCases, id: \.rawValue) {
switch $0 {
case .system:
Text("System").tag($0)
case .light:
Text("Light").tag($0)
case .dark:
Text("Dark").tag($0)
}
}
} label: {
Text("Widget color scheme")
}
HStack(alignment: .firstTextBaseline) {
TextField(text: .init(get: {
"\(Int(settings.preferWidgetToStayInsideEditorWhenWidthGreaterThan))"
}, set: {
settings
.preferWidgetToStayInsideEditorWhenWidthGreaterThan =
Double(Int($0) ?? 0)
})) {
Text("Prefer widget to be inside editor\nwhen width greater than")
.multilineTextAlignment(.trailing)
}
.textFieldStyle(.roundedBorder)
Text("pt")
}
KeyboardShortcuts.Recorder("Hotkey to Toggle Widgets", name: .showHideWidget) { _ in
// It's not used in this app!
KeyboardShortcuts.disable(.showHideWidget)
}
Toggle(isOn: $settings.showHideWidgetShortcutGlobally) {
Text("Enable the Hotkey Globally")
}
Toggle(isOn: $settings.hideCircularWidget) {
Text("Hide indicator widget")
}
}.padding()
}
}
struct WidgetPositionIcon: View {
var position: SuggestionWidgetPositionMode
var isSelected: Bool
var body: some View {
ZStack {
Rectangle()
.fill(Color(nsColor: .textBackgroundColor))
Rectangle()
.fill(Color.accentColor.opacity(0.2))
.frame(width: 120, height: 20)
}
.frame(width: 120, height: 80)
}
}
struct LargeIconPicker<
Data: RandomAccessCollection,
ID: Hashable,
Content: View,
Label: View
>: View {
@Binding var selection: Data.Element
var data: Data
var id: KeyPath
var builder: (Data.Element, _ isSelected: Bool) -> Content
var label: () -> Label
@ViewBuilder
var content: some View {
HStack {
ForEach(data, id: id) { item in
let isSelected = selection[keyPath: id] == item[keyPath: id]
Button(action: {
selection = item
}) {
builder(item, isSelected)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(
isSelected ? Color.accentColor : Color.primary.opacity(0.1),
style: .init(lineWidth: 2)
)
}
}.buttonStyle(.plain)
}
}
}
var body: some View {
if #available(macOS 13.0, *) {
LabeledContent {
content
} label: {
label()
}
} else {
VStack {
label()
content
}
}
}
}
struct GeneralView_Previews: PreviewProvider {
static var previews: some View {
GeneralView(store: .init(initialState: .init(), reducer: { General() }))
.frame(height: 800)
}
}
================================================
FILE: Core/Sources/HostApp/HandleToast.swift
================================================
import Dependencies
import SwiftUI
import Toast
struct ToastHandler: View {
@ObservedObject var toastController: ToastController
let namespace: String?
init(toastController: ToastController, namespace: String?) {
_toastController = .init(wrappedValue: toastController)
self.namespace = namespace
}
var body: some View {
VStack(spacing: 4) {
ForEach(toastController.messages) { message in
if let n = message.namespace, n != namespace {
EmptyView()
} else {
message.content
.foregroundColor(.white)
.padding(8)
.background({
switch message.type {
case .info: return Color.accentColor
case .error: return Color(nsColor: .systemRed)
case .warning: return Color(nsColor: .systemOrange)
}
}() as Color, in: RoundedRectangle(cornerRadius: 8))
.shadow(color: Color.black.opacity(0.2), radius: 4)
}
}
}
.padding()
.allowsHitTesting(false)
}
}
extension View {
func handleToast(namespace: String? = nil) -> some View {
@Dependency(\.toastController) var toastController
return overlay(alignment: .bottom) {
ToastHandler(toastController: toastController, namespace: namespace)
}.environment(\.toast) { [toastController] content, type in
toastController.toast(content: content, type: type, namespace: namespace)
}
}
}
================================================
FILE: Core/Sources/HostApp/HostApp.swift
================================================
import Client
import ComposableArchitecture
import Foundation
import KeyboardShortcuts
#if canImport(LicenseManagement)
import ProHostApp
#endif
extension KeyboardShortcuts.Name {
static let showHideWidget = Self("ShowHideWidget")
}
@Reducer
struct HostApp {
@ObservableState
struct State: Equatable {
var general = General.State()
var chatModelManagement = ChatModelManagement.State()
var embeddingModelManagement = EmbeddingModelManagement.State()
var webSearchSettings = WebSearchSettings.State()
}
enum Action {
case appear
case general(General.Action)
case chatModelManagement(ChatModelManagement.Action)
case embeddingModelManagement(EmbeddingModelManagement.Action)
case webSearchSettings(WebSearchSettings.Action)
}
@Dependency(\.toast) var toast
init() {
KeyboardShortcuts.userDefaults = .shared
}
var body: some ReducerOf {
Scope(state: \.general, action: \.general) {
General()
}
Scope(state: \.chatModelManagement, action: \.chatModelManagement) {
ChatModelManagement()
}
Scope(state: \.embeddingModelManagement, action: \.embeddingModelManagement) {
EmbeddingModelManagement()
}
Scope(state: \.webSearchSettings, action: \.webSearchSettings) {
WebSearchSettings()
}
Reduce { _, action in
switch action {
case .appear:
#if canImport(ProHostApp)
ProHostApp.start()
#endif
return .none
case .general:
return .none
case .chatModelManagement:
return .none
case .embeddingModelManagement:
return .none
case .webSearchSettings:
return .none
}
}
}
}
import Dependencies
import Keychain
import Preferences
struct UserDefaultsDependencyKey: DependencyKey {
static var liveValue: UserDefaultsType = UserDefaults.shared
static var previewValue: UserDefaultsType = {
let it = UserDefaults(suiteName: "HostAppPreview")!
it.removePersistentDomain(forName: "HostAppPreview")
return it
}()
static var testValue: UserDefaultsType = {
let it = UserDefaults(suiteName: "HostAppTest")!
it.removePersistentDomain(forName: "HostAppTest")
return it
}()
}
extension DependencyValues {
var userDefaults: UserDefaultsType {
get { self[UserDefaultsDependencyKey.self] }
set { self[UserDefaultsDependencyKey.self] = newValue }
}
}
struct APIKeyKeychainDependencyKey: DependencyKey {
static var liveValue: KeychainType = Keychain.apiKey
static var previewValue: KeychainType = FakeKeyChain()
static var testValue: KeychainType = FakeKeyChain()
}
extension DependencyValues {
var apiKeyKeychain: KeychainType {
get { self[APIKeyKeychainDependencyKey.self] }
set { self[APIKeyKeychainDependencyKey.self] = newValue }
}
}
================================================
FILE: Core/Sources/HostApp/IsPreview.swift
================================================
import Foundation
var isPreview: Bool { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" }
================================================
FILE: Core/Sources/HostApp/LaunchAgentManager.swift
================================================
import Foundation
import LaunchAgentManager
extension LaunchAgentManager {
init() {
self.init(
serviceIdentifier: Bundle.main
.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String +
".CommunicationBridge",
executableURL: Bundle.main.bundleURL
.appendingPathComponent("Contents")
.appendingPathComponent("Applications")
.appendingPathComponent("CommunicationBridge"),
bundleIdentifier: Bundle.main
.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String
)
}
}
================================================
FILE: Core/Sources/HostApp/ServiceView.swift
================================================
import ComposableArchitecture
import SwiftUI
struct ServiceView: View {
let store: StoreOf
@State var tag = 0
var body: some View {
WithPerceptionTracking {
SidebarTabView(tag: $tag) {
WithPerceptionTracking {
ScrollView {
GitHubCopilotView().padding()
}.sidebarItem(
tag: 0,
title: "GitHub Copilot",
subtitle: "Suggestion",
image: "globe"
)
ScrollView {
CodeiumView().padding()
}.sidebarItem(
tag: 1,
title: "Codeium",
subtitle: "Suggestion",
image: "globe"
)
ChatModelManagementView(store: store.scope(
state: \.chatModelManagement,
action: \.chatModelManagement
)).sidebarItem(
tag: 2,
title: "Chat Models",
subtitle: "Chat, Modification",
image: "globe"
)
EmbeddingModelManagementView(store: store.scope(
state: \.embeddingModelManagement,
action: \.embeddingModelManagement
)).sidebarItem(
tag: 3,
title: "Embedding Models",
subtitle: "Chat, Modification",
image: "globe"
)
WebSearchView(store: store.scope(
state: \.webSearchSettings,
action: \.webSearchSettings
)).sidebarItem(
tag: 4,
title: "Web Search",
subtitle: "Chat, Modification",
image: "globe"
)
ScrollView {
OtherSuggestionServicesView().padding()
}.sidebarItem(
tag: 5,
title: "Other Suggestion Services",
subtitle: "Suggestion",
image: "globe"
)
}
}
}
}
}
struct AccountView_Previews: PreviewProvider {
static var previews: some View {
ServiceView(store: .init(initialState: .init(), reducer: { HostApp() }))
}
}
================================================
FILE: Core/Sources/HostApp/SharedComponents/CodeHighlightThemePicker.swift
================================================
import Foundation
import Preferences
import SwiftUI
public struct CodeHighlightThemePicker: View {
public enum Scenario {
case suggestion
case promptToCode
case chat
}
let scenario: Scenario
public init(scenario: Scenario) {
self.scenario = scenario
}
public var body: some View {
switch scenario {
case .suggestion:
SuggestionThemePicker()
case .promptToCode:
PromptToCodeThemePicker()
case .chat:
ChatThemePicker()
}
}
struct SuggestionThemePicker: View {
@AppStorage(\.syncSuggestionHighlightTheme) var sync: Bool
var body: some View {
SyncToggle(sync: $sync)
}
}
struct PromptToCodeThemePicker: View {
@AppStorage(\.syncPromptToCodeHighlightTheme) var sync: Bool
var body: some View {
SyncToggle(sync: $sync)
}
}
struct ChatThemePicker: View {
@AppStorage(\.syncChatCodeHighlightTheme) var sync: Bool
var body: some View {
SyncToggle(sync: $sync)
}
}
struct SyncToggle: View {
@Binding var sync: Bool
var body: some View {
VStack(alignment: .leading) {
Toggle(isOn: $sync) {
Text("Sync color scheme with Xcode")
}
Text("To refresh the theme, you must activate the extension service app once.")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
}
#Preview {
CodeHighlightThemePicker.SyncToggle(sync: .constant(true))
CodeHighlightThemePicker.SyncToggle(sync: .constant(false))
}
================================================
FILE: Core/Sources/HostApp/SharedComponents/EditableText.swift
================================================
import Foundation
import SwiftUI
// Hack to disable smart quotes and dashes in TextEditor
extension NSTextView {
open override var frame: CGRect {
didSet {
self.isAutomaticQuoteSubstitutionEnabled = false
self.isAutomaticDashSubstitutionEnabled = false
}
}
}
struct EditableText: View {
var text: Binding
@State var isEditing: Bool = false
var body: some View {
Button(action: {
isEditing = true
}) {
HStack(alignment: .top) {
Text(text.wrappedValue)
.font(Font.system(.body, design: .monospaced))
.padding(4)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
.background {
RoundedRectangle(cornerRadius: 4)
.fill(Color(nsColor: .textBackgroundColor))
}
.overlay {
RoundedRectangle(cornerRadius: 4)
.stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1))
}
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 14)
.padding(4)
.background(
Color.primary.opacity(0.1),
in: RoundedRectangle(cornerRadius: 4)
)
}
}
.buttonStyle(.plain)
.sheet(isPresented: $isEditing) {
VStack {
TextEditor(text: text)
.font(Font.system(.body, design: .monospaced))
.padding(4)
.frame(minHeight: 120)
.multilineTextAlignment(.leading)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
)
Button(action: {
isEditing = false
}) {
Text("Done")
}
}
.padding()
.frame(width: 600, height: 500)
.background(Color(nsColor: .windowBackgroundColor))
}
}
}
================================================
FILE: Core/Sources/HostApp/SidebarTabView.swift
================================================
import SwiftUI
private struct SidebarItem: Identifiable, Equatable {
var id: Int { tag }
var tag: Int
var title: String
var subtitle: String? = nil
var image: String? = nil
}
private struct SidebarItemPreferenceKey: PreferenceKey {
static var defaultValue: [SidebarItem] = []
static func reduce(value: inout [SidebarItem], nextValue: () -> [SidebarItem]) {
value.append(contentsOf: nextValue())
}
}
private struct SidebarTabTagKey: EnvironmentKey {
static var defaultValue: Int = 0
}
private extension EnvironmentValues {
var sidebarTabTag: Int {
get { self[SidebarTabTagKey.self] }
set { self[SidebarTabTagKey.self] = newValue }
}
}
private struct SidebarTabViewWrapper: View {
@Environment(\.sidebarTabTag) var sidebarTabTag
var tag: Int
var title: String
var subtitle: String? = nil
var image: String? = nil
var content: () -> Content
var body: some View {
Group {
if tag == sidebarTabTag {
content()
} else {
Color.clear
}
}
.preference(
key: SidebarItemPreferenceKey.self,
value: [.init(tag: tag, title: title, subtitle: subtitle, image: image)]
)
}
}
extension View {
func sidebarItem(
tag: Int,
title: String,
subtitle: String? = nil,
image: String? = nil
) -> some View {
SidebarTabViewWrapper(
tag: tag,
title: title,
subtitle: subtitle,
image: image,
content: { self }
)
}
}
struct SidebarTabView: View {
@State private var sidebarItems = [SidebarItem]()
@Binding var tag: Int
@ViewBuilder var views: () -> Content
var body: some View {
HStack(spacing: 0) {
ScrollView {
VStack(alignment: .leading) {
ForEach(sidebarItems) { item in
Button(action: {
tag = item.tag
}) {
HStack {
if let image = item.image {
Image(systemName: image)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
}
VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.foregroundStyle(.primary)
if let subtitle = item.subtitle {
Text(subtitle)
.lineSpacing(0)
.font(.caption)
.foregroundStyle(.secondary)
.opacity(0.5)
.multilineTextAlignment(.leading)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
Color.primary.opacity(tag == item.tag ? 0.1 : 0),
in: RoundedRectangle(cornerRadius: 4)
)
.padding(.horizontal, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
.frame(width: 200)
.padding(.vertical, 8)
}
.background(Color.primary.opacity(0.05))
Divider()
ZStack(alignment: .topLeading) {
views()
}
}
.environment(\.sidebarTabTag, tag)
.onPreferenceChange(SidebarItemPreferenceKey.self) { items in
sidebarItems = items
}
}
}
struct SidebarTabView_Previews: PreviewProvider {
static var previews: some View {
SidebarTabView(tag: .constant(0)) {
Color.red.sidebarItem(
tag: 0,
title: "Hello",
subtitle: "Meow\nMeow",
image: "person.circle.fill"
)
Color.blue.sidebarItem(
tag: 1,
title: "World",
image: "person.circle.fill"
)
Color.blue.sidebarItem(
tag: 3,
title: "Pikachu",
image: "person.circle.fill"
)
}
}
}
================================================
FILE: Core/Sources/HostApp/TabContainer.swift
================================================
import ComposableArchitecture
import Dependencies
import Foundation
import LaunchAgentManager
import SharedUIComponents
import SwiftUI
import Toast
import UpdateChecker
@MainActor
let hostAppStore: StoreOf = .init(initialState: .init(), reducer: { HostApp() })
public struct TabContainer: View {
let store: StoreOf
@ObservedObject var toastController: ToastController
@State private var tabBarItems = [TabBarItem]()
@State var tag: Int = 0
var externalTabContainer: ExternalTabContainer {
ExternalTabContainer.tabContainer(for: "TabContainer")
}
public init() {
toastController = ToastControllerDependencyKey.liveValue
store = hostAppStore
}
init(store: StoreOf, toastController: ToastController) {
self.store = store
self.toastController = toastController
}
public var body: some View {
WithPerceptionTracking {
VStack(spacing: 0) {
TabBar(tag: $tag, tabBarItems: tabBarItems)
.padding(.bottom, 8)
Divider()
ZStack(alignment: .center) {
GeneralView(store: store.scope(state: \.general, action: \.general))
.tabBarItem(
tag: 0,
title: "General",
image: "app.gift"
)
ServiceView(store: store).tabBarItem(
tag: 1,
title: "Service",
image: "globe"
)
FeatureSettingsView().tabBarItem(
tag: 2,
title: "Feature",
image: "star.square"
)
CustomCommandView(store: customCommandStore).tabBarItem(
tag: 3,
title: "Custom Command",
image: "command.square"
)
ForEach(0..: View {
@Environment(\.tabBarTabTag) var tabBarTabTag
var tag: Int
var title: String
var image: String
var content: () -> Content
var body: some View {
Group {
if tag == tabBarTabTag {
content()
} else {
Color.clear
}
}
.preference(
key: TabBarItemPreferenceKey.self,
value: [.init(tag: tag, title: title, image: image)]
)
}
}
private extension View {
func tabBarItem(
tag: Int,
title: String,
image: String
) -> some View {
TabBarTabViewWrapper(
tag: tag,
title: title,
image: image,
content: { self }
)
}
}
private struct TabBarItem: Identifiable, Equatable {
var id: Int { tag }
var tag: Int
var title: String
var image: String
}
private struct TabBarItemPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value.append(contentsOf: nextValue())
}
}
private struct TabBarTabTagKey: EnvironmentKey {
static var defaultValue: Int = 0
}
private extension EnvironmentValues {
var tabBarTabTag: Int {
get { self[TabBarTabTagKey.self] }
set { self[TabBarTabTagKey.self] = newValue }
}
}
struct UpdateCheckerKey: EnvironmentKey {
static var defaultValue: UpdateChecker = .init(
hostBundle: nil,
shouldAutomaticallyCheckForUpdate: false
)
}
public extension EnvironmentValues {
var updateChecker: UpdateChecker {
get { self[UpdateCheckerKey.self] }
set { self[UpdateCheckerKey.self] = newValue }
}
}
// MARK: - Previews
struct TabContainer_Previews: PreviewProvider {
static var previews: some View {
TabContainer()
.frame(width: 800)
}
}
struct TabContainer_Toasts_Previews: PreviewProvider {
static var previews: some View {
TabContainer(
store: .init(initialState: .init(), reducer: { HostApp() }),
toastController: .init(messages: [
.init(id: UUID(), type: .info, content: Text("info")),
.init(id: UUID(), type: .error, content: Text("error")),
.init(id: UUID(), type: .warning, content: Text("warning")),
])
)
.frame(width: 800)
}
}
================================================
FILE: Core/Sources/KeyBindingManager/KeyBindingManager.swift
================================================
import Foundation
import Workspace
public final class KeyBindingManager {
let tabToAcceptSuggestion: TabToAcceptSuggestion
public init() {
tabToAcceptSuggestion = .init()
}
public func start() {
tabToAcceptSuggestion.start()
}
@MainActor
public func stopForExit() {
tabToAcceptSuggestion.stopForExit()
}
}
================================================
FILE: Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift
================================================
import ActiveApplicationMonitor
import AppKit
import CGEventOverride
import CommandHandler
import Dependencies
import Foundation
import Logger
import Preferences
import SuggestionBasic
import UserDefaultsObserver
import Workspace
import XcodeInspector
final class TabToAcceptSuggestion {
let hook: CGEventHookType = CGEventHook(eventsOfInterest: [.keyDown]) { message in
Logger.service.debug("TabToAcceptSuggestion: \(message)")
}
@Dependency(\.workspacePool) var workspacePool
@Dependency(\.commandHandler) var commandHandler
private var CGEventObservationTask: Task?
private var isObserving: Bool { CGEventObservationTask != nil }
private let userDefaultsObserver = UserDefaultsObserver(
object: UserDefaults.shared, forKeyPaths: [
UserDefaultPreferenceKeys().acceptSuggestionWithTab.key,
UserDefaultPreferenceKeys().dismissSuggestionWithEsc.key,
], context: nil
)
private var stoppedForExit = false
struct ObservationKey: Hashable {}
var canTapToAcceptSuggestion: Bool {
UserDefaults.shared.value(for: \.acceptSuggestionWithTab)
}
var canEscToDismissSuggestion: Bool {
UserDefaults.shared.value(for: \.dismissSuggestionWithEsc)
}
@MainActor
func stopForExit() {
stoppedForExit = true
stopObservation()
}
init() {
_ = ThreadSafeAccessToXcodeInspector.shared
hook.add(
.init(
eventsOfInterest: [.keyDown],
convert: { [weak self] _, _, event in
self?.handleEvent(event) ?? .unchanged
}
),
forKey: ObservationKey()
)
}
func start() {
Task { [weak self] in
for await _ in ActiveApplicationMonitor.shared.createInfoStream() {
guard let self else { return }
try Task.checkCancellation()
Task { @MainActor in
if ActiveApplicationMonitor.shared.activeXcode != nil {
self.startObservation()
} else {
self.stopObservation()
}
}
}
}
userDefaultsObserver.onChange = { [weak self] in
guard let self else { return }
Task { @MainActor in
if self.canTapToAcceptSuggestion || self.canEscToDismissSuggestion {
self.startObservation()
} else {
self.stopObservation()
}
}
}
}
@MainActor
func startObservation() {
guard !stoppedForExit else { return }
guard canTapToAcceptSuggestion || canEscToDismissSuggestion else { return }
hook.activateIfPossible()
}
@MainActor
func stopObservation() {
hook.deactivate()
}
func handleEvent(_ event: CGEvent) -> CGEventManipulation.Result {
let keycode = Int(event.getIntegerValueField(.keyboardEventKeycode))
let tab = 48
let esc = 53
switch keycode {
case tab:
return handleTab(event.flags)
case esc:
return handleEsc(event.flags)
default:
return .unchanged
}
}
func handleTab(_ flags: CGEventFlags) -> CGEventManipulation.Result {
Logger.service.info("TabToAcceptSuggestion: Tab")
guard let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL
else {
Logger.service.info("TabToAcceptSuggestion: No active document")
return .unchanged
}
let language = languageIdentifierFromFileURL(fileURL)
if flags.contains(.maskHelp) { return .unchanged }
let requiredFlagsToTrigger: CGEventFlags = {
var all = CGEventFlags()
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierShift) {
all.insert(.maskShift)
}
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierControl) {
all.insert(.maskControl)
}
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierOption) {
all.insert(.maskAlternate)
}
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierCommand) {
all.insert(.maskCommand)
}
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierOnlyForSwift) {
if language == .builtIn(.swift) {
return all
} else {
return []
}
} else {
return all
}
}()
let flagsToAvoidWhenNotRequired: [CGEventFlags] = [
.maskShift, .maskCommand, .maskHelp, .maskSecondaryFn,
]
guard flags.contains(requiredFlagsToTrigger) else {
Logger.service.info("TabToAcceptSuggestion: Modifier not found")
return .unchanged
}
for flag in flagsToAvoidWhenNotRequired {
if flags.contains(flag), !requiredFlagsToTrigger.contains(flag) {
return .unchanged
}
}
guard canTapToAcceptSuggestion else {
Logger.service.info("TabToAcceptSuggestion: Feature not available")
return .unchanged
}
guard ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil
else {
Logger.service.info("TabToAcceptSuggestion: Xcode not found")
return .unchanged
}
guard let editor = ThreadSafeAccessToXcodeInspector.shared.focusedEditor
else {
Logger.service.info("TabToAcceptSuggestion: No editor found")
return .unchanged
}
guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL)
else {
Logger.service.info("TabToAcceptSuggestion: No file found")
return .unchanged
}
guard let presentingSuggestion = filespace.presentingSuggestion
else {
Logger.service.info(
"TabToAcceptSuggestion: No presenting found for \(filespace.fileURL.lastPathComponent), found \(filespace.suggestions.count) suggestion, index \(filespace.suggestionIndex)."
)
return .unchanged
}
let editorContent = editor.getContent()
let shouldAcceptSuggestion = Self.checkIfAcceptSuggestion(
lines: editorContent.lines,
cursorPosition: editorContent.cursorPosition,
codeMetadata: filespace.codeMetadata,
presentingSuggestionText: presentingSuggestion.text
)
if shouldAcceptSuggestion {
Logger.service.info("TabToAcceptSuggestion: Accept")
if flags.contains(.maskControl),
!requiredFlagsToTrigger.contains(.maskControl)
{
Task { await commandHandler.acceptActiveSuggestionLineInGroup(atIndex: nil)
}
} else {
Task { await commandHandler.acceptSuggestion() }
}
return .discarded
} else {
Logger.service.info("TabToAcceptSuggestion: Should not accept")
return .unchanged
}
}
func handleEsc(_ flags: CGEventFlags) -> CGEventManipulation.Result {
guard
!flags.contains(.maskShift),
!flags.contains(.maskControl),
!flags.contains(.maskAlternate),
!flags.contains(.maskCommand),
!flags.contains(.maskHelp),
canEscToDismissSuggestion
else { return .unchanged }
guard
let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL,
ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil,
let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL),
filespace.presentingSuggestion != nil
else { return .unchanged }
Task { await commandHandler.dismissSuggestion() }
return .discarded
}
}
extension TabToAcceptSuggestion {
static func checkIfAcceptSuggestion(
lines: [String],
cursorPosition: CursorPosition,
codeMetadata: FilespaceCodeMetadata,
presentingSuggestionText: String
) -> Bool {
let line = cursorPosition.line
guard line >= 0, line < lines.endIndex else {
return true
}
let col = cursorPosition.character
let prefixEndIndex = lines[line].utf16.index(
lines[line].utf16.startIndex,
offsetBy: col,
limitedBy: lines[line].utf16.endIndex
) ?? lines[line].utf16.endIndex
let prefix = String(lines[line][..
Label
\(serviceIdentifier)
Program
\(executableURL.path)
MachServices