From 5eefc33cb07fb5b6df2c52ab98e008c972f0c354 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Sat, 12 Sep 2026 01:21:51 +0800 Subject: [PATCH] Refactor TFI for project structure --- ...swift => AISettingsIntegrationTests.swift} | 4 +- ...yTests.swift => MetricsJourneyTests.swift} | 2 +- ...ortTests.swift => SettingsPortTests.swift} | 26 +- .../TerminalCommandDraftControllerTests.swift | 51 ++ Frameworks/Resources/Localizable.xcstrings | 3 + .../Agent/CommandGenerationCoordinator.swift | 120 +++++ .../TerminalCommandDraftController.swift | 99 ++++ .../TermindApplication/AppLanguage.swift | 4 +- .../RemoteSessionConnectionController.swift | 77 ++- .../FeatureModels/SkillLibraryModel.swift | 6 +- ...redSettingsKey.swift => SettingsKey.swift} | 2 +- .../Sessions/SSHShellIntegrationEvent.swift | 11 + .../TermindDomain/Sessions/TerminalFind.swift | 14 + .../Agent/ContextAttachmentImporter.swift | 4 +- .../Cloud/SettingsSyncCoordinator.swift | 14 +- .../Cloud/VaultSyncCoordinator.swift | 14 +- .../SettingsSyncStore.swift | 16 +- .../SharedRepositoryAliases.swift | 16 - .../SwiftDataAIConfigurationStore.swift | 2 - .../SwiftDataAISecretStore.swift | 2 - .../SwiftDataAgentRunRepository.swift | 2 - .../SwiftDataOutboxStore.swift | 2 - .../TerminalPreferences+Defaults.swift | 40 +- .../ConnectionDecisionForms.swift | 10 +- .../SSH/RemoteConnectionCoordinator.swift | 1 + .../SSH/SSHSessionActor.swift | 10 - .../Transfers/SFTPTransferCoordinator.swift | 12 +- Memory/ARCHITECTURE.md | 23 +- Memory/MOBILE_ROLLOUT.md | 15 + Memory/REFACTOR.md | 21 + TFI/App/MobileAppDependencies.swift | 36 ++ TFI/App/MobileAppModel.swift | 43 +- TFI/{Features => App}/MobileRootView.swift | 7 +- .../MobileRuntimeVersions.swift | 0 TFI/App/TermindIOSApp.swift | 4 +- .../Agent}/MobileAssistantModel.swift | 0 .../MobileWorkspaceConnectionServices.swift | 28 ++ .../FeatureModels}/MobileKeyEditorModel.swift | 0 .../FeatureModels}/MobileMCPEditorModel.swift | 0 .../FeatureModels}/MobileModelDiscovery.swift | 0 .../FeatureModels}/MobileModelTests.swift | 0 .../MobileProviderEditorModel.swift | 0 .../FeatureModels}/MobileSecretDraft.swift | 0 .../MobileSkillEditorModel.swift | 0 .../Metrics}/MobileMetricsModel.swift | 0 TFI/Design/MobileDesignTokens.swift | 16 + .../MobileInterfaceEnvironment.swift | 22 + .../{ => AI}/MobileAssistantComposer.swift | 0 .../MobileAssistantConversation.swift | 0 .../{ => AI}/MobileAssistantInteraction.swift | 12 +- .../{ => AI}/MobileSlashCommandPanel.swift | 0 .../{ => Assistant}/MobileAssistantView.swift | 3 +- .../{ => Commerce}/MobileProView.swift | 0 .../MobileConnectionPicker.swift | 0 .../{ => HostEditor}/MobileHostEditor.swift | 0 .../MobileCredentialsView.swift | 0 .../{ => Keychain}/MobileKeyEditor.swift | 0 .../MobileKnownHostsView.swift | 0 .../{ => Metrics}/MobileMetricsView.swift | 0 .../{ => Ports}/MobileForwardingEditor.swift | 0 .../{ => SFTP}/MobileRemoteFileEditor.swift | 0 .../{ => Settings}/MobileAISettingsView.swift | 0 .../MobileAppSettingsView.swift | 2 +- .../{ => Settings}/MobileMCPSettings.swift | 0 .../{ => Settings}/MobileModelFields.swift | 0 .../MobileSFTPSettingsView.swift | 14 +- .../{ => Settings}/MobileSettingsView.swift | 0 .../{ => Settings}/MobileSkillSettings.swift | 0 .../MobileSyncSettingsView.swift | 12 +- .../MobileTerminalSettingsView.swift | 0 TFI/Features/Shell/MobileNavigation.swift | 31 ++ .../{ => Snippets}/MobileSnippetsView.swift | 0 .../MobileCommandHistoryView.swift | 0 .../MobileTerminalKeyboard.swift | 0 .../{ => Transfers}/MobileTransfersView.swift | 0 .../{ => Vault}/MobileVaultView.swift | 0 .../Workspaces}/MobileWorkspace.swift | 438 +++--------------- .../MobileWorkspaceDescriptor.swift | 0 .../MobileWorkspaceView.swift | 6 +- ...bileWorkspaceConnectionServices+Live.swift | 37 ++ TFI/Platform/MobileTerminalAppearance.swift | 21 +- .../MobileConnectionJourneyView.swift | 4 +- .../Components}/MobileEditorSheet.swift | 0 .../Components}/MobileOverflowMenuLabel.swift | 0 .../Components}/MobileSheetPresentation.swift | 4 +- .../Components}/MobileToolbarSearch.swift | 0 .../Managed}/MobileCards.swift | 20 +- TFITests/MobileAppTests.swift | 34 +- TFITests/MobileNavigationParityTests.swift | 36 +- TFITests/MobileSettingsPortTests.swift | 12 +- TFM/App/AppAppearance.swift | 2 +- TFM/App/AppDependencies.swift | 1 + TFM/App/AppModel.swift | 1 + TFM/App/AppReviewController.swift | 24 +- ...ingsKey.swift => DesktopSettingsKey.swift} | 61 +-- TFM/App/MCPServerController.swift | 20 +- TFM/App/OnboardingController.swift | 6 +- TFM/App/ProAccessController.swift | 4 - TFM/App/RootView.swift | 5 +- TFM/App/ScreenshotDemo+Metrics.swift | 1 + TFM/App/ScreenshotDemo.swift | 2 + TFM/App/TermindApp.swift | 8 +- .../Agent/AgentBackgroundCommands.swift | 9 - .../Agent/AgentBuiltInSFTPTools.swift | 12 - .../Agent/AgentBuiltInSSHTools.swift | 7 - .../Agent/AgentCommandOutputStream.swift | 3 - TFM/Application/Agent/AgentConnections.swift | 10 - .../Agent/AgentSSHInteraction.swift | 8 - .../Agent/AgentScratchTransferTools.swift | 4 - .../Agent/ConversationTitleController.swift | 3 - TFM/Application/Agent/GetMetricsTool.swift | 3 - TFM/Application/Agent/MetricsToolOutput.swift | 3 - .../Agent/TermindAgentPolicy.swift | 3 - TFM/Application/Agent/TermindAgentTools.swift | 4 - .../Commands/AppCommandRegistry.swift | 1 + .../Commands/ChatSlashCommand.swift | 5 - .../Connections/ConnectionJourney.swift | 5 - .../Connections/RemoteSessionHostStore.swift | 3 - .../Connections/SSHAuthenticationPrompt.swift | 3 - .../SSHConnectionPreparation.swift | 3 - .../Connections/SSHConnectionTypes.swift | 13 - .../Events/HostDeletionHandlers.swift | 4 - TFM/Application/Events/OutboxRunner.swift | 3 - TFM/Application/Events/OutboxStore.swift | 4 - .../AIConfigurationModel+Demo.swift | 1 + .../FeatureModels/AIConfigurationModel.swift | 3 - .../AssistantConversationCommands.swift | 1 + .../FeatureModels/AssistantLibraryModel.swift | 4 - .../FeatureModels/CommandHistoryModel.swift | 3 - .../ConnectionHistoryModel.swift | 3 - .../CredentialLibraryModel.swift | 3 - .../ForwardingLibraryModel.swift | 3 - .../FeatureModels/HostLibraryModel+Demo.swift | 1 + .../FeatureModels/HostLibraryModel.swift | 3 - .../KnownHostsLibraryModel.swift | 3 - ...del.swift => SSHImportModel+Desktop.swift} | 2 - .../SkillLibraryModel+BuiltIn.swift | 3 - .../FeatureModels/SkillLibraryModel.swift | 3 - .../FeatureModels/SnippetLibraryModel.swift | 3 - .../FeatureModels/TerminalSettingsModel.swift | 3 - .../FeatureModels/TransferLibraryModel.swift | 1 + .../MCPServer/MCPServerTools.swift | 1 + TFM/Application/Metrics/MetricsServices.swift | 8 - TFM/Application/Metrics/MetricsSnapshot.swift | 5 - .../Metrics/ServerMetricsModel.swift | 4 - .../AIConfigurationPersistence.swift | 5 - .../CommandHistoryRepository.swift | 4 - .../ConnectionHistoryRepository.swift | 4 - .../Repositories/SSHImportBatch.swift | 3 - .../Repositories/SSHImportReading.swift | 3 - .../SkillLibraryPersistence.swift | 3 - .../TerminalPreferencesPersistence.swift | 3 - .../Repositories/VaultPersistence.swift | 6 - TFM/Application/Repositories/VaultStore.swift | 3 - .../Security/AISecretStoring.swift | 3 - TFM/Application/Security/SecretStoring.swift | 5 - .../Security/StoredSSHKeyCredential.swift | 5 - .../Speech/SpeechTranscription.swift | 6 - TFM/Application/Transfers/RemotePath.swift | 4 - .../Transfers/SFTPTransferCoordinator.swift | 9 - TFM/Design/TagField.swift | 1 + TFM/Design/TerminalTheme.swift | 22 +- .../LinuxDistributionPresentation.swift | 5 - TFM/Features/AI/AgentToolPresentation.swift | 1 + TFM/Features/AI/CommandCardView.swift | 1 + .../AI/ContextAttachmentImporter.swift | 3 - TFM/Features/AI/ContextAttachmentOpener.swift | 1 + TFM/Features/AI/ContextGaugeView.swift | 1 + .../AI/ConversationComposerState.swift | 3 - TFM/Features/AI/ConversationModel.swift | 20 - .../AI/ConversationTranscriptView.swift | 5 +- TFM/Features/AI/ConversationViews.swift | 1 + .../AI/Markdown/MarkdownSoftBreaks.swift | 1 + TFM/Features/AI/MarkdownBlockListView.swift | 1 + TFM/Features/AI/MarkdownCodeBlockView.swift | 1 + TFM/Features/AI/MessageViews.swift | 1 + .../AI/SessionContext+Presentation.swift | 1 + TFM/Features/AI/SlashCommandMenuView.swift | 1 + TFM/Features/AI/TranscriptListView.swift | 1 + TFM/Features/AI/TranscriptNativeRows.swift | 1 + TFM/Features/Assistant/AssistantView.swift | 9 +- TFM/Features/Commerce/ProPaywallView.swift | 2 + .../ExternalAuthCoordinator.swift | 1 + .../ExternalRequests/ExternalAuthWindow.swift | 5 +- .../HostChooser/HostChooserView.swift | 1 + TFM/Features/Keychain/KeychainPageView.swift | 4 +- .../Metrics/MetricsConnectionNotice.swift | 1 + TFM/Features/Metrics/MetricsDiskTable.swift | 3 - TFM/Features/Metrics/MetricsGPUSection.swift | 3 - .../Metrics/MetricsInstrumentBody.swift | 3 - TFM/Features/Metrics/MetricsJourneyView.swift | 1 + .../Metrics/MetricsManagedCardPage.swift | 1 + TFM/Features/Metrics/MetricsView.swift | 5 +- .../Ports/PortForwardingEditorState.swift | 1 + TFM/Features/Ports/PortForwardingView.swift | 1 + TFM/Features/SFTP/SFTPBrowserView.swift | 2 + TFM/Features/SFTP/SFTPPermissions.swift | 3 - TFM/Features/SFTP/SFTPSessionModel.swift | 30 +- TFM/Features/SSHImport/SSHImportView.swift | 1 + .../Settings/MCPServerEditorSheet.swift | 1 + .../Settings/ModelProviderEditorSheet.swift | 1 + TFM/Features/Settings/SettingsPageView.swift | 11 +- TFM/Features/Settings/SyncSettings.swift | 1 + ...rminalCustomConfigurationEditorSheet.swift | 1 + TFM/Features/Shell/WindowRoute.swift | 1 + TFM/Features/Shell/WindowTitle.swift | 1 + TFM/Features/Sidebar/WindowSidebarView.swift | 5 +- TFM/Features/Snippets/SnippetsPageView.swift | 1 + TFM/Features/StatusBar/StatusBarView.swift | 4 +- TFM/Features/Terminal/SSHSessionModel.swift | 142 +----- .../Terminal/SerialSessionModel.swift | 1 + .../Terminal/TerminalPlaceholderView.swift | 3 + TFM/Features/Workspaces/Pane.swift | 4 +- TFM/Features/Workspaces/WindowModel.swift | 2 + TFM/Features/Workspaces/WindowState.swift | 1 + TFM/Features/Workspaces/Workspace.swift | 154 ++---- .../Workspaces/WorkspaceNavigatorView.swift | 5 +- .../Cloud/SettingsSyncCoordinator.swift | 3 - .../Cloud/SettingsSyncJournal.swift | 5 - .../Cloud/SettingsSyncPersistence.swift | 6 - .../Cloud/VaultSyncCoordinator.swift | 3 - .../Cloud/VaultSyncPersistence.swift | 6 - TFM/Infrastructure/Commerce/TrialClock.swift | 3 - .../Database/AIPersistence.swift | 3 - .../Database/AgentPersistence.swift | 3 - ...ft => CommandHistoryModel+SwiftData.swift} | 3 +- ...=> ConnectionHistoryModel+SwiftData.swift} | 3 +- .../Database/HostPersistence.swift | 3 - .../Database/SkillPersistence.swift | 3 - .../Database/SwiftDataHostRepository.swift | 3 - .../Database/SwiftDataOutboxStore.swift | 3 - .../Metrics/SSHMetricsSampler.swift | 4 - .../PersistentModelFactories.swift | 3 + .../SSH/PortForwardingController.swift | 4 - .../SSH/RemoteConnectionCoordinator.swift | 4 - TFM/Infrastructure/SSH/SFTPClient.swift | 9 - .../SFTPTransferCoordinator+Production.swift | 2 + TFM/Infrastructure/SSH/SSHExecSession.swift | 8 - .../SSH/SSHForwardingSession.swift | 4 - TFM/Infrastructure/SSH/SSHKeyMaterial.swift | 5 - TFM/Infrastructure/SSH/SSHRoutedSocket.swift | 4 - TFM/Infrastructure/SSH/SSHSessionActor.swift | 8 - .../SSH/SSHSocketConnector.swift | 4 - .../SSH/SSHUserCertificate.swift | 4 - ...er.swift => SSHImportReader+Desktop.swift} | 2 - ...tore.swift => AISecretStore+Desktop.swift} | 3 +- ...wift => KeychainSecretStore+Desktop.swift} | 3 - .../Security/SecureEnclaveKey.swift | 5 - .../AppleSpeechTranscriptionService.swift | 3 - .../Speech/SpeechAudioInput.swift | 5 - .../AppRelauncher.swift} | 4 - TFM/Platform/MacOSSpeechAudioInput.swift | 2 + TFM/Platform/MetricsMenuBarController.swift | 3 +- TFM/Platform/MetricsWindowPresenter.swift | 1 + ...redChrome.swift => ChromeComponents.swift} | 0 TFM/UI/Components/ConnectionJourneyView.swift | 12 +- TFM/UI/Components/WindowToolbar.swift | 1 + TFMTests/AIProviderPersistenceTests.swift | 5 + TFMTests/AgentApprovalQueueTests.swift | 6 + TFMTests/AgentBackgroundCommandTests.swift | 6 + .../AgentBuiltInToolAvailabilityTests.swift | 6 + TFMTests/AgentCompactionTests.swift | 6 + TFMTests/AgentConnectionPoolTests.swift | 6 + TFMTests/AgentHostAuthenticationTests.swift | 5 + .../AgentInterruptedRunRecoveryTests.swift | 6 + TFMTests/AgentPersistenceTests.swift | 5 + TFMTests/AgentPlanModeTests.swift | 5 + TFMTests/AgentRuntimeTests.swift | 5 + TFMTests/AgentScratchTests.swift | 6 + TFMTests/AgentSkillTests.swift | 5 + TFMTests/AgentTokenUsageTests.swift | 6 + TFMTests/AgentToolPresentationTests.swift | 5 + TFMTests/AgentToolProgressTests.swift | 6 + TFMTests/AppAppearanceTests.swift | 22 +- TFMTests/AppLanguageTests.swift | 6 + TFMTests/AppReviewControllerTests.swift | 16 +- TFMTests/ArchitectureBoundaryTests.swift | 103 +++- .../AssistantConversationListDropTests.swift | 7 + TFMTests/AssistantSearchTests.swift | 6 + TFMTests/AuthorizedKeyExportTests.swift | 6 + TFMTests/BroadcastInputTests.swift | 4 + TFMTests/CardMarqueeSelectionTests.swift | 6 + TFMTests/ChatComposerStateTests.swift | 7 +- TFMTests/ChatMessageTimestampTests.swift | 6 + TFMTests/ChatSlashCommandTests.swift | 6 + TFMTests/ColorSystemTests.swift | 5 + TFMTests/CommandHistoryTests.swift | 4 + TFMTests/CommerceConfigurationTests.swift | 5 + TFMTests/ConnectionHistoryTests.swift | 4 + TFMTests/ConnectionJourneyTests.swift | 5 + TFMTests/ContextAttachmentImporterTests.swift | 6 + TFMTests/DataProtectionKeychainTests.swift | 5 + TFMTests/ExternalRequestTests.swift | 5 + TFMTests/FileURLDropTargetTests.swift | 7 + TFMTests/GetMetricsToolTests.swift | 4 + TFMTests/HostLibraryMoveTests.swift | 5 + TFMTests/IdentityCredentialTests.swift | 5 + TFMTests/KeychainOrderTests.swift | 5 + TFMTests/ListViewMigrationTests.swift | 5 + TFMTests/LocalModelConfiguration.swift | 6 + TFMTests/LoginItemTests.swift | 18 +- TFMTests/LoopbackHTTPServerTests.swift | 10 +- TFMTests/MCPClientSetupCommandTests.swift | 6 + TFMTests/MCPServerSessionTests.swift | 5 + TFMTests/MCPServerToolsTests.swift | 5 + TFMTests/ManagedChooserTests.swift | 6 + TFMTests/MarkdownParsingTests.swift | 6 + TFMTests/MenuSymbolTests.swift | 6 + TFMTests/MetricsInteractionTests.swift | 11 +- TFMTests/MetricsLimitTests.swift | 4 + TFMTests/MetricsPresentationTests.swift | 4 + TFMTests/MetricsSnapshotTests.swift | 5 + TFMTests/OnboardingControllerTests.swift | 12 +- .../PersistentWorkspaceDeckLayoutTests.swift | 6 + TFMTests/PortForwardingEditorTests.swift | 5 + TFMTests/PortForwardingFeatureTests.swift | 5 + TFMTests/ProAccessTests.swift | 5 + .../ProviderEndpointIntegrationTests.swift | 5 + TFMTests/QuickOpenTests.swift | 5 + TFMTests/RemoteWindowTitleTests.swift | 6 + TFMTests/RepositoryOutboxTests.swift | 4 + TFMTests/RuntimeVersionsTests.swift | 6 + TFMTests/SFTPFeatureTests.swift | 5 + TFMTests/SFTPHistoryTests.swift | 5 + TFMTests/SFTPSessionTests.swift | 5 + TFMTests/SSHImportTests.swift | 5 + TFMTests/SSHUserCertificateTests.swift | 5 + TFMTests/ScreenshotDemoTests.swift | 5 + TFMTests/ScrubberWebClientTests.swift | 6 + TFMTests/SerialFeatureTests.swift | 5 + TFMTests/ServerMetricsTests.swift | 4 + TFMTests/SessionContextTests.swift | 5 + TFMTests/SettingsDetailControlTests.swift | 4 + TFMTests/SettingsNavigationTests.swift | 6 + TFMTests/SettingsSyncTests.swift | 4 + TFMTests/ShellIntegrationBootstrapTests.swift | 5 + TFMTests/SnippetFeatureTests.swift | 5 + TFMTests/SoftwareKeyFileImporterTests.swift | 6 + TFMTests/StatusBarPresentationTests.swift | 5 + TFMTests/StoreKitTrialTests.swift | 5 + TFMTests/TerminalFindTests.swift | 9 +- TFMTests/TerminalThemeTests.swift | 20 +- TFMTests/TranscriptListViewTests.swift | 6 + TFMTests/TranscriptPerformanceTests.swift | 6 + TFMTests/UserInputModalityTests.swift | 6 + TFMTests/VaultSelectionTests.swift | 5 + TFMTests/VaultSyncTests.swift | 4 + TFMTests/WebSearchTests.swift | 6 + TFMTests/WindowSidebarTests.swift | 6 + TFMTests/WindowStateTests.swift | 4 + TFMTests/WindowToolbarFindTests.swift | 5 + Termind.xcodeproj/project.pbxproj | 12 +- .../WorkspaceActivityAttributes.swift | 0 353 files changed, 1678 insertions(+), 1491 deletions(-) rename FrameworkTests/{SharedAISettingsTests.swift => AISettingsIntegrationTests.swift} (98%) rename FrameworkTests/{SharedMetricsJourneyTests.swift => MetricsJourneyTests.swift} (99%) rename FrameworkTests/{SharedSettingsPortTests.swift => SettingsPortTests.swift} (84%) create mode 100644 FrameworkTests/TerminalCommandDraftControllerTests.swift create mode 100644 Frameworks/TermindApplication/Agent/CommandGenerationCoordinator.swift create mode 100644 Frameworks/TermindApplication/Agent/TerminalCommandDraftController.swift rename {TFM/Application => Frameworks/TermindApplication}/Connections/RemoteSessionConnectionController.swift (51%) rename Frameworks/TermindApplication/{SharedSettingsKey.swift => SettingsKey.swift} (98%) create mode 100644 Frameworks/TermindDomain/Sessions/SSHShellIntegrationEvent.swift create mode 100644 Frameworks/TermindDomain/Sessions/TerminalFind.swift delete mode 100644 Frameworks/TermindPersistence/SharedRepositoryAliases.swift create mode 100644 TFI/App/MobileAppDependencies.swift rename TFI/{Features => App}/MobileRootView.swift (98%) rename TFI/{Platform => App}/MobileRuntimeVersions.swift (100%) rename TFI/{App => Application/Agent}/MobileAssistantModel.swift (100%) create mode 100644 TFI/Application/Connections/MobileWorkspaceConnectionServices.swift rename TFI/{App => Application/FeatureModels}/MobileKeyEditorModel.swift (100%) rename TFI/{App => Application/FeatureModels}/MobileMCPEditorModel.swift (100%) rename TFI/{App => Application/FeatureModels}/MobileModelDiscovery.swift (100%) rename TFI/{App => Application/FeatureModels}/MobileModelTests.swift (100%) rename TFI/{App => Application/FeatureModels}/MobileProviderEditorModel.swift (100%) rename TFI/{App => Application/FeatureModels}/MobileSecretDraft.swift (100%) rename TFI/{App => Application/FeatureModels}/MobileSkillEditorModel.swift (100%) rename TFI/{App => Application/Metrics}/MobileMetricsModel.swift (100%) create mode 100644 TFI/Design/MobileDesignTokens.swift create mode 100644 TFI/DesignSystem/MobileInterfaceEnvironment.swift rename TFI/Features/{ => AI}/MobileAssistantComposer.swift (100%) rename TFI/Features/{ => AI}/MobileAssistantConversation.swift (100%) rename TFI/Features/{ => AI}/MobileAssistantInteraction.swift (94%) rename TFI/Features/{ => AI}/MobileSlashCommandPanel.swift (100%) rename TFI/Features/{ => Assistant}/MobileAssistantView.swift (98%) rename TFI/Features/{ => Commerce}/MobileProView.swift (100%) rename TFI/Features/{ => HostChooser}/MobileConnectionPicker.swift (100%) rename TFI/Features/{ => HostEditor}/MobileHostEditor.swift (100%) rename TFI/Features/{ => Keychain}/MobileCredentialsView.swift (100%) rename TFI/Features/{ => Keychain}/MobileKeyEditor.swift (100%) rename TFI/Features/{ => KnownHosts}/MobileKnownHostsView.swift (100%) rename TFI/Features/{ => Metrics}/MobileMetricsView.swift (100%) rename TFI/Features/{ => Ports}/MobileForwardingEditor.swift (100%) rename TFI/Features/{ => SFTP}/MobileRemoteFileEditor.swift (100%) rename TFI/Features/{ => Settings}/MobileAISettingsView.swift (100%) rename TFI/Features/{ => Settings}/MobileAppSettingsView.swift (95%) rename TFI/Features/{ => Settings}/MobileMCPSettings.swift (100%) rename TFI/Features/{ => Settings}/MobileModelFields.swift (100%) rename TFI/Features/{ => Settings}/MobileSFTPSettingsView.swift (76%) rename TFI/Features/{ => Settings}/MobileSettingsView.swift (100%) rename TFI/Features/{ => Settings}/MobileSkillSettings.swift (100%) rename TFI/Features/{ => Settings}/MobileSyncSettingsView.swift (86%) rename TFI/Features/{ => Settings}/MobileTerminalSettingsView.swift (100%) create mode 100644 TFI/Features/Shell/MobileNavigation.swift rename TFI/Features/{ => Snippets}/MobileSnippetsView.swift (100%) rename TFI/Features/{ => Terminal}/MobileCommandHistoryView.swift (100%) rename TFI/Features/{ => Terminal}/MobileTerminalKeyboard.swift (100%) rename TFI/Features/{ => Transfers}/MobileTransfersView.swift (100%) rename TFI/Features/{ => Vault}/MobileVaultView.swift (100%) rename TFI/{App => Features/Workspaces}/MobileWorkspace.swift (74%) rename TFI/{App => Features/Workspaces}/MobileWorkspaceDescriptor.swift (100%) rename TFI/Features/{ => Workspaces}/MobileWorkspaceView.swift (98%) create mode 100644 TFI/Infrastructure/SSH/MobileWorkspaceConnectionServices+Live.swift rename TFI/{Features => UI/Components}/MobileConnectionJourneyView.swift (99%) rename TFI/{Features => UI/Components}/MobileEditorSheet.swift (100%) rename TFI/{Features => UI/Components}/MobileOverflowMenuLabel.swift (100%) rename TFI/{Features => UI/Components}/MobileSheetPresentation.swift (87%) rename TFI/{Features => UI/Components}/MobileToolbarSearch.swift (100%) rename TFI/{Features => UI/Managed}/MobileCards.swift (89%) rename TFM/App/{SettingsKey.swift => DesktopSettingsKey.swift} (50%) delete mode 100644 TFM/App/ProAccessController.swift delete mode 100644 TFM/Application/Agent/AgentBackgroundCommands.swift delete mode 100644 TFM/Application/Agent/AgentBuiltInSFTPTools.swift delete mode 100644 TFM/Application/Agent/AgentBuiltInSSHTools.swift delete mode 100644 TFM/Application/Agent/AgentCommandOutputStream.swift delete mode 100644 TFM/Application/Agent/AgentConnections.swift delete mode 100644 TFM/Application/Agent/AgentSSHInteraction.swift delete mode 100644 TFM/Application/Agent/AgentScratchTransferTools.swift delete mode 100644 TFM/Application/Agent/ConversationTitleController.swift delete mode 100644 TFM/Application/Agent/GetMetricsTool.swift delete mode 100644 TFM/Application/Agent/MetricsToolOutput.swift delete mode 100644 TFM/Application/Agent/TermindAgentPolicy.swift delete mode 100644 TFM/Application/Agent/TermindAgentTools.swift delete mode 100644 TFM/Application/Commands/ChatSlashCommand.swift delete mode 100644 TFM/Application/Connections/ConnectionJourney.swift delete mode 100644 TFM/Application/Connections/RemoteSessionHostStore.swift delete mode 100644 TFM/Application/Connections/SSHAuthenticationPrompt.swift delete mode 100644 TFM/Application/Connections/SSHConnectionPreparation.swift delete mode 100644 TFM/Application/Connections/SSHConnectionTypes.swift delete mode 100644 TFM/Application/Events/HostDeletionHandlers.swift delete mode 100644 TFM/Application/Events/OutboxRunner.swift delete mode 100644 TFM/Application/Events/OutboxStore.swift delete mode 100644 TFM/Application/FeatureModels/AIConfigurationModel.swift delete mode 100644 TFM/Application/FeatureModels/AssistantLibraryModel.swift delete mode 100644 TFM/Application/FeatureModels/CommandHistoryModel.swift delete mode 100644 TFM/Application/FeatureModels/ConnectionHistoryModel.swift delete mode 100644 TFM/Application/FeatureModels/CredentialLibraryModel.swift delete mode 100644 TFM/Application/FeatureModels/ForwardingLibraryModel.swift delete mode 100644 TFM/Application/FeatureModels/HostLibraryModel.swift delete mode 100644 TFM/Application/FeatureModels/KnownHostsLibraryModel.swift rename TFM/Application/FeatureModels/{SSHImportModel.swift => SSHImportModel+Desktop.swift} (79%) delete mode 100644 TFM/Application/FeatureModels/SkillLibraryModel+BuiltIn.swift delete mode 100644 TFM/Application/FeatureModels/SkillLibraryModel.swift delete mode 100644 TFM/Application/FeatureModels/SnippetLibraryModel.swift delete mode 100644 TFM/Application/FeatureModels/TerminalSettingsModel.swift delete mode 100644 TFM/Application/Metrics/MetricsServices.swift delete mode 100644 TFM/Application/Metrics/MetricsSnapshot.swift delete mode 100644 TFM/Application/Metrics/ServerMetricsModel.swift delete mode 100644 TFM/Application/Repositories/AIConfigurationPersistence.swift delete mode 100644 TFM/Application/Repositories/CommandHistoryRepository.swift delete mode 100644 TFM/Application/Repositories/ConnectionHistoryRepository.swift delete mode 100644 TFM/Application/Repositories/SSHImportBatch.swift delete mode 100644 TFM/Application/Repositories/SSHImportReading.swift delete mode 100644 TFM/Application/Repositories/SkillLibraryPersistence.swift delete mode 100644 TFM/Application/Repositories/TerminalPreferencesPersistence.swift delete mode 100644 TFM/Application/Repositories/VaultPersistence.swift delete mode 100644 TFM/Application/Repositories/VaultStore.swift delete mode 100644 TFM/Application/Security/AISecretStoring.swift delete mode 100644 TFM/Application/Security/SecretStoring.swift delete mode 100644 TFM/Application/Security/StoredSSHKeyCredential.swift delete mode 100644 TFM/Application/Speech/SpeechTranscription.swift delete mode 100644 TFM/Application/Transfers/RemotePath.swift delete mode 100644 TFM/Application/Transfers/SFTPTransferCoordinator.swift delete mode 100644 TFM/DesignSystem/LinuxDistributionPresentation.swift delete mode 100644 TFM/Features/AI/ContextAttachmentImporter.swift delete mode 100644 TFM/Features/AI/ConversationComposerState.swift delete mode 100644 TFM/Features/AI/ConversationModel.swift delete mode 100644 TFM/Features/Metrics/MetricsDiskTable.swift delete mode 100644 TFM/Features/Metrics/MetricsGPUSection.swift delete mode 100644 TFM/Features/Metrics/MetricsInstrumentBody.swift delete mode 100644 TFM/Features/SFTP/SFTPPermissions.swift delete mode 100644 TFM/Infrastructure/Cloud/SettingsSyncCoordinator.swift delete mode 100644 TFM/Infrastructure/Cloud/SettingsSyncJournal.swift delete mode 100644 TFM/Infrastructure/Cloud/SettingsSyncPersistence.swift delete mode 100644 TFM/Infrastructure/Cloud/VaultSyncCoordinator.swift delete mode 100644 TFM/Infrastructure/Cloud/VaultSyncPersistence.swift delete mode 100644 TFM/Infrastructure/Commerce/TrialClock.swift delete mode 100644 TFM/Infrastructure/Database/AIPersistence.swift delete mode 100644 TFM/Infrastructure/Database/AgentPersistence.swift rename TFM/Infrastructure/Database/{SwiftDataCommandHistoryRepository.swift => CommandHistoryModel+SwiftData.swift} (71%) rename TFM/Infrastructure/Database/{SwiftDataConnectionHistoryRepository.swift => ConnectionHistoryModel+SwiftData.swift} (71%) delete mode 100644 TFM/Infrastructure/Database/HostPersistence.swift delete mode 100644 TFM/Infrastructure/Database/SkillPersistence.swift delete mode 100644 TFM/Infrastructure/Database/SwiftDataHostRepository.swift delete mode 100644 TFM/Infrastructure/Database/SwiftDataOutboxStore.swift delete mode 100644 TFM/Infrastructure/Metrics/SSHMetricsSampler.swift delete mode 100644 TFM/Infrastructure/SSH/PortForwardingController.swift delete mode 100644 TFM/Infrastructure/SSH/RemoteConnectionCoordinator.swift delete mode 100644 TFM/Infrastructure/SSH/SFTPClient.swift delete mode 100644 TFM/Infrastructure/SSH/SSHExecSession.swift delete mode 100644 TFM/Infrastructure/SSH/SSHForwardingSession.swift delete mode 100644 TFM/Infrastructure/SSH/SSHKeyMaterial.swift delete mode 100644 TFM/Infrastructure/SSH/SSHRoutedSocket.swift delete mode 100644 TFM/Infrastructure/SSH/SSHSessionActor.swift delete mode 100644 TFM/Infrastructure/SSH/SSHSocketConnector.swift delete mode 100644 TFM/Infrastructure/SSH/SSHUserCertificate.swift rename TFM/Infrastructure/SSHImport/{SSHImportReader.swift => SSHImportReader+Desktop.swift} (77%) rename TFM/Infrastructure/Security/{AISecretStore.swift => AISecretStore+Desktop.swift} (79%) rename TFM/Infrastructure/Security/{KeychainSecretStore.swift => KeychainSecretStore+Desktop.swift} (52%) delete mode 100644 TFM/Infrastructure/Security/SecureEnclaveKey.swift delete mode 100644 TFM/Infrastructure/Speech/AppleSpeechTranscriptionService.swift delete mode 100644 TFM/Infrastructure/Speech/SpeechAudioInput.swift rename TFM/{App/AppLanguage.swift => Platform/AppRelauncher.swift} (83%) rename TFM/UI/Components/{SharedChrome.swift => ChromeComponents.swift} (100%) rename {TFIActivityShared => WorkspaceActivitySupport}/WorkspaceActivityAttributes.swift (100%) diff --git a/FrameworkTests/SharedAISettingsTests.swift b/FrameworkTests/AISettingsIntegrationTests.swift similarity index 98% rename from FrameworkTests/SharedAISettingsTests.swift rename to FrameworkTests/AISettingsIntegrationTests.swift index a2316faa..87f00091 100644 --- a/FrameworkTests/SharedAISettingsTests.swift +++ b/FrameworkTests/AISettingsIntegrationTests.swift @@ -8,7 +8,7 @@ import TermindPersistence import Testing @Suite @MainActor -struct SharedAISettingsTests { +struct AISettingsIntegrationTests { @Test func rolesResolveOnlyEnabledChatModelsAndFallBackToChat() { var provider = ModelProvider(name: "Enabled") provider.models = [AIModel(id: "chat"), AIModel(id: "embedding", kind: .embedding)] @@ -51,7 +51,7 @@ struct SharedAISettingsTests { @Test func syncedLibrarySeedsOnceAndPreservesOtherSettings() throws { let container = try TermindPersistence.makeContainer(inMemory: true) - let domain = "SharedAISettingsTests.\(UUID())" + let domain = "AISettingsIntegrationTests.\(UUID())" let defaults = UserDefaults(suiteName: domain)! defer { defaults.removePersistentDomain(forName: domain) } let persistence = SettingsSyncPersistence(context: container.mainContext, defaults: defaults) diff --git a/FrameworkTests/SharedMetricsJourneyTests.swift b/FrameworkTests/MetricsJourneyTests.swift similarity index 99% rename from FrameworkTests/SharedMetricsJourneyTests.swift rename to FrameworkTests/MetricsJourneyTests.swift index 49de6522..d8cd5da6 100644 --- a/FrameworkTests/SharedMetricsJourneyTests.swift +++ b/FrameworkTests/MetricsJourneyTests.swift @@ -4,7 +4,7 @@ import TermindDomain import Testing @Suite @MainActor -struct SharedMetricsJourneyTests { +struct MetricsJourneyTests { @Test func offlineRestoredMetricsReconcileOnMainActor() async throws { let identity = Identity(name: "Deploy", username: "deploy") var host = TermindDomain.Host(name: "Monitor", host: "example.invalid") diff --git a/FrameworkTests/SharedSettingsPortTests.swift b/FrameworkTests/SettingsPortTests.swift similarity index 84% rename from FrameworkTests/SharedSettingsPortTests.swift rename to FrameworkTests/SettingsPortTests.swift index df1ee1d2..23e4fee3 100644 --- a/FrameworkTests/SharedSettingsPortTests.swift +++ b/FrameworkTests/SettingsPortTests.swift @@ -8,7 +8,7 @@ import TermindPersistence import Testing @Suite @MainActor -struct SharedSettingsPortTests { +struct SettingsPortTests { @Test func failedTerminalSaveKeepsTheAppliedPreferences() { let persistence = FailingTerminalPreferences() let model = TerminalSettingsModel(persistence: persistence) @@ -22,7 +22,7 @@ struct SharedSettingsPortTests { @Test func mobileSettingsEditsPreserveSkillsMCPAndUnresolvedRoles() throws { let container = try TermindPersistence.makeContainer(inMemory: true) - let domain = "SharedSettingsPortTests.\(UUID())" + let domain = "SettingsPortTests.\(UUID())" let defaults = UserDefaults(suiteName: domain)! defer { defaults.removePersistentDomain(forName: domain) } let store = SettingsSyncPersistence(context: container.mainContext, defaults: defaults) @@ -44,7 +44,7 @@ struct SharedSettingsPortTests { @Test func applyingRemoteTerminalDoesNotEnterLocalWriteCallback() throws { let container = try TermindPersistence.makeContainer(inMemory: true) - let domain = "SharedSettingsPortTests.\(UUID())" + let domain = "SettingsPortTests.\(UUID())" let defaults = UserDefaults(suiteName: domain)! defer { defaults.removePersistentDomain(forName: domain) } let store = SettingsSyncPersistence(context: container.mainContext, defaults: defaults) @@ -58,22 +58,22 @@ struct SharedSettingsPortTests { key: payload.key, version: .next(after: nil, writerID: UUID(), now: .now), payload: payload) let result = try store.applyRemote([envelope]) #expect(result.snapshot.terminal == preferences) - #expect(defaults.double(forKey: SharedSettingsKey.terminalFontSize) == 19) + #expect(defaults.double(forKey: SettingsKey.terminalFontSize) == 19) #expect(callbacks == 0) } @Test func syncDefaultsPreserveIndependentSelectionsWhenPaused() { - let domain = "SharedSettingsPortTests.\(UUID())" + let domain = "SettingsPortTests.\(UUID())" let defaults = UserDefaults(suiteName: domain)! defer { defaults.removePersistentDomain(forName: domain) } - SharedSettingsKey.migrateSyncPreferences(in: defaults) - #expect(defaults.bool(forKey: SharedSettingsKey.iCloudSyncEnabled)) - #expect(defaults.object(forKey: SharedSettingsKey.settingsSyncEnabled) == nil) - defaults.set(true, forKey: SharedSettingsKey.settingsSyncEnabled) - defaults.set(false, forKey: SharedSettingsKey.iCloudSyncEnabled) - SharedSettingsKey.migrateSyncPreferences(in: defaults) - #expect(!defaults.bool(forKey: SharedSettingsKey.iCloudSyncEnabled)) - #expect(defaults.bool(forKey: SharedSettingsKey.settingsSyncEnabled)) + SettingsKey.migrateSyncPreferences(in: defaults) + #expect(defaults.bool(forKey: SettingsKey.iCloudSyncEnabled)) + #expect(defaults.object(forKey: SettingsKey.settingsSyncEnabled) == nil) + defaults.set(true, forKey: SettingsKey.settingsSyncEnabled) + defaults.set(false, forKey: SettingsKey.iCloudSyncEnabled) + SettingsKey.migrateSyncPreferences(in: defaults) + #expect(!defaults.bool(forKey: SettingsKey.iCloudSyncEnabled)) + #expect(defaults.bool(forKey: SettingsKey.settingsSyncEnabled)) } @Test func rolesTakePrecedenceOverLastSelectedModelAndDeletedModelsFallBack() { diff --git a/FrameworkTests/TerminalCommandDraftControllerTests.swift b/FrameworkTests/TerminalCommandDraftControllerTests.swift new file mode 100644 index 00000000..f0f9a12f --- /dev/null +++ b/FrameworkTests/TerminalCommandDraftControllerTests.swift @@ -0,0 +1,51 @@ +import Foundation +import TermindApplication +import Testing + +@Suite @MainActor +struct TerminalCommandDraftControllerTests { + @Test + func readsOneDraftAndRejectsConcurrentRequests() async throws { + let controller = TerminalCommandDraftController() + var writes: [Data] = [] + let first = Task { + try await controller.read(isAvailable: true) { data in + writes.append(data) + } + } + await Task.yield() + + await #expect(throws: TerminalDraftError.self) { + try await controller.read(isAvailable: true) { _ in } + } + controller.receive("list files") + + #expect(try await first.value == "list files") + #expect(writes == [TerminalCommandDraftController.requestPayload]) + } + + @Test + func replacementUsesBracketedPasteWithoutSubmitting() { + let payload = TerminalCommandDraftController().replacementPayload(for: "ls -la") + + #expect(payload == Data("\u{1B}[777;2~\u{1B}[200~ls -la\u{1B}[201~".utf8)) + #expect(!payload.contains(0x0A)) + #expect(!payload.contains(0x0D)) + } + + @Test + func cancellationResumesThePendingReader() async { + let controller = TerminalCommandDraftController() + let read = Task { + try await controller.read(isAvailable: true) { _ in } + } + await Task.yield() + + controller.cancel(CancellationError()) + + await #expect(throws: CancellationError.self) { + try await read.value + } + #expect(!controller.hasPendingRequest) + } +} diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index 65d5374d..fdc7278d 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -3082,6 +3082,7 @@ } }, "Command Generator is already reading the current input." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -14794,6 +14795,7 @@ } }, "The remote shell did not return its current input." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -15774,6 +15776,7 @@ } }, "Type something at the shell prompt before generating a command." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { diff --git a/Frameworks/TermindApplication/Agent/CommandGenerationCoordinator.swift b/Frameworks/TermindApplication/Agent/CommandGenerationCoordinator.swift new file mode 100644 index 00000000..6b1cd395 --- /dev/null +++ b/Frameworks/TermindApplication/Agent/CommandGenerationCoordinator.swift @@ -0,0 +1,120 @@ +import Foundation + +public nonisolated enum CommandGenerationError: LocalizedError, Sendable { + case emptyDraft + + public var errorDescription: String? { + String(localized: "Type something at the shell prompt before generating a command.") + } +} + +@MainActor +@Observable +public final class CommandGenerationCoordinator { + public typealias ReadContext = @MainActor @Sendable () async throws -> CommandGeneratorContext + public typealias IsCurrent = @MainActor @Sendable () -> Bool + public typealias Generate = @Sendable (CommandGeneratorContext) async throws -> String + public typealias Insert = @MainActor @Sendable (String) async -> Void + + public private(set) var state = CommandGeneratorState.idle + public var isRunning: Bool { task != nil } + + private var task: Task? + private var dismissTask: Task? + private var generation: UInt64 = 0 + + public init() {} + + public func start( + targetCount: Int, + readContext: @escaping ReadContext, + isCurrent: @escaping IsCurrent, + generate: @escaping Generate, + insert: @escaping Insert + ) { + cancel() + generation &+= 1 + let generation = generation + state = .readingDraft + task = Task { [weak self] in + do { + let context = try await readContext() + try Task.checkCancellation() + guard !context.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + throw CommandGenerationError.emptyDraft + } + guard isCurrent() else { throw CancellationError() } + self?.state = .generating + let command = try await Self.withTimeout { try await generate(context) } + try Task.checkCancellation() + guard isCurrent() else { throw CancellationError() } + await insert(command) + self?.finish(generation: generation, state: .inserted(targetCount: targetCount), after: .seconds(2)) + } catch is CancellationError { + self?.finishCancellation(generation: generation) + } catch { + self?.finish(generation: generation, state: .failed(error.localizedDescription), after: .seconds(3)) + } + } + } + + public func cancel() { + generation &+= 1 + task?.cancel() + task = nil + dismissTask?.cancel() + dismissTask = nil + state = .idle + } + + public func fail(_ message: String) { + cancel() + state = .failed(message) + scheduleDismiss(generation: generation, after: .seconds(3)) + } + + public func replaceStateForPresentation(_ state: CommandGeneratorState) { + cancel() + self.state = state + } + + private func finishCancellation(generation: UInt64) { + guard self.generation == generation else { return } + task = nil + state = .idle + } + + private func finish(generation: UInt64, state: CommandGeneratorState, after delay: Duration) { + guard self.generation == generation else { return } + task = nil + self.state = state + scheduleDismiss(generation: generation, after: delay) + } + + private func scheduleDismiss(generation: UInt64, after delay: Duration) { + dismissTask?.cancel() + dismissTask = Task { [weak self] in + try? await Task.sleep(for: delay) + guard !Task.isCancelled, self?.generation == generation else { return } + self?.state = .idle + self?.dismissTask = nil + } + } + + private nonisolated static func withTimeout( + _ operation: @escaping @Sendable () async throws -> String + ) async throws -> String { + try await withThrowingTaskGroup(of: String.self) { group in + group.addTask { try await operation() } + group.addTask { + try await Task.sleep(for: .seconds(30)) + throw CommandGeneratorError.timedOut + } + guard let result = try await group.next() else { + throw CommandGeneratorError.invalidResponse + } + group.cancelAll() + return result + } + } +} diff --git a/Frameworks/TermindApplication/Agent/TerminalCommandDraftController.swift b/Frameworks/TermindApplication/Agent/TerminalCommandDraftController.swift new file mode 100644 index 00000000..60d0a701 --- /dev/null +++ b/Frameworks/TermindApplication/Agent/TerminalCommandDraftController.swift @@ -0,0 +1,99 @@ +import Foundation + +public nonisolated enum TerminalDraftError: LocalizedError, Sendable { + case unavailable + case alreadyPending + case timedOut + + public var errorDescription: String? { + switch self { + case .unavailable: + String( + localized: + "Command Generator requires a supported shell prompt with Shell Integration enabled." + ) + case .alreadyPending: + String(localized: "Command Generator is already reading the current input.") + case .timedOut: + String(localized: "The remote shell did not return its current input.") + } + } +} + +@MainActor +public final class TerminalCommandDraftController { + public typealias Write = @MainActor @Sendable (Data) async -> Void + + private struct Request { + var id: UUID + var continuation: CheckedContinuation + } + + public static let supportedShells: Set = ["bash", "zsh", "fish"] + public static let requestPayload = Data("\u{1B}[777;1~".utf8) + + private static let clearPayload = Data("\u{1B}[777;2~".utf8) + private static let bracketedPasteStart = Data("\u{1B}[200~".utf8) + private static let bracketedPasteEnd = Data("\u{1B}[201~".utf8) + + private var request: Request? + private var timeoutTask: Task? + + public var hasPendingRequest: Bool { request != nil } + + public init() {} + + public func read(isAvailable: Bool, write: @escaping Write) async throws -> String { + guard isAvailable else { throw TerminalDraftError.unavailable } + guard request == nil else { throw TerminalDraftError.alreadyPending } + let id = UUID() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + request = Request(id: id, continuation: continuation) + Task { await write(Self.requestPayload) } + timeoutTask?.cancel() + timeoutTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(2)) + guard !Task.isCancelled else { return } + self?.cancel(id: id, error: TerminalDraftError.timedOut) + } + } + } onCancel: { + Task { @MainActor [weak self] in + self?.cancel(id: id, error: CancellationError()) + } + } + } + + public func receive(_ draft: String) { + guard let request else { return } + finish(request) + request.continuation.resume(returning: draft) + } + + public func cancel(_ error: any Error = TerminalDraftError.unavailable) { + guard let request else { return } + cancel(id: request.id, error: error) + } + + public func replacementPayload(for command: String) -> Data { + var data = Self.clearPayload + data.append(Self.bracketedPasteStart) + data.append(Data(command.utf8)) + data.append(Self.bracketedPasteEnd) + return data + } + + private func cancel(id: UUID, error: any Error) { + guard request?.id == id, let request else { return } + finish(request) + request.continuation.resume(throwing: error) + } + + private func finish(_ request: Request) { + guard self.request?.id == request.id else { return } + self.request = nil + timeoutTask?.cancel() + timeoutTask = nil + } +} diff --git a/Frameworks/TermindApplication/AppLanguage.swift b/Frameworks/TermindApplication/AppLanguage.swift index 85efc46b..9668a18e 100644 --- a/Frameworks/TermindApplication/AppLanguage.swift +++ b/Frameworks/TermindApplication/AppLanguage.swift @@ -42,7 +42,7 @@ public nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendabl } public nonisolated static func stored(in defaults: UserDefaults = .standard) -> AppLanguage { - guard let rawValue = defaults.string(forKey: SharedSettingsKey.appLanguage) else { + guard let rawValue = defaults.string(forKey: SettingsKey.appLanguage) else { return .system } return AppLanguage(rawValue: rawValue) ?? .system @@ -54,7 +54,7 @@ public nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendabl public nonisolated static func store( _ language: AppLanguage, in defaults: UserDefaults = .standard ) { - defaults.set(language.rawValue, forKey: SharedSettingsKey.appLanguage) + defaults.set(language.rawValue, forKey: SettingsKey.appLanguage) switch language { case .system: defaults.removeObject(forKey: "AppleLanguages") diff --git a/TFM/Application/Connections/RemoteSessionConnectionController.swift b/Frameworks/TermindApplication/Connections/RemoteSessionConnectionController.swift similarity index 51% rename from TFM/Application/Connections/RemoteSessionConnectionController.swift rename to Frameworks/TermindApplication/Connections/RemoteSessionConnectionController.swift index fd8a3df1..b5cd9014 100644 --- a/TFM/Application/Connections/RemoteSessionConnectionController.swift +++ b/Frameworks/TermindApplication/Connections/RemoteSessionConnectionController.swift @@ -1,5 +1,4 @@ import Foundation -import TermindApplication import TermindDomain import struct TermindDomain.Host @@ -8,23 +7,75 @@ import struct TermindDomain.Host /// Transport-specific models own their I/O, while this object owns the common /// connection journey and retry lifecycle shown by both Terminal and SFTP. @Observable -final class RemoteSessionConnectionController { - let journey = ConnectionJourney() - let reconnect = SessionReconnectController() - var pendingHostKey: SSHHostKeyPrompt? - var temporaryHostKeys: [Host.ID: SSHHostKeyIdentity] = [:] +public final class RemoteSessionConnectionController { + public let journey = ConnectionJourney() + public let reconnect: SessionReconnectController + public var pendingHostKey: SSHHostKeyPrompt? + public var temporaryHostKeys: [Host.ID: SSHHostKeyIdentity] = [:] + public private(set) var hasConnectedOnce = false + public private(set) var isRecovering = false private var pendingHostKeyHostID: Host.ID? - var reconnectState: ReconnectState? { reconnect.state } + public var reconnectState: ReconnectState? { reconnect.state } - func disconnect() { + public init(reconnect: SessionReconnectController = SessionReconnectController()) { + self.reconnect = reconnect + } + + public func disconnect() { clearHostKeyPrompt() temporaryHostKeys.removeAll() reconnect.stop() + hasConnectedOnce = false + isRecovering = false journey.hide() } - func trustHostKey( + @discardableResult + public func markConnected() -> Bool { + let wasRecovering = isRecovering + let initial = !hasConnectedOnce + hasConnectedOnce = true + isRecovering = false + reconnect.markConnected(initial: initial) + return wasRecovering + } + + @discardableResult + public func scheduleRecovery( + after error: String, retry: @escaping SessionReconnectController.RetryAction + ) -> Bool { + guard hasConnectedOnce else { return false } + let announcesLoss = !isRecovering + isRecovering = true + reconnect.schedule(after: error, retry: retry) + return announcesLoss + } + + public func pauseForUserInteraction() { + reconnect.pauseForUserInteraction() + } + + @discardableResult + public func stopRecovery() -> ReconnectState? { + let state = reconnect.state + reconnect.stop() + isRecovering = false + return state + } + + public func suspendForForegroundResume(reconnecting: Bool) { + reconnect.stop() + isRecovering = reconnecting && hasConnectedOnce + } + + public func resetRecovery() { + reconnect.stop() + hasConnectedOnce = false + isRecovering = false + } + + public func trustHostKey( _ observation: SSHHostKeyObservation, using store: any RemoteSessionHostStore ) { store.trust( @@ -38,7 +89,7 @@ final class RemoteSessionConnectionController { journey.resume() } - func useTemporaryHostKey(_ observation: SSHHostKeyObservation) -> Bool { + public func useTemporaryHostKey(_ observation: SSHHostKeyObservation) -> Bool { guard let pendingHostKeyHostID else { return false } temporaryHostKeys[pendingHostKeyHostID] = observation.identity clearHostKeyPrompt() @@ -46,7 +97,7 @@ final class RemoteSessionConnectionController { return true } - func waitForHostKey( + public func waitForHostKey( _ prompt: SSHHostKeyPrompt, hostID: Host.ID, fingerprint: String ) { pendingHostKey = prompt @@ -55,7 +106,7 @@ final class RemoteSessionConnectionController { journey.waitForHostKey(at: hostID, fingerprint: fingerprint) } - func progressHandler( + public func progressHandler( attemptID: UUID, isCurrent: @escaping @MainActor @Sendable (UUID) -> Bool ) -> SSHRouteProgressHandler { { [weak self] event in @@ -66,7 +117,7 @@ final class RemoteSessionConnectionController { } } - func presentReconnectJourney( + public func presentReconnectJourney( host: Host?, using store: any RemoteSessionHostStore, purpose: ConnectionPurpose, username: String? ) { diff --git a/Frameworks/TermindApplication/FeatureModels/SkillLibraryModel.swift b/Frameworks/TermindApplication/FeatureModels/SkillLibraryModel.swift index 6c46febe..a99224f0 100644 --- a/Frameworks/TermindApplication/FeatureModels/SkillLibraryModel.swift +++ b/Frameworks/TermindApplication/FeatureModels/SkillLibraryModel.swift @@ -48,7 +48,7 @@ public final class SkillLibraryModel { /// screenshot build get the library they were handed and nothing else. private func seedBuiltInSkills() { guard let seedDefaults else { return } - let seeded = seedDefaults.integer(forKey: SharedSettingsKey.seededSkillGeneration) + let seeded = seedDefaults.integer(forKey: SettingsKey.seededSkillGeneration) guard seeded < AgentSkillLibrary.currentGeneration else { return } for skill in AgentSkillLibrary.builtIns(after: seeded) { var installed = skill @@ -59,7 +59,7 @@ public final class SkillLibraryModel { // thing that can make this run twice. guard storageError == nil else { return } seedDefaults.set( - AgentSkillLibrary.currentGeneration, forKey: SharedSettingsKey.seededSkillGeneration + AgentSkillLibrary.currentGeneration, forKey: SettingsKey.seededSkillGeneration ) } @@ -166,7 +166,7 @@ public final class SkillLibraryModel { // Nothing left for seeding to add, whatever generation this store was // written by. seedDefaults?.set( - AgentSkillLibrary.currentGeneration, forKey: SharedSettingsKey.seededSkillGeneration + AgentSkillLibrary.currentGeneration, forKey: SettingsKey.seededSkillGeneration ) return shipped.count } diff --git a/Frameworks/TermindApplication/SharedSettingsKey.swift b/Frameworks/TermindApplication/SettingsKey.swift similarity index 98% rename from Frameworks/TermindApplication/SharedSettingsKey.swift rename to Frameworks/TermindApplication/SettingsKey.swift index e0c090f9..5d3f1b51 100644 --- a/Frameworks/TermindApplication/SharedSettingsKey.swift +++ b/Frameworks/TermindApplication/SettingsKey.swift @@ -1,6 +1,6 @@ import Foundation -public nonisolated enum SharedSettingsKey { +public nonisolated enum SettingsKey { public static let appLanguage = "app.language" public static let defaultICloudSyncEnabled = true public static let defaultSettingsSyncEnabled = false diff --git a/Frameworks/TermindDomain/Sessions/SSHShellIntegrationEvent.swift b/Frameworks/TermindDomain/Sessions/SSHShellIntegrationEvent.swift new file mode 100644 index 00000000..c51666cf --- /dev/null +++ b/Frameworks/TermindDomain/Sessions/SSHShellIntegrationEvent.swift @@ -0,0 +1,11 @@ +import Foundation + +public nonisolated enum SSHShellIntegrationEvent: Sendable { + case available(shell: String, systemInfo: RemoteSystemInfo?, homeDirectory: String?) + case systemInfo(RemoteSystemInfo) + case homeResolutionUnavailable + case promptReady + case commandStarted(String) + case commandFinished(Int32, duration: TimeInterval?) + case draft(String) +} diff --git a/Frameworks/TermindDomain/Sessions/TerminalFind.swift b/Frameworks/TermindDomain/Sessions/TerminalFind.swift new file mode 100644 index 00000000..ef378465 --- /dev/null +++ b/Frameworks/TermindDomain/Sessions/TerminalFind.swift @@ -0,0 +1,14 @@ +/// Search order follows Ghostty and VS Code's terminal find widget. A search +/// starts at the bottom of the buffer, so "previous" moves toward older output. +public nonisolated enum TerminalFindDirection: String, Sendable { + case previous + case next +} + +public nonisolated enum TerminalFindBinding { + public static func search(_ query: String) -> String { "search:\(query)" } + public static func navigate(_ direction: TerminalFindDirection) -> String { + "navigate_search:\(direction.rawValue)" + } + public static let end = "end_search" +} diff --git a/Frameworks/TermindInfrastructure/Agent/ContextAttachmentImporter.swift b/Frameworks/TermindInfrastructure/Agent/ContextAttachmentImporter.swift index 6d319c29..c12b7a92 100644 --- a/Frameworks/TermindInfrastructure/Agent/ContextAttachmentImporter.swift +++ b/Frameworks/TermindInfrastructure/Agent/ContextAttachmentImporter.swift @@ -182,7 +182,7 @@ nonisolated public enum ContextAttachmentImporter { let archive = try officeArchive(data) let shared: [String] if let xml = try archiveData(archive, path: "xl/sharedStrings.xml") { - shared = try SharedStringsParser().parse(xml) + shared = try SpreadsheetSharedStringsParser().parse(xml) } else { shared = [] } @@ -361,7 +361,7 @@ nonisolated private final class ParagraphTextParser: NSObject, XMLParserDelegate } } -nonisolated private final class SharedStringsParser: NSObject, XMLParserDelegate { +nonisolated private final class SpreadsheetSharedStringsParser: NSObject, XMLParserDelegate { public var strings: [String] = [] public var value = "" public var text = "" diff --git a/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift b/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift index cc655e21..912a939f 100644 --- a/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift +++ b/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift @@ -37,11 +37,11 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc public var isEnabled: Bool { guard - defaults.object(forKey: SharedSettingsKey.iCloudSyncEnabled) as? Bool - ?? SharedSettingsKey.defaultICloudSyncEnabled + defaults.object(forKey: SettingsKey.iCloudSyncEnabled) as? Bool + ?? SettingsKey.defaultICloudSyncEnabled else { return false } - return defaults.object(forKey: SharedSettingsKey.settingsSyncEnabled) as? Bool - ?? SharedSettingsKey.defaultSettingsSyncEnabled + return defaults.object(forKey: SettingsKey.settingsSyncEnabled) as? Bool + ?? SettingsKey.defaultSettingsSyncEnabled } public init( @@ -78,7 +78,7 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc } public func setEnabled(_ enabled: Bool) { - defaults.set(enabled, forKey: SharedSettingsKey.settingsSyncEnabled) + defaults.set(enabled, forKey: SettingsKey.settingsSyncEnabled) if enabled { start() } else { pause() } } @@ -487,13 +487,13 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc private func prepareForCurrentEnvironment() throws { guard - defaults.string(forKey: SharedSettingsKey.settingsSyncEnvironment) + defaults.string(forKey: SettingsKey.settingsSyncEnvironment) != environmentIdentifier else { return } try journal.markAllEntriesNeedsUploadAfterEnvironmentChange() try context.save() - defaults.set(environmentIdentifier, forKey: SharedSettingsKey.settingsSyncEnvironment) + defaults.set(environmentIdentifier, forKey: SettingsKey.settingsSyncEnvironment) terminalError = nil } diff --git a/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift b/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift index b8dbe88a..9f879fdb 100644 --- a/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift +++ b/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift @@ -34,11 +34,11 @@ public final class VaultSyncCoordinator: NSObject, CKSyncEngineDelegate, @unchec public var isEnabled: Bool { guard - defaults.object(forKey: SharedSettingsKey.iCloudSyncEnabled) as? Bool - ?? SharedSettingsKey.defaultICloudSyncEnabled + defaults.object(forKey: SettingsKey.iCloudSyncEnabled) as? Bool + ?? SettingsKey.defaultICloudSyncEnabled else { return false } - return defaults.object(forKey: SharedSettingsKey.vaultSyncEnabled) as? Bool - ?? SharedSettingsKey.defaultVaultSyncEnabled + return defaults.object(forKey: SettingsKey.vaultSyncEnabled) as? Bool + ?? SettingsKey.defaultVaultSyncEnabled } public init( @@ -72,7 +72,7 @@ public final class VaultSyncCoordinator: NSObject, CKSyncEngineDelegate, @unchec } public func setEnabled(_ enabled: Bool) { - defaults.set(enabled, forKey: SharedSettingsKey.vaultSyncEnabled) + defaults.set(enabled, forKey: SettingsKey.vaultSyncEnabled) if enabled { start() } else { pause() } } @@ -460,13 +460,13 @@ public final class VaultSyncCoordinator: NSObject, CKSyncEngineDelegate, @unchec /// retaining every local envelope and its conflict-resolution version. private func prepareForCurrentEnvironment() throws { guard - defaults.string(forKey: SharedSettingsKey.vaultSyncEnvironment) + defaults.string(forKey: SettingsKey.vaultSyncEnvironment) != environmentIdentifier else { return } try journal.markAllEntriesNeedsUploadAfterEnvironmentChange() try context.save() - defaults.set(environmentIdentifier, forKey: SharedSettingsKey.vaultSyncEnvironment) + defaults.set(environmentIdentifier, forKey: SettingsKey.vaultSyncEnvironment) terminalError = nil } diff --git a/Frameworks/TermindPersistence/SettingsSyncStore.swift b/Frameworks/TermindPersistence/SettingsSyncStore.swift index 29368143..03b7638c 100644 --- a/Frameworks/TermindPersistence/SettingsSyncStore.swift +++ b/Frameworks/TermindPersistence/SettingsSyncStore.swift @@ -61,7 +61,7 @@ public final class SettingsSyncPersistence { } public func prepare() throws { - let seeded = defaults.integer(forKey: SharedSettingsKey.seededSkillGeneration) + let seeded = defaults.integer(forKey: SettingsKey.seededSkillGeneration) var mutations = SettingsMutationSet.empty try context.commit { if try !journal.state().bootstrapCompleted { @@ -96,7 +96,7 @@ public final class SettingsSyncPersistence { mutations.keys.insert(terminal.key) } } - defaults.set(AgentSkillLibrary.currentGeneration, forKey: SharedSettingsKey.seededSkillGeneration) + defaults.set(AgentSkillLibrary.currentGeneration, forKey: SettingsKey.seededSkillGeneration) let snapshot = try load() mirror(snapshot.webSearch) snapshot.terminal.mirror(to: defaults) @@ -243,17 +243,17 @@ public final class SettingsSyncPersistence { if let row = try context.fetch(FetchDescriptor()).first { return row } let row = PersistentAIWebSearchPreferences( AIWebSearchPreferences( - isEnabled: defaults.object(forKey: SharedSettingsKey.webSearchEnabled) as? Bool - ?? SharedSettingsKey.defaultWebSearchEnabled, - prefersBuiltIn: defaults.object(forKey: SharedSettingsKey.webSearchBuiltIn) as? Bool - ?? SharedSettingsKey.defaultWebSearchBuiltIn)) + isEnabled: defaults.object(forKey: SettingsKey.webSearchEnabled) as? Bool + ?? SettingsKey.defaultWebSearchEnabled, + prefersBuiltIn: defaults.object(forKey: SettingsKey.webSearchBuiltIn) as? Bool + ?? SettingsKey.defaultWebSearchBuiltIn)) context.insert(row) return row } private func mirror(_ value: AIWebSearchPreferences) { - defaults.set(value.isEnabled, forKey: SharedSettingsKey.webSearchEnabled) - defaults.set(value.prefersBuiltIn, forKey: SharedSettingsKey.webSearchBuiltIn) + defaults.set(value.isEnabled, forKey: SettingsKey.webSearchEnabled) + defaults.set(value.prefersBuiltIn, forKey: SettingsKey.webSearchBuiltIn) } } diff --git a/Frameworks/TermindPersistence/SharedRepositoryAliases.swift b/Frameworks/TermindPersistence/SharedRepositoryAliases.swift deleted file mode 100644 index 719b7cc8..00000000 --- a/Frameworks/TermindPersistence/SharedRepositoryAliases.swift +++ /dev/null @@ -1,16 +0,0 @@ -public typealias SharedHostPersistence = HostPersistence -public typealias SharedVaultSyncPersistenceError = VaultSyncPersistenceError -public typealias SharedVaultRemoteMerge = VaultRemoteMerge -public typealias SharedVaultOutgoingEntry = VaultOutgoingEntry -public typealias SharedVaultSyncJournal = VaultSyncJournal -public typealias SharedSwiftDataHostRepository = SwiftDataHostRepository -public typealias SharedSettingsOutgoingEntry = SettingsOutgoingEntry -public typealias SharedSettingsRemoteMerge = SettingsRemoteMerge -public typealias SharedSettingsSyncJournal = SettingsSyncJournal -public typealias SharedSettingsSyncPersistence = SettingsSyncPersistence -public typealias SharedSettingsSyncSnapshot = SettingsSyncSnapshot -public typealias SharedSkillPersistence = SkillPersistence -public typealias SharedSwiftDataCommandHistoryRepository = SwiftDataCommandHistoryRepository -public typealias SharedSwiftDataConnectionHistoryRepository = SwiftDataConnectionHistoryRepository -public typealias SharedSyncedAIConfigurationPersistence = SyncedAIConfigurationPersistence -public typealias SharedSyncedSkillLibraryPersistence = SyncedSkillLibraryPersistence diff --git a/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift b/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift index cbbc2fe0..835ac8ed 100644 --- a/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift +++ b/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift @@ -114,5 +114,3 @@ public final class AIPersistence { } extension AIPersistence: AIConfigurationPersisting {} - -public typealias SharedAIPersistence = AIPersistence diff --git a/Frameworks/TermindPersistence/SwiftDataAISecretStore.swift b/Frameworks/TermindPersistence/SwiftDataAISecretStore.swift index d9147eb8..b33edab6 100644 --- a/Frameworks/TermindPersistence/SwiftDataAISecretStore.swift +++ b/Frameworks/TermindPersistence/SwiftDataAISecretStore.swift @@ -99,5 +99,3 @@ public final class AISecretStore: AISecretStoring { return row } } - -public typealias SharedAISecretStore = AISecretStore diff --git a/Frameworks/TermindPersistence/SwiftDataAgentRunRepository.swift b/Frameworks/TermindPersistence/SwiftDataAgentRunRepository.swift index e07322c4..bfc87960 100644 --- a/Frameworks/TermindPersistence/SwiftDataAgentRunRepository.swift +++ b/Frameworks/TermindPersistence/SwiftDataAgentRunRepository.swift @@ -655,5 +655,3 @@ public final class SwiftDataAgentRunRepository: AgentRunPersisting, Sendable { } catch { context.rollback() } } } - -public typealias SharedAgentRunRepository = SwiftDataAgentRunRepository diff --git a/Frameworks/TermindPersistence/SwiftDataOutboxStore.swift b/Frameworks/TermindPersistence/SwiftDataOutboxStore.swift index 275ef593..21ef2217 100644 --- a/Frameworks/TermindPersistence/SwiftDataOutboxStore.swift +++ b/Frameworks/TermindPersistence/SwiftDataOutboxStore.swift @@ -80,5 +80,3 @@ public final class SwiftDataOutboxStore: OutboxStore { try context.savePendingChanges() } } - -public typealias SharedOutboxStore = SwiftDataOutboxStore diff --git a/Frameworks/TermindPersistence/TerminalPreferences+Defaults.swift b/Frameworks/TermindPersistence/TerminalPreferences+Defaults.swift index f8ae9448..824c912c 100644 --- a/Frameworks/TermindPersistence/TerminalPreferences+Defaults.swift +++ b/Frameworks/TermindPersistence/TerminalPreferences+Defaults.swift @@ -6,32 +6,32 @@ import TermindDomain extension TerminalPreferences { public nonisolated init(defaults: UserDefaults) { self.init() - fontSize = defaults.object(forKey: SharedSettingsKey.terminalFontSize) as? Double ?? fontSize - fontFollowTheme = defaults.object(forKey: SharedSettingsKey.terminalFontFollowTheme) as? Bool ?? fontFollowTheme - darkTheme = defaults.string(forKey: SharedSettingsKey.terminalThemeDark) ?? darkTheme - lightTheme = defaults.string(forKey: SharedSettingsKey.terminalThemeLight) ?? lightTheme + fontSize = defaults.object(forKey: SettingsKey.terminalFontSize) as? Double ?? fontSize + fontFollowTheme = defaults.object(forKey: SettingsKey.terminalFontFollowTheme) as? Bool ?? fontFollowTheme + darkTheme = defaults.string(forKey: SettingsKey.terminalThemeDark) ?? darkTheme + lightTheme = defaults.string(forKey: SettingsKey.terminalThemeLight) ?? lightTheme darkCustomEnabled = - defaults.object(forKey: SharedSettingsKey.terminalCustomConfigurationDarkEnabled) as? Bool ?? false + defaults.object(forKey: SettingsKey.terminalCustomConfigurationDarkEnabled) as? Bool ?? false lightCustomEnabled = - defaults.object(forKey: SharedSettingsKey.terminalCustomConfigurationLightEnabled) as? Bool ?? false - darkCustomConfiguration = defaults.string(forKey: SharedSettingsKey.terminalCustomConfigurationDark) ?? "" - lightCustomConfiguration = defaults.string(forKey: SharedSettingsKey.terminalCustomConfigurationLight) ?? "" - shellIntegration = defaults.object(forKey: SharedSettingsKey.shellIntegration) as? Bool ?? shellIntegration + defaults.object(forKey: SettingsKey.terminalCustomConfigurationLightEnabled) as? Bool ?? false + darkCustomConfiguration = defaults.string(forKey: SettingsKey.terminalCustomConfigurationDark) ?? "" + lightCustomConfiguration = defaults.string(forKey: SettingsKey.terminalCustomConfigurationLight) ?? "" + shellIntegration = defaults.object(forKey: SettingsKey.shellIntegration) as? Bool ?? shellIntegration remoteWindowTitleInMenuBar = - defaults.object(forKey: SharedSettingsKey.remoteWindowTitleInMenuBar) as? Bool + defaults.object(forKey: SettingsKey.remoteWindowTitleInMenuBar) as? Bool ?? remoteWindowTitleInMenuBar } public nonisolated func mirror(to defaults: UserDefaults) { - defaults.set(fontSize, forKey: SharedSettingsKey.terminalFontSize) - defaults.set(fontFollowTheme, forKey: SharedSettingsKey.terminalFontFollowTheme) - defaults.set(darkTheme, forKey: SharedSettingsKey.terminalThemeDark) - defaults.set(lightTheme, forKey: SharedSettingsKey.terminalThemeLight) - defaults.set(darkCustomEnabled, forKey: SharedSettingsKey.terminalCustomConfigurationDarkEnabled) - defaults.set(lightCustomEnabled, forKey: SharedSettingsKey.terminalCustomConfigurationLightEnabled) - defaults.set(darkCustomConfiguration, forKey: SharedSettingsKey.terminalCustomConfigurationDark) - defaults.set(lightCustomConfiguration, forKey: SharedSettingsKey.terminalCustomConfigurationLight) - defaults.set(shellIntegration, forKey: SharedSettingsKey.shellIntegration) - defaults.set(remoteWindowTitleInMenuBar, forKey: SharedSettingsKey.remoteWindowTitleInMenuBar) + defaults.set(fontSize, forKey: SettingsKey.terminalFontSize) + defaults.set(fontFollowTheme, forKey: SettingsKey.terminalFontFollowTheme) + defaults.set(darkTheme, forKey: SettingsKey.terminalThemeDark) + defaults.set(lightTheme, forKey: SettingsKey.terminalThemeLight) + defaults.set(darkCustomEnabled, forKey: SettingsKey.terminalCustomConfigurationDarkEnabled) + defaults.set(lightCustomEnabled, forKey: SettingsKey.terminalCustomConfigurationLightEnabled) + defaults.set(darkCustomConfiguration, forKey: SettingsKey.terminalCustomConfigurationDark) + defaults.set(lightCustomConfiguration, forKey: SettingsKey.terminalCustomConfigurationLight) + defaults.set(shellIntegration, forKey: SettingsKey.shellIntegration) + defaults.set(remoteWindowTitleInMenuBar, forKey: SettingsKey.remoteWindowTitleInMenuBar) } } diff --git a/Frameworks/TermindPresentation/ConnectionDecisionForms.swift b/Frameworks/TermindPresentation/ConnectionDecisionForms.swift index 4efef92f..b22c8f2b 100644 --- a/Frameworks/TermindPresentation/ConnectionDecisionForms.swift +++ b/Frameworks/TermindPresentation/ConnectionDecisionForms.swift @@ -9,7 +9,7 @@ public protocol ConnectionFormKeyboardAdapting { extension View { @ViewBuilder - fileprivate func sharedConnectionFormChrome(isEnabled: Bool) -> some View { + fileprivate func connectionFormChrome(isEnabled: Bool) -> some View { if isEnabled { padding(18).background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14)) .overlay { RoundedRectangle(cornerRadius: 14).strokeBorder(.quaternary) } @@ -19,7 +19,7 @@ extension View { } } -public struct SharedSSHAuthenticationForm: View { +public struct SSHAuthenticationForm: View { let prompt: SSHAuthenticationPrompt let submit: (Identity.ID?, String, String?, String?, SSHKeyRef.ID?, Bool) -> Void let cancel: () -> Void @@ -71,7 +71,7 @@ public struct SharedSSHAuthenticationForm: View { public var body: some View { form - .sharedConnectionFormChrome(isEnabled: usesContainerChrome) + .connectionFormChrome(isEnabled: usesContainerChrome) .background { if let keyboard { keyboard.bridge(canSubmit: canContinue, submit: { commit(save: !needsPassphrase) }, cancel: cancel) @@ -325,7 +325,7 @@ public struct SharedSSHAuthenticationForm: View { } } -public struct SharedSSHHostKeyConfirmationForm: View { +public struct SSHHostKeyConfirmationForm: View { let prompt: SSHHostKeyPrompt var displayAddress: String? = nil let proceed: () -> Void @@ -357,7 +357,7 @@ public struct SharedSSHHostKeyConfirmationForm: View { public var body: some View { form - .sharedConnectionFormChrome(isEnabled: usesContainerChrome) + .connectionFormChrome(isEnabled: usesContainerChrome) .background { if let keyboard { keyboard.bridge(canSubmit: true, submit: { trust?() ?? proceed() }, cancel: deny) } } diff --git a/Frameworks/TermindRemoteInfrastructure/SSH/RemoteConnectionCoordinator.swift b/Frameworks/TermindRemoteInfrastructure/SSH/RemoteConnectionCoordinator.swift index e3239a6a..3becdd95 100644 --- a/Frameworks/TermindRemoteInfrastructure/SSH/RemoteConnectionCoordinator.swift +++ b/Frameworks/TermindRemoteInfrastructure/SSH/RemoteConnectionCoordinator.swift @@ -1,5 +1,6 @@ import Foundation import TermindApplication +import TermindDomain /// Application-owned construction and one-shot operation boundary for all /// SSH/SFTP transports. Features receive this service instead of constructing diff --git a/Frameworks/TermindRemoteInfrastructure/SSH/SSHSessionActor.swift b/Frameworks/TermindRemoteInfrastructure/SSH/SSHSessionActor.swift index 4f9d84d0..af9985ca 100644 --- a/Frameworks/TermindRemoteInfrastructure/SSH/SSHSessionActor.swift +++ b/Frameworks/TermindRemoteInfrastructure/SSH/SSHSessionActor.swift @@ -518,16 +518,6 @@ public nonisolated struct RemoteCommandEventFilter: Sendable { } } -public nonisolated enum SSHShellIntegrationEvent: Sendable { - case available(shell: String, systemInfo: RemoteSystemInfo?, homeDirectory: String?) - case systemInfo(RemoteSystemInfo) - case homeResolutionUnavailable - case promptReady - case commandStarted(String) - case commandFinished(Int32, duration: TimeInterval?) - case draft(String) -} - public actor SSHSessionActor { public typealias OutputHandler = @Sendable (Data) -> Void public typealias ExitHandler = @Sendable (Result) -> Void diff --git a/Frameworks/TermindRemoteInfrastructure/Transfers/SFTPTransferCoordinator.swift b/Frameworks/TermindRemoteInfrastructure/Transfers/SFTPTransferCoordinator.swift index cdf18e5c..f590edc7 100644 --- a/Frameworks/TermindRemoteInfrastructure/Transfers/SFTPTransferCoordinator.swift +++ b/Frameworks/TermindRemoteInfrastructure/Transfers/SFTPTransferCoordinator.swift @@ -115,14 +115,14 @@ public final class SFTPTransferCoordinator { self.clientFactory = clientFactory let defaults = UserDefaults.standard let normalizedGlobal = Self.normalizedLimit( - globalLimit ?? defaults.object(forKey: SharedSettingsKey.sftpGlobalTransferLimit) as? Int - ?? SharedSettingsKey.defaultSFTPGlobalTransferLimit + globalLimit ?? defaults.object(forKey: SettingsKey.sftpGlobalTransferLimit) as? Int + ?? SettingsKey.defaultSFTPGlobalTransferLimit ) self.globalLimit = normalizedGlobal self.perHostLimit = min( Self.normalizedLimit( - perHostLimit ?? defaults.object(forKey: SharedSettingsKey.sftpPerHostTransferLimit) as? Int - ?? SharedSettingsKey.defaultSFTPPerHostTransferLimit + perHostLimit ?? defaults.object(forKey: SettingsKey.sftpPerHostTransferLimit) as? Int + ?? SettingsKey.defaultSFTPPerHostTransferLimit ), normalizedGlobal ) @@ -136,8 +136,8 @@ public final class SFTPTransferCoordinator { private static func normalizedLimit(_ value: Int) -> Int { min( - max(value, SharedSettingsKey.sftpTransferLimitRange.lowerBound), - SharedSettingsKey.sftpTransferLimitRange.upperBound + max(value, SettingsKey.sftpTransferLimitRange.lowerBound), + SettingsKey.sftpTransferLimitRange.upperBound ) } diff --git a/Memory/ARCHITECTURE.md b/Memory/ARCHITECTURE.md index dbf9c195..8a76e9dd 100644 --- a/Memory/ARCHITECTURE.md +++ b/Memory/ARCHITECTURE.md @@ -26,7 +26,15 @@ remains a versioned remote SPM dependency developed in the sibling repository. orchestration. `TFI` owns its native mobile entry point and scene navigation. Platform code can consume frameworks; frameworks cannot consume either app. +Both app targets use the same top-level layer vocabulary: `App`, `Application`, +`Design`, `DesignSystem`, `Features`, `Infrastructure`, `Platform`, and `UI`. +TFI feature code is grouped by capability inside that structure; production SSH +construction lives in Infrastructure and reusable mobile chrome lives in UI. + The frameworks have explicit public boundaries and do not re-export imports. +App targets import framework owners directly. They do not recreate framework +names with forwarding type aliases, and framework types do not use `Shared` as +a namespace: the module and responsibility-specific name express ownership. `Host` is explicitly imported from Domain where Foundation's same-named type would otherwise make lookup ambiguous. Presentation of session-context chips belongs to the desktop feature extension, not the domain value. @@ -89,15 +97,22 @@ infrastructure, persistence, or presentation responsibility. - A workspace owns SSH/SFTP/serial panes plus its session-scoped assistant conversation. Workspace content is full width and has no left sidebar. - Session models own live SSH/SFTP/serial resources and terminal/file-browser state. -- Command Generator's tool-free model request service and validated context live in Application and are shared by - both apps. Each platform session model owns prompt/draft capture, stale-generation cancellation, and command - insertion because those operations belong to its live terminal. Generated commands are bracketed-pasted into the - current edit buffer and are never submitted automatically. +- Command Generator's tool-free model request service, draft protocol and stale-generation lifecycle live in + Application and are shared by both apps. Each platform session supplies its live terminal context and insertion + closure. Generated commands are bracketed-pasted into the current edit buffer and are never submitted automatically. +- `TerminalCommandDraftController` owns the shared draft request protocol, timeout, + cancellation and bracketed-paste payload. `CommandGenerationCoordinator` owns + the shared model timeout, stale-snapshot gates and transient presentation state; + platform sessions provide only their live context, validity check and insertion closure. - A mobile SSH session delivers Shell Integration and transport-exit callbacks through one session-scoped ordered consumer on MainActor. Reconnect, background expiration, and explicit close terminate that consumer; callbacks from an older connection generation cannot reorder prompt state or mutate its replacement session. Authenticated integration installation and command-finished events are prompt-boundary readiness evidence on mobile; the following prompt marker is idempotent, so losing or duplicating it cannot strand or cancel Command Generator. +- SSH, SFTP, and mobile Workspaces share `RemoteSessionConnectionController` for + first-connect and recovery state, retry timing, Journey ownership, and host-key + decisions. `SSHShellIntegrationEvent` is a Domain session value emitted by the + Remote Infrastructure transport. - A clean mobile SSH shell exit closes its Workspace through the app-owned lifecycle, while transport failures keep the Workspace alive for automatic recovery. Removing the selected Workspace also clears the mobile navigation destination so the UI returns to the Workspace list instead of retaining an ended terminal surface. diff --git a/Memory/MOBILE_ROLLOUT.md b/Memory/MOBILE_ROLLOUT.md index 35a5ad57..8192e804 100644 --- a/Memory/MOBILE_ROLLOUT.md +++ b/Memory/MOBILE_ROLLOUT.md @@ -182,3 +182,18 @@ own `__termind_*` helpers. Mobile also treats authenticated installation and com duplicate prompt markers are idempotent. Seventeen shared bootstrap tests, including a real bash runtime regression, and all six focused mobile Command Generator tests passed. The root-cause build was installed and launched on the connected iPad at 22:30. The affected live bash host then completed Command Generator successfully on that iPad. + +## Layered mobile source and canonical framework names — 2026-09-12 + +TFI now uses the same App/Application/Design/DesignSystem/Features/Infrastructure/Platform/UI layer vocabulary as +TFM. Workspace transport construction moved from the feature model into Infrastructure, mobile layout constants +and interface idiom are centralized, and navigation metadata lives in Shell. The shared connection controller now +owns recovery state for desktop SSH, desktop SFTP, and mobile Workspaces. + +The desktop compatibility forwarding layer was removed. Both apps import canonical framework types directly; +temporary `Shared` names were restored to responsibility-specific names. macOS and iOS simulator builds, +ArchitectureBoundaryTests, and the complete iOS test action pass. + +Terminal draft reads and Command Generator execution now use the same Application controllers on both platforms, +including cancellation, timeouts, stale-context rejection and bracketed-paste insertion. Focused mobile Command +Generator and desktop Broadcast Input/Terminal Find tests pass after the extraction. diff --git a/Memory/REFACTOR.md b/Memory/REFACTOR.md index 359055d8..51ab4c1d 100644 --- a/Memory/REFACTOR.md +++ b/Memory/REFACTOR.md @@ -1087,3 +1087,24 @@ and chat content. Strict formatting and Chinese localization checks passed. ## 2026-09-11 — Shared Assistant execution (in progress) Evidence: MobileAssistantModel registers no tools, while TFM/App/AppModel builds the complete runtime and ConversationModel owns its event and interaction queues. Move the existing model, tool adapters, import and speech services into their owning frameworks, then share runtime composition and conversation commands. Expected reduction: remove the mobile duplicate chat loop and avoid a second implementation of approximately 5,000 lines of Agent state/tool behavior. Verification pending macOS/iOS builds and regression suites. + +## 2026-09-12 — Remove app-target framework forwarding aliases + +- Evidence: `rg '^typealias ' TFM -g '*.swift'` found 224 module-level aliases in 96 files; 86 files contained no behavior beyond imports and aliases. Persistence also exported a second `Shared*` alias vocabulary solely for that forwarding layer. +- Action: delete the pure forwarders, migrate the six desktop convenience extensions to responsibility-named adapter files, use canonical framework types directly, and add an architecture guard that rejects forwarding aliases and public `Shared` namespaces. +- Estimated reduction: 86 source files and 224 app-target alias declarations, plus 20 Persistence compatibility aliases. +- Status: implemented. TFM and TFI builds pass, ArchitectureBoundaryTests pass, and the complete TFI test action passes. + +## 2026-09-12 — Align the mobile target with desktop layers + +- Evidence: TFI placed application models beside its entry point and all presentation in one flat Features directory; transport factories were embedded in `MobileWorkspace` and device-idiom checks were repeated in four views. +- Action: organize TFI under the eight desktop layers, move the workspace connection port and production implementation to Application/Infrastructure, extract mobile design tokens and an injectable interface-idiom environment, and move navigation metadata into Shell. +- Estimated reduction: no intentional feature reduction; the change removes one duplicate reconnect state machine and centralizes four device-idiom branches. +- Status: implemented. Both app builds, ArchitectureBoundaryTests, and the complete TFI test action pass. + +## 2026-09-12 — Share terminal draft and generation lifecycles + +- Evidence: desktop and mobile separately defined the same shell allowlist, draft request/clear escape payloads, two-second draft timeout, thirty-second model timeout, cancellation token and transient success/failure state. +- Action: extract `TerminalCommandDraftController` and `CommandGenerationCoordinator` into Application and make both terminal implementations supply only context, validity and insertion closures. +- Estimated reduction: approximately 190 duplicated lifecycle lines and one duplicate protocol constant set. +- Status: implemented. Mobile Command Generator, desktop Broadcast Input, Terminal Find, and direct draft-controller tests pass. diff --git a/TFI/App/MobileAppDependencies.swift b/TFI/App/MobileAppDependencies.swift new file mode 100644 index 00000000..d034c3e0 --- /dev/null +++ b/TFI/App/MobileAppDependencies.swift @@ -0,0 +1,36 @@ +import Foundation +import TermindApplication + +@MainActor +struct MobileAppDependencies { + var hosts: (any HostRepository)? + var inMemory: Bool + var defaults: UserDefaults? + var background: any MobileBackgroundTaskManaging + var workspaceConnections: MobileWorkspaceConnectionServices + + static func live() -> MobileAppDependencies { + MobileAppDependencies( + hosts: nil, + inMemory: false, + defaults: .standard, + background: MobileBackgroundController(), + workspaceConnections: .live + ) + } + + static func testing( + hosts: (any HostRepository)? = nil, + defaults: UserDefaults? = nil, + background: any MobileBackgroundTaskManaging = MobileBackgroundController(), + workspaceConnections: MobileWorkspaceConnectionServices = .live + ) -> MobileAppDependencies { + MobileAppDependencies( + hosts: hosts, + inMemory: true, + defaults: defaults, + background: background, + workspaceConnections: workspaceConnections + ) + } +} diff --git a/TFI/App/MobileAppModel.swift b/TFI/App/MobileAppModel.swift index 6cb7d856..74b016bb 100644 --- a/TFI/App/MobileAppModel.swift +++ b/TFI/App/MobileAppModel.swift @@ -44,11 +44,27 @@ final class MobileAppModel { private var hasActivated = false private var scenePhases: [UUID: ScenePhase] = [:] - init( + convenience init( hosts: (any HostRepository)? = nil, inMemory: Bool = false, defaults: UserDefaults? = .standard, background: any MobileBackgroundTaskManaging = MobileBackgroundController(), workspaceConnectionServices: MobileWorkspaceConnectionServices = .live ) throws { + try self.init( + dependencies: MobileAppDependencies( + hosts: hosts, + inMemory: inMemory, + defaults: defaults, + background: background, + workspaceConnections: workspaceConnectionServices + )) + } + + init(dependencies: MobileAppDependencies) throws { + let hosts = dependencies.hosts + let inMemory = dependencies.inMemory + let defaults = dependencies.defaults + let background = dependencies.background + let workspaceConnectionServices = dependencies.workspaceConnections self.background = background self.workspaceConnectionServices = workspaceConnectionServices let memoryOnly = inMemory || hosts != nil @@ -74,7 +90,7 @@ final class MobileAppModel { ?? "F6SUNDBBN9.com.akinokaede.termind") let settingsDefaults = memoryOnly ? UserDefaults(suiteName: "termind.tests.\(UUID().uuidString)")! : defaults ?? .standard - SharedSettingsKey.migrateSyncPreferences(in: settingsDefaults) + SettingsKey.migrateSyncPreferences(in: settingsDefaults) let settingsPersistence = SettingsSyncPersistence(context: ModelContext(container), defaults: settingsDefaults) try settingsPersistence.prepare() let aiSecrets = AISecretStore(context: settingsPersistence.context, keychain: secrets) @@ -341,26 +357,3 @@ final class MobileAppModel { } } } - -nonisolated enum MobileDestination: String, CaseIterable, Identifiable { - case vault, metrics, workspaces, assistant, settings - var id: Self { self } - var title: String { - switch self { - case .vault: String(localized: "Vault") - case .assistant: String(localized: "Assistant") - case .workspaces: String(localized: "Workspaces") - case .metrics: String(localized: "Metrics") - case .settings: String(localized: "Settings") - } - } - var symbol: String { - switch self { - case .vault: "server.rack" - case .assistant: "sparkles" - case .workspaces: "terminal" - case .metrics: "gauge.with.dots.needle.33percent" - case .settings: "gearshape" - } - } -} diff --git a/TFI/Features/MobileRootView.swift b/TFI/App/MobileRootView.swift similarity index 98% rename from TFI/Features/MobileRootView.swift rename to TFI/App/MobileRootView.swift index 767ceb25..85607185 100644 --- a/TFI/Features/MobileRootView.swift +++ b/TFI/App/MobileRootView.swift @@ -8,6 +8,7 @@ import struct TermindDomain.Host struct MobileRootView: View { let app: MobileAppModel @Environment(\.scenePhase) private var scenePhase + @Environment(\.mobileInterfaceIdiom) private var interfaceIdiom @State private var sceneID = UUID() @State private var tab: MobileTab = .vault @State private var selection: MobileDestination? = .vault @@ -17,7 +18,7 @@ struct MobileRootView: View { var body: some View { Group { - if UIDevice.current.userInterfaceIdiom != .pad { + if interfaceIdiom != .pad { TabView(selection: $tab) { NavigationStack { vault } .tabItem { Label("Vault", systemImage: "server.rack") }.tag(MobileTab.vault) @@ -246,7 +247,3 @@ struct MobileRootView: View { workspaceID = nil } } - -nonisolated enum MobileTab: String, CaseIterable { - case vault, workspaces, assistant, settings -} diff --git a/TFI/Platform/MobileRuntimeVersions.swift b/TFI/App/MobileRuntimeVersions.swift similarity index 100% rename from TFI/Platform/MobileRuntimeVersions.swift rename to TFI/App/MobileRuntimeVersions.swift diff --git a/TFI/App/TermindIOSApp.swift b/TFI/App/TermindIOSApp.swift index f87f4e9d..8bcb8bce 100644 --- a/TFI/App/TermindIOSApp.swift +++ b/TFI/App/TermindIOSApp.swift @@ -11,7 +11,8 @@ struct TermindIOSApp: App { init() { startup = Result { - try MobileAppModel(inMemory: ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil) + let isTesting = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + return try MobileAppModel(dependencies: isTesting ? .testing() : .live()) } } @@ -21,6 +22,7 @@ struct TermindIOSApp: App { case .success(let app): MobileRootView(app: app) .modelContainer(app.container) + .environment(\.mobileInterfaceIdiom, MobileInterfaceIdiom.current) .environment(\.locale, launchLanguage.locale) .preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil) case .failure: diff --git a/TFI/App/MobileAssistantModel.swift b/TFI/Application/Agent/MobileAssistantModel.swift similarity index 100% rename from TFI/App/MobileAssistantModel.swift rename to TFI/Application/Agent/MobileAssistantModel.swift diff --git a/TFI/Application/Connections/MobileWorkspaceConnectionServices.swift b/TFI/Application/Connections/MobileWorkspaceConnectionServices.swift new file mode 100644 index 00000000..96aff42c --- /dev/null +++ b/TFI/Application/Connections/MobileWorkspaceConnectionServices.swift @@ -0,0 +1,28 @@ +import Foundation +import TermindApplication +import TermindDomain +import TermindRemoteInfrastructure + +import struct TermindDomain.Host + +@MainActor +struct MobileWorkspaceConnectionServices { + var discoverHostKey: + (Host.ID, String, Int, SSHTransportProxy?, [SSHRouteHopConfiguration]) async throws -> SSHHostKeyObservation + var makeSSHSession: + ( + @escaping SSHSessionActor.OutputHandler, @escaping SSHSessionActor.ExitHandler, + @escaping SSHSessionActor.ShellIntegrationHandler + ) -> SSHSessionActor + var connectSSH: + ( + SSHSessionActor, SSHConnectionConfiguration, Bool, UInt16, UInt16, + SSHRouteProgressHandler? + ) async throws -> Void + var connectSFTP: + ( + SSHConnectionConfiguration, SSHRouteProgressHandler?, + (@Sendable (String) -> Void)? + ) async throws -> any SFTPClientProtocol + var writeSSH: (SSHSessionActor, Data) async -> Void +} diff --git a/TFI/App/MobileKeyEditorModel.swift b/TFI/Application/FeatureModels/MobileKeyEditorModel.swift similarity index 100% rename from TFI/App/MobileKeyEditorModel.swift rename to TFI/Application/FeatureModels/MobileKeyEditorModel.swift diff --git a/TFI/App/MobileMCPEditorModel.swift b/TFI/Application/FeatureModels/MobileMCPEditorModel.swift similarity index 100% rename from TFI/App/MobileMCPEditorModel.swift rename to TFI/Application/FeatureModels/MobileMCPEditorModel.swift diff --git a/TFI/App/MobileModelDiscovery.swift b/TFI/Application/FeatureModels/MobileModelDiscovery.swift similarity index 100% rename from TFI/App/MobileModelDiscovery.swift rename to TFI/Application/FeatureModels/MobileModelDiscovery.swift diff --git a/TFI/App/MobileModelTests.swift b/TFI/Application/FeatureModels/MobileModelTests.swift similarity index 100% rename from TFI/App/MobileModelTests.swift rename to TFI/Application/FeatureModels/MobileModelTests.swift diff --git a/TFI/App/MobileProviderEditorModel.swift b/TFI/Application/FeatureModels/MobileProviderEditorModel.swift similarity index 100% rename from TFI/App/MobileProviderEditorModel.swift rename to TFI/Application/FeatureModels/MobileProviderEditorModel.swift diff --git a/TFI/App/MobileSecretDraft.swift b/TFI/Application/FeatureModels/MobileSecretDraft.swift similarity index 100% rename from TFI/App/MobileSecretDraft.swift rename to TFI/Application/FeatureModels/MobileSecretDraft.swift diff --git a/TFI/App/MobileSkillEditorModel.swift b/TFI/Application/FeatureModels/MobileSkillEditorModel.swift similarity index 100% rename from TFI/App/MobileSkillEditorModel.swift rename to TFI/Application/FeatureModels/MobileSkillEditorModel.swift diff --git a/TFI/App/MobileMetricsModel.swift b/TFI/Application/Metrics/MobileMetricsModel.swift similarity index 100% rename from TFI/App/MobileMetricsModel.swift rename to TFI/Application/Metrics/MobileMetricsModel.swift diff --git a/TFI/Design/MobileDesignTokens.swift b/TFI/Design/MobileDesignTokens.swift new file mode 100644 index 00000000..cfc60538 --- /dev/null +++ b/TFI/Design/MobileDesignTokens.swift @@ -0,0 +1,16 @@ +import CoreGraphics + +/// Mobile layout values shared by managed collection surfaces. +nonisolated enum MobileCardLayout { + static let minimumHeight: CGFloat = 104 + static let maximumVisibleTags = 2 + + static func columns(width: CGFloat, isPad: Bool, accessibilitySize: Bool) -> Int { + guard isPad, !accessibilitySize else { return 1 } + return max(1, Int((max(0, width) + 12) / 332)) + } + + static func visibleTags(_ tags: [String]) -> ArraySlice { + tags.prefix(maximumVisibleTags) + } +} diff --git a/TFI/DesignSystem/MobileInterfaceEnvironment.swift b/TFI/DesignSystem/MobileInterfaceEnvironment.swift new file mode 100644 index 00000000..13a06161 --- /dev/null +++ b/TFI/DesignSystem/MobileInterfaceEnvironment.swift @@ -0,0 +1,22 @@ +import SwiftUI +import UIKit + +nonisolated enum MobileInterfaceIdiom: Sendable { + case phone + case pad + + @MainActor static var current: Self { + UIDevice.current.userInterfaceIdiom == .pad ? .pad : .phone + } +} + +private struct MobileInterfaceIdiomKey: EnvironmentKey { + static let defaultValue = MobileInterfaceIdiom.phone +} + +extension EnvironmentValues { + var mobileInterfaceIdiom: MobileInterfaceIdiom { + get { self[MobileInterfaceIdiomKey.self] } + set { self[MobileInterfaceIdiomKey.self] = newValue } + } +} diff --git a/TFI/Features/MobileAssistantComposer.swift b/TFI/Features/AI/MobileAssistantComposer.swift similarity index 100% rename from TFI/Features/MobileAssistantComposer.swift rename to TFI/Features/AI/MobileAssistantComposer.swift diff --git a/TFI/Features/MobileAssistantConversation.swift b/TFI/Features/AI/MobileAssistantConversation.swift similarity index 100% rename from TFI/Features/MobileAssistantConversation.swift rename to TFI/Features/AI/MobileAssistantConversation.swift diff --git a/TFI/Features/MobileAssistantInteraction.swift b/TFI/Features/AI/MobileAssistantInteraction.swift similarity index 94% rename from TFI/Features/MobileAssistantInteraction.swift rename to TFI/Features/AI/MobileAssistantInteraction.swift index aebf2c2b..155ddbf9 100644 --- a/TFI/Features/MobileAssistantInteraction.swift +++ b/TFI/Features/AI/MobileAssistantInteraction.swift @@ -33,9 +33,9 @@ struct MobileAssistantInteraction: View { ScrollView { switch interaction { case .sshAuthentication(let request): - SharedMobileAuthentication(request: request, conversation: conversation, hosts: hosts) + MobileAssistantAuthenticationForm(request: request, conversation: conversation, hosts: hosts) case .sshHostKey(let request): - SharedMobileHostKey(request: request, conversation: conversation) + MobileAssistantHostKeyForm(request: request, conversation: conversation) case .questions: questions } @@ -104,12 +104,12 @@ struct MobileAssistantInteraction: View { } } -private struct SharedMobileAuthentication: View { +private struct MobileAssistantAuthenticationForm: View { let request: AgentSSHAuthenticationRequest let conversation: ConversationModel let hosts: HostLibraryModel var body: some View { - SharedSSHAuthenticationForm( + SSHAuthenticationForm( prompt: request.journeyPrompt, submit: { identity, username, password, passphrase, key, save in conversation.respondToSSHAuthentication( @@ -124,11 +124,11 @@ private struct SharedMobileAuthentication: View { } } -private struct SharedMobileHostKey: View { +private struct MobileAssistantHostKeyForm: View { let request: AgentSSHHostKeyRequest let conversation: ConversationModel var body: some View { - SharedSSHHostKeyConfirmationForm( + SSHHostKeyConfirmationForm( prompt: request.prompt, displayAddress: request.address, proceed: { conversation.respondToSSHHostKey(.continueOnce) }, trust: request.allowsPersistentTrust ? { conversation.respondToSSHHostKey(.trustAndContinue) } : nil, diff --git a/TFI/Features/MobileSlashCommandPanel.swift b/TFI/Features/AI/MobileSlashCommandPanel.swift similarity index 100% rename from TFI/Features/MobileSlashCommandPanel.swift rename to TFI/Features/AI/MobileSlashCommandPanel.swift diff --git a/TFI/Features/MobileAssistantView.swift b/TFI/Features/Assistant/MobileAssistantView.swift similarity index 98% rename from TFI/Features/MobileAssistantView.swift rename to TFI/Features/Assistant/MobileAssistantView.swift index 833a2227..bc800b0d 100644 --- a/TFI/Features/MobileAssistantView.swift +++ b/TFI/Features/Assistant/MobileAssistantView.swift @@ -6,11 +6,12 @@ import TermindDomain struct MobileAssistantView: View { let app: MobileAppModel @Bindable var model: MobileAssistantModel + @Environment(\.mobileInterfaceIdiom) private var interfaceIdiom @State private var history = false var body: some View { GeometryReader { geometry in - if UIDevice.current.userInterfaceIdiom == .pad && geometry.size.width >= 760 { + if interfaceIdiom == .pad && geometry.size.width >= 760 { HStack(spacing: 0) { MobileAssistantLibrary(model: model, selected: {}) .frame(width: 280) diff --git a/TFI/Features/MobileProView.swift b/TFI/Features/Commerce/MobileProView.swift similarity index 100% rename from TFI/Features/MobileProView.swift rename to TFI/Features/Commerce/MobileProView.swift diff --git a/TFI/Features/MobileConnectionPicker.swift b/TFI/Features/HostChooser/MobileConnectionPicker.swift similarity index 100% rename from TFI/Features/MobileConnectionPicker.swift rename to TFI/Features/HostChooser/MobileConnectionPicker.swift diff --git a/TFI/Features/MobileHostEditor.swift b/TFI/Features/HostEditor/MobileHostEditor.swift similarity index 100% rename from TFI/Features/MobileHostEditor.swift rename to TFI/Features/HostEditor/MobileHostEditor.swift diff --git a/TFI/Features/MobileCredentialsView.swift b/TFI/Features/Keychain/MobileCredentialsView.swift similarity index 100% rename from TFI/Features/MobileCredentialsView.swift rename to TFI/Features/Keychain/MobileCredentialsView.swift diff --git a/TFI/Features/MobileKeyEditor.swift b/TFI/Features/Keychain/MobileKeyEditor.swift similarity index 100% rename from TFI/Features/MobileKeyEditor.swift rename to TFI/Features/Keychain/MobileKeyEditor.swift diff --git a/TFI/Features/MobileKnownHostsView.swift b/TFI/Features/KnownHosts/MobileKnownHostsView.swift similarity index 100% rename from TFI/Features/MobileKnownHostsView.swift rename to TFI/Features/KnownHosts/MobileKnownHostsView.swift diff --git a/TFI/Features/MobileMetricsView.swift b/TFI/Features/Metrics/MobileMetricsView.swift similarity index 100% rename from TFI/Features/MobileMetricsView.swift rename to TFI/Features/Metrics/MobileMetricsView.swift diff --git a/TFI/Features/MobileForwardingEditor.swift b/TFI/Features/Ports/MobileForwardingEditor.swift similarity index 100% rename from TFI/Features/MobileForwardingEditor.swift rename to TFI/Features/Ports/MobileForwardingEditor.swift diff --git a/TFI/Features/MobileRemoteFileEditor.swift b/TFI/Features/SFTP/MobileRemoteFileEditor.swift similarity index 100% rename from TFI/Features/MobileRemoteFileEditor.swift rename to TFI/Features/SFTP/MobileRemoteFileEditor.swift diff --git a/TFI/Features/MobileAISettingsView.swift b/TFI/Features/Settings/MobileAISettingsView.swift similarity index 100% rename from TFI/Features/MobileAISettingsView.swift rename to TFI/Features/Settings/MobileAISettingsView.swift diff --git a/TFI/Features/MobileAppSettingsView.swift b/TFI/Features/Settings/MobileAppSettingsView.swift similarity index 95% rename from TFI/Features/MobileAppSettingsView.swift rename to TFI/Features/Settings/MobileAppSettingsView.swift index 9e12caed..27565d9b 100644 --- a/TFI/Features/MobileAppSettingsView.swift +++ b/TFI/Features/Settings/MobileAppSettingsView.swift @@ -3,7 +3,7 @@ import TermindApplication struct MobileAppSettingsView: View { @AppStorage("mobile.appearance") private var appearance = "system" - @AppStorage(SharedSettingsKey.appLanguage) private var language = AppLanguage.system.rawValue + @AppStorage(SettingsKey.appLanguage) private var language = AppLanguage.system.rawValue @State private var launchLanguage = AppLanguage.stored() var body: some View { diff --git a/TFI/Features/MobileMCPSettings.swift b/TFI/Features/Settings/MobileMCPSettings.swift similarity index 100% rename from TFI/Features/MobileMCPSettings.swift rename to TFI/Features/Settings/MobileMCPSettings.swift diff --git a/TFI/Features/MobileModelFields.swift b/TFI/Features/Settings/MobileModelFields.swift similarity index 100% rename from TFI/Features/MobileModelFields.swift rename to TFI/Features/Settings/MobileModelFields.swift diff --git a/TFI/Features/MobileSFTPSettingsView.swift b/TFI/Features/Settings/MobileSFTPSettingsView.swift similarity index 76% rename from TFI/Features/MobileSFTPSettingsView.swift rename to TFI/Features/Settings/MobileSFTPSettingsView.swift index 7edbf759..34f9bacb 100644 --- a/TFI/Features/MobileSFTPSettingsView.swift +++ b/TFI/Features/Settings/MobileSFTPSettingsView.swift @@ -8,25 +8,25 @@ nonisolated struct MobileSFTPTransferLimits: Equatable, Sendable { init(global: Int, perHost: Int) { self.global = min( - max(global, SharedSettingsKey.sftpTransferLimitRange.lowerBound), - SharedSettingsKey.sftpTransferLimitRange.upperBound + max(global, SettingsKey.sftpTransferLimitRange.lowerBound), + SettingsKey.sftpTransferLimitRange.upperBound ) self.perHost = min( - max(perHost, SharedSettingsKey.sftpTransferLimitRange.lowerBound), self.global) + max(perHost, SettingsKey.sftpTransferLimitRange.lowerBound), self.global) } } struct MobileSFTPSettingsView: View { let transfers: SFTPTransferCoordinator - @AppStorage(SharedSettingsKey.sftpGlobalTransferLimit) private var globalLimit = SharedSettingsKey + @AppStorage(SettingsKey.sftpGlobalTransferLimit) private var globalLimit = SettingsKey .defaultSFTPGlobalTransferLimit - @AppStorage(SharedSettingsKey.sftpPerHostTransferLimit) private var perHostLimit = SharedSettingsKey + @AppStorage(SettingsKey.sftpPerHostTransferLimit) private var perHostLimit = SettingsKey .defaultSFTPPerHostTransferLimit var body: some View { Form { Section("SFTP") { - Stepper(value: $globalLimit, in: SharedSettingsKey.sftpTransferLimitRange) { + Stepper(value: $globalLimit, in: SettingsKey.sftpTransferLimitRange) { LabeledContent("Concurrent Transfers") { Text(globalLimit, format: .number).monospacedDigit() } @@ -50,7 +50,7 @@ struct MobileSFTPSettingsView: View { } private var perHostRange: ClosedRange { - let minimum = SharedSettingsKey.sftpTransferLimitRange.lowerBound + let minimum = SettingsKey.sftpTransferLimitRange.lowerBound return minimum...max(globalLimit, minimum) } diff --git a/TFI/Features/MobileSettingsView.swift b/TFI/Features/Settings/MobileSettingsView.swift similarity index 100% rename from TFI/Features/MobileSettingsView.swift rename to TFI/Features/Settings/MobileSettingsView.swift diff --git a/TFI/Features/MobileSkillSettings.swift b/TFI/Features/Settings/MobileSkillSettings.swift similarity index 100% rename from TFI/Features/MobileSkillSettings.swift rename to TFI/Features/Settings/MobileSkillSettings.swift diff --git a/TFI/Features/MobileSyncSettingsView.swift b/TFI/Features/Settings/MobileSyncSettingsView.swift similarity index 86% rename from TFI/Features/MobileSyncSettingsView.swift rename to TFI/Features/Settings/MobileSyncSettingsView.swift index 56f36bf8..642d3a53 100644 --- a/TFI/Features/MobileSyncSettingsView.swift +++ b/TFI/Features/Settings/MobileSyncSettingsView.swift @@ -4,19 +4,19 @@ import TermindDomain struct MobileSyncSettingsView: View { let app: MobileAppModel - @AppStorage(SharedSettingsKey.iCloudSyncEnabled) private var enabled = true - @AppStorage(SharedSettingsKey.vaultSyncEnabled) private var vault = true - @AppStorage(SharedSettingsKey.settingsSyncEnabled) private var settings = false + @AppStorage(SettingsKey.iCloudSyncEnabled) private var enabled = true + @AppStorage(SettingsKey.vaultSyncEnabled) private var vault = true + @AppStorage(SettingsKey.settingsSyncEnabled) private var settings = false var body: some View { Form { Section { Toggle( "iCloud Sync", - isOn: selection(enabled && app.pro.canUse(.vaultSync), SharedSettingsKey.iCloudSyncEnabled)) + isOn: selection(enabled && app.pro.canUse(.vaultSync), SettingsKey.iCloudSyncEnabled)) } Section { - Toggle("Vault", isOn: selection(vault, SharedSettingsKey.vaultSyncEnabled)) + Toggle("Vault", isOn: selection(vault, SettingsKey.vaultSyncEnabled)) if vault { status( app.vaultSync?.status, retry: { app.vaultSync?.retry() }, @@ -26,7 +26,7 @@ struct MobileSyncSettingsView: View { .secondary) }.disabled(!enabled || !app.pro.canUse(.vaultSync)) Section { - Toggle("Settings", isOn: selection(settings, SharedSettingsKey.settingsSyncEnabled)) + Toggle("Settings", isOn: selection(settings, SettingsKey.settingsSyncEnabled)) if settings { status( app.settingsSync?.status, retry: { app.settingsSync?.retry() }, diff --git a/TFI/Features/MobileTerminalSettingsView.swift b/TFI/Features/Settings/MobileTerminalSettingsView.swift similarity index 100% rename from TFI/Features/MobileTerminalSettingsView.swift rename to TFI/Features/Settings/MobileTerminalSettingsView.swift diff --git a/TFI/Features/Shell/MobileNavigation.swift b/TFI/Features/Shell/MobileNavigation.swift new file mode 100644 index 00000000..f5c94d41 --- /dev/null +++ b/TFI/Features/Shell/MobileNavigation.swift @@ -0,0 +1,31 @@ +import Foundation + +nonisolated enum MobileDestination: String, CaseIterable, Identifiable { + case vault, metrics, workspaces, assistant, settings + + var id: Self { self } + + var title: String { + switch self { + case .vault: String(localized: "Vault") + case .assistant: String(localized: "Assistant") + case .workspaces: String(localized: "Workspaces") + case .metrics: String(localized: "Metrics") + case .settings: String(localized: "Settings") + } + } + + var symbol: String { + switch self { + case .vault: "server.rack" + case .assistant: "sparkles" + case .workspaces: "terminal" + case .metrics: "gauge.with.dots.needle.33percent" + case .settings: "gearshape" + } + } +} + +nonisolated enum MobileTab: String, CaseIterable { + case vault, workspaces, assistant, settings +} diff --git a/TFI/Features/MobileSnippetsView.swift b/TFI/Features/Snippets/MobileSnippetsView.swift similarity index 100% rename from TFI/Features/MobileSnippetsView.swift rename to TFI/Features/Snippets/MobileSnippetsView.swift diff --git a/TFI/Features/MobileCommandHistoryView.swift b/TFI/Features/Terminal/MobileCommandHistoryView.swift similarity index 100% rename from TFI/Features/MobileCommandHistoryView.swift rename to TFI/Features/Terminal/MobileCommandHistoryView.swift diff --git a/TFI/Features/MobileTerminalKeyboard.swift b/TFI/Features/Terminal/MobileTerminalKeyboard.swift similarity index 100% rename from TFI/Features/MobileTerminalKeyboard.swift rename to TFI/Features/Terminal/MobileTerminalKeyboard.swift diff --git a/TFI/Features/MobileTransfersView.swift b/TFI/Features/Transfers/MobileTransfersView.swift similarity index 100% rename from TFI/Features/MobileTransfersView.swift rename to TFI/Features/Transfers/MobileTransfersView.swift diff --git a/TFI/Features/MobileVaultView.swift b/TFI/Features/Vault/MobileVaultView.swift similarity index 100% rename from TFI/Features/MobileVaultView.swift rename to TFI/Features/Vault/MobileVaultView.swift diff --git a/TFI/App/MobileWorkspace.swift b/TFI/Features/Workspaces/MobileWorkspace.swift similarity index 74% rename from TFI/App/MobileWorkspace.swift rename to TFI/Features/Workspaces/MobileWorkspace.swift index fe638f84..363a71f9 100644 --- a/TFI/App/MobileWorkspace.swift +++ b/TFI/Features/Workspaces/MobileWorkspace.swift @@ -10,61 +10,6 @@ import TermindRemoteInfrastructure import struct TermindDomain.Host -@MainActor -struct MobileWorkspaceConnectionServices { - var discoverHostKey: - (Host.ID, String, Int, SSHTransportProxy?, [SSHRouteHopConfiguration]) async throws -> SSHHostKeyObservation - var makeSSHSession: - ( - @escaping SSHSessionActor.OutputHandler, @escaping SSHSessionActor.ExitHandler, - @escaping SSHSessionActor.ShellIntegrationHandler - ) -> SSHSessionActor - var connectSSH: - ( - SSHSessionActor, SSHConnectionConfiguration, Bool, UInt16, UInt16, - SSHRouteProgressHandler? - ) async throws -> Void - var connectSFTP: - ( - SSHConnectionConfiguration, SSHRouteProgressHandler?, - (@Sendable (String) -> Void)? - ) async throws -> any SFTPClientProtocol - var writeSSH: (SSHSessionActor, Data) async -> Void - - static var live: MobileWorkspaceConnectionServices { - let coordinator = RemoteConnectionCoordinator() - return MobileWorkspaceConnectionServices( - discoverHostKey: { hostID, host, port, proxy, jumps in - try await SSHSessionActor.discoverHostKey( - hostID: hostID, host: host, port: port, proxy: proxy, jumps: jumps) - }, - makeSSHSession: { output, exit, shellIntegration in - coordinator.makeSSHSession(output: output, exit: exit, shellIntegration: shellIntegration) - }, - connectSSH: { session, configuration, shellIntegrationEnabled, columns, rows, progress in - try await session.connect( - hostID: configuration.hostID, - host: configuration.host, - port: configuration.port, - username: configuration.username, - credentials: configuration.credentials, - expectedHostKeys: configuration.expectedHostKeys, - proxy: configuration.proxy, - jumps: configuration.jumps, - shellIntegrationEnabled: shellIntegrationEnabled, - columns: columns, - rows: rows, - progress: progress - ) - }, - connectSFTP: { configuration, progress, connectionFailure in - try await coordinator.connectSFTP( - using: configuration, progress: progress, connectionFailure: connectionFailure) - }, - writeSSH: { session, data in await session.write(data) }) - } -} - private enum MobileWorkspaceConnectionResumeIntent: Equatable { case none case whenOpened @@ -76,11 +21,6 @@ private nonisolated enum MobileSSHSessionEvent: Sendable { case shellIntegration(SSHShellIntegrationEvent) } -private struct MobilePendingDraftRequest { - var id: UUID - var continuation: CheckedContinuation -} - nonisolated enum MobileUploadNotice: Equatable, Sendable { case preparing(String) case queued(Int, String) @@ -115,8 +55,8 @@ final class MobileWorkspace: Identifiable { } } private(set) var busy = false - var reconnectState: ReconnectState? { reconnect.state } - let journey = ConnectionJourney() + var reconnectState: ReconnectState? { connection.reconnectState } + var journey: ConnectionJourney { connection.journey } var operationError: String? private var connectionFailure: String? private var pendingHostKeyHostID: UUID? @@ -126,7 +66,10 @@ final class MobileWorkspace: Identifiable { var authenticationRequired = false var username = "" private var authenticationHostID: UUID? - private var temporaryKeys: [UUID: SSHHostKeyIdentity] = [:] + private var temporaryKeys: [UUID: SSHHostKeyIdentity] { + get { connection.temporaryHostKeys } + set { connection.temporaryHostKeys = newValue } + } private var submissions: [UUID: SSHAuthenticationSubmission] = [:] var path = "/" private(set) var files: [RemoteFile] = [] @@ -146,16 +89,13 @@ final class MobileWorkspace: Identifiable { private var shellIntegrationAvailable = false private(set) var pendingInitialCommand: String? private(set) var workingDirectory: String? - private(set) var commandGeneratorState: CommandGeneratorState = .idle + var commandGeneratorState: CommandGeneratorState { commandGeneration.state } private var integratedShell: String? private var remoteSystemInfo: RemoteSystemInfo? private var isShellPromptReady = false private var commandGeneratorRevision: UInt64 = 0 - private var commandGenerationSequence: UInt64 = 0 - private var commandGeneratorTask: Task? - private var commandGeneratorDismissTask: Task? - private var pendingDraftRequest: MobilePendingDraftRequest? - private var draftRequestTimeoutTask: Task? + private let commandGeneration = CommandGenerationCoordinator() + private let commandDraft = TerminalCommandDraftController() private var workingDirectoryObservation: AnyCancellable? private let surfaceOwner = MobileTerminalSurface() let terminal: TerminalViewState @@ -168,7 +108,7 @@ final class MobileWorkspace: Identifiable { private var generation = UUID() private let hosts: any RemoteSessionHostStore private let connectionServices: MobileWorkspaceConnectionServices - private let reconnect: MobileWorkspaceReconnectState + private let connection: RemoteSessionConnectionController private var viewport = InMemoryTerminalViewport(columns: 80, rows: 24) var uploadConflictCount: Int { pendingUpload?.batch.conflictCount ?? 0 } @@ -225,7 +165,7 @@ final class MobileWorkspace: Identifiable { for: hosts.host(descriptor.hostID)?.detectedDistribution)?.asset) pendingInitialCommand = initialCommand self.connectionServices = connectionServices - reconnect = MobileWorkspaceReconnectState(controller: reconnectController) + connection = RemoteSessionConnectionController(reconnect: reconnectController) connectionResumeIntent = connectsWhenOpened ? .whenOpened : .none let relay = MobileTerminalRelay() backend = InMemoryTerminalSession( @@ -318,22 +258,24 @@ final class MobileWorkspace: Identifiable { func presentFind() { let wasOpen = isFindPresented isFindPresented = true - if !wasOpen && !findQuery.isEmpty { _ = terminal.performBindingAction("search:\(findQuery)") } + if !wasOpen && !findQuery.isEmpty { + _ = terminal.performBindingAction(TerminalFindBinding.search(findQuery)) + } } func updateFindQuery(_ query: String) { findQuery = query - _ = terminal.performBindingAction("search:\(query)") + _ = terminal.performBindingAction(TerminalFindBinding.search(query)) } - func navigateFind(previous: Bool) { + func navigateFind(_ direction: TerminalFindDirection) { presentFind() guard !findQuery.isEmpty else { return } - _ = terminal.performBindingAction(previous ? "navigate_search:previous" : "navigate_search:next") + _ = terminal.performBindingAction(TerminalFindBinding.navigate(direction)) } func dismissFind() { - _ = terminal.performBindingAction("end_search") + _ = terminal.performBindingAction(TerminalFindBinding.end) isFindPresented = false terminal.requestFocus() } @@ -345,7 +287,7 @@ final class MobileWorkspace: Identifiable { } func generateCommand(using generator: any CommandGenerating) { - if commandGeneratorTask != nil { + if commandGeneration.isRunning { cancelCommandGeneration() return } @@ -365,81 +307,38 @@ final class MobileWorkspace: Identifiable { return } - commandGeneratorDismissTask?.cancel() - commandGenerationSequence &+= 1 - let sequence = commandGenerationSequence let connectionGeneration = generation let revision = commandGeneratorRevision - commandGeneratorState = .readingDraft - commandGeneratorTask = Task { [weak self, generator] in - do { - guard let self else { return } + let includesSystemInfo = terminalSettings.preferences.shellIntegration + commandGeneration.start( + targetCount: 1, + readContext: { [weak self] in + guard let self else { throw CancellationError() } let draft = try await requestCurrentDraft() - try Task.checkCancellation() - guard !draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - throw MobileCommandGenerationError.emptyDraft - } - guard - commandGenerationSnapshotIsCurrent( - sequence: sequence, - connectionGeneration: connectionGeneration, - revision: revision, - shell: integratedShell, - workingDirectory: workingDirectory - ) - else { throw CancellationError() } - - commandGeneratorState = .generating - let context = CommandGeneratorContext( + return CommandGeneratorContext( draft: draft, shell: integratedShell, cwd: workingDirectory, - remoteSystemInfo: terminalSettings.preferences.shellIntegration - ? remoteSystemInfo : nil + remoteSystemInfo: includesSystemInfo ? remoteSystemInfo : nil ) - let command = try await mobileCommandGenerationWithTimeout { - try await generator.generate(context) - } - try Task.checkCancellation() - guard - commandGenerationSnapshotIsCurrent( - sequence: sequence, - connectionGeneration: connectionGeneration, - revision: revision, - shell: integratedShell, - workingDirectory: workingDirectory - ) - else { throw CancellationError() } - - await replaceCurrentDraft(with: command) - finishCommandGeneration( - sequence: sequence, - state: .inserted(targetCount: 1), - dismissAfter: .seconds(2) - ) - } catch is CancellationError { - self?.finishCancelledCommandGeneration(sequence: sequence) - } catch { - self?.finishCommandGeneration( - sequence: sequence, - state: .failed(error.localizedDescription), - dismissAfter: .seconds(3) - ) - } - } + }, + isCurrent: { [weak self] in + self?.commandGenerationSnapshotIsCurrent( + connectionGeneration: connectionGeneration, + revision: revision, + shell: integratedShell, + workingDirectory: workingDirectory + ) == true + }, + generate: { try await generator.generate($0) }, + insert: { [weak self] command in await self?.replaceCurrentDraft(with: command) } + ) } func cancelCommandGeneration() { - guard commandGeneratorTask != nil || pendingDraftRequest != nil else { return } - commandGenerationSequence &+= 1 - commandGeneratorTask?.cancel() - commandGeneratorTask = nil - commandGeneratorDismissTask?.cancel() - commandGeneratorDismissTask = nil - if let request = pendingDraftRequest { - failDraftRequest(id: request.id, with: CancellationError()) - } - commandGeneratorState = .idle + guard commandGeneration.isRunning || commandDraft.hasPendingRequest else { return } + commandGeneration.cancel() + commandDraft.cancel(CancellationError()) } func receiveWorkingDirectory(_ path: String?) { @@ -452,7 +351,7 @@ final class MobileWorkspace: Identifiable { var canGenerateFromCurrentDraft: Bool { connected && shellIntegrationAvailable && isShellPromptReady - && Self.supportedGeneratorShells.contains(integratedShell ?? "") + && TerminalCommandDraftController.supportedShells.contains(integratedShell ?? "") } func receiveInteractiveTerminalInput(_ data: Data) async { @@ -466,70 +365,23 @@ final class MobileWorkspace: Identifiable { } private func requestCurrentDraft() async throws -> String { - guard canGenerateFromCurrentDraft else { throw MobileCommandDraftError.unavailable } - guard pendingDraftRequest == nil else { throw MobileCommandDraftError.alreadyPending } - let requestID = UUID() - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { - (continuation: CheckedContinuation) in - pendingDraftRequest = MobilePendingDraftRequest( - id: requestID, - continuation: continuation - ) - Task { [weak self] in - await self?.writeSSHDirectly(Self.commandGeneratorDraftRequest) - } - draftRequestTimeoutTask?.cancel() - draftRequestTimeoutTask = Task { [weak self] in - try? await Task.sleep(for: .seconds(2)) - guard !Task.isCancelled else { return } - self?.failDraftRequest( - id: requestID, - with: MobileCommandDraftError.timedOut - ) - } - } - } onCancel: { - Task { @MainActor [weak self] in - self?.failDraftRequest(id: requestID, with: CancellationError()) - } + try await commandDraft.read(isAvailable: canGenerateFromCurrentDraft) { [weak self] data in + await self?.writeSSHDirectly(data) } } private func replaceCurrentDraft(with command: String) async { - var data = Self.commandGeneratorDraftClear - data.append(Self.bracketedPasteStart) - data.append(Data(command.utf8)) - data.append(Self.bracketedPasteEnd) - await writeSSHDirectly(data) + await writeSSHDirectly(commandDraft.replacementPayload(for: command)) terminal.requestFocus() } - private func failDraftRequest(id: UUID, with error: any Error) { - guard pendingDraftRequest?.id == id, let request = pendingDraftRequest else { return } - pendingDraftRequest = nil - draftRequestTimeoutTask?.cancel() - draftRequestTimeoutTask = nil - request.continuation.resume(throwing: error) - } - - private func completeDraftRequest(with draft: String) { - guard let request = pendingDraftRequest else { return } - pendingDraftRequest = nil - draftRequestTimeoutTask?.cancel() - draftRequestTimeoutTask = nil - request.continuation.resume(returning: draft) - } - private func commandGenerationSnapshotIsCurrent( - sequence: UInt64, connectionGeneration: UUID, revision: UInt64, shell: String, workingDirectory: String ) -> Bool { - commandGenerationSequence == sequence - && generation == connectionGeneration + generation == connectionGeneration && commandGeneratorRevision == revision && integratedShell == shell && self.workingDirectory == workingDirectory @@ -538,7 +390,7 @@ final class MobileWorkspace: Identifiable { private func invalidateCommandGeneration() { commandGeneratorRevision &+= 1 - if commandGeneratorTask != nil { cancelCommandGeneration() } + if commandGeneration.isRunning { cancelCommandGeneration() } } private func resetCommandGeneratorShellState() { @@ -548,53 +400,12 @@ final class MobileWorkspace: Identifiable { isShellPromptReady = false shellIntegrationAvailable = false workingDirectory = nil - if let request = pendingDraftRequest { - failDraftRequest(id: request.id, with: MobileCommandDraftError.unavailable) - } + commandDraft.cancel() commandGeneratorRevision &+= 1 } - private func commandGenerationDidFinish(sequence: UInt64) -> Bool { - guard commandGenerationSequence == sequence else { return false } - commandGeneratorTask = nil - return true - } - - private func finishCancelledCommandGeneration(sequence: UInt64) { - guard commandGenerationDidFinish(sequence: sequence) else { return } - commandGeneratorState = .idle - } - - private func finishCommandGeneration( - sequence: UInt64, - state: CommandGeneratorState, - dismissAfter: Duration - ) { - guard commandGenerationDidFinish(sequence: sequence) else { return } - commandGeneratorState = state - scheduleCommandGenerationDismiss(sequence: sequence, after: dismissAfter) - } - private func showCommandGenerationFailure(_ message: String) { - commandGenerationSequence &+= 1 - commandGeneratorTask?.cancel() - commandGeneratorTask = nil - commandGeneratorDismissTask?.cancel() - commandGeneratorState = .failed(message) - scheduleCommandGenerationDismiss( - sequence: commandGenerationSequence, - after: .seconds(3) - ) - } - - private func scheduleCommandGenerationDismiss(sequence: UInt64, after delay: Duration) { - commandGeneratorDismissTask?.cancel() - commandGeneratorDismissTask = Task { [weak self] in - try? await Task.sleep(for: delay) - guard !Task.isCancelled, self?.commandGenerationSequence == sequence else { return } - self?.commandGeneratorState = .idle - self?.commandGeneratorDismissTask = nil - } + commandGeneration.fail(message) } func receiveShellEvent(_ event: SSHShellIntegrationEvent) { @@ -642,7 +453,7 @@ final class MobileWorkspace: Identifiable { for: id, hostID: descriptor.hostID, exitStatus: status, duration: duration) activeHistoryID = nil case .draft(let draft): - completeDraftRequest(with: draft) + commandDraft.receive(draft) } } @@ -650,7 +461,7 @@ final class MobileWorkspace: Identifiable { guard !busy, !connected else { return } cancelPendingUpload() stopSSHEventConsumer() - let reconnecting = reconnect.isRecovering + let reconnecting = connection.isRecovering let token = UUID() generation = token activeHistoryID = nil @@ -660,7 +471,7 @@ final class MobileWorkspace: Identifiable { status = String(localized: reconnecting ? "Reconnecting" : "Connecting") guard let host = hosts.host(descriptor.hostID) else { let message = String(localized: "The host is no longer available.") - reconnect.stopRecovery() + connection.stopRecovery() connectionFailure = message status = String(localized: "Disconnected") busy = false @@ -700,7 +511,7 @@ final class MobileWorkspace: Identifiable { } pendingHostKeyHostID = endpoint.id pendingHostKey = observation - reconnect.pauseForUserInteraction() + connection.pauseForUserInteraction() journey.waitForHostKey(at: endpoint.id, fingerprint: observation.fingerprint) status = String(localized: "Waiting for approval") return @@ -751,7 +562,7 @@ final class MobileWorkspace: Identifiable { guard generation == token, !Task.isCancelled else { return } transferConfiguration = config submissions.removeAll() - let wasReconnecting = reconnect.markConnected() + let wasReconnecting = connection.markConnected() connected = true status = String(localized: "Connected") journey.succeed() @@ -777,7 +588,7 @@ final class MobileWorkspace: Identifiable { } if authenticationRequired { requiresUserInteraction = true - reconnect.pauseForUserInteraction() + connection.pauseForUserInteraction() authenticationMode = sshError.isServerAuthenticationChallenge ? .serverChallenge : .localIdentity authenticationHostID = sshError.routeHostID ?? descriptor.hostID @@ -809,10 +620,10 @@ final class MobileWorkspace: Identifiable { } } if !requiresUserInteraction { - if (reconnect.hasConnectedOnce || reconnecting) && !preventsAutomaticReconnect { + if (connection.hasConnectedOnce || reconnecting) && !preventsAutomaticReconnect { scheduleReconnect(after: error.localizedDescription) } else { - reconnect.stopRecovery() + connection.stopRecovery() journey.fail(error.localizedDescription) status = String(localized: "Disconnected") } @@ -833,11 +644,11 @@ final class MobileWorkspace: Identifiable { } func retryReconnectNow() { - reconnect.retryNow() + connection.reconnect.retryNow() } func stopReconnecting() { - guard let reconnectState = reconnect.stopReconnecting() else { return } + guard let reconnectState = connection.stopRecovery() else { return } cancelPendingUpload() stopSSHEventConsumer() resetCommandGeneratorShellState() @@ -872,7 +683,7 @@ final class MobileWorkspace: Identifiable { cancelPendingUpload() stopSSHEventConsumer() resetCommandGeneratorShellState() - let reconnecting = reconnect.hasConnectedOnce + let reconnecting = connection.hasConnectedOnce connectionResumeIntent = .foreground generation = UUID() attempt?.cancel() @@ -893,7 +704,7 @@ final class MobileWorkspace: Identifiable { submissions.removeAll() connectionFailure = nil operationError = nil - reconnect.suspendForForegroundResume(reconnecting: reconnecting) + connection.suspendForForegroundResume(reconnecting: reconnecting) status = String(localized: reconnecting ? "Reconnecting" : "Connecting") presentConnectionJourney(mode: reconnecting ? .reconnection : .connection) Task { @@ -922,7 +733,7 @@ final class MobileWorkspace: Identifiable { switch result { case .success: connectionResumeIntent = .none - reconnect.reset() + connection.resetRecovery() status = String(localized: "Disconnected") didRequestClose?(id) case .failure(let failure): @@ -931,7 +742,7 @@ final class MobileWorkspace: Identifiable { } private func handleConnectionFailure(_ message: String) { - guard reconnect.hasConnectedOnce else { return } + guard connection.hasConnectedOnce else { return } scheduleReconnect(after: message) } @@ -939,7 +750,7 @@ final class MobileWorkspace: Identifiable { cancelPendingUpload() stopSSHEventConsumer() resetCommandGeneratorShellState() - let announcesLoss = reconnect.schedule(after: message) { [weak self] in self?.connect() } + let announcesLoss = connection.scheduleRecovery(after: message) { [weak self] in self?.connect() } generation = UUID() attempt?.cancel() attempt = nil @@ -991,7 +802,7 @@ final class MobileWorkspace: Identifiable { resetCommandGeneratorShellState() connectionResumeIntent = .none journey.hide() - reconnect.reset() + connection.resetRecovery() connectionFailure = nil operationError = nil pendingHostKey = nil @@ -1094,7 +905,7 @@ final class MobileWorkspace: Identifiable { } private var hasActiveConnectionFlow: Bool { - if connected || busy || reconnect.isRecovering || authenticationRequired || pendingHostKey != nil { + if connected || busy || connection.isRecovering || authenticationRequired || pendingHostKey != nil { return true } switch journey.phase { @@ -1362,14 +1173,8 @@ final class MobileWorkspace: Identifiable { && !name.unicodeScalars.contains(where: { CharacterSet.controlCharacters.contains($0) }) } - private static let supportedGeneratorShells: Set = ["bash", "zsh", "fish"] - private static let commandGeneratorDraftRequest = Data("\u{1B}[777;1~".utf8) - private static let commandGeneratorDraftClear = Data("\u{1B}[777;2~".utf8) - private static let bracketedPasteStart = Data("\u{1B}[200~".utf8) - private static let bracketedPasteEnd = Data("\u{1B}[201~".utf8) - private func handleSFTPConnectionError(_ error: any Error, connection: UUID) -> Bool { - guard generation == connection, reconnect.hasConnectedOnce, let sftpError = error as? SFTPError else { + guard generation == connection, self.connection.hasConnectedOnce, let sftpError = error as? SFTPError else { return false } switch sftpError { @@ -1392,115 +1197,6 @@ final class MobileWorkspace: Identifiable { } } -private nonisolated enum MobileCommandDraftError: LocalizedError { - case unavailable - case alreadyPending - case timedOut - - var errorDescription: String? { - switch self { - case .unavailable: - String( - localized: - "Command Generator requires a supported shell prompt with Shell Integration enabled." - ) - case .alreadyPending: - String(localized: "Command Generator is already reading the current input.") - case .timedOut: - String(localized: "The remote shell did not return its current input.") - } - } -} - -private nonisolated enum MobileCommandGenerationError: LocalizedError { - case emptyDraft - - var errorDescription: String? { - String(localized: "Type something at the shell prompt before generating a command.") - } -} - -private nonisolated func mobileCommandGenerationWithTimeout( - _ operation: @escaping @Sendable () async throws -> String -) async throws -> String { - try await withThrowingTaskGroup(of: String.self) { group in - group.addTask { try await operation() } - group.addTask { - try await Task.sleep(for: .seconds(30)) - throw CommandGeneratorError.timedOut - } - guard let result = try await group.next() else { - throw CommandGeneratorError.invalidResponse - } - group.cancelAll() - return result - } -} - -@MainActor -@Observable -final class MobileWorkspaceReconnectState { - private let controller: SessionReconnectController - private(set) var hasConnectedOnce = false - private(set) var isRecovering = false - - var state: ReconnectState? { controller.state } - - init(controller: SessionReconnectController = SessionReconnectController()) { - self.controller = controller - } - - @discardableResult - func markConnected() -> Bool { - let wasRecovering = isRecovering - let initialConnection = !hasConnectedOnce - hasConnectedOnce = true - isRecovering = false - controller.markConnected(initial: initialConnection) - return wasRecovering - } - - @discardableResult - func schedule(after message: String, retry: @escaping SessionReconnectController.RetryAction) -> Bool { - guard hasConnectedOnce else { return false } - let announcesLoss = !isRecovering - isRecovering = true - controller.schedule(after: message, retry: retry) - return announcesLoss - } - - func pauseForUserInteraction() { - controller.pauseForUserInteraction() - } - - func retryNow() { - controller.retryNow() - } - - func stopReconnecting() -> ReconnectState? { - guard let state else { return nil } - controller.stop() - isRecovering = false - return state - } - - func stopRecovery() { - controller.stop() - isRecovering = false - } - - func suspendForForegroundResume(reconnecting: Bool) { - controller.stop() - isRecovering = reconnecting && hasConnectedOnce - } - - func reset() { - controller.stop() - hasConnectedOnce = false - isRecovering = false - } -} - @MainActor private final class MobileTerminalRelay { weak var owner: MobileWorkspace? diff --git a/TFI/App/MobileWorkspaceDescriptor.swift b/TFI/Features/Workspaces/MobileWorkspaceDescriptor.swift similarity index 100% rename from TFI/App/MobileWorkspaceDescriptor.swift rename to TFI/Features/Workspaces/MobileWorkspaceDescriptor.swift diff --git a/TFI/Features/MobileWorkspaceView.swift b/TFI/Features/Workspaces/MobileWorkspaceView.swift similarity index 98% rename from TFI/Features/MobileWorkspaceView.swift rename to TFI/Features/Workspaces/MobileWorkspaceView.swift index 92da6d5f..52ff1680 100644 --- a/TFI/Features/MobileWorkspaceView.swift +++ b/TFI/Features/Workspaces/MobileWorkspaceView.swift @@ -124,11 +124,11 @@ struct MobileWorkspaceView: View { .keyboardShortcut("i", modifiers: .command) Divider() Button("Next Match", systemImage: "chevron.down") { - workspace.navigateFind(previous: false) + workspace.navigateFind(.next) } .keyboardShortcut("g", modifiers: .command) Button("Previous Match", systemImage: "chevron.up") { - workspace.navigateFind(previous: true) + workspace.navigateFind(.previous) } .keyboardShortcut("g", modifiers: [.command, .shift]) Divider() @@ -459,7 +459,7 @@ private struct MobileTerminalSearchModifier: ViewModifier { ) .textInputAutocapitalization(.never) .autocorrectionDisabled() - .onSubmit(of: .search) { workspace.navigateFind(previous: true) } + .onSubmit(of: .search) { workspace.navigateFind(.previous) } } private var query: Binding { diff --git a/TFI/Infrastructure/SSH/MobileWorkspaceConnectionServices+Live.swift b/TFI/Infrastructure/SSH/MobileWorkspaceConnectionServices+Live.swift new file mode 100644 index 00000000..2bf14dd9 --- /dev/null +++ b/TFI/Infrastructure/SSH/MobileWorkspaceConnectionServices+Live.swift @@ -0,0 +1,37 @@ +import Foundation +import TermindRemoteInfrastructure + +extension MobileWorkspaceConnectionServices { + static var live: MobileWorkspaceConnectionServices { + let coordinator = RemoteConnectionCoordinator() + return MobileWorkspaceConnectionServices( + discoverHostKey: { hostID, host, port, proxy, jumps in + try await SSHSessionActor.discoverHostKey( + hostID: hostID, host: host, port: port, proxy: proxy, jumps: jumps) + }, + makeSSHSession: { output, exit, shellIntegration in + coordinator.makeSSHSession(output: output, exit: exit, shellIntegration: shellIntegration) + }, + connectSSH: { session, configuration, shellIntegrationEnabled, columns, rows, progress in + try await session.connect( + hostID: configuration.hostID, + host: configuration.host, + port: configuration.port, + username: configuration.username, + credentials: configuration.credentials, + expectedHostKeys: configuration.expectedHostKeys, + proxy: configuration.proxy, + jumps: configuration.jumps, + shellIntegrationEnabled: shellIntegrationEnabled, + columns: columns, + rows: rows, + progress: progress + ) + }, + connectSFTP: { configuration, progress, connectionFailure in + try await coordinator.connectSFTP( + using: configuration, progress: progress, connectionFailure: connectionFailure) + }, + writeSSH: { session, data in await session.write(data) }) + } +} diff --git a/TFI/Platform/MobileTerminalAppearance.swift b/TFI/Platform/MobileTerminalAppearance.swift index 977b022a..9f74ad7e 100644 --- a/TFI/Platform/MobileTerminalAppearance.swift +++ b/TFI/Platform/MobileTerminalAppearance.swift @@ -4,9 +4,6 @@ import GhosttyTheme import SwiftUI import TermindDomain -typealias TerminalTheme = TermindTerminalPalette -typealias GhosttyCustomConfiguration = TerminalCustomConfiguration - nonisolated extension TermindTerminalPalette.RGB { var color: Color { Color(.sRGB, red: r, green: g, blue: b, opacity: 1) } } @@ -43,7 +40,7 @@ nonisolated extension TerminalCustomConfiguration { nonisolated enum TermindTerminalThemeCatalog { static let allThemes: [GhosttyThemeDefinition] = { - let termindThemes = TerminalTheme.all.map(\.ghosttyDefinition) + let termindThemes = TermindTerminalPalette.all.map(\.ghosttyDefinition) let termindNames = Set(termindThemes.map(\.name)) return termindThemes + GhosttyThemeCatalog.allThemes.filter { !termindNames.contains($0.name) } }() @@ -60,8 +57,8 @@ nonisolated enum TermindTerminalThemeCatalog { theme(named: name).flatMap { $0.isDark ? nil : $0 } } - static let defaultDark = TerminalTheme.termindDark.ghosttyDefinition - static let defaultLight = TerminalTheme.termindLight.ghosttyDefinition + static let defaultDark = TermindTerminalPalette.termindDark.ghosttyDefinition + static let defaultLight = TermindTerminalPalette.termindLight.ghosttyDefinition static func terminalConfiguration( themeName: String, @@ -73,7 +70,7 @@ nonisolated enum TermindTerminalThemeCatalog { let selected = theme(named: themeName).flatMap { $0.isDark == isDark ? $0 : nil } ?? fallback let base = selected.toTerminalConfiguration() guard customEnabled else { return base } - return GhosttyCustomConfiguration(customText).applying(to: base) + return TerminalCustomConfiguration(customText).applying(to: base) } static func background( @@ -81,17 +78,17 @@ nonisolated enum TermindTerminalThemeCatalog { customEnabled: Bool, customText: String, isDark: Bool - ) -> TerminalTheme.RGB { + ) -> TermindTerminalPalette.RGB { let fallback = isDark ? defaultDark : defaultLight let selected = theme(named: themeName).flatMap { $0.isDark == isDark ? $0 : nil } ?? fallback if customEnabled, - let value = GhosttyCustomConfiguration(customText).lastValue(for: "background"), - let customBackground = TerminalTheme.RGB(hex: value) + let value = TerminalCustomConfiguration(customText).lastValue(for: "background"), + let customBackground = TermindTerminalPalette.RGB(hex: value) { return customBackground } - return TerminalTheme.RGB(hex: selected.background) - ?? (isDark ? TerminalTheme.termindDark.background : TerminalTheme.termindLight.background) + return TermindTerminalPalette.RGB(hex: selected.background) + ?? (isDark ? TermindTerminalPalette.termindDark.background : TermindTerminalPalette.termindLight.background) } } diff --git a/TFI/Features/MobileConnectionJourneyView.swift b/TFI/UI/Components/MobileConnectionJourneyView.swift similarity index 99% rename from TFI/Features/MobileConnectionJourneyView.swift rename to TFI/UI/Components/MobileConnectionJourneyView.swift index dbd8a653..63ac75a2 100644 --- a/TFI/Features/MobileConnectionJourneyView.swift +++ b/TFI/UI/Components/MobileConnectionJourneyView.swift @@ -87,12 +87,12 @@ struct MobileConnectionJourneyView: View { if journey.showsLogs { logs } else if let authenticationPrompt { - SharedSSHAuthenticationForm( + SSHAuthenticationForm( prompt: authenticationPrompt, submit: submit, cancel: cancel, library: library ) .id(authenticationPrompt.id) } else if let hostKey { - SharedSSHHostKeyConfirmationForm( + SSHHostKeyConfirmationForm( prompt: hostKey, displayAddress: hostKey.observation.hostname, proceed: { accept(false) }, trust: { accept(true) }, deny: cancel) } else if case .failed(let message) = journey.phase { diff --git a/TFI/Features/MobileEditorSheet.swift b/TFI/UI/Components/MobileEditorSheet.swift similarity index 100% rename from TFI/Features/MobileEditorSheet.swift rename to TFI/UI/Components/MobileEditorSheet.swift diff --git a/TFI/Features/MobileOverflowMenuLabel.swift b/TFI/UI/Components/MobileOverflowMenuLabel.swift similarity index 100% rename from TFI/Features/MobileOverflowMenuLabel.swift rename to TFI/UI/Components/MobileOverflowMenuLabel.swift diff --git a/TFI/Features/MobileSheetPresentation.swift b/TFI/UI/Components/MobileSheetPresentation.swift similarity index 87% rename from TFI/Features/MobileSheetPresentation.swift rename to TFI/UI/Components/MobileSheetPresentation.swift index 6ea6a629..064c3221 100644 --- a/TFI/Features/MobileSheetPresentation.swift +++ b/TFI/UI/Components/MobileSheetPresentation.swift @@ -1,13 +1,13 @@ import SwiftUI -import UIKit /// iPad forms use the system's centered presentation. Detents are reserved for iPhone trays. private struct MobileSheetPresentation: ViewModifier { var compact: Bool @Environment(\.dynamicTypeSize) private var typeSize + @Environment(\.mobileInterfaceIdiom) private var interfaceIdiom @ViewBuilder func body(content: Content) -> some View { - if UIDevice.current.userInterfaceIdiom == .pad { + if interfaceIdiom == .pad { content.presentationSizing(.form) .presentationDragIndicator(.hidden) } else { diff --git a/TFI/Features/MobileToolbarSearch.swift b/TFI/UI/Components/MobileToolbarSearch.swift similarity index 100% rename from TFI/Features/MobileToolbarSearch.swift rename to TFI/UI/Components/MobileToolbarSearch.swift diff --git a/TFI/Features/MobileCards.swift b/TFI/UI/Managed/MobileCards.swift similarity index 89% rename from TFI/Features/MobileCards.swift rename to TFI/UI/Managed/MobileCards.swift index a9345fe7..4a620333 100644 --- a/TFI/Features/MobileCards.swift +++ b/TFI/UI/Managed/MobileCards.swift @@ -1,23 +1,6 @@ import SwiftUI import UIKit -/// Width is the content width, after the page's safe-area margins. -nonisolated enum MobileCardLayout { - /// The natural height of a touch card with a 36pt header, one chip row, - /// 12pt between them and 16pt of padding on each edge. - static let minimumHeight: CGFloat = 104 - static let maximumVisibleTags = 2 - - static func columns(width: CGFloat, isPad: Bool, accessibilitySize: Bool) -> Int { - guard isPad, !accessibilitySize else { return 1 } - return max(1, Int((max(0, width) + 12) / 332)) - } - - static func visibleTags(_ tags: [String]) -> ArraySlice { - tags.prefix(maximumVisibleTags) - } -} - struct MobileCardPage: View { @ViewBuilder var content: Content @@ -32,6 +15,7 @@ struct MobileCardPage: View { struct MobileCardGrid: View { @Environment(\.dynamicTypeSize) private var typeSize + @Environment(\.mobileInterfaceIdiom) private var interfaceIdiom @State private var width: CGFloat = 0 @ViewBuilder var content: Content @@ -40,7 +24,7 @@ struct MobileCardGrid: View { columns: Array( repeating: GridItem(.flexible(), spacing: 12, alignment: .top), count: MobileCardLayout.columns( - width: width, isPad: UIDevice.current.userInterfaceIdiom == .pad, + width: width, isPad: interfaceIdiom == .pad, accessibilitySize: typeSize.isAccessibilitySize)), alignment: .leading, spacing: 12 ) { content } diff --git a/TFITests/MobileAppTests.swift b/TFITests/MobileAppTests.swift index f71af275..3536d4ad 100644 --- a/TFITests/MobileAppTests.swift +++ b/TFITests/MobileAppTests.swift @@ -122,7 +122,7 @@ struct MobileTerminalShiftStateTests { } @Suite @MainActor -struct MobileWorkspaceReconnectStateTests { +struct MobileWorkspaceConnectionRecoveryTests { @Test func backgroundExpirationResumesEveryAffectedWorkspaceOnForeground() async throws { let background = MobileTestBackgroundTaskManager() let connections = MobileSFTPConnectionBox( @@ -483,43 +483,43 @@ struct MobileWorkspaceReconnectStateTests { @Test func onlySuccessfulSessionsEnterAutomaticRecovery() { let controller = SessionReconnectController(sleep: { _ in try await Task.sleep(for: .seconds(3_600)) }) - let state = MobileWorkspaceReconnectState(controller: controller) - defer { state.reset() } + let state = RemoteSessionConnectionController(reconnect: controller) + defer { state.resetRecovery() } - #expect(!state.schedule(after: "initial failure") {}) - #expect(state.state == nil) + #expect(!state.scheduleRecovery(after: "initial failure") {}) + #expect(state.reconnectState == nil) #expect(!state.markConnected()) - #expect(state.schedule(after: "connection lost") {}) + #expect(state.scheduleRecovery(after: "connection lost") {}) #expect(state.isRecovering) - #expect(state.state?.attempt == 1) - #expect(!state.schedule(after: "still offline") {}) - #expect(state.state?.attempt == 2) + #expect(state.reconnectState?.attempt == 1) + #expect(!state.scheduleRecovery(after: "still offline") {}) + #expect(state.reconnectState?.attempt == 2) } @Test func stoppingRecoveryKeepsManualReconnectAvailableAndResetClearsHistory() { let controller = SessionReconnectController(sleep: { _ in try await Task.sleep(for: .seconds(3_600)) }) - let state = MobileWorkspaceReconnectState(controller: controller) + let state = RemoteSessionConnectionController(reconnect: controller) _ = state.markConnected() - _ = state.schedule(after: "offline") {} + _ = state.scheduleRecovery(after: "offline") {} - #expect(state.stopReconnecting()?.lastError == "offline") - #expect(state.state == nil) + #expect(state.stopRecovery()?.lastError == "offline") + #expect(state.reconnectState == nil) #expect(state.hasConnectedOnce) #expect(!state.isRecovering) - state.reset() + state.resetRecovery() #expect(!state.hasConnectedOnce) #expect(!state.isRecovering) } @Test func aRecoveredConnectionReportsRecoveryAndClearsRetryState() { let controller = SessionReconnectController(sleep: { _ in try await Task.sleep(for: .seconds(3_600)) }) - let state = MobileWorkspaceReconnectState(controller: controller) + let state = RemoteSessionConnectionController(reconnect: controller) _ = state.markConnected() - _ = state.schedule(after: "offline") {} + _ = state.scheduleRecovery(after: "offline") {} #expect(state.markConnected()) - #expect(state.state == nil) + #expect(state.reconnectState == nil) #expect(state.hasConnectedOnce) #expect(!state.isRecovering) } diff --git a/TFITests/MobileNavigationParityTests.swift b/TFITests/MobileNavigationParityTests.swift index ffe75488..54151b19 100644 --- a/TFITests/MobileNavigationParityTests.swift +++ b/TFITests/MobileNavigationParityTests.swift @@ -21,7 +21,7 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let rootSource = try String( - contentsOf: root.appending(path: "TFI/Features/MobileRootView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/App/MobileRootView.swift"), encoding: .utf8) #expect(!rootSource.contains(".navigationTitle(\"Termind\")")) } @@ -30,9 +30,9 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let rootSource = try String( - contentsOf: root.appending(path: "TFI/Features/MobileRootView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/App/MobileRootView.swift"), encoding: .utf8) let vaultSource = try String( - contentsOf: root.appending(path: "TFI/Features/MobileVaultView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Vault/MobileVaultView.swift"), encoding: .utf8) #expect(rootSource.contains("NavigationStack { vault }")) #expect(rootSource.contains("case .vault: sidebarVault")) @@ -46,7 +46,7 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let source = try String( - contentsOf: root.appending(path: "TFI/Features/MobileAISettingsView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Settings/MobileAISettingsView.swift"), encoding: .utf8) let index = try #require(source.components(separatedBy: "enum MobileAISettingsDestination").first) #expect(index.contains("NavigationLink {")) #expect(!index.contains("NavigationLink(value:")) @@ -65,7 +65,7 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let source = try String( - contentsOf: root.appending(path: "TFI/Features/MobileMetricsView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Metrics/MobileMetricsView.swift"), encoding: .utf8) let processes = try #require(source.components(separatedBy: "struct MobileMetricsProcessesView").last) #expect(processes.contains(".contentMargins(.top, 0, for: .scrollContent)")) @@ -76,18 +76,18 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let workspace = try String( - contentsOf: root.appending(path: "TFI/Features/MobileWorkspaceView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Workspaces/MobileWorkspaceView.swift"), encoding: .utf8) #expect(!workspace.contains("\"Connection Error\"")) #expect(workspace.contains("get: { workspace.operationError != nil }")) #expect(workspace.contains("cancel: { closeWorkspace(workspace.id) }")) let metrics = try String( - contentsOf: root.appending(path: "TFI/Features/MobileMetricsView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Metrics/MobileMetricsView.swift"), encoding: .utf8) #expect(metrics.contains("app.metrics.core.setEnabled(false, for: server)")) #expect(metrics.contains("dismiss()")) let rootView = try String( - contentsOf: root.appending(path: "TFI/Features/MobileRootView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/App/MobileRootView.swift"), encoding: .utf8) #expect(rootView.contains("workspace.activateWhenOpened()")) let appModel = try String( contentsOf: root.appending(path: "TFI/App/MobileAppModel.swift"), encoding: .utf8) @@ -100,14 +100,14 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let workspaceList = try String( - contentsOf: root.appending(path: "TFI/Features/MobileRootView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/App/MobileRootView.swift"), encoding: .utf8) #expect(workspaceList.contains(".contextMenu {")) #expect( workspaceList.contains( "Button(\"Close\", systemImage: \"xmark\", role: .destructive) { closeWorkspace(workspace.id) }")) let toolbar = try String( - contentsOf: root.appending(path: "TFI/Features/MobileConnectionPicker.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/HostChooser/MobileConnectionPicker.swift"), encoding: .utf8) #expect(toolbar.contains("Menu {")) #expect(toolbar.contains("Button(\"Close All Workspaces\"")) #expect(toolbar.contains("systemImage: \"rectangle.stack.badge.minus\", role: .destructive")) @@ -120,7 +120,7 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let workspace = try String( - contentsOf: root.appending(path: "TFI/Features/MobileWorkspaceView.swift"), + contentsOf: root.appending(path: "TFI/Features/Workspaces/MobileWorkspaceView.swift"), encoding: .utf8 ) @@ -136,7 +136,7 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let source = try String( - contentsOf: root.appending(path: "TFI/Features/MobileKnownHostsView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/KnownHosts/MobileKnownHostsView.swift"), encoding: .utf8) #expect(source.contains("MobileActionCard")) #expect(source.contains("symbol: \"touchid\"")) @@ -155,12 +155,12 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() let metrics = try String( - contentsOf: root.appending(path: "TFI/Features/MobileMetricsView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Metrics/MobileMetricsView.swift"), encoding: .utf8) #expect(metrics.contains(".scrollContentBackground(.hidden)")) #expect(metrics.contains(".background(Color(uiColor: .systemBackground))")) let cards = try String( - contentsOf: root.appending(path: "TFI/Features/MobileCards.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/UI/Managed/MobileCards.swift"), encoding: .utf8) let actionCard = try #require(cards.components(separatedBy: "struct MobileActionCard").last) #expect(actionCard.contains(".mobileCardSurface()")) #expect(actionCard.contains(".contextMenu {")) @@ -172,19 +172,19 @@ struct MobileNavigationParityTests { #expect(!cards.contains(".frame(width: 320")) let vault = try String( - contentsOf: root.appending(path: "TFI/Features/MobileVaultView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Vault/MobileVaultView.swift"), encoding: .utf8) #expect(vault.contains("MobileCardPage {")) #expect(!vault.contains("List {")) #expect(vault.contains("app.library.setExpanded(!section.isExpanded, for: id)")) #expect(vault.contains("app.library.setUngroupedExpanded(!section.isExpanded)")) let rootView = try String( - contentsOf: root.appending(path: "TFI/Features/MobileRootView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/App/MobileRootView.swift"), encoding: .utf8) #expect(rootView.contains("ContentUnavailableView(\n \"No Workspaces\"")) #expect(rootView.contains(".buttonStyle(.plain)")) let pro = try String( - contentsOf: root.appending(path: "TFI/Features/MobileProView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Commerce/MobileProView.swift"), encoding: .utf8) #expect(!pro.contains("Arrange multiple panes and broadcast input")) #expect(!pro.contains(".safeAreaInset(edge: .bottom)")) #expect(pro.contains("featureList\n purchaseActions")) @@ -195,7 +195,7 @@ struct MobileNavigationParityTests { .deletingLastPathComponent() .deletingLastPathComponent() let workspace = try String( - contentsOf: root.appending(path: "TFI/Features/MobileWorkspaceView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Workspaces/MobileWorkspaceView.swift"), encoding: .utf8) let segments = workspace.components( separatedBy: "Button(\"Close\", systemImage: \"xmark\", role: .destructive)") diff --git a/TFITests/MobileSettingsPortTests.swift b/TFITests/MobileSettingsPortTests.swift index c5c9cb94..b1bbea05 100644 --- a/TFITests/MobileSettingsPortTests.swift +++ b/TFITests/MobileSettingsPortTests.swift @@ -12,11 +12,11 @@ import Testing @Suite @MainActor struct MobileSettingsPortTests { @Test func sftpSettingsDefaultsAndNormalizationMatchDesktop() { - #expect(SharedSettingsKey.sftpGlobalTransferLimit == "sftp.transfers.globalLimit") - #expect(SharedSettingsKey.sftpPerHostTransferLimit == "sftp.transfers.perHostLimit") - #expect(SharedSettingsKey.defaultSFTPGlobalTransferLimit == 3) - #expect(SharedSettingsKey.defaultSFTPPerHostTransferLimit == 2) - #expect(SharedSettingsKey.sftpTransferLimitRange == 1...8) + #expect(SettingsKey.sftpGlobalTransferLimit == "sftp.transfers.globalLimit") + #expect(SettingsKey.sftpPerHostTransferLimit == "sftp.transfers.perHostLimit") + #expect(SettingsKey.defaultSFTPGlobalTransferLimit == 3) + #expect(SettingsKey.defaultSFTPPerHostTransferLimit == 2) + #expect(SettingsKey.sftpTransferLimitRange == 1...8) let defaults = MobileSFTPTransferLimits(global: 3, perHost: 2) #expect(defaults.global == 3) @@ -192,7 +192,7 @@ struct MobileSettingsPortTests { #expect(workspace.isFindPresented) workspace.dismissFind() - workspace.navigateFind(previous: false) + workspace.navigateFind(.next) #expect(workspace.findQuery == "retained") #expect(workspace.isFindPresented) } diff --git a/TFM/App/AppAppearance.swift b/TFM/App/AppAppearance.swift index 456b9961..c712cfad 100644 --- a/TFM/App/AppAppearance.swift +++ b/TFM/App/AppAppearance.swift @@ -15,7 +15,7 @@ nonisolated enum AppAppearance: String, CaseIterable, Identifiable, Sendable { } static func stored(in defaults: UserDefaults = .standard) -> AppAppearance { - defaults.string(forKey: SettingsKey.appAppearance) + defaults.string(forKey: DesktopSettingsKey.appAppearance) .flatMap(Self.init(rawValue:)) ?? .system } } diff --git a/TFM/App/AppDependencies.swift b/TFM/App/AppDependencies.swift index fd150fab..e970daa4 100644 --- a/TFM/App/AppDependencies.swift +++ b/TFM/App/AppDependencies.swift @@ -3,6 +3,7 @@ import Foundation import TermindApplication import TermindDomain import TermindInfrastructure +import TermindRemoteInfrastructure import struct TermindDomain.Host diff --git a/TFM/App/AppModel.swift b/TFM/App/AppModel.swift index 4c864d2d..ea0a3bb4 100644 --- a/TFM/App/AppModel.swift +++ b/TFM/App/AppModel.swift @@ -4,6 +4,7 @@ import Foundation import TermindApplication import TermindDomain import TermindInfrastructure +import TermindPersistence import TermindRemoteInfrastructure import struct TermindDomain.Host diff --git a/TFM/App/AppReviewController.swift b/TFM/App/AppReviewController.swift index 051ad17c..89f07dc9 100644 --- a/TFM/App/AppReviewController.swift +++ b/TFM/App/AppReviewController.swift @@ -118,26 +118,26 @@ final class AppReviewController { contexts[windowID]?.canRequest == true, isEligible else { return } self.pending = nil - var versions = defaults?.stringArray(forKey: SettingsKey.reviewRequestedVersions) ?? [] + var versions = defaults?.stringArray(forKey: DesktopSettingsKey.reviewRequestedVersions) ?? [] versions.append(version) - defaults?.set(versions, forKey: SettingsKey.reviewRequestedVersions) - defaults?.set(now(), forKey: SettingsKey.reviewLastAttempt) + defaults?.set(versions, forKey: DesktopSettingsKey.reviewRequestedVersions) + defaults?.set(now(), forKey: DesktopSettingsKey.reviewLastAttempt) request() } func recordManualReview() { pending = nil - defaults?.set(now(), forKey: SettingsKey.reviewLastAttempt) + defaults?.set(now(), forKey: DesktopSettingsKey.reviewLastAttempt) } private var isEligible: Bool { guard isEnabled, !version.isEmpty, workspaceSeconds >= 300, - let firstUse = defaults?.object(forKey: SettingsKey.reviewFirstUse) as? Date, + let firstUse = defaults?.object(forKey: DesktopSettingsKey.reviewFirstUse) as? Date, now().timeIntervalSince(firstUse) >= 7 * 86_400, - (defaults?.stringArray(forKey: SettingsKey.reviewActiveDays)?.count ?? 0) >= 3, - !(defaults?.stringArray(forKey: SettingsKey.reviewRequestedVersions) ?? []).contains(version) + (defaults?.stringArray(forKey: DesktopSettingsKey.reviewActiveDays)?.count ?? 0) >= 3, + !(defaults?.stringArray(forKey: DesktopSettingsKey.reviewRequestedVersions) ?? []).contains(version) else { return false } - if let lastAttempt = defaults?.object(forKey: SettingsKey.reviewLastAttempt) as? Date { + if let lastAttempt = defaults?.object(forKey: DesktopSettingsKey.reviewLastAttempt) as? Date { return now().timeIntervalSince(lastAttempt) >= 120 * 86_400 } return true @@ -154,16 +154,16 @@ final class AppReviewController { private func recordActiveDay() { let date = now() - if defaults?.object(forKey: SettingsKey.reviewFirstUse) == nil { - defaults?.set(date, forKey: SettingsKey.reviewFirstUse) + if defaults?.object(forKey: DesktopSettingsKey.reviewFirstUse) == nil { + defaults?.set(date, forKey: DesktopSettingsKey.reviewFirstUse) } - var days = defaults?.stringArray(forKey: SettingsKey.reviewActiveDays) ?? [] + var days = defaults?.stringArray(forKey: DesktopSettingsKey.reviewActiveDays) ?? [] // Three distinct local dates prove eligibility; no growing usage log. guard days.count < 3 else { return } let parts = calendar.dateComponents([.era, .year, .month, .day], from: date) let key = "\(parts.era ?? 0)-\(parts.year ?? 0)-\(parts.month ?? 0)-\(parts.day ?? 0)" guard !days.contains(key) else { return } days.append(key) - defaults?.set(days, forKey: SettingsKey.reviewActiveDays) + defaults?.set(days, forKey: DesktopSettingsKey.reviewActiveDays) } } diff --git a/TFM/App/SettingsKey.swift b/TFM/App/DesktopSettingsKey.swift similarity index 50% rename from TFM/App/SettingsKey.swift rename to TFM/App/DesktopSettingsKey.swift index 235e13a4..f261fa8e 100644 --- a/TFM/App/SettingsKey.swift +++ b/TFM/App/DesktopSettingsKey.swift @@ -7,33 +7,16 @@ import TermindApplication /// String literals scattered across views are how a preference silently splits /// into two preferences that disagree. Everything user-facing that lives in /// `UserDefaults` rather than SwiftData is named here. -nonisolated enum SettingsKey { +nonisolated enum DesktopSettingsKey { // Device-local review invitation history; never included in Settings sync. static let reviewFirstUse = "app.review.firstUse" static let reviewActiveDays = "app.review.activeDays" static let reviewLastAttempt = "app.review.lastAttempt" static let reviewRequestedVersions = "app.review.requestedVersions" - /// App language selection. `system` means normal Bundle negotiation. static let showMetricsInMenuBar = "metrics.showInMenuBar" - static let appLanguage = SharedSettingsKey.appLanguage /// App-wide appearance. `system` inherits changes from macOS. static let appAppearance = "app.appearance" - static let iCloudSyncEnabled = SharedSettingsKey.iCloudSyncEnabled - static let defaultICloudSyncEnabled = SharedSettingsKey.defaultICloudSyncEnabled - static let settingsSyncEnabled = SharedSettingsKey.settingsSyncEnabled - static let settingsSyncEnvironment = SharedSettingsKey.settingsSyncEnvironment - static let defaultSettingsSyncEnabled = SharedSettingsKey.defaultSettingsSyncEnabled - static let vaultSyncEnabled = SharedSettingsKey.vaultSyncEnabled - /// The CloudKit environment owning the persisted sync engine state and - /// record system fields. Debug and TestFlight share the app container, but - /// CloudKit development change tags are invalid in production (and vice - /// versa), so an environment switch must rebind the local journal. - static let vaultSyncEnvironment = SharedSettingsKey.vaultSyncEnvironment - static let defaultVaultSyncEnabled = SharedSettingsKey.defaultVaultSyncEnabled - static func migrateSyncPreferences(in defaults: UserDefaults = .standard) { - SharedSettingsKey.migrateSyncPreferences(in: defaults) - } static let mcpServerEnabled = "mcpServer.enabled" static let mcpServerPort = "mcpServer.port" @@ -50,24 +33,9 @@ nonisolated enum SettingsKey { /// shrunk to its status dot. App-wide: it is a standing preference about /// how much of the pane you want covered, not a property of one session. static let statusBarExpanded = "statusBar.expanded" - /// Terminal palette name, matched against `GhosttyThemeCatalog`. - static let terminalThemeDark = SharedSettingsKey.terminalThemeDark - static let terminalThemeLight = SharedSettingsKey.terminalThemeLight - static let terminalCustomConfigurationDarkEnabled = SharedSettingsKey.terminalCustomConfigurationDarkEnabled - static let terminalCustomConfigurationLightEnabled = SharedSettingsKey.terminalCustomConfigurationLightEnabled - static let terminalCustomConfigurationDark = SharedSettingsKey.terminalCustomConfigurationDark - static let terminalCustomConfigurationLight = SharedSettingsKey.terminalCustomConfigurationLight - static let terminalFontFollowTheme = SharedSettingsKey.terminalFontFollowTheme - static let defaultTerminalThemeDark = SharedSettingsKey.defaultTerminalThemeDark - static let defaultTerminalThemeLight = SharedSettingsKey.defaultTerminalThemeLight - static let terminalFontSize = SharedSettingsKey.terminalFontSize - /// Enable session-scoped hooks that report remote shell and system context. - static let shellIntegration = SharedSettingsKey.shellIntegration static var isShellIntegrationEnabled: Bool { - UserDefaults.standard.object(forKey: shellIntegration) as? Bool ?? true + UserDefaults.standard.object(forKey: SettingsKey.shellIntegration) as? Bool ?? true } - /// Mirror the focused SSH terminal's OSC 0/2 title in the macOS menu bar. - static let remoteWindowTitleInMenuBar = SharedSettingsKey.remoteWindowTitleInMenuBar static let defaultRemoteWindowTitleInMenuBarEnabled = true static let onboardingQuickOpenCompleted = "onboarding.quickOpen.completed" static let onboardingStatusBarPathCompleted = "onboarding.statusBarPath.completed" @@ -94,24 +62,9 @@ nonisolated enum SettingsKey { } /// The last approval posture selected in the Assistant composer. static let agentPermissionMode = "assistant.permissionMode" - /// How far through `AgentSkillLibrary` the starter skills have been copied - /// into the user's library. A mark rather than a flag, so a later release - /// can add one without resurrecting the ones they deleted. - static let seededSkillGeneration = SharedSettingsKey.seededSkillGeneration - static let sftpGlobalTransferLimit = SharedSettingsKey.sftpGlobalTransferLimit - static let sftpPerHostTransferLimit = SharedSettingsKey.sftpPerHostTransferLimit - static let defaultSFTPGlobalTransferLimit = SharedSettingsKey.defaultSFTPGlobalTransferLimit - static let defaultSFTPPerHostTransferLimit = SharedSettingsKey.defaultSFTPPerHostTransferLimit - static let sftpTransferLimitRange = SharedSettingsKey.sftpTransferLimitRange - - /// Let the assistant search the web. - /// - /// On by default because ScrubberKit searches from this Mac without an - /// account or API key. - static let webSearchEnabled = SharedSettingsKey.webSearchEnabled - static let defaultWebSearchEnabled = SharedSettingsKey.defaultWebSearchEnabled static var isWebSearchEnabled: Bool { - UserDefaults.standard.object(forKey: webSearchEnabled) as? Bool ?? defaultWebSearchEnabled + UserDefaults.standard.object(forKey: SettingsKey.webSearchEnabled) as? Bool + ?? SettingsKey.defaultWebSearchEnabled } /// Let a provider run the search on its own servers where it can. /// @@ -124,10 +77,8 @@ nonisolated enum SettingsKey { /// search is a *guess*: `ModelProvider.supportsNativeWebSearch(model:)` /// infers it from the model id, and a gateway can make that inference wrong /// in either direction. This is the escape hatch for when it is. - static let webSearchBuiltIn = SharedSettingsKey.webSearchBuiltIn - static let defaultWebSearchBuiltIn = SharedSettingsKey.defaultWebSearchBuiltIn static var isBuiltInWebSearchEnabled: Bool { - UserDefaults.standard.object(forKey: webSearchBuiltIn) as? Bool - ?? defaultWebSearchBuiltIn + UserDefaults.standard.object(forKey: SettingsKey.webSearchBuiltIn) as? Bool + ?? SettingsKey.defaultWebSearchBuiltIn } } diff --git a/TFM/App/MCPServerController.swift b/TFM/App/MCPServerController.swift index 08c39d4e..0d46c0f0 100644 --- a/TFM/App/MCPServerController.swift +++ b/TFM/App/MCPServerController.swift @@ -1,5 +1,7 @@ import Foundation import Security +import TermindApplication +import TermindRemoteInfrastructure @MainActor @Observable @@ -64,11 +66,11 @@ final class MCPServerController { self.canUsePro = canUsePro self.requestPro = requestPro isEnabled = - defaults.object(forKey: SettingsKey.mcpServerEnabled) as? Bool - ?? SettingsKey.defaultMCPServerEnabled + defaults.object(forKey: DesktopSettingsKey.mcpServerEnabled) as? Bool + ?? DesktopSettingsKey.defaultMCPServerEnabled port = - defaults.object(forKey: SettingsKey.mcpServerPort) as? Int - ?? SettingsKey.defaultMCPServerPort + defaults.object(forKey: DesktopSettingsKey.mcpServerPort) as? Int + ?? DesktopSettingsKey.defaultMCPServerPort let initialDisabledTools = Self.readDisabledTools(defaults) disabledTools = initialDisabledTools tools = MCPServerTools( @@ -92,7 +94,7 @@ final class MCPServerController { func setEnabled(_ enabled: Bool) { if enabled, !requestPro() { return } isEnabled = enabled - defaults.set(enabled, forKey: SettingsKey.mcpServerEnabled) + defaults.set(enabled, forKey: DesktopSettingsKey.mcpServerEnabled) Task { [weak self] in if enabled { await self?.start() } else { await self?.stop() } } @@ -112,7 +114,7 @@ final class MCPServerController { let clamped = min(max(value, 1_024), 65_535) guard clamped != port else { return } port = clamped - defaults.set(clamped, forKey: SettingsKey.mcpServerPort) + defaults.set(clamped, forKey: DesktopSettingsKey.mcpServerPort) if isEnabled { Task { [weak self] in await self?.restart() } } } @@ -120,7 +122,7 @@ final class MCPServerController { guard requestPro() else { return } guard MCPServerTools.exportedNames.contains(name) else { return } if enabled { disabledTools.remove(name) } else { disabledTools.insert(name) } - defaults.set(Array(disabledTools).sorted(), forKey: SettingsKey.mcpServerDisabledTools) + defaults.set(Array(disabledTools).sorted(), forKey: DesktopSettingsKey.mcpServerDisabledTools) tools.replaceDisabledTools( disabledTools, hosts: hosts, metrics: metrics, connections: connections, authentication: authentication, background: background @@ -163,7 +165,7 @@ final class MCPServerController { guard isEnabled, canUsePro() else { return } guard let token = ensureToken(), let validPort = UInt16(exactly: port) else { isEnabled = false - defaults.set(false, forKey: SettingsKey.mcpServerEnabled) + defaults.set(false, forKey: DesktopSettingsKey.mcpServerEnabled) return } status = .starting @@ -198,7 +200,7 @@ final class MCPServerController { } private static func readDisabledTools(_ defaults: UserDefaults) -> Set { - Set((defaults.array(forKey: SettingsKey.mcpServerDisabledTools) as? [String]) ?? []) + Set((defaults.array(forKey: DesktopSettingsKey.mcpServerDisabledTools) as? [String]) ?? []) .intersection(MCPServerTools.exportedNames) } diff --git a/TFM/App/OnboardingController.swift b/TFM/App/OnboardingController.swift index 80f65d55..8dbb9f84 100644 --- a/TFM/App/OnboardingController.swift +++ b/TFM/App/OnboardingController.swift @@ -20,9 +20,9 @@ nonisolated enum OnboardingStep: String, CaseIterable, Identifiable, Sendable { var completionKey: String { switch self { - case .quickOpen: SettingsKey.onboardingQuickOpenCompleted - case .statusBarPath: SettingsKey.onboardingStatusBarPathCompleted - case .commandGenerator: SettingsKey.onboardingCommandGeneratorCompleted + case .quickOpen: DesktopSettingsKey.onboardingQuickOpenCompleted + case .statusBarPath: DesktopSettingsKey.onboardingStatusBarPathCompleted + case .commandGenerator: DesktopSettingsKey.onboardingCommandGeneratorCompleted } } } diff --git a/TFM/App/ProAccessController.swift b/TFM/App/ProAccessController.swift deleted file mode 100644 index fe478384..00000000 --- a/TFM/App/ProAccessController.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindInfrastructure - -typealias ProAccessController = TermindInfrastructure.ProAccessController -typealias ProAccessControllerError = TermindInfrastructure.ProAccessControllerError diff --git a/TFM/App/RootView.swift b/TFM/App/RootView.swift index 1ac3ad66..f50e1f1a 100644 --- a/TFM/App/RootView.swift +++ b/TFM/App/RootView.swift @@ -1,5 +1,6 @@ @preconcurrency import AppKit import SwiftUI +import TermindApplication /// One window with one native Navigator column and one persistent detail deck. /// @@ -15,9 +16,9 @@ struct RootView: View { @State private var window: WindowModel @State private var layoutWidth = Metrics.Window.defaultWidth @State private var automaticQuickOpenReady = false - @AppStorage(SettingsKey.statusBarExpanded) private var isStatusBarExpanded = true + @AppStorage(DesktopSettingsKey.statusBarExpanded) private var isStatusBarExpanded = true @AppStorage(SettingsKey.remoteWindowTitleInMenuBar) private var showsRemoteWindowTitle = - SettingsKey.defaultRemoteWindowTitleInMenuBarEnabled + DesktopSettingsKey.defaultRemoteWindowTitleInMenuBarEnabled init(window: WindowModel = WindowModel()) { _window = State(initialValue: window) diff --git a/TFM/App/ScreenshotDemo+Metrics.swift b/TFM/App/ScreenshotDemo+Metrics.swift index 117fdc63..e55b85dd 100644 --- a/TFM/App/ScreenshotDemo+Metrics.swift +++ b/TFM/App/ScreenshotDemo+Metrics.swift @@ -1,3 +1,4 @@ +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFM/App/ScreenshotDemo.swift b/TFM/App/ScreenshotDemo.swift index 9088f63c..789ca1b1 100644 --- a/TFM/App/ScreenshotDemo.swift +++ b/TFM/App/ScreenshotDemo.swift @@ -1,3 +1,5 @@ +import TermindApplication + #if DEBUG import Foundation diff --git a/TFM/App/TermindApp.swift b/TFM/App/TermindApp.swift index b244fb87..0c6a7103 100644 --- a/TFM/App/TermindApp.swift +++ b/TFM/App/TermindApp.swift @@ -1,7 +1,10 @@ import AgentKitScrubber import SwiftData import SwiftUI +import TermindApplication +import TermindInfrastructure import TermindPersistence +import TermindRemoteInfrastructure #if canImport(AppKit) import AppKit @@ -245,7 +248,8 @@ struct TermindApp: App { } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - let menuEnabled = UserDefaults.standard.object(forKey: SettingsKey.showMetricsInMenuBar) as? Bool ?? true + let menuEnabled = + UserDefaults.standard.object(forKey: DesktopSettingsKey.showMetricsInMenuBar) as? Bool ?? true return !menuEnabled && app?.serverMetrics.keepsRunning != true } @@ -265,7 +269,7 @@ struct TermindApp: App { /// happens first wins, and the observer is torn down either way so a /// window the user opens later is never hidden out from under them. private func openHiddenIfLaunchedAtLogin() { - guard LoginLaunch.wasLaunchedAtLogin, SettingsKey.opensAtLoginQuietly else { return } + guard LoginLaunch.wasLaunchedAtLogin, DesktopSettingsKey.opensAtLoginQuietly else { return } NSApp.hide(nil) quietLaunchObserver = NotificationCenter.default.addObserver( forName: NSWindow.didBecomeKeyNotification, object: nil, queue: .main diff --git a/TFM/Application/Agent/AgentBackgroundCommands.swift b/TFM/Application/Agent/AgentBackgroundCommands.swift deleted file mode 100644 index f264b3c9..00000000 --- a/TFM/Application/Agent/AgentBackgroundCommands.swift +++ /dev/null @@ -1,9 +0,0 @@ -import TermindRemoteInfrastructure - -typealias AgentCommandOutcome = TermindRemoteInfrastructure.AgentCommandOutcome -typealias AgentIdleSweeper = TermindRemoteInfrastructure.AgentIdleSweeper -typealias AgentBackgroundCommands = TermindRemoteInfrastructure.AgentBackgroundCommands -typealias AgentCommandHandover = TermindRemoteInfrastructure.AgentCommandHandover -typealias AgentCommandNotStarted = TermindRemoteInfrastructure.AgentCommandNotStarted -typealias AgentCompletionSignal = TermindRemoteInfrastructure.AgentCompletionSignal -typealias AgentBackgroundCommandError = TermindRemoteInfrastructure.AgentBackgroundCommandError diff --git a/TFM/Application/Agent/AgentBuiltInSFTPTools.swift b/TFM/Application/Agent/AgentBuiltInSFTPTools.swift deleted file mode 100644 index d1f3e4f3..00000000 --- a/TFM/Application/Agent/AgentBuiltInSFTPTools.swift +++ /dev/null @@ -1,12 +0,0 @@ -import TermindRemoteInfrastructure - -typealias SFTPToolDefinition = TermindRemoteInfrastructure.SFTPToolDefinition -typealias SFTPListTool = TermindRemoteInfrastructure.SFTPListTool -typealias SFTPStatTool = TermindRemoteInfrastructure.SFTPStatTool -typealias SFTPReadTool = TermindRemoteInfrastructure.SFTPReadTool -typealias SFTPWriteTool = TermindRemoteInfrastructure.SFTPWriteTool -typealias SFTPMkdirTool = TermindRemoteInfrastructure.SFTPMkdirTool -typealias SFTPMoveTool = TermindRemoteInfrastructure.SFTPMoveTool -typealias SFTPChmodTool = TermindRemoteInfrastructure.SFTPChmodTool -typealias SFTPDeleteTool = TermindRemoteInfrastructure.SFTPDeleteTool -typealias SFTPToolPresentation = TermindRemoteInfrastructure.SFTPToolPresentation diff --git a/TFM/Application/Agent/AgentBuiltInSSHTools.swift b/TFM/Application/Agent/AgentBuiltInSSHTools.swift deleted file mode 100644 index dfbfa2c4..00000000 --- a/TFM/Application/Agent/AgentBuiltInSSHTools.swift +++ /dev/null @@ -1,7 +0,0 @@ -import TermindRemoteInfrastructure - -typealias ListHostsTool = TermindRemoteInfrastructure.ListHostsTool -typealias SSHWaitTool = TermindRemoteInfrastructure.SSHWaitTool -typealias TerminalSuggestTool = TermindRemoteInfrastructure.TerminalSuggestTool -typealias SSHExecTool = TermindRemoteInfrastructure.SSHExecTool -typealias SSHCommandPresentation = TermindRemoteInfrastructure.SSHCommandPresentation diff --git a/TFM/Application/Agent/AgentCommandOutputStream.swift b/TFM/Application/Agent/AgentCommandOutputStream.swift deleted file mode 100644 index 55344725..00000000 --- a/TFM/Application/Agent/AgentCommandOutputStream.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindRemoteInfrastructure - -typealias AgentCommandOutputStream = TermindRemoteInfrastructure.AgentCommandOutputStream diff --git a/TFM/Application/Agent/AgentConnections.swift b/TFM/Application/Agent/AgentConnections.swift deleted file mode 100644 index 25180fea..00000000 --- a/TFM/Application/Agent/AgentConnections.swift +++ /dev/null @@ -1,10 +0,0 @@ -import TermindRemoteInfrastructure - -typealias AgentHostTarget = TermindRemoteInfrastructure.AgentHostTarget -typealias AgentHostGroupTarget = TermindRemoteInfrastructure.AgentHostGroupTarget -typealias AgentHostListing = TermindRemoteInfrastructure.AgentHostListing -typealias AgentHostResolver = TermindRemoteInfrastructure.AgentHostResolver -typealias AgentSSHConfigurationSession = TermindRemoteInfrastructure.AgentSSHConfigurationSession -typealias AgentSSHExecConnection = TermindRemoteInfrastructure.AgentSSHExecConnection -typealias AgentSSHAuthenticationRegistry = TermindRemoteInfrastructure.AgentSSHAuthenticationRegistry -typealias AgentConnectionPool = TermindRemoteInfrastructure.AgentConnectionPool diff --git a/TFM/Application/Agent/AgentSSHInteraction.swift b/TFM/Application/Agent/AgentSSHInteraction.swift deleted file mode 100644 index 817d7a1a..00000000 --- a/TFM/Application/Agent/AgentSSHInteraction.swift +++ /dev/null @@ -1,8 +0,0 @@ -import TermindApplication - -typealias AgentSSHAuthenticationRequest = TermindApplication.AgentSSHAuthenticationRequest -typealias AgentSSHAuthenticationAnswer = TermindApplication.AgentSSHAuthenticationAnswer -typealias AgentSSHAuthenticationResponse = TermindApplication.AgentSSHAuthenticationResponse -typealias AgentSSHHostKeyRequest = TermindApplication.AgentSSHHostKeyRequest -typealias AgentSSHHostKeyResponse = TermindApplication.AgentSSHHostKeyResponse -typealias AgentSSHInteraction = TermindApplication.AgentSSHInteraction diff --git a/TFM/Application/Agent/AgentScratchTransferTools.swift b/TFM/Application/Agent/AgentScratchTransferTools.swift deleted file mode 100644 index b3ca7079..00000000 --- a/TFM/Application/Agent/AgentScratchTransferTools.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindRemoteInfrastructure - -typealias ScratchPullTool = TermindRemoteInfrastructure.ScratchPullTool -typealias ScratchPushTool = TermindRemoteInfrastructure.ScratchPushTool diff --git a/TFM/Application/Agent/ConversationTitleController.swift b/TFM/Application/Agent/ConversationTitleController.swift deleted file mode 100644 index 5e5cb4df..00000000 --- a/TFM/Application/Agent/ConversationTitleController.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias ConversationTitleController = TermindApplication.ConversationTitleController diff --git a/TFM/Application/Agent/GetMetricsTool.swift b/TFM/Application/Agent/GetMetricsTool.swift deleted file mode 100644 index a8c0b77a..00000000 --- a/TFM/Application/Agent/GetMetricsTool.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindRemoteInfrastructure - -typealias GetMetricsTool = TermindRemoteInfrastructure.GetMetricsTool diff --git a/TFM/Application/Agent/MetricsToolOutput.swift b/TFM/Application/Agent/MetricsToolOutput.swift deleted file mode 100644 index dc618ae3..00000000 --- a/TFM/Application/Agent/MetricsToolOutput.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindRemoteInfrastructure - -typealias MetricsToolOutput = TermindRemoteInfrastructure.MetricsToolOutput diff --git a/TFM/Application/Agent/TermindAgentPolicy.swift b/TFM/Application/Agent/TermindAgentPolicy.swift deleted file mode 100644 index 6e8378c6..00000000 --- a/TFM/Application/Agent/TermindAgentPolicy.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias TermindAgentPolicy = TermindApplication.TermindAgentPolicy diff --git a/TFM/Application/Agent/TermindAgentTools.swift b/TFM/Application/Agent/TermindAgentTools.swift deleted file mode 100644 index 1d087849..00000000 --- a/TFM/Application/Agent/TermindAgentTools.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindRemoteInfrastructure - -typealias TermindAgentToolEnvironment = TermindRemoteInfrastructure.TermindAgentToolEnvironment -typealias TermindAgentTools = TermindRemoteInfrastructure.TermindAgentTools diff --git a/TFM/Application/Commands/AppCommandRegistry.swift b/TFM/Application/Commands/AppCommandRegistry.swift index 92bd945c..c34d1e03 100644 --- a/TFM/Application/Commands/AppCommandRegistry.swift +++ b/TFM/Application/Commands/AppCommandRegistry.swift @@ -1,4 +1,5 @@ import Foundation +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFM/Application/Commands/ChatSlashCommand.swift b/TFM/Application/Commands/ChatSlashCommand.swift deleted file mode 100644 index c15f1d89..00000000 --- a/TFM/Application/Commands/ChatSlashCommand.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindApplication - -typealias ChatSlashCommand = TermindApplication.ChatSlashCommand -typealias ChatSlashCommandInput = TermindApplication.ChatSlashCommandInput -typealias ChatSlashCommandCompletion = TermindApplication.ChatSlashCommandCompletion diff --git a/TFM/Application/Connections/ConnectionJourney.swift b/TFM/Application/Connections/ConnectionJourney.swift deleted file mode 100644 index 99ac1bcd..00000000 --- a/TFM/Application/Connections/ConnectionJourney.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindApplication - -typealias ConnectionPurpose = TermindApplication.ConnectionPurpose -typealias ConnectionEndpoint = TermindApplication.ConnectionEndpoint -typealias ConnectionJourney = TermindApplication.ConnectionJourney diff --git a/TFM/Application/Connections/RemoteSessionHostStore.swift b/TFM/Application/Connections/RemoteSessionHostStore.swift deleted file mode 100644 index 94f27261..00000000 --- a/TFM/Application/Connections/RemoteSessionHostStore.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias RemoteSessionHostStore = TermindApplication.RemoteSessionHostStore diff --git a/TFM/Application/Connections/SSHAuthenticationPrompt.swift b/TFM/Application/Connections/SSHAuthenticationPrompt.swift deleted file mode 100644 index 389fd1c0..00000000 --- a/TFM/Application/Connections/SSHAuthenticationPrompt.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias SSHAuthenticationPrompt = TermindApplication.SSHAuthenticationPrompt diff --git a/TFM/Application/Connections/SSHConnectionPreparation.swift b/TFM/Application/Connections/SSHConnectionPreparation.swift deleted file mode 100644 index b1462ac9..00000000 --- a/TFM/Application/Connections/SSHConnectionPreparation.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias SSHConnectionPreparation = TermindApplication.SSHConnectionPreparation diff --git a/TFM/Application/Connections/SSHConnectionTypes.swift b/TFM/Application/Connections/SSHConnectionTypes.swift deleted file mode 100644 index 933ea7cc..00000000 --- a/TFM/Application/Connections/SSHConnectionTypes.swift +++ /dev/null @@ -1,13 +0,0 @@ -import TermindApplication - -typealias SSHHostKeyObservation = TermindApplication.SSHHostKeyObservation -typealias SSHHostKeyPrompt = TermindApplication.SSHHostKeyPrompt -typealias SSHCredentials = TermindApplication.SSHCredentials -typealias SSHConnectionConfiguration = TermindApplication.SSHConnectionConfiguration -typealias SSHCredentialResolution = TermindApplication.SSHCredentialResolution -typealias TermindSSHError = TermindApplication.TermindSSHError -typealias SSHTransportProxy = TermindApplication.SSHTransportProxy -typealias SSHRouteHopConfiguration = TermindApplication.SSHRouteHopConfiguration -typealias SSHRouteEvent = TermindApplication.SSHRouteEvent -typealias SSHRouteProgressHandler = TermindApplication.SSHRouteProgressHandler -typealias SSHAuthenticationSubmission = TermindApplication.SSHAuthenticationSubmission diff --git a/TFM/Application/Events/HostDeletionHandlers.swift b/TFM/Application/Events/HostDeletionHandlers.swift deleted file mode 100644 index 3a0f2f15..00000000 --- a/TFM/Application/Events/HostDeletionHandlers.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindApplication - -typealias RepositoryHostDeletionHandler = TermindApplication.RepositoryHostDeletionHandler -typealias RuntimeHostDeletionHandler = TermindApplication.RuntimeHostDeletionHandler diff --git a/TFM/Application/Events/OutboxRunner.swift b/TFM/Application/Events/OutboxRunner.swift deleted file mode 100644 index f2a6222d..00000000 --- a/TFM/Application/Events/OutboxRunner.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias OutboxRunner = TermindApplication.OutboxRunner diff --git a/TFM/Application/Events/OutboxStore.swift b/TFM/Application/Events/OutboxStore.swift deleted file mode 100644 index b99c4014..00000000 --- a/TFM/Application/Events/OutboxStore.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindApplication - -typealias OutboxStore = TermindApplication.OutboxStore -typealias OutboxEventHandler = TermindApplication.OutboxEventHandler diff --git a/TFM/Application/FeatureModels/AIConfigurationModel+Demo.swift b/TFM/Application/FeatureModels/AIConfigurationModel+Demo.swift index e69111b4..feb5cd9f 100644 --- a/TFM/Application/FeatureModels/AIConfigurationModel+Demo.swift +++ b/TFM/Application/FeatureModels/AIConfigurationModel+Demo.swift @@ -1,5 +1,6 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain // MARK: - Demo data diff --git a/TFM/Application/FeatureModels/AIConfigurationModel.swift b/TFM/Application/FeatureModels/AIConfigurationModel.swift deleted file mode 100644 index 32f1ff34..00000000 --- a/TFM/Application/FeatureModels/AIConfigurationModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias AIConfigurationModel = TermindApplication.AIConfigurationModel diff --git a/TFM/Application/FeatureModels/AssistantConversationCommands.swift b/TFM/Application/FeatureModels/AssistantConversationCommands.swift index 2ef34537..0ec160d4 100644 --- a/TFM/Application/FeatureModels/AssistantConversationCommands.swift +++ b/TFM/Application/FeatureModels/AssistantConversationCommands.swift @@ -1,4 +1,5 @@ import Foundation +import TermindApplication import TermindDomain /// One injected command surface for durable Assistant conversation mutations. diff --git a/TFM/Application/FeatureModels/AssistantLibraryModel.swift b/TFM/Application/FeatureModels/AssistantLibraryModel.swift deleted file mode 100644 index e0339394..00000000 --- a/TFM/Application/FeatureModels/AssistantLibraryModel.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindApplication - -typealias AssistantConversationStore = TermindApplication.AssistantConversationStore -typealias AssistantLibraryModel = TermindApplication.AssistantLibraryModel diff --git a/TFM/Application/FeatureModels/CommandHistoryModel.swift b/TFM/Application/FeatureModels/CommandHistoryModel.swift deleted file mode 100644 index 8c2dc760..00000000 --- a/TFM/Application/FeatureModels/CommandHistoryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias CommandHistoryModel = TermindApplication.CommandHistoryModel diff --git a/TFM/Application/FeatureModels/ConnectionHistoryModel.swift b/TFM/Application/FeatureModels/ConnectionHistoryModel.swift deleted file mode 100644 index dc759880..00000000 --- a/TFM/Application/FeatureModels/ConnectionHistoryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias ConnectionHistoryModel = TermindApplication.ConnectionHistoryModel diff --git a/TFM/Application/FeatureModels/CredentialLibraryModel.swift b/TFM/Application/FeatureModels/CredentialLibraryModel.swift deleted file mode 100644 index 6b03c3ed..00000000 --- a/TFM/Application/FeatureModels/CredentialLibraryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias CredentialLibraryModel = TermindApplication.CredentialLibraryModel diff --git a/TFM/Application/FeatureModels/ForwardingLibraryModel.swift b/TFM/Application/FeatureModels/ForwardingLibraryModel.swift deleted file mode 100644 index 98cbd8b9..00000000 --- a/TFM/Application/FeatureModels/ForwardingLibraryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias ForwardingLibraryModel = TermindApplication.ForwardingLibraryModel diff --git a/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift b/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift index 3c757d9d..af970a80 100644 --- a/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift +++ b/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift @@ -1,4 +1,5 @@ import Foundation +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFM/Application/FeatureModels/HostLibraryModel.swift b/TFM/Application/FeatureModels/HostLibraryModel.swift deleted file mode 100644 index 6c5846f1..00000000 --- a/TFM/Application/FeatureModels/HostLibraryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias HostLibraryModel = TermindApplication.HostLibraryModel diff --git a/TFM/Application/FeatureModels/KnownHostsLibraryModel.swift b/TFM/Application/FeatureModels/KnownHostsLibraryModel.swift deleted file mode 100644 index 766cde32..00000000 --- a/TFM/Application/FeatureModels/KnownHostsLibraryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias KnownHostsLibraryModel = TermindApplication.KnownHostsLibraryModel diff --git a/TFM/Application/FeatureModels/SSHImportModel.swift b/TFM/Application/FeatureModels/SSHImportModel+Desktop.swift similarity index 79% rename from TFM/Application/FeatureModels/SSHImportModel.swift rename to TFM/Application/FeatureModels/SSHImportModel+Desktop.swift index 82adf757..f0606e35 100644 --- a/TFM/Application/FeatureModels/SSHImportModel.swift +++ b/TFM/Application/FeatureModels/SSHImportModel+Desktop.swift @@ -1,8 +1,6 @@ import Foundation import TermindApplication -typealias SSHImportModel = TermindApplication.SSHImportModel - extension SSHImportModel { convenience init(library: HostLibraryModel, reader: any SSHImportReading) { self.init(library: library, reader: reader, defaultUsername: NSUserName()) diff --git a/TFM/Application/FeatureModels/SkillLibraryModel+BuiltIn.swift b/TFM/Application/FeatureModels/SkillLibraryModel+BuiltIn.swift deleted file mode 100644 index f6a1a2bb..00000000 --- a/TFM/Application/FeatureModels/SkillLibraryModel+BuiltIn.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias AgentSkillLibrary = TermindApplication.AgentSkillLibrary diff --git a/TFM/Application/FeatureModels/SkillLibraryModel.swift b/TFM/Application/FeatureModels/SkillLibraryModel.swift deleted file mode 100644 index 89dee443..00000000 --- a/TFM/Application/FeatureModels/SkillLibraryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias SkillLibraryModel = TermindApplication.SkillLibraryModel diff --git a/TFM/Application/FeatureModels/SnippetLibraryModel.swift b/TFM/Application/FeatureModels/SnippetLibraryModel.swift deleted file mode 100644 index 091a9dd7..00000000 --- a/TFM/Application/FeatureModels/SnippetLibraryModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias SnippetLibraryModel = TermindApplication.SnippetLibraryModel diff --git a/TFM/Application/FeatureModels/TerminalSettingsModel.swift b/TFM/Application/FeatureModels/TerminalSettingsModel.swift deleted file mode 100644 index 64c94218..00000000 --- a/TFM/Application/FeatureModels/TerminalSettingsModel.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias TerminalSettingsModel = TermindApplication.TerminalSettingsModel diff --git a/TFM/Application/FeatureModels/TransferLibraryModel.swift b/TFM/Application/FeatureModels/TransferLibraryModel.swift index a3c6a24a..c2733928 100644 --- a/TFM/Application/FeatureModels/TransferLibraryModel.swift +++ b/TFM/Application/FeatureModels/TransferLibraryModel.swift @@ -1,4 +1,5 @@ import Foundation +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFM/Application/MCPServer/MCPServerTools.swift b/TFM/Application/MCPServer/MCPServerTools.swift index d30c13e5..b4a310ae 100644 --- a/TFM/Application/MCPServer/MCPServerTools.swift +++ b/TFM/Application/MCPServer/MCPServerTools.swift @@ -1,6 +1,7 @@ import AgentKit import Foundation import TermindApplication +import TermindRemoteInfrastructure @MainActor final class MCPServerTools { diff --git a/TFM/Application/Metrics/MetricsServices.swift b/TFM/Application/Metrics/MetricsServices.swift deleted file mode 100644 index 9316b94b..00000000 --- a/TFM/Application/Metrics/MetricsServices.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import TermindApplication -import TermindDomain - -typealias MetricsClock = TermindApplication.MetricsClock - -typealias MetricsSampling = TermindApplication.MetricsSampling -typealias MetricsConfigurationStoring = TermindApplication.MetricsConfigurationStoring diff --git a/TFM/Application/Metrics/MetricsSnapshot.swift b/TFM/Application/Metrics/MetricsSnapshot.swift deleted file mode 100644 index c369fc34..00000000 --- a/TFM/Application/Metrics/MetricsSnapshot.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindApplication - -typealias MetricsSnapshotReading = TermindApplication.MetricsSnapshotReading -typealias MetricsMonitorState = TermindApplication.MetricsMonitorState -typealias MetricsSnapshot = TermindApplication.MetricsSnapshot diff --git a/TFM/Application/Metrics/ServerMetricsModel.swift b/TFM/Application/Metrics/ServerMetricsModel.swift deleted file mode 100644 index e74b2cf1..00000000 --- a/TFM/Application/Metrics/ServerMetricsModel.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindApplication - -typealias ServerMetric = TermindApplication.ServerMetric -typealias ServerMetricsModel = TermindApplication.ServerMetricsModel diff --git a/TFM/Application/Repositories/AIConfigurationPersistence.swift b/TFM/Application/Repositories/AIConfigurationPersistence.swift deleted file mode 100644 index e1c2eacd..00000000 --- a/TFM/Application/Repositories/AIConfigurationPersistence.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindApplication - -typealias AIConfigurationSnapshot = TermindApplication.AIConfigurationSnapshot -typealias AIConfigurationPersisting = TermindApplication.AIConfigurationPersisting -typealias AIWebSearchPersisting = TermindApplication.AIWebSearchPersisting diff --git a/TFM/Application/Repositories/CommandHistoryRepository.swift b/TFM/Application/Repositories/CommandHistoryRepository.swift deleted file mode 100644 index 4a061981..00000000 --- a/TFM/Application/Repositories/CommandHistoryRepository.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindApplication - -typealias CommandHistoryRepository = TermindApplication.CommandHistoryRepository -typealias InMemoryCommandHistoryRepository = TermindApplication.InMemoryCommandHistoryRepository diff --git a/TFM/Application/Repositories/ConnectionHistoryRepository.swift b/TFM/Application/Repositories/ConnectionHistoryRepository.swift deleted file mode 100644 index 87506150..00000000 --- a/TFM/Application/Repositories/ConnectionHistoryRepository.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindApplication - -typealias ConnectionHistoryRepository = TermindApplication.ConnectionHistoryRepository -typealias InMemoryConnectionHistoryRepository = TermindApplication.InMemoryConnectionHistoryRepository diff --git a/TFM/Application/Repositories/SSHImportBatch.swift b/TFM/Application/Repositories/SSHImportBatch.swift deleted file mode 100644 index 481365eb..00000000 --- a/TFM/Application/Repositories/SSHImportBatch.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias SSHImportBatch = TermindApplication.SSHImportBatch diff --git a/TFM/Application/Repositories/SSHImportReading.swift b/TFM/Application/Repositories/SSHImportReading.swift deleted file mode 100644 index b38c18c9..00000000 --- a/TFM/Application/Repositories/SSHImportReading.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias SSHImportReading = TermindApplication.SSHImportReading diff --git a/TFM/Application/Repositories/SkillLibraryPersistence.swift b/TFM/Application/Repositories/SkillLibraryPersistence.swift deleted file mode 100644 index a9ccba0f..00000000 --- a/TFM/Application/Repositories/SkillLibraryPersistence.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias SkillLibraryPersisting = TermindApplication.SkillLibraryPersisting diff --git a/TFM/Application/Repositories/TerminalPreferencesPersistence.swift b/TFM/Application/Repositories/TerminalPreferencesPersistence.swift deleted file mode 100644 index 1f71cde5..00000000 --- a/TFM/Application/Repositories/TerminalPreferencesPersistence.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias TerminalPreferencesPersisting = TermindApplication.TerminalPreferencesPersisting diff --git a/TFM/Application/Repositories/VaultPersistence.swift b/TFM/Application/Repositories/VaultPersistence.swift deleted file mode 100644 index 34ed32db..00000000 --- a/TFM/Application/Repositories/VaultPersistence.swift +++ /dev/null @@ -1,6 +0,0 @@ -import TermindApplication -import TermindDomain - -typealias VaultSnapshot = TermindDomain.VaultSnapshot -typealias VaultRemoteApplyResult = TermindDomain.VaultRemoteApplyResult -typealias VaultPersisting = TermindApplication.VaultPersisting diff --git a/TFM/Application/Repositories/VaultStore.swift b/TFM/Application/Repositories/VaultStore.swift deleted file mode 100644 index 613d0b8b..00000000 --- a/TFM/Application/Repositories/VaultStore.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias VaultStore = TermindApplication.VaultStore diff --git a/TFM/Application/Security/AISecretStoring.swift b/TFM/Application/Security/AISecretStoring.swift deleted file mode 100644 index 706be2d7..00000000 --- a/TFM/Application/Security/AISecretStoring.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias AISecretStoring = TermindApplication.AISecretStoring diff --git a/TFM/Application/Security/SecretStoring.swift b/TFM/Application/Security/SecretStoring.swift deleted file mode 100644 index b7fd8446..00000000 --- a/TFM/Application/Security/SecretStoring.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindApplication - -typealias SSHPrivateKeySecret = TermindApplication.SSHPrivateKeySecret -typealias SecretStoring = TermindApplication.SecretStoring -typealias InMemorySecretStore = TermindApplication.InMemorySecretStore diff --git a/TFM/Application/Security/StoredSSHKeyCredential.swift b/TFM/Application/Security/StoredSSHKeyCredential.swift deleted file mode 100644 index c8108583..00000000 --- a/TFM/Application/Security/StoredSSHKeyCredential.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindApplication - -typealias SecureEnclaveKeyCredential = TermindApplication.SecureEnclaveKeyCredential -typealias StoredSSHKeyCredential = TermindApplication.StoredSSHKeyCredential -typealias SecureEnclaveKeyMaterial = TermindApplication.SecureEnclaveKeyMaterial diff --git a/TFM/Application/Speech/SpeechTranscription.swift b/TFM/Application/Speech/SpeechTranscription.swift deleted file mode 100644 index 662a6a87..00000000 --- a/TFM/Application/Speech/SpeechTranscription.swift +++ /dev/null @@ -1,6 +0,0 @@ -import TermindApplication - -typealias SpeechTranscriptionEvent = TermindApplication.SpeechTranscriptionEvent -typealias SpeechTranscriptionFailure = TermindApplication.SpeechTranscriptionFailure -typealias SpeechTranscriptionService = TermindApplication.SpeechTranscriptionService -typealias SpeechTranscriptionCoordinator = TermindApplication.SpeechTranscriptionCoordinator diff --git a/TFM/Application/Transfers/RemotePath.swift b/TFM/Application/Transfers/RemotePath.swift deleted file mode 100644 index 99f0bfbe..00000000 --- a/TFM/Application/Transfers/RemotePath.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindDomain - -typealias RemotePath = TermindDomain.RemotePath -typealias RemoteHomeResolutionState = TermindDomain.RemoteHomeResolutionState diff --git a/TFM/Application/Transfers/SFTPTransferCoordinator.swift b/TFM/Application/Transfers/SFTPTransferCoordinator.swift deleted file mode 100644 index 4ef8d5a0..00000000 --- a/TFM/Application/Transfers/SFTPTransferCoordinator.swift +++ /dev/null @@ -1,9 +0,0 @@ -import TermindRemoteInfrastructure - -typealias SFTPRemoteItemsPayload = TermindRemoteInfrastructure.SFTPRemoteItemsPayload -typealias SFTPTransferConnection = TermindRemoteInfrastructure.SFTPTransferConnection -typealias SFTPRemoteCopySource = TermindRemoteInfrastructure.SFTPRemoteCopySource -typealias SFTPRemoteManifestEntry = TermindRemoteInfrastructure.SFTPRemoteManifestEntry -typealias TransferConflictPolicy = TermindRemoteInfrastructure.TransferConflictPolicy -typealias SFTPTransferSummary = TermindRemoteInfrastructure.SFTPTransferSummary -typealias SFTPTransferCoordinator = TermindRemoteInfrastructure.SFTPTransferCoordinator diff --git a/TFM/Design/TagField.swift b/TFM/Design/TagField.swift index ab3cbd3d..8340f0dc 100644 --- a/TFM/Design/TagField.swift +++ b/TFM/Design/TagField.swift @@ -1,6 +1,7 @@ import AppKit import ListViewKit import SwiftUI +import TermindApplication /// A token field: the tags already chosen, shown as removable chips, over a /// search field that narrows the tags in use elsewhere and offers to create the diff --git a/TFM/Design/TerminalTheme.swift b/TFM/Design/TerminalTheme.swift index 21deb2b1..40bd1f7b 100644 --- a/TFM/Design/TerminalTheme.swift +++ b/TFM/Design/TerminalTheme.swift @@ -2,15 +2,13 @@ import Foundation import GhosttyTerminal import GhosttyTheme import SwiftUI +import TermindApplication /// Fallback colors for the SwiftUI chrome around the terminal. /// /// These palettes drive both the terminal surface and its SwiftUI chrome, which /// keeps banners and empty areas visually continuous around the grid. import TermindDomain -typealias TerminalTheme = TermindTerminalPalette -typealias GhosttyCustomConfiguration = TerminalCustomConfiguration - nonisolated extension TermindTerminalPalette.RGB { var color: Color { Color(.sRGB, red: r, green: g, blue: b, opacity: 1) } } @@ -140,7 +138,7 @@ struct TerminalAppearanceSettings: DynamicProperty { /// should one ever be added upstream. nonisolated enum TermindTerminalThemeCatalog { static let allThemes: [GhosttyThemeDefinition] = { - let termindThemes = TerminalTheme.all.map(\.ghosttyDefinition) + let termindThemes = TermindTerminalPalette.all.map(\.ghosttyDefinition) let termindNames = Set(termindThemes.map(\.name)) return termindThemes + GhosttyThemeCatalog.allThemes.filter { !termindNames.contains($0.name) } }() @@ -157,8 +155,8 @@ nonisolated enum TermindTerminalThemeCatalog { theme(named: name).flatMap { $0.isDark ? nil : $0 } } - static let defaultDark = TerminalTheme.termindDark.ghosttyDefinition - static let defaultLight = TerminalTheme.termindLight.ghosttyDefinition + static let defaultDark = TermindTerminalPalette.termindDark.ghosttyDefinition + static let defaultLight = TermindTerminalPalette.termindLight.ghosttyDefinition static func terminalConfiguration( themeName: String, @@ -170,7 +168,7 @@ nonisolated enum TermindTerminalThemeCatalog { let selected = theme(named: themeName).flatMap { $0.isDark == isDark ? $0 : nil } ?? fallback let base = selected.toTerminalConfiguration() guard customEnabled else { return base } - return GhosttyCustomConfiguration(customText).applying(to: base) + return TerminalCustomConfiguration(customText).applying(to: base) } static func background( @@ -178,16 +176,16 @@ nonisolated enum TermindTerminalThemeCatalog { customEnabled: Bool, customText: String, isDark: Bool - ) -> TerminalTheme.RGB { + ) -> TermindTerminalPalette.RGB { let fallback = isDark ? defaultDark : defaultLight let selected = theme(named: themeName).flatMap { $0.isDark == isDark ? $0 : nil } ?? fallback if customEnabled, - let value = GhosttyCustomConfiguration(customText).lastValue(for: "background"), - let customBackground = TerminalTheme.RGB(hex: value) + let value = TerminalCustomConfiguration(customText).lastValue(for: "background"), + let customBackground = TermindTerminalPalette.RGB(hex: value) { return customBackground } - return TerminalTheme.RGB(hex: selected.background) - ?? (isDark ? TerminalTheme.termindDark.background : TerminalTheme.termindLight.background) + return TermindTerminalPalette.RGB(hex: selected.background) + ?? (isDark ? TermindTerminalPalette.termindDark.background : TermindTerminalPalette.termindLight.background) } } diff --git a/TFM/DesignSystem/LinuxDistributionPresentation.swift b/TFM/DesignSystem/LinuxDistributionPresentation.swift deleted file mode 100644 index 061ca680..00000000 --- a/TFM/DesignSystem/LinuxDistributionPresentation.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindPresentation - -typealias LinuxDistributionIconEntry = TermindPresentation.LinuxDistributionIconEntry -typealias LinuxDistributionIconCatalog = TermindPresentation.LinuxDistributionIconCatalog -typealias LinuxDistributionBrandColor = TermindPresentation.LinuxDistributionBrandColor diff --git a/TFM/Features/AI/AgentToolPresentation.swift b/TFM/Features/AI/AgentToolPresentation.swift index ab8b5b48..171d00a9 100644 --- a/TFM/Features/AI/AgentToolPresentation.swift +++ b/TFM/Features/AI/AgentToolPresentation.swift @@ -1,6 +1,7 @@ import AgentKit import Foundation import TermindApplication +import TermindRemoteInfrastructure /// Turns tool protocol data into the small amount of language the transcript needs. /// Execution, approval and replay continue to use the original call and result unchanged. diff --git a/TFM/Features/AI/CommandCardView.swift b/TFM/Features/AI/CommandCardView.swift index 3ed6059b..84181569 100644 --- a/TFM/Features/AI/CommandCardView.swift +++ b/TFM/Features/AI/CommandCardView.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain extension CommandRisk { diff --git a/TFM/Features/AI/ContextAttachmentImporter.swift b/TFM/Features/AI/ContextAttachmentImporter.swift deleted file mode 100644 index 15745cdc..00000000 --- a/TFM/Features/AI/ContextAttachmentImporter.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindInfrastructure - -typealias ContextAttachmentImporter = TermindInfrastructure.ContextAttachmentImporter diff --git a/TFM/Features/AI/ContextAttachmentOpener.swift b/TFM/Features/AI/ContextAttachmentOpener.swift index 73971a17..4d063dbb 100644 --- a/TFM/Features/AI/ContextAttachmentOpener.swift +++ b/TFM/Features/AI/ContextAttachmentOpener.swift @@ -1,6 +1,7 @@ import AgentKit import AppKit import Foundation +import TermindApplication import UniformTypeIdentifiers nonisolated enum ContextAttachmentOpener { diff --git a/TFM/Features/AI/ContextGaugeView.swift b/TFM/Features/AI/ContextGaugeView.swift index 50f610ba..09fe2c26 100644 --- a/TFM/Features/AI/ContextGaugeView.swift +++ b/TFM/Features/AI/ContextGaugeView.swift @@ -1,5 +1,6 @@ import AgentKit import SwiftUI +import TermindApplication /// How full the model's context window is, as a ring. /// diff --git a/TFM/Features/AI/ConversationComposerState.swift b/TFM/Features/AI/ConversationComposerState.swift deleted file mode 100644 index 149d56d0..00000000 --- a/TFM/Features/AI/ConversationComposerState.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindApplication - -typealias ConversationComposerState = TermindApplication.ConversationComposerState diff --git a/TFM/Features/AI/ConversationModel.swift b/TFM/Features/AI/ConversationModel.swift deleted file mode 100644 index ad78bafd..00000000 --- a/TFM/Features/AI/ConversationModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -import TermindApplication - -typealias SuggestedCommand = TermindApplication.SuggestedCommand -typealias ContextChip = TermindApplication.ContextChip -typealias ContextChipReference = TermindApplication.ContextChipReference -typealias PendingContextAttachment = TermindApplication.PendingContextAttachment -typealias ChatMessage = TermindApplication.ChatMessage -typealias AgentToolCard = TermindApplication.AgentToolCard -typealias PendingSteeringMessage = TermindApplication.PendingSteeringMessage -typealias TimelineItem = TermindApplication.TimelineItem -typealias AgentToolBatch = TermindApplication.AgentToolBatch -typealias AgentToolReasoning = TermindApplication.AgentToolReasoning -typealias AgentWorkItem = TermindApplication.AgentWorkItem -typealias AgentWorkGroup = TermindApplication.AgentWorkGroup -typealias AgentWorkPresentation = TermindApplication.AgentWorkPresentation -typealias AgentCompactionActivity = TermindApplication.AgentCompactionActivity -typealias AgentCompactionMark = TermindApplication.AgentCompactionMark -typealias AgentPlanCard = TermindApplication.AgentPlanCard -typealias ConversationModel = TermindApplication.ConversationModel -typealias WeakAgentUserInteraction = TermindApplication.WeakAgentUserInteraction diff --git a/TFM/Features/AI/ConversationTranscriptView.swift b/TFM/Features/AI/ConversationTranscriptView.swift index cbca70f9..22a03676 100644 --- a/TFM/Features/AI/ConversationTranscriptView.swift +++ b/TFM/Features/AI/ConversationTranscriptView.swift @@ -1,5 +1,6 @@ import AgentKit import SwiftUI +import TermindApplication import TermindDomain import struct TermindDomain.Host @@ -591,7 +592,7 @@ private struct AgentSSHAuthenticationCard: View { .padding(.bottom, Metrics.Space.s) } - SSHAuthenticationForm( + DesktopSSHAuthenticationForm( prompt: request.journeyPrompt, submit: submit, cancel: { @@ -635,7 +636,7 @@ private struct AgentSSHHostKeyCard: View { } .padding(.bottom, Metrics.Space.s) } - SSHHostKeyConfirmationForm( + DesktopSSHHostKeyConfirmationForm( prompt: request.prompt, displayAddress: request.address, proceed: { conversation.respondToSSHHostKey(.continueOnce) }, diff --git a/TFM/Features/AI/ConversationViews.swift b/TFM/Features/AI/ConversationViews.swift index 7da796e5..0d93b767 100644 --- a/TFM/Features/AI/ConversationViews.swift +++ b/TFM/Features/AI/ConversationViews.swift @@ -2,6 +2,7 @@ import AgentKit import AppKit import ListViewKit import SwiftUI +import TermindApplication import TermindDomain import TermindInfrastructure import UniformTypeIdentifiers diff --git a/TFM/Features/AI/Markdown/MarkdownSoftBreaks.swift b/TFM/Features/AI/Markdown/MarkdownSoftBreaks.swift index 50442907..c59c50f9 100644 --- a/TFM/Features/AI/Markdown/MarkdownSoftBreaks.swift +++ b/TFM/Features/AI/Markdown/MarkdownSoftBreaks.swift @@ -1,4 +1,5 @@ import Foundation +import TermindApplication /// Turns paragraph-internal newlines into hard breaks before parsing. /// diff --git a/TFM/Features/AI/MarkdownBlockListView.swift b/TFM/Features/AI/MarkdownBlockListView.swift index 7c7ecfca..9fa646ac 100644 --- a/TFM/Features/AI/MarkdownBlockListView.swift +++ b/TFM/Features/AI/MarkdownBlockListView.swift @@ -3,6 +3,7 @@ import Combine import MarkdownParser import MarkdownView import SwiftUI +import TermindApplication /// The Markdown entry point used by the transcript. /// diff --git a/TFM/Features/AI/MarkdownCodeBlockView.swift b/TFM/Features/AI/MarkdownCodeBlockView.swift index c9409dde..d35e696e 100644 --- a/TFM/Features/AI/MarkdownCodeBlockView.swift +++ b/TFM/Features/AI/MarkdownCodeBlockView.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain /// The one fenced block Termind owns instead of MarkdownView: a shell command diff --git a/TFM/Features/AI/MessageViews.swift b/TFM/Features/AI/MessageViews.swift index 40ab459e..89d1a8c4 100644 --- a/TFM/Features/AI/MessageViews.swift +++ b/TFM/Features/AI/MessageViews.swift @@ -1,4 +1,5 @@ import Foundation +import TermindApplication nonisolated enum ChatMessageTimestampFormatter { static func string( diff --git a/TFM/Features/AI/SessionContext+Presentation.swift b/TFM/Features/AI/SessionContext+Presentation.swift index e20ba282..802c54f8 100644 --- a/TFM/Features/AI/SessionContext+Presentation.swift +++ b/TFM/Features/AI/SessionContext+Presentation.swift @@ -1,3 +1,4 @@ +import TermindApplication import TermindDomain extension SessionContext { diff --git a/TFM/Features/AI/SlashCommandMenuView.swift b/TFM/Features/AI/SlashCommandMenuView.swift index 3e53c4db..0b868fc1 100644 --- a/TFM/Features/AI/SlashCommandMenuView.swift +++ b/TFM/Features/AI/SlashCommandMenuView.swift @@ -1,6 +1,7 @@ import AppKit import ListViewKit import SwiftUI +import TermindApplication /// The palette that opens when a draft starts with `/`. /// diff --git a/TFM/Features/AI/TranscriptListView.swift b/TFM/Features/AI/TranscriptListView.swift index 82be6e18..cf04a14f 100644 --- a/TFM/Features/AI/TranscriptListView.swift +++ b/TFM/Features/AI/TranscriptListView.swift @@ -3,6 +3,7 @@ import AppKit import ListViewKit import Observation import SwiftUI +import TermindApplication nonisolated struct TranscriptEntry: Identifiable, Hashable, Sendable { enum Kind: Hashable, Sendable { diff --git a/TFM/Features/AI/TranscriptNativeRows.swift b/TFM/Features/AI/TranscriptNativeRows.swift index 7df5e50d..4a2be86d 100644 --- a/TFM/Features/AI/TranscriptNativeRows.swift +++ b/TFM/Features/AI/TranscriptNativeRows.swift @@ -3,6 +3,7 @@ import AppKit import ListViewKit import MarkdownView import SwiftUI +import TermindApplication import TermindDomain @MainActor diff --git a/TFM/Features/Assistant/AssistantView.swift b/TFM/Features/Assistant/AssistantView.swift index d6ef8b6e..032411f0 100644 --- a/TFM/Features/Assistant/AssistantView.swift +++ b/TFM/Features/Assistant/AssistantView.swift @@ -1,6 +1,7 @@ import AppKit import ListViewKit import SwiftUI +import TermindApplication /// Assistant's conversation navigator inside the window's shared sidebar: the /// chat list, over a bar holding the two things you do to the list itself. @@ -277,8 +278,8 @@ private struct AssistantConversationList: View { @Environment(WindowModel.self) private var window @Environment(\.locale) private var locale - @AppStorage(SettingsKey.pinnedChatsSectionExpanded) private var pinnedExpanded = true - @AppStorage(SettingsKey.chatsSectionExpanded) private var chatsExpanded = true + @AppStorage(DesktopSettingsKey.pinnedChatsSectionExpanded) private var pinnedExpanded = true + @AppStorage(DesktopSettingsKey.chatsSectionExpanded) private var chatsExpanded = true @State private var deleteTarget: ConversationModel? @State private var renamingID: ConversationModel.ID? @State private var renameTitle = "" @@ -568,8 +569,8 @@ nonisolated struct AssistantConversationListItem: Identifiable, Hashable, Sendab /// see `AssistantSectionHeaderRow`. var expansionKey: String { switch self { - case .pinned: SettingsKey.pinnedChatsSectionExpanded - case .chats: SettingsKey.chatsSectionExpanded + case .pinned: DesktopSettingsKey.pinnedChatsSectionExpanded + case .chats: DesktopSettingsKey.chatsSectionExpanded } } } diff --git a/TFM/Features/Commerce/ProPaywallView.swift b/TFM/Features/Commerce/ProPaywallView.swift index 593d555b..3554d2e0 100644 --- a/TFM/Features/Commerce/ProPaywallView.swift +++ b/TFM/Features/Commerce/ProPaywallView.swift @@ -1,4 +1,6 @@ import SwiftUI +import TermindApplication +import TermindInfrastructure struct ProPaywallView: View { @Environment(AppModel.self) private var app diff --git a/TFM/Features/ExternalRequests/ExternalAuthCoordinator.swift b/TFM/Features/ExternalRequests/ExternalAuthCoordinator.swift index b7cb2b6e..fbec1f61 100644 --- a/TFM/Features/ExternalRequests/ExternalAuthCoordinator.swift +++ b/TFM/Features/ExternalRequests/ExternalAuthCoordinator.swift @@ -1,5 +1,6 @@ import AgentKit import Foundation +import TermindApplication #if canImport(AppKit) import AppKit diff --git a/TFM/Features/ExternalRequests/ExternalAuthWindow.swift b/TFM/Features/ExternalRequests/ExternalAuthWindow.swift index 64de979d..0dcec4c6 100644 --- a/TFM/Features/ExternalRequests/ExternalAuthWindow.swift +++ b/TFM/Features/ExternalRequests/ExternalAuthWindow.swift @@ -1,6 +1,7 @@ import AgentKit import AppKit import SwiftUI +import TermindApplication struct ExternalAuthWindowPresenter: View { static let sceneID = "external-ssh-authentication" @@ -102,7 +103,7 @@ struct ExternalAuthWindow: View { private func requestView(_ pending: ExternalAuthCoordinator.Pending) -> some View { switch pending.request { case .ssh(let request): - SSHAuthenticationForm( + DesktopSSHAuthenticationForm( prompt: request.journeyPrompt, submit: { identityID, username, password, passphrase, keyID, save in app.externalAuthentication.respondSSH( @@ -133,7 +134,7 @@ struct ExternalAuthWindow: View { ) .id(pending.id) case .hostKey(let request): - SSHHostKeyConfirmationForm( + DesktopSSHHostKeyConfirmationForm( prompt: request.prompt, displayAddress: request.address, proceed: { diff --git a/TFM/Features/HostChooser/HostChooserView.swift b/TFM/Features/HostChooser/HostChooserView.swift index 4c2f4bd5..f129ddb3 100644 --- a/TFM/Features/HostChooser/HostChooserView.swift +++ b/TFM/Features/HostChooser/HostChooserView.swift @@ -2,6 +2,7 @@ import AppKit import Darwin import ListViewKit import SwiftUI +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFM/Features/Keychain/KeychainPageView.swift b/TFM/Features/Keychain/KeychainPageView.swift index 6d980eeb..fa40beba 100644 --- a/TFM/Features/Keychain/KeychainPageView.swift +++ b/TFM/Features/Keychain/KeychainPageView.swift @@ -1,6 +1,7 @@ import AppKit import ListViewKit import SwiftUI +import TermindApplication import TermindDomain import TermindPresentation import TermindRemoteInfrastructure @@ -352,9 +353,6 @@ struct KeychainImportDetail: View { } } -typealias SoftwareKeyFileImportResult = TermindRemoteInfrastructure.SoftwareKeyFileImportResult -typealias SoftwareKeyFileImporter = TermindRemoteInfrastructure.SoftwareKeyFileImporter - private struct SoftwareKeyEditor: View { private enum FileChoice { case privateKey diff --git a/TFM/Features/Metrics/MetricsConnectionNotice.swift b/TFM/Features/Metrics/MetricsConnectionNotice.swift index c871f305..2fd63cf1 100644 --- a/TFM/Features/Metrics/MetricsConnectionNotice.swift +++ b/TFM/Features/Metrics/MetricsConnectionNotice.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication /// Connection failures stay actionable even though healthy metrics need no status footer. struct MetricsConnectionNotice: View { diff --git a/TFM/Features/Metrics/MetricsDiskTable.swift b/TFM/Features/Metrics/MetricsDiskTable.swift deleted file mode 100644 index 771de53c..00000000 --- a/TFM/Features/Metrics/MetricsDiskTable.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPresentation - -typealias MetricsDiskTable = TermindPresentation.MetricsDiskTable diff --git a/TFM/Features/Metrics/MetricsGPUSection.swift b/TFM/Features/Metrics/MetricsGPUSection.swift deleted file mode 100644 index cc58b78f..00000000 --- a/TFM/Features/Metrics/MetricsGPUSection.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPresentation - -typealias MetricsGPUSection = TermindPresentation.MetricsGPUSection diff --git a/TFM/Features/Metrics/MetricsInstrumentBody.swift b/TFM/Features/Metrics/MetricsInstrumentBody.swift deleted file mode 100644 index 8f760779..00000000 --- a/TFM/Features/Metrics/MetricsInstrumentBody.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPresentation - -typealias MetricsInstrumentBody = TermindPresentation.MetricsInstrumentBody diff --git a/TFM/Features/Metrics/MetricsJourneyView.swift b/TFM/Features/Metrics/MetricsJourneyView.swift index f0d5dd85..90c12685 100644 --- a/TFM/Features/Metrics/MetricsJourneyView.swift +++ b/TFM/Features/Metrics/MetricsJourneyView.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication /// Metrics uses the same authentication and host-key decisions as SSH/SFTP, /// without creating an interactive terminal or a second transport owner. diff --git a/TFM/Features/Metrics/MetricsManagedCardPage.swift b/TFM/Features/Metrics/MetricsManagedCardPage.swift index 23b459bb..225ab2b7 100644 --- a/TFM/Features/Metrics/MetricsManagedCardPage.swift +++ b/TFM/Features/Metrics/MetricsManagedCardPage.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication /// Uses the same native selection, hover, marquee and drag renderer as Hosts. struct MetricsManagedCardPage: ManagedCardPage { diff --git a/TFM/Features/Metrics/MetricsView.swift b/TFM/Features/Metrics/MetricsView.swift index cfd63cd9..3aa564ae 100644 --- a/TFM/Features/Metrics/MetricsView.swift +++ b/TFM/Features/Metrics/MetricsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain import TermindPresentation @@ -174,8 +175,6 @@ struct ServerMetricsCard: View { } -typealias MetricsRing = TermindPresentation.MetricsRing - struct MetricsProcessList: View { let server: ServerMetric @Environment(WindowModel.self) private var window @@ -241,8 +240,6 @@ struct MetricsProcessList: View { } } -typealias MetricsFormat = TermindPresentation.MetricsFormat - struct MetricsDevicePicker: View { let server: ServerMetric let model: ServerMetricsModel diff --git a/TFM/Features/Ports/PortForwardingEditorState.swift b/TFM/Features/Ports/PortForwardingEditorState.swift index 7c76444b..555ec18f 100644 --- a/TFM/Features/Ports/PortForwardingEditorState.swift +++ b/TFM/Features/Ports/PortForwardingEditorState.swift @@ -1,5 +1,6 @@ import Foundation import TermindDomain +import TermindRemoteInfrastructure import struct TermindDomain.Host diff --git a/TFM/Features/Ports/PortForwardingView.swift b/TFM/Features/Ports/PortForwardingView.swift index c483feba..73ffde2a 100644 --- a/TFM/Features/Ports/PortForwardingView.swift +++ b/TFM/Features/Ports/PortForwardingView.swift @@ -1,6 +1,7 @@ import SwiftUI import TermindDomain import TermindPresentation +import TermindRemoteInfrastructure import struct TermindDomain.Host diff --git a/TFM/Features/SFTP/SFTPBrowserView.swift b/TFM/Features/SFTP/SFTPBrowserView.swift index 8be36f55..06cbf176 100644 --- a/TFM/Features/SFTP/SFTPBrowserView.swift +++ b/TFM/Features/SFTP/SFTPBrowserView.swift @@ -1,6 +1,8 @@ import ListViewKit import SwiftUI +import TermindDomain import TermindPresentation +import TermindRemoteInfrastructure import UniformTypeIdentifiers #if canImport(AppKit) diff --git a/TFM/Features/SFTP/SFTPPermissions.swift b/TFM/Features/SFTP/SFTPPermissions.swift deleted file mode 100644 index a2b5ac28..00000000 --- a/TFM/Features/SFTP/SFTPPermissions.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindDomain - -typealias SFTPPermissionDraft = TermindDomain.SFTPPermissionDraft diff --git a/TFM/Features/SFTP/SFTPSessionModel.swift b/TFM/Features/SFTP/SFTPSessionModel.swift index c3d8f773..2af71127 100644 --- a/TFM/Features/SFTP/SFTPSessionModel.swift +++ b/TFM/Features/SFTP/SFTPSessionModel.swift @@ -261,8 +261,6 @@ final class SFTPSessionModel: Identifiable { } private var connectionAttemptID: UUID? private var reconnectController: SessionReconnectController { connection.reconnect } - private var hasConnectedOnce = false - private var isRecoveringConnection = false private var operationCancellation: TransferCancellationToken? weak var navigationRouter: (any SFTPSessionNavigationRouting)? private weak var statusStore: (any RemoteSessionHostStore)? @@ -291,7 +289,7 @@ final class SFTPSessionModel: Identifiable { self.homeResolution = homeResolution self.automaticRefreshInterval = automaticRefreshInterval if client != nil { status = .connected } - if client != nil { hasConnectedOnce = true } + if client != nil { _ = connection.markConnected() } } var reconnectState: ReconnectState? { connection.reconnectState } @@ -359,14 +357,13 @@ final class SFTPSessionModel: Identifiable { } private func startConnection(using store: any RemoteSessionHostStore) { - let reconnecting = hasConnectedOnce && isRecoveringConnection + let reconnecting = connection.hasConnectedOnce && connection.isRecovering homeResolution = .pending didResolveHomeForConnection = false bindStatus(to: store) connectionTask?.cancel() guard let host = store.host(hostID) else { - reconnectController.stop() - isRecoveringConnection = false + connection.stopRecovery() let message = String(localized: "The host is no longer available.") status = .failed(message) homeResolution = .unavailable @@ -459,11 +456,8 @@ final class SFTPSessionModel: Identifiable { self.suppliedAuthentications.removeAll() self.temporaryHostKeys.removeAll() self.pendingAuthentication = nil - let initialConnection = !self.hasConnectedOnce let presentsConnectionJourney = self.connectionJourney.isPresented - self.hasConnectedOnce = true - self.isRecoveringConnection = false - self.reconnectController.markConnected(initial: initialConnection) + _ = self.connection.markConnected() self.status = .connected if presentsConnectionJourney { self.connectionJourney.succeed() @@ -517,7 +511,7 @@ final class SFTPSessionModel: Identifiable { preserveSubmission: retryNeedsPassphrase && submitted != nil ) } catch { - if self.hasConnectedOnce || reconnecting { + if self.connection.hasConnectedOnce || reconnecting { self.scheduleReconnect(after: error.localizedDescription, using: store) } else { self.status = .failed(error.localizedDescription) @@ -549,8 +543,6 @@ final class SFTPSessionModel: Identifiable { connectionTask = nil connectionAttemptID = nil connection.disconnect() - hasConnectedOnce = false - isRecoveringConnection = false automaticRefreshTask?.cancel() automaticRefreshTask = nil uploadRefreshTask?.cancel() @@ -575,8 +567,7 @@ final class SFTPSessionModel: Identifiable { func stopReconnecting() { guard let reconnectState else { return } - reconnectController.stop() - isRecoveringConnection = false + connection.stopRecovery() connectionAttemptID = nil connectionTask?.cancel() connectionTask = nil @@ -1340,7 +1331,7 @@ final class SFTPSessionModel: Identifiable { hostID: host.id, fingerprint: observation.fingerprint ) } catch { - if hasConnectedOnce { + if connection.hasConnectedOnce { scheduleReconnect(after: error.localizedDescription, using: store) } else { status = .failed(error.localizedDescription) @@ -1354,7 +1345,7 @@ final class SFTPSessionModel: Identifiable { requiresPassphrase: Bool? = nil, preserveSubmission: Bool = false ) { - reconnectController.pauseForUserInteraction() + connection.pauseForUserInteraction() if !preserveSubmission { suppliedAuthentications[host.id] = nil } let identity = store.identity(for: host) let submission = suppliedAuthentications[host.id] @@ -1392,14 +1383,13 @@ final class SFTPSessionModel: Identifiable { } private func handleConnectionFailure(_ message: String, attemptID: UUID?) { - guard hasConnectedOnce, connectionAttemptID == attemptID, + guard connection.hasConnectedOnce, connectionAttemptID == attemptID, let store = statusStore else { return } scheduleReconnect(after: message, using: store) } private func scheduleReconnect(after message: String, using store: any RemoteSessionHostStore) { - isRecoveringConnection = true connectionAttemptID = nil connectionTask?.cancel() connectionTask = nil @@ -1417,7 +1407,7 @@ final class SFTPSessionModel: Identifiable { host: host, using: store, purpose: .sftpSession, username: host.flatMap { store.identity(for: $0)?.username } ) - reconnectController.schedule(after: message) { [weak self, weak store] in + connection.scheduleRecovery(after: message) { [weak self, weak store] in guard let self, let store else { return } self.startConnection(using: store) } diff --git a/TFM/Features/SSHImport/SSHImportView.swift b/TFM/Features/SSHImport/SSHImportView.swift index fb39f0ce..c9f85624 100644 --- a/TFM/Features/SSHImport/SSHImportView.swift +++ b/TFM/Features/SSHImport/SSHImportView.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain struct SSHImportView: View { diff --git a/TFM/Features/Settings/MCPServerEditorSheet.swift b/TFM/Features/Settings/MCPServerEditorSheet.swift index 99f66b63..d476c14b 100644 --- a/TFM/Features/Settings/MCPServerEditorSheet.swift +++ b/TFM/Features/Settings/MCPServerEditorSheet.swift @@ -1,5 +1,6 @@ import AgentKit import SwiftUI +import TermindApplication @Observable final class MCPServerEditorSession { diff --git a/TFM/Features/Settings/ModelProviderEditorSheet.swift b/TFM/Features/Settings/ModelProviderEditorSheet.swift index f8dd9846..583ff54c 100644 --- a/TFM/Features/Settings/ModelProviderEditorSheet.swift +++ b/TFM/Features/Settings/ModelProviderEditorSheet.swift @@ -1,5 +1,6 @@ import AgentKit import SwiftUI +import TermindApplication import TermindDomain import TermindPresentation diff --git a/TFM/Features/Settings/SettingsPageView.swift b/TFM/Features/Settings/SettingsPageView.swift index 3c9eb6c9..6405e839 100644 --- a/TFM/Features/Settings/SettingsPageView.swift +++ b/TFM/Features/Settings/SettingsPageView.swift @@ -1,6 +1,7 @@ import AgentKit import GhosttyTheme import SwiftUI +import TermindApplication import TermindDomain import struct TermindDomain.Host @@ -352,8 +353,8 @@ struct SettingsFormPage: View { } private struct AppSettings: View { - @AppStorage(SettingsKey.showMetricsInMenuBar) private var showMetricsInMenuBar = true - @AppStorage(SettingsKey.appAppearance) private var appearanceID = AppAppearance.system.rawValue + @AppStorage(DesktopSettingsKey.showMetricsInMenuBar) private var showMetricsInMenuBar = true + @AppStorage(DesktopSettingsKey.appAppearance) private var appearanceID = AppAppearance.system.rawValue @AppStorage(SettingsKey.appLanguage) private var languageID = AppLanguage.system.rawValue @State private var launchLanguage = AppLanguage.stored() @@ -517,9 +518,9 @@ private struct ProSettings: View { private struct StartupSettings: View { @Environment(\.appearsActive) private var appearsActive @State private var loginItem = LoginItemController() - @AppStorage(SettingsKey.openAtLoginQuietly) private var opensQuietly = - SettingsKey.defaultOpenAtLoginQuietly - @AppStorage(SettingsKey.restoreWorkspaces) private var restoreWorkspaces = true + @AppStorage(DesktopSettingsKey.openAtLoginQuietly) private var opensQuietly = + DesktopSettingsKey.defaultOpenAtLoginQuietly + @AppStorage(DesktopSettingsKey.restoreWorkspaces) private var restoreWorkspaces = true var body: some View { Section("Startup") { diff --git a/TFM/Features/Settings/SyncSettings.swift b/TFM/Features/Settings/SyncSettings.swift index a311b49d..dfc349dc 100644 --- a/TFM/Features/Settings/SyncSettings.swift +++ b/TFM/Features/Settings/SyncSettings.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain struct SyncSettings: View { diff --git a/TFM/Features/Settings/TerminalCustomConfigurationEditorSheet.swift b/TFM/Features/Settings/TerminalCustomConfigurationEditorSheet.swift index b9b1b932..2e7ff163 100644 --- a/TFM/Features/Settings/TerminalCustomConfigurationEditorSheet.swift +++ b/TFM/Features/Settings/TerminalCustomConfigurationEditorSheet.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain import UniformTypeIdentifiers diff --git a/TFM/Features/Shell/WindowRoute.swift b/TFM/Features/Shell/WindowRoute.swift index a901d53e..b2eb8882 100644 --- a/TFM/Features/Shell/WindowRoute.swift +++ b/TFM/Features/Shell/WindowRoute.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFM/Features/Shell/WindowTitle.swift b/TFM/Features/Shell/WindowTitle.swift index 96d97c9a..3b01f817 100644 --- a/TFM/Features/Shell/WindowTitle.swift +++ b/TFM/Features/Shell/WindowTitle.swift @@ -1,4 +1,5 @@ import Foundation +import TermindApplication /// What the titlebar calls whatever is in front. /// diff --git a/TFM/Features/Sidebar/WindowSidebarView.swift b/TFM/Features/Sidebar/WindowSidebarView.swift index 5759ae5c..169f9e78 100644 --- a/TFM/Features/Sidebar/WindowSidebarView.swift +++ b/TFM/Features/Sidebar/WindowSidebarView.swift @@ -1,6 +1,7 @@ import AppKit import ListViewKit import SwiftUI +import TermindApplication import TermindDomain import struct TermindDomain.Host @@ -301,7 +302,7 @@ private struct SnippetSidebarSectionHeaderRow: View { let title: String let count: Int - @AppStorage(SettingsKey.snippetsSidebarCollapsedSections) + @AppStorage(DesktopSettingsKey.snippetsSidebarCollapsedSections) private var collapsedSections = "" private var isExpanded: Bool { @@ -464,7 +465,7 @@ private struct SnippetsSidebarView: View { @State private var query = "" @State private var destructiveSnippet: Snippet? @State private var selectedSnippetID: Snippet.ID? - @AppStorage(SettingsKey.snippetsSidebarCollapsedSections) + @AppStorage(DesktopSettingsKey.snippetsSidebarCollapsedSections) private var collapsedSections = "" var body: some View { diff --git a/TFM/Features/Snippets/SnippetsPageView.swift b/TFM/Features/Snippets/SnippetsPageView.swift index 59a56d54..995b0ebc 100644 --- a/TFM/Features/Snippets/SnippetsPageView.swift +++ b/TFM/Features/Snippets/SnippetsPageView.swift @@ -1,6 +1,7 @@ import AppKit import ListViewKit import SwiftUI +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFM/Features/StatusBar/StatusBarView.swift b/TFM/Features/StatusBar/StatusBarView.swift index 7e0e4c57..434919a2 100644 --- a/TFM/Features/StatusBar/StatusBarView.swift +++ b/TFM/Features/StatusBar/StatusBarView.swift @@ -1,7 +1,9 @@ import AppKit import ListViewKit import SwiftUI +import TermindApplication import TermindDomain +import TermindRemoteInfrastructure import struct TermindDomain.Host @@ -47,7 +49,7 @@ struct StatusBarView: View { @Environment(AppModel.self) private var app @Environment(WindowModel.self) private var window @Environment(\.accessibilityReduceMotion) private var reduceMotion - @AppStorage(SettingsKey.statusBarExpanded) private var isExpanded = true + @AppStorage(DesktopSettingsKey.statusBarExpanded) private var isExpanded = true private var pane: Pane? { window.selectedWorkspace?.focusedPane } private var showsSessionContext: Bool { pane?.kind.showsStatusBar == true } diff --git a/TFM/Features/Terminal/SSHSessionModel.swift b/TFM/Features/Terminal/SSHSessionModel.swift index 6b39fcff..b56dd800 100644 --- a/TFM/Features/Terminal/SSHSessionModel.swift +++ b/TFM/Features/Terminal/SSHSessionModel.swift @@ -4,6 +4,7 @@ import GhosttyTerminal import GhosttyTheme import TermindApplication import TermindDomain +import TermindRemoteInfrastructure import struct TermindDomain.Host @@ -85,42 +86,6 @@ protocol SSHSessionInputRouting: AnyObject { func closeSSHSessionPane(afterExit sshSession: SSHSessionModel) } -/// Search order follows Ghostty and VS Code's terminal find widget. A search -/// starts at the bottom of the buffer, so "previous" moves toward older output -/// and is the operation bound to Return and the upward arrow. -nonisolated enum TerminalFindDirection: String, Sendable { - case previous - case next - - var bindingAction: String { "navigate_search:\(rawValue)" } -} - -nonisolated enum TerminalFindBinding { - static func search(_ query: String) -> String { "search:\(query)" } - static func navigate(_ direction: TerminalFindDirection) -> String { - direction.bindingAction - } - static let end = "end_search" -} - -nonisolated enum TerminalDraftError: LocalizedError, Sendable { - case unavailable, alreadyPending, timedOut - - var errorDescription: String? { - switch self { - case .unavailable: - String( - localized: - "Command Generator requires a supported shell prompt with Shell Integration enabled." - ) - case .alreadyPending: - String(localized: "Command Generator is already reading the current input.") - case .timedOut: - String(localized: "The remote shell did not return its current input.") - } - } -} - /// One terminal sshSession inside a pane. /// /// The screen buffer lives inside libghostty. This model bridges its in-memory @@ -208,12 +173,7 @@ final class SSHSessionModel: Identifiable { } private var activeShellCommand: ActiveShellCommand? private(set) var commandGeneratorRevision: UInt64 = 0 - private struct PendingDraftRequest { - var id: UUID - var continuation: CheckedContinuation - } - private var pendingDraftRequest: PendingDraftRequest? - private var draftRequestTimeoutTask: Task? + private let commandDraft = TerminalCommandDraftController() /// Non-nil while a connection is waiting for the user to approve a first or /// additional server fingerprint. No credential is sent before this clears. @@ -241,8 +201,6 @@ final class SSHSessionModel: Identifiable { private var connectionTask: Task? private var connectionAttemptID: UUID? private var reconnectController: SessionReconnectController { connection.reconnect } - private var hasConnectedOnce = false - private var isRecoveringConnection = false private var workingDirectoryObservation: AnyCancellable? private var remoteWindowTitleObservation: AnyCancellable? /// The exact authenticated route used by the live terminal, retained only @@ -441,31 +399,12 @@ final class SSHSessionModel: Identifiable { var canGenerateFromCurrentDraft: Bool { status.isLive && shellIntegrationAvailable && isShellPromptReady - && Self.supportedGeneratorShells.contains(integratedShell ?? "") + && TerminalCommandDraftController.supportedShells.contains(integratedShell ?? "") } func requestCurrentDraft() async throws -> String { - guard canGenerateFromCurrentDraft else { throw TerminalDraftError.unavailable } - guard pendingDraftRequest == nil else { throw TerminalDraftError.alreadyPending } - let requestID = UUID() - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { - (continuation: CheckedContinuation) in - pendingDraftRequest = PendingDraftRequest( - id: requestID, continuation: continuation - ) - writeInputDirectly(Self.commandGeneratorDraftRequest) - draftRequestTimeoutTask?.cancel() - draftRequestTimeoutTask = Task { [weak self] in - try? await Task.sleep(for: .seconds(2)) - guard !Task.isCancelled else { return } - self?.failDraftRequest(id: requestID, with: TerminalDraftError.timedOut) - } - } - } onCancel: { - Task { @MainActor [weak self] in - self?.failDraftRequest(id: requestID, with: CancellationError()) - } + try await commandDraft.read(isAvailable: canGenerateFromCurrentDraft) { [weak self] data in + self?.writeInputDirectly(data) } } @@ -473,27 +412,7 @@ final class SSHSessionModel: Identifiable { /// then inserts one literal command as a bracketed paste. There is /// intentionally no carriage return in this payload. func replaceCurrentDraft(with command: String) { - var data = Self.commandGeneratorDraftClear - data.append(Self.bracketedPasteStart) - data.append(Data(command.utf8)) - data.append(Self.bracketedPasteEnd) - writeInputDirectly(data) - } - - private func failDraftRequest(id: UUID, with error: any Error) { - guard pendingDraftRequest?.id == id, let request = pendingDraftRequest else { return } - pendingDraftRequest = nil - draftRequestTimeoutTask?.cancel() - draftRequestTimeoutTask = nil - request.continuation.resume(throwing: error) - } - - private func completeDraftRequest(with draft: String) { - guard let request = pendingDraftRequest else { return } - pendingDraftRequest = nil - draftRequestTimeoutTask?.cancel() - draftRequestTimeoutTask = nil - request.continuation.resume(returning: draft) + writeInputDirectly(commandDraft.replacementPayload(for: command)) } private func invalidateCommandGeneration() { @@ -507,9 +426,7 @@ final class SSHSessionModel: Identifiable { isShellPromptReady = false shellIntegrationAvailable = false activeShellCommand = nil - if let request = pendingDraftRequest { - failDraftRequest(id: request.id, with: TerminalDraftError.unavailable) - } + commandDraft.cancel() invalidateCommandGeneration() } @@ -519,12 +436,6 @@ final class SSHSessionModel: Identifiable { homeResolution = .unavailable } - private static let supportedGeneratorShells: Set = ["bash", "zsh", "fish"] - private static let commandGeneratorDraftRequest = Data("\u{1B}[777;1~".utf8) - private static let commandGeneratorDraftClear = Data("\u{1B}[777;2~".utf8) - private static let bracketedPasteStart = Data("\u{1B}[200~".utf8) - private static let bracketedPasteEnd = Data("\u{1B}[201~".utf8) - func connect(using store: any RemoteSessionHostStore) { startConnection(using: store) } @@ -532,8 +443,8 @@ final class SSHSessionModel: Identifiable { private func startConnection(using store: any RemoteSessionHostStore) { resetCommandGeneratorShellState() remoteWindowTitle = nil - homeResolution = SettingsKey.isShellIntegrationEnabled ? .pending : .unavailable - let reconnecting = hasConnectedOnce && isRecoveringConnection + homeResolution = DesktopSettingsKey.isShellIntegrationEnabled ? .pending : .unavailable + let reconnecting = connection.hasConnectedOnce && connection.isRecovering workingDirectory = nil transferConfiguration = nil commandExcludedFromHistory = nil @@ -543,8 +454,7 @@ final class SSHSessionModel: Identifiable { if let ssh { Task { await ssh.disconnect() } } ssh = nil guard let host = store.host(hostID) ?? transientHost else { - reconnectController.stop() - isRecoveringConnection = false + connection.stopRecovery() pendingInitialCommand = nil connectionAttemptID = nil status = .failed(String(localized: "The host is no longer available.")) @@ -937,8 +847,6 @@ final class SSHSessionModel: Identifiable { suppliedAuthentications.removeAll() connectionAttemptID = nil connection.disconnect() - hasConnectedOnce = false - isRecoveringConnection = false connectionTask?.cancel() connectionTask = nil if let ssh { Task { await ssh.disconnect() } } @@ -953,8 +861,7 @@ final class SSHSessionModel: Identifiable { func stopReconnecting() { guard let reconnectState else { return } remoteWindowTitle = nil - reconnectController.stop() - isRecoveringConnection = false + connection.stopRecovery() connectionAttemptID = nil connectionTask?.cancel() connectionTask = nil @@ -970,7 +877,7 @@ final class SSHSessionModel: Identifiable { proxy: SSHTransportProxy?, jumps: [SSHRouteHopConfiguration], attemptID: UUID ) async throws { - let wasReconnecting = isRecoveringConnection + let wasReconnecting = connection.isRecovering let presentsConnectionJourney = connectionJourney.isPresented let configuration = SSHConnectionConfiguration( hostID: host.id, host: host.host, port: host.port, username: username, @@ -1001,7 +908,7 @@ final class SSHSessionModel: Identifiable { credentials: configuration.credentials, expectedHostKeys: configuration.expectedHostKeys, proxy: configuration.proxy, jumps: configuration.jumps, - shellIntegrationEnabled: SettingsKey.isShellIntegrationEnabled, + shellIntegrationEnabled: DesktopSettingsKey.isShellIntegrationEnabled, columns: latestViewport.columns, rows: latestViewport.rows, progress: progressHandler(attemptID: attemptID) ) @@ -1010,10 +917,7 @@ final class SSHSessionModel: Identifiable { transferConfiguration = configuration suppliedAuthentications.removeAll() temporaryHostKeys.removeAll() - let initialConnection = !hasConnectedOnce - hasConnectedOnce = true - isRecoveringConnection = false - reconnectController.markConnected(initial: initialConnection) + _ = connection.markConnected() status = .connected if presentsConnectionJourney { connectionJourney.succeed() @@ -1048,9 +952,7 @@ final class SSHSessionModel: Identifiable { ssh = nil switch result { case .success: - reconnectController.stop() - hasConnectedOnce = false - isRecoveringConnection = false + connection.resetRecovery() status = .disconnected inputRouter?.closeSSHSessionPane(afterExit: self) case .failure(let error): @@ -1062,7 +964,7 @@ final class SSHSessionModel: Identifiable { _ error: any Error, store: any RemoteSessionHostStore, reconnecting: Bool ) { connectionAttemptID = nil - if hasConnectedOnce || reconnecting { + if connection.hasConnectedOnce || reconnecting { scheduleReconnect(after: error.localizedDescription, using: store) } else { pendingInitialCommand = nil @@ -1074,19 +976,17 @@ final class SSHSessionModel: Identifiable { } private func beginAutomaticReconnect(after message: String) { - guard hasConnectedOnce, let store = statusStore else { + guard connection.hasConnectedOnce, let store = statusStore else { status = .failed(message) return } appendTerminalNotice(String(localized: "Connection lost; reconnecting…")) - isRecoveringConnection = true scheduleReconnect(after: message, using: store) } private func scheduleReconnect(after message: String, using store: any RemoteSessionHostStore) { resetCommandGeneratorShellState() remoteWindowTitle = nil - isRecoveringConnection = true connectionAttemptID = nil connectionTask?.cancel() connectionTask = nil @@ -1099,7 +999,7 @@ final class SSHSessionModel: Identifiable { host: host, using: store, purpose: .sshSession, username: host.flatMap { store.identity(for: $0)?.username } ?? usernameHint ) - reconnectController.schedule(after: message) { [weak self, weak store] in + connection.scheduleRecovery(after: message) { [weak self, weak store] in guard let self, let store else { return } self.startConnection(using: store) } @@ -1116,7 +1016,7 @@ final class SSHSessionModel: Identifiable { mode: SSHAuthenticationPrompt.Mode = .localIdentity, preserveSubmission: Bool = false ) { - reconnectController.pauseForUserInteraction() + connection.pauseForUserInteraction() if !preserveSubmission { suppliedAuthentications[host.id] = nil } pendingAuthentication = SSHAuthenticationPrompt( host: host, @@ -1180,7 +1080,7 @@ final class SSHSessionModel: Identifiable { } catch { guard connectionAttemptID == attemptID else { return } connectionAttemptID = nil - if hasConnectedOnce { + if connection.hasConnectedOnce { scheduleReconnect(after: error.localizedDescription, using: store) } else { status = .failed(error.localizedDescription) @@ -1304,7 +1204,7 @@ final class SSHSessionModel: Identifiable { case .commandFinished(let status, let duration): recordCommandCompletion(exitStatus: status, reportedDuration: duration) case .draft(let draft): - completeDraftRequest(with: draft) + commandDraft.receive(draft) } } diff --git a/TFM/Features/Terminal/SerialSessionModel.swift b/TFM/Features/Terminal/SerialSessionModel.swift index 0af94e97..fa0d9a56 100644 --- a/TFM/Features/Terminal/SerialSessionModel.swift +++ b/TFM/Features/Terminal/SerialSessionModel.swift @@ -1,6 +1,7 @@ import Combine import GhosttyTerminal import SwiftUI +import TermindApplication import TermindDomain @MainActor diff --git a/TFM/Features/Terminal/TerminalPlaceholderView.swift b/TFM/Features/Terminal/TerminalPlaceholderView.swift index fae2850b..a5ec3f27 100644 --- a/TFM/Features/Terminal/TerminalPlaceholderView.swift +++ b/TFM/Features/Terminal/TerminalPlaceholderView.swift @@ -1,5 +1,8 @@ import GhosttyTerminal import SwiftUI +import TermindApplication +import TermindDomain +import TermindRemoteInfrastructure /// A libghostty terminal surface with Termind's connection and suggestion chrome. struct TerminalPlaceholderView: View { diff --git a/TFM/Features/Workspaces/Pane.swift b/TFM/Features/Workspaces/Pane.swift index e193e9bd..b1703ea4 100644 --- a/TFM/Features/Workspaces/Pane.swift +++ b/TFM/Features/Workspaces/Pane.swift @@ -1,5 +1,7 @@ import Foundation +import TermindApplication import TermindDomain +import TermindRemoteInfrastructure import struct TermindDomain.Host @@ -256,7 +258,7 @@ final class Pane: Identifiable { } var remoteSystemInfo: RemoteSystemInfo? { - guard SettingsKey.isShellIntegrationEnabled else { return nil } + guard DesktopSettingsKey.isShellIntegrationEnabled else { return nil } guard case .sshSession(let sshSession) = content else { return nil } return sshSession.remoteSystemInfo } diff --git a/TFM/Features/Workspaces/WindowModel.swift b/TFM/Features/Workspaces/WindowModel.swift index a703533f..9fa7753a 100644 --- a/TFM/Features/Workspaces/WindowModel.swift +++ b/TFM/Features/Workspaces/WindowModel.swift @@ -1,6 +1,8 @@ import Foundation import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindRemoteInfrastructure import struct TermindDomain.Host diff --git a/TFM/Features/Workspaces/WindowState.swift b/TFM/Features/Workspaces/WindowState.swift index 999411d1..fe9f32d2 100644 --- a/TFM/Features/Workspaces/WindowState.swift +++ b/TFM/Features/Workspaces/WindowState.swift @@ -1,4 +1,5 @@ import SwiftUI +import TermindApplication import TermindDomain /// Window-scoped ownership for the dynamic workspace collection and its diff --git a/TFM/Features/Workspaces/Workspace.swift b/TFM/Features/Workspaces/Workspace.swift index c50892a6..7ab5db09 100644 --- a/TFM/Features/Workspaces/Workspace.swift +++ b/TFM/Features/Workspaces/Workspace.swift @@ -1,6 +1,7 @@ import Foundation import TermindApplication import TermindDomain +import TermindRemoteInfrastructure import struct TermindDomain.Host @@ -58,10 +59,11 @@ final class Workspace: Identifiable { cancelCommandGeneration() } } - var commandGeneratorState: CommandGeneratorState = .idle - private var commandGeneratorTask: Task? - private var commandGeneratorDismissTask: Task? - private var commandGenerationSequence: UInt64 = 0 + var commandGeneratorState: CommandGeneratorState { + get { commandGeneration.state } + set { commandGeneration.replaceStateForPresentation(newValue) } + } + private let commandGeneration = CommandGenerationCoordinator() init(pane: Pane) { self.layout = .leaf(pane) @@ -238,7 +240,7 @@ final class Workspace: Identifiable { } func generateFocusedCommand(using controller: any CommandGenerating) { - if commandGeneratorTask != nil { + if commandGeneration.isRunning { cancelCommandGeneration() return } @@ -262,9 +264,6 @@ final class Workspace: Identifiable { return } - commandGeneratorDismissTask?.cancel() - commandGenerationSequence &+= 1 - let generation = commandGenerationSequence let targetIDs = targets.map(\.id) let revisions = Dictionary( uniqueKeysWithValues: targets.map { @@ -272,75 +271,41 @@ final class Workspace: Identifiable { }) let focusedID = focusedPaneID let wasBroadcasting = broadcastsInput && canBroadcast - commandGeneratorState = .readingDraft - - commandGeneratorTask = Task { [weak self, controller] in - do { + let includesSystemInfo = DesktopSettingsKey.isShellIntegrationEnabled + commandGeneration.start( + targetCount: targets.count, + readContext: { let draft = try await origin.requestCurrentDraft() - try Task.checkCancellation() - guard !draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - throw WorkspaceCommandGenerationError.emptyDraft - } - guard - self?.commandGenerationSnapshotIsCurrent( - generation: generation, focusedID: focusedID, - wasBroadcasting: wasBroadcasting, targetIDs: targetIDs, - revisions: revisions - ) == true - else { throw CancellationError() } - - self?.commandGeneratorState = .generating - let context = CommandGeneratorContext( + return CommandGeneratorContext( draft: draft, shell: shell, cwd: cwd, - remoteSystemInfo: SettingsKey.isShellIntegrationEnabled - ? origin.remoteSystemInfo : nil - ) - let command = try await commandGenerationWithTimeout { - try await controller.generate(context) - } - try Task.checkCancellation() - guard - self?.commandGenerationSnapshotIsCurrent( - generation: generation, focusedID: focusedID, - wasBroadcasting: wasBroadcasting, targetIDs: targetIDs, - revisions: revisions - ) == true - else { throw CancellationError() } - - for target in targets { - target.replaceCurrentDraft(with: command) - } - self?.finishCommandGeneration( - generation: generation, - state: .inserted(targetCount: targets.count), dismissAfter: .seconds(2) - ) - } catch is CancellationError { - self?.finishCancelledCommandGeneration(generation: generation) - } catch { - self?.finishCommandGeneration( - generation: generation, - state: .failed(error.localizedDescription), dismissAfter: .seconds(3) + remoteSystemInfo: includesSystemInfo ? origin.remoteSystemInfo : nil ) + }, + isCurrent: { [weak self] in + self?.commandGenerationSnapshotIsCurrent( + focusedID: focusedID, + wasBroadcasting: wasBroadcasting, + targetIDs: targetIDs, + revisions: revisions + ) == true + }, + generate: { try await controller.generate($0) }, + insert: { command in + for target in targets { target.replaceCurrentDraft(with: command) } } - } + ) } func cancelCommandGeneration() { - guard commandGeneratorTask != nil else { return } - commandGenerationSequence &+= 1 - commandGeneratorTask?.cancel() - commandGeneratorTask = nil - commandGeneratorDismissTask?.cancel() - commandGeneratorDismissTask = nil - commandGeneratorState = .idle + guard commandGeneration.isRunning else { return } + commandGeneration.cancel() } private func commandGenerationSnapshotIsCurrent( - generation: UInt64, focusedID: Pane.ID, wasBroadcasting: Bool, + focusedID: Pane.ID, wasBroadcasting: Bool, targetIDs: [SSHSessionModel.ID], revisions: [SSHSessionModel.ID: UInt64] ) -> Bool { - guard commandGenerationSequence == generation, - focusedPaneID == focusedID, + guard focusedPaneID == focusedID, (broadcastsInput && canBroadcast) == wasBroadcasting else { return false } let targets = wasBroadcasting ? broadcastTargets : focusedPane.sshSession.map { [$0] } ?? [] @@ -350,40 +315,8 @@ final class Workspace: Identifiable { } } - private func finishCancelledCommandGeneration(generation: UInt64) { - guard commandGenerationSequence == generation else { return } - commandGeneratorTask = nil - commandGeneratorState = .idle - } - - private func finishCommandGeneration( - generation: UInt64, state: CommandGeneratorState, dismissAfter: Duration - ) { - guard commandGenerationSequence == generation else { return } - commandGeneratorTask = nil - commandGeneratorState = state - scheduleCommandGenerationDismiss(generation: generation, after: dismissAfter) - } - private func showCommandGenerationFailure(_ message: String) { - commandGenerationSequence &+= 1 - commandGeneratorTask?.cancel() - commandGeneratorTask = nil - commandGeneratorDismissTask?.cancel() - commandGeneratorState = .failed(message) - scheduleCommandGenerationDismiss( - generation: commandGenerationSequence, after: .seconds(3) - ) - } - - private func scheduleCommandGenerationDismiss(generation: UInt64, after delay: Duration) { - commandGeneratorDismissTask?.cancel() - commandGeneratorDismissTask = Task { [weak self] in - try? await Task.sleep(for: delay) - guard !Task.isCancelled, self?.commandGenerationSequence == generation else { return } - self?.commandGeneratorState = .idle - self?.commandGeneratorDismissTask = nil - } + commandGeneration.fail(message) } /// Keeps each session-to-Workspace relationship correct as panes are merged, @@ -483,31 +416,6 @@ extension Workspace: SSHSessionInputRouting { } } -nonisolated private enum WorkspaceCommandGenerationError: LocalizedError { - case emptyDraft - - var errorDescription: String? { - String(localized: "Type something at the shell prompt before generating a command.") - } -} - -nonisolated private func commandGenerationWithTimeout( - _ operation: @escaping @Sendable () async throws -> String -) async throws -> String { - try await withThrowingTaskGroup(of: String.self) { group in - group.addTask { try await operation() } - group.addTask { - try await Task.sleep(for: .seconds(30)) - throw CommandGeneratorError.timedOut - } - guard let result = try await group.next() else { - throw CommandGeneratorError.invalidResponse - } - group.cancelAll() - return result - } -} - // MARK: - Convenience constructors extension Workspace { diff --git a/TFM/Features/Workspaces/WorkspaceNavigatorView.swift b/TFM/Features/Workspaces/WorkspaceNavigatorView.swift index d62085aa..4e0bbea9 100644 --- a/TFM/Features/Workspaces/WorkspaceNavigatorView.swift +++ b/TFM/Features/Workspaces/WorkspaceNavigatorView.swift @@ -1,5 +1,6 @@ import AppKit import SwiftUI +import TermindApplication import UniformTypeIdentifiers /// The native source list for Vault and Workspaces. @@ -9,8 +10,8 @@ struct WorkspaceNavigatorView: View { @Environment(WindowModel.self) private var window @Environment(\.locale) private var locale - @AppStorage(SettingsKey.vaultSectionExpanded) private var vaultExpanded = true - @AppStorage(SettingsKey.workspaceSectionExpanded) private var workspaceExpanded = true + @AppStorage(DesktopSettingsKey.vaultSectionExpanded) private var vaultExpanded = true + @AppStorage(DesktopSettingsKey.workspaceSectionExpanded) private var workspaceExpanded = true @State private var workspaceDropTarget: WorkspaceSidebarDropTarget? @State private var workspaceFrames: [Workspace.ID: CGRect] = [:] diff --git a/TFM/Infrastructure/Cloud/SettingsSyncCoordinator.swift b/TFM/Infrastructure/Cloud/SettingsSyncCoordinator.swift deleted file mode 100644 index 9965a0ca..00000000 --- a/TFM/Infrastructure/Cloud/SettingsSyncCoordinator.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindInfrastructure - -typealias SettingsSyncCoordinator = TermindInfrastructure.SettingsSyncCoordinator diff --git a/TFM/Infrastructure/Cloud/SettingsSyncJournal.swift b/TFM/Infrastructure/Cloud/SettingsSyncJournal.swift deleted file mode 100644 index 1ac07510..00000000 --- a/TFM/Infrastructure/Cloud/SettingsSyncJournal.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindPersistence - -typealias SettingsRemoteMerge = SharedSettingsRemoteMerge -typealias SettingsOutgoingEntry = SharedSettingsOutgoingEntry -typealias SettingsSyncJournal = SharedSettingsSyncJournal diff --git a/TFM/Infrastructure/Cloud/SettingsSyncPersistence.swift b/TFM/Infrastructure/Cloud/SettingsSyncPersistence.swift deleted file mode 100644 index b6182e4e..00000000 --- a/TFM/Infrastructure/Cloud/SettingsSyncPersistence.swift +++ /dev/null @@ -1,6 +0,0 @@ -import TermindPersistence - -typealias SettingsSyncSnapshot = SharedSettingsSyncSnapshot -typealias SettingsSyncPersistence = SharedSettingsSyncPersistence -typealias SyncedAIConfigurationPersistence = SharedSyncedAIConfigurationPersistence -typealias SyncedSkillLibraryPersistence = SharedSyncedSkillLibraryPersistence diff --git a/TFM/Infrastructure/Cloud/VaultSyncCoordinator.swift b/TFM/Infrastructure/Cloud/VaultSyncCoordinator.swift deleted file mode 100644 index bf2e8ba3..00000000 --- a/TFM/Infrastructure/Cloud/VaultSyncCoordinator.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindInfrastructure - -typealias VaultSyncCoordinator = TermindInfrastructure.VaultSyncCoordinator diff --git a/TFM/Infrastructure/Cloud/VaultSyncPersistence.swift b/TFM/Infrastructure/Cloud/VaultSyncPersistence.swift deleted file mode 100644 index bbcdf158..00000000 --- a/TFM/Infrastructure/Cloud/VaultSyncPersistence.swift +++ /dev/null @@ -1,6 +0,0 @@ -import TermindPersistence - -typealias VaultSyncPersistenceError = SharedVaultSyncPersistenceError -typealias VaultRemoteMerge = SharedVaultRemoteMerge -typealias VaultOutgoingEntry = SharedVaultOutgoingEntry -typealias VaultSyncJournal = SharedVaultSyncJournal diff --git a/TFM/Infrastructure/Commerce/TrialClock.swift b/TFM/Infrastructure/Commerce/TrialClock.swift deleted file mode 100644 index 3d6e260a..00000000 --- a/TFM/Infrastructure/Commerce/TrialClock.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindInfrastructure - -typealias TrialClock = TermindInfrastructure.TrialClock diff --git a/TFM/Infrastructure/Database/AIPersistence.swift b/TFM/Infrastructure/Database/AIPersistence.swift deleted file mode 100644 index ed3ce564..00000000 --- a/TFM/Infrastructure/Database/AIPersistence.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPersistence - -typealias AIPersistence = SharedAIPersistence diff --git a/TFM/Infrastructure/Database/AgentPersistence.swift b/TFM/Infrastructure/Database/AgentPersistence.swift deleted file mode 100644 index 13f2815e..00000000 --- a/TFM/Infrastructure/Database/AgentPersistence.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPersistence - -typealias SwiftDataAgentRunRepository = SharedAgentRunRepository diff --git a/TFM/Infrastructure/Database/SwiftDataCommandHistoryRepository.swift b/TFM/Infrastructure/Database/CommandHistoryModel+SwiftData.swift similarity index 71% rename from TFM/Infrastructure/Database/SwiftDataCommandHistoryRepository.swift rename to TFM/Infrastructure/Database/CommandHistoryModel+SwiftData.swift index c819792e..74363c5b 100644 --- a/TFM/Infrastructure/Database/SwiftDataCommandHistoryRepository.swift +++ b/TFM/Infrastructure/Database/CommandHistoryModel+SwiftData.swift @@ -1,8 +1,7 @@ import SwiftData +import TermindApplication import TermindPersistence -typealias SwiftDataCommandHistoryRepository = SharedSwiftDataCommandHistoryRepository - extension CommandHistoryModel { convenience init(modelContext: ModelContext) { self.init(repository: SwiftDataCommandHistoryRepository(context: modelContext)) diff --git a/TFM/Infrastructure/Database/SwiftDataConnectionHistoryRepository.swift b/TFM/Infrastructure/Database/ConnectionHistoryModel+SwiftData.swift similarity index 71% rename from TFM/Infrastructure/Database/SwiftDataConnectionHistoryRepository.swift rename to TFM/Infrastructure/Database/ConnectionHistoryModel+SwiftData.swift index a9da2029..8eee655e 100644 --- a/TFM/Infrastructure/Database/SwiftDataConnectionHistoryRepository.swift +++ b/TFM/Infrastructure/Database/ConnectionHistoryModel+SwiftData.swift @@ -1,8 +1,7 @@ import SwiftData +import TermindApplication import TermindPersistence -typealias SwiftDataConnectionHistoryRepository = SharedSwiftDataConnectionHistoryRepository - extension ConnectionHistoryModel { convenience init(modelContext: ModelContext) { self.init(repository: SwiftDataConnectionHistoryRepository(context: modelContext)) diff --git a/TFM/Infrastructure/Database/HostPersistence.swift b/TFM/Infrastructure/Database/HostPersistence.swift deleted file mode 100644 index b539f9bc..00000000 --- a/TFM/Infrastructure/Database/HostPersistence.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPersistence - -typealias HostPersistence = SharedHostPersistence diff --git a/TFM/Infrastructure/Database/SkillPersistence.swift b/TFM/Infrastructure/Database/SkillPersistence.swift deleted file mode 100644 index 16e157b0..00000000 --- a/TFM/Infrastructure/Database/SkillPersistence.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPersistence - -typealias SkillPersistence = SharedSkillPersistence diff --git a/TFM/Infrastructure/Database/SwiftDataHostRepository.swift b/TFM/Infrastructure/Database/SwiftDataHostRepository.swift deleted file mode 100644 index 64c68bca..00000000 --- a/TFM/Infrastructure/Database/SwiftDataHostRepository.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPersistence - -typealias SwiftDataHostRepository = SharedSwiftDataHostRepository diff --git a/TFM/Infrastructure/Database/SwiftDataOutboxStore.swift b/TFM/Infrastructure/Database/SwiftDataOutboxStore.swift deleted file mode 100644 index 7a2d8a60..00000000 --- a/TFM/Infrastructure/Database/SwiftDataOutboxStore.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindPersistence - -typealias SwiftDataOutboxStore = SharedOutboxStore diff --git a/TFM/Infrastructure/Metrics/SSHMetricsSampler.swift b/TFM/Infrastructure/Metrics/SSHMetricsSampler.swift deleted file mode 100644 index 7120316e..00000000 --- a/TFM/Infrastructure/Metrics/SSHMetricsSampler.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindRemoteInfrastructure - -typealias SSHMetricsSampler = TermindRemoteInfrastructure.SSHMetricsSampler -typealias MetricsPreferences = TermindRemoteInfrastructure.MetricsPreferences diff --git a/TFM/Infrastructure/Repositories/PersistentModelFactories.swift b/TFM/Infrastructure/Repositories/PersistentModelFactories.swift index 5ab1ddaa..84af06a2 100644 --- a/TFM/Infrastructure/Repositories/PersistentModelFactories.swift +++ b/TFM/Infrastructure/Repositories/PersistentModelFactories.swift @@ -1,5 +1,8 @@ import Foundation import SwiftData +import TermindApplication +import TermindPersistence +import TermindRemoteInfrastructure extension HostLibraryModel { convenience init( diff --git a/TFM/Infrastructure/SSH/PortForwardingController.swift b/TFM/Infrastructure/SSH/PortForwardingController.swift deleted file mode 100644 index 06e33c76..00000000 --- a/TFM/Infrastructure/SSH/PortForwardingController.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindRemoteInfrastructure - -typealias PortForwardingRuntimeState = TermindRemoteInfrastructure.PortForwardingRuntimeState -typealias PortForwardingController = TermindRemoteInfrastructure.PortForwardingController diff --git a/TFM/Infrastructure/SSH/RemoteConnectionCoordinator.swift b/TFM/Infrastructure/SSH/RemoteConnectionCoordinator.swift deleted file mode 100644 index cfd26166..00000000 --- a/TFM/Infrastructure/SSH/RemoteConnectionCoordinator.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias RemoteConnectionCoordinator = TermindRemoteInfrastructure.RemoteConnectionCoordinator diff --git a/TFM/Infrastructure/SSH/SFTPClient.swift b/TFM/Infrastructure/SSH/SFTPClient.swift deleted file mode 100644 index 982254c4..00000000 --- a/TFM/Infrastructure/SSH/SFTPClient.swift +++ /dev/null @@ -1,9 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias RemoteFile = TermindRemoteInfrastructure.RemoteFile -typealias TransferCancellationToken = TermindRemoteInfrastructure.TransferCancellationToken -typealias SFTPProgress = TermindRemoteInfrastructure.SFTPProgress -typealias SFTPClientProtocol = TermindRemoteInfrastructure.SFTPClientProtocol -typealias SFTPError = TermindRemoteInfrastructure.SFTPError -typealias SFTPClient = TermindRemoteInfrastructure.SFTPClient diff --git a/TFM/Infrastructure/SSH/SFTPTransferCoordinator+Production.swift b/TFM/Infrastructure/SSH/SFTPTransferCoordinator+Production.swift index 5998d573..b5f7c2ef 100644 --- a/TFM/Infrastructure/SSH/SFTPTransferCoordinator+Production.swift +++ b/TFM/Infrastructure/SSH/SFTPTransferCoordinator+Production.swift @@ -1,4 +1,6 @@ import Foundation +import TermindApplication +import TermindRemoteInfrastructure extension SFTPTransferCoordinator { convenience init( diff --git a/TFM/Infrastructure/SSH/SSHExecSession.swift b/TFM/Infrastructure/SSH/SSHExecSession.swift deleted file mode 100644 index 902ceffc..00000000 --- a/TFM/Infrastructure/SSH/SSHExecSession.swift +++ /dev/null @@ -1,8 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias SSHExecResult = TermindRemoteInfrastructure.SSHExecResult -typealias SSHExecSessionError = TermindRemoteInfrastructure.SSHExecSessionError -typealias SSHExecSessionProtocol = TermindRemoteInfrastructure.SSHExecSessionProtocol -typealias SSHExecOutputHandler = TermindRemoteInfrastructure.SSHExecOutputHandler -typealias SSHExecSession = TermindRemoteInfrastructure.SSHExecSession diff --git a/TFM/Infrastructure/SSH/SSHForwardingSession.swift b/TFM/Infrastructure/SSH/SSHForwardingSession.swift deleted file mode 100644 index 3472cc00..00000000 --- a/TFM/Infrastructure/SSH/SSHForwardingSession.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias SSHForwardingSession = TermindRemoteInfrastructure.SSHForwardingSession diff --git a/TFM/Infrastructure/SSH/SSHKeyMaterial.swift b/TFM/Infrastructure/SSH/SSHKeyMaterial.swift deleted file mode 100644 index f559f34d..00000000 --- a/TFM/Infrastructure/SSH/SSHKeyMaterial.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias SSHKeyMaterialError = TermindRemoteInfrastructure.SSHKeyMaterialError -typealias SSHKeyMaterial = TermindRemoteInfrastructure.SSHKeyMaterial diff --git a/TFM/Infrastructure/SSH/SSHRoutedSocket.swift b/TFM/Infrastructure/SSH/SSHRoutedSocket.swift deleted file mode 100644 index 0aa941af..00000000 --- a/TFM/Infrastructure/SSH/SSHRoutedSocket.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias SSHRoutedSocket = TermindRemoteInfrastructure.SSHRoutedSocket diff --git a/TFM/Infrastructure/SSH/SSHSessionActor.swift b/TFM/Infrastructure/SSH/SSHSessionActor.swift deleted file mode 100644 index f30862f4..00000000 --- a/TFM/Infrastructure/SSH/SSHSessionActor.swift +++ /dev/null @@ -1,8 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias RemoteShellIntegrationBootstrap = TermindRemoteInfrastructure.RemoteShellIntegrationBootstrap -typealias RemoteCommandEventFilter = TermindRemoteInfrastructure.RemoteCommandEventFilter -typealias SSHShellIntegrationEvent = TermindRemoteInfrastructure.SSHShellIntegrationEvent -typealias SSHSessionActor = TermindRemoteInfrastructure.SSHSessionActor -typealias SSHSessionWorker = TermindRemoteInfrastructure.SSHSessionWorker diff --git a/TFM/Infrastructure/SSH/SSHSocketConnector.swift b/TFM/Infrastructure/SSH/SSHSocketConnector.swift deleted file mode 100644 index 86cb5acc..00000000 --- a/TFM/Infrastructure/SSH/SSHSocketConnector.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias SSHSocketConnector = TermindRemoteInfrastructure.SSHSocketConnector diff --git a/TFM/Infrastructure/SSH/SSHUserCertificate.swift b/TFM/Infrastructure/SSH/SSHUserCertificate.swift deleted file mode 100644 index f426b6a2..00000000 --- a/TFM/Infrastructure/SSH/SSHUserCertificate.swift +++ /dev/null @@ -1,4 +0,0 @@ -import TermindDomain - -typealias SSHUserCertificateError = TermindDomain.SSHUserCertificateError -typealias SSHUserCertificate = TermindDomain.SSHUserCertificate diff --git a/TFM/Infrastructure/SSHImport/SSHImportReader.swift b/TFM/Infrastructure/SSHImport/SSHImportReader+Desktop.swift similarity index 77% rename from TFM/Infrastructure/SSHImport/SSHImportReader.swift rename to TFM/Infrastructure/SSHImport/SSHImportReader+Desktop.swift index f7733f0b..cbc5c9d9 100644 --- a/TFM/Infrastructure/SSHImport/SSHImportReader.swift +++ b/TFM/Infrastructure/SSHImport/SSHImportReader+Desktop.swift @@ -1,8 +1,6 @@ import Foundation import TermindRemoteInfrastructure -typealias SSHImportReader = TermindRemoteInfrastructure.SSHImportReader - extension SSHImportReader { init() { self.init( diff --git a/TFM/Infrastructure/Security/AISecretStore.swift b/TFM/Infrastructure/Security/AISecretStore+Desktop.swift similarity index 79% rename from TFM/Infrastructure/Security/AISecretStore.swift rename to TFM/Infrastructure/Security/AISecretStore+Desktop.swift index ccf2bc90..61065e2a 100644 --- a/TFM/Infrastructure/Security/AISecretStore.swift +++ b/TFM/Infrastructure/Security/AISecretStore+Desktop.swift @@ -1,7 +1,6 @@ import SwiftData import TermindPersistence - -typealias AISecretStore = SharedAISecretStore +import TermindRemoteInfrastructure extension AISecretStore { convenience init(context: ModelContext) { self.init(context: context, keychain: KeychainSecretStore()) } diff --git a/TFM/Infrastructure/Security/KeychainSecretStore.swift b/TFM/Infrastructure/Security/KeychainSecretStore+Desktop.swift similarity index 52% rename from TFM/Infrastructure/Security/KeychainSecretStore.swift rename to TFM/Infrastructure/Security/KeychainSecretStore+Desktop.swift index 14c846f7..32af65f3 100644 --- a/TFM/Infrastructure/Security/KeychainSecretStore.swift +++ b/TFM/Infrastructure/Security/KeychainSecretStore+Desktop.swift @@ -1,9 +1,6 @@ import TermindInfrastructure import TermindRemoteInfrastructure -typealias KeychainSecretError = TermindRemoteInfrastructure.KeychainSecretError -typealias KeychainSecretStore = TermindRemoteInfrastructure.KeychainSecretStore - extension KeychainSecretStore { init() { self.init(accessGroup: ApplicationIdentity.keychainAccessGroup) } } diff --git a/TFM/Infrastructure/Security/SecureEnclaveKey.swift b/TFM/Infrastructure/Security/SecureEnclaveKey.swift deleted file mode 100644 index 24ad8fbc..00000000 --- a/TFM/Infrastructure/Security/SecureEnclaveKey.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindInfrastructure -import TermindRemoteInfrastructure - -typealias SecureEnclaveKeyError = TermindRemoteInfrastructure.SecureEnclaveKeyError -typealias SecureEnclaveKey = TermindRemoteInfrastructure.SecureEnclaveKey diff --git a/TFM/Infrastructure/Speech/AppleSpeechTranscriptionService.swift b/TFM/Infrastructure/Speech/AppleSpeechTranscriptionService.swift deleted file mode 100644 index 94dd426f..00000000 --- a/TFM/Infrastructure/Speech/AppleSpeechTranscriptionService.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TermindInfrastructure - -typealias AppleSpeechTranscriptionService = TermindInfrastructure.AppleSpeechTranscriptionService diff --git a/TFM/Infrastructure/Speech/SpeechAudioInput.swift b/TFM/Infrastructure/Speech/SpeechAudioInput.swift deleted file mode 100644 index 1b4594e1..00000000 --- a/TFM/Infrastructure/Speech/SpeechAudioInput.swift +++ /dev/null @@ -1,5 +0,0 @@ -import TermindInfrastructure - -typealias SpeechAudioBufferHandler = TermindInfrastructure.SpeechAudioBufferHandler -typealias SpeechAudioLevelHandler = TermindInfrastructure.SpeechAudioLevelHandler -typealias SpeechAudioInput = TermindInfrastructure.SpeechAudioInput diff --git a/TFM/App/AppLanguage.swift b/TFM/Platform/AppRelauncher.swift similarity index 83% rename from TFM/App/AppLanguage.swift rename to TFM/Platform/AppRelauncher.swift index 9887c1fe..25c84f09 100644 --- a/TFM/App/AppLanguage.swift +++ b/TFM/Platform/AppRelauncher.swift @@ -1,13 +1,9 @@ import Foundation -import TermindApplication #if canImport(AppKit) import AppKit #endif -typealias AppLanguage = TermindApplication.AppLanguage -typealias AppLocalization = TermindApplication.AppLocalization - #if canImport(AppKit) @MainActor enum AppRelauncher { diff --git a/TFM/Platform/MacOSSpeechAudioInput.swift b/TFM/Platform/MacOSSpeechAudioInput.swift index 38ddcbb4..b5e320a9 100644 --- a/TFM/Platform/MacOSSpeechAudioInput.swift +++ b/TFM/Platform/MacOSSpeechAudioInput.swift @@ -1,5 +1,7 @@ @preconcurrency import AVFoundation import Foundation +import TermindApplication +import TermindInfrastructure /// macOS microphone permission and AVAudioEngine lifecycle for Apple Speech. @MainActor diff --git a/TFM/Platform/MetricsMenuBarController.swift b/TFM/Platform/MetricsMenuBarController.swift index 54afbe02..9d735611 100644 --- a/TFM/Platform/MetricsMenuBarController.swift +++ b/TFM/Platform/MetricsMenuBarController.swift @@ -1,5 +1,6 @@ import AppKit import SwiftUI +import TermindApplication /// Owns the status item and popover independently of window and SwiftUI scene lifetimes. @MainActor @@ -33,7 +34,7 @@ final class MetricsMenuBarController: NSObject, NSPopoverDelegate { } func refresh() { - let enabled = defaults.object(forKey: SettingsKey.showMetricsInMenuBar) as? Bool ?? true + let enabled = defaults.object(forKey: DesktopSettingsKey.showMetricsInMenuBar) as? Bool ?? true guard enabled else { popover.close() if let item { NSStatusBar.system.removeStatusItem(item) } diff --git a/TFM/Platform/MetricsWindowPresenter.swift b/TFM/Platform/MetricsWindowPresenter.swift index 9c85a0d4..7452db59 100644 --- a/TFM/Platform/MetricsWindowPresenter.swift +++ b/TFM/Platform/MetricsWindowPresenter.swift @@ -1,5 +1,6 @@ import AppKit import SwiftUI +import TermindApplication /// Native window discovery stays outside the application monitoring service. @MainActor diff --git a/TFM/UI/Components/SharedChrome.swift b/TFM/UI/Components/ChromeComponents.swift similarity index 100% rename from TFM/UI/Components/SharedChrome.swift rename to TFM/UI/Components/ChromeComponents.swift diff --git a/TFM/UI/Components/ConnectionJourneyView.swift b/TFM/UI/Components/ConnectionJourneyView.swift index 0b5d388c..6e916812 100644 --- a/TFM/UI/Components/ConnectionJourneyView.swift +++ b/TFM/UI/Components/ConnectionJourneyView.swift @@ -221,14 +221,14 @@ struct ConnectionJourneyView: View { if journey.showsLogs { logs } else if let prompt = authenticationPrompt { - SSHAuthenticationForm( + DesktopSSHAuthenticationForm( prompt: prompt, submit: submitAuthentication, cancel: cancel ) .id(prompt.id) } else if let hostKey { - SSHHostKeyConfirmationForm( + DesktopSSHHostKeyConfirmationForm( prompt: hostKey, proceed: { continueHostKey(hostKey.observation) }, trust: { trustHostKey(hostKey.observation) }, @@ -500,14 +500,14 @@ private struct JourneyReconnectStatus: View { /// Shared SSH credential form used by both the connection Journey and Agent. /// The containing surface owns placement and queue state; this view owns the /// established Custom/Identity choices, button language, and submission flow. -struct SSHAuthenticationForm: View { +struct DesktopSSHAuthenticationForm: View { let prompt: SSHAuthenticationPrompt let submit: (Identity.ID?, String, String?, String?, SSHKeyRef.ID?, Bool) -> Void let cancel: () -> Void var usesContainerChrome = true @Environment(AppModel.self) private var app var body: some View { - SharedSSHAuthenticationForm( + TermindPresentation.SSHAuthenticationForm( prompt: prompt, submit: submit, cancel: cancel, usesContainerChrome: false, library: app.hostLibrary, keyboard: DesktopConnectionFormKeyboard() ) @@ -515,7 +515,7 @@ struct SSHAuthenticationForm: View { } } -struct SSHHostKeyConfirmationForm: View { +struct DesktopSSHHostKeyConfirmationForm: View { let prompt: SSHHostKeyPrompt var displayAddress: String? = nil let proceed: () -> Void @@ -523,7 +523,7 @@ struct SSHHostKeyConfirmationForm: View { let deny: () -> Void var usesContainerChrome = true var body: some View { - SharedSSHHostKeyConfirmationForm( + TermindPresentation.SSHHostKeyConfirmationForm( prompt: prompt, displayAddress: displayAddress, proceed: proceed, trust: trust, deny: deny, usesContainerChrome: false, keyboard: DesktopConnectionFormKeyboard() ) diff --git a/TFM/UI/Components/WindowToolbar.swift b/TFM/UI/Components/WindowToolbar.swift index 1574f5cf..263e1def 100644 --- a/TFM/UI/Components/WindowToolbar.swift +++ b/TFM/UI/Components/WindowToolbar.swift @@ -1,5 +1,6 @@ import AppKit import SwiftUI +import TermindApplication import TermindDomain import struct TermindDomain.Host diff --git a/TFMTests/AIProviderPersistenceTests.swift b/TFMTests/AIProviderPersistenceTests.swift index b7451b91..16533894 100644 --- a/TFMTests/AIProviderPersistenceTests.swift +++ b/TFMTests/AIProviderPersistenceTests.swift @@ -1,7 +1,12 @@ import AgentKit import Foundation import SwiftData +import TermindApplication +import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentApprovalQueueTests.swift b/TFMTests/AgentApprovalQueueTests.swift index 51701ba2..226a1890 100644 --- a/TFMTests/AgentApprovalQueueTests.swift +++ b/TFMTests/AgentApprovalQueueTests.swift @@ -1,6 +1,12 @@ import AgentKit import Foundation import Observation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentBackgroundCommandTests.swift b/TFMTests/AgentBackgroundCommandTests.swift index 186cbdf9..27ab64ac 100644 --- a/TFMTests/AgentBackgroundCommandTests.swift +++ b/TFMTests/AgentBackgroundCommandTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentBuiltInToolAvailabilityTests.swift b/TFMTests/AgentBuiltInToolAvailabilityTests.swift index b70fbf18..9357c71b 100644 --- a/TFMTests/AgentBuiltInToolAvailabilityTests.swift +++ b/TFMTests/AgentBuiltInToolAvailabilityTests.swift @@ -1,6 +1,12 @@ import AgentKit import AgentKitScrubber import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentCompactionTests.swift b/TFMTests/AgentCompactionTests.swift index 754258bf..57071e1e 100644 --- a/TFMTests/AgentCompactionTests.swift +++ b/TFMTests/AgentCompactionTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentConnectionPoolTests.swift b/TFMTests/AgentConnectionPoolTests.swift index 7aa58f2a..4b7776ae 100644 --- a/TFMTests/AgentConnectionPoolTests.swift +++ b/TFMTests/AgentConnectionPoolTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentHostAuthenticationTests.swift b/TFMTests/AgentHostAuthenticationTests.swift index d4ae5869..15458d4d 100644 --- a/TFMTests/AgentHostAuthenticationTests.swift +++ b/TFMTests/AgentHostAuthenticationTests.swift @@ -1,6 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/AgentInterruptedRunRecoveryTests.swift b/TFMTests/AgentInterruptedRunRecoveryTests.swift index bc91b818..3ea44fe6 100644 --- a/TFMTests/AgentInterruptedRunRecoveryTests.swift +++ b/TFMTests/AgentInterruptedRunRecoveryTests.swift @@ -1,6 +1,12 @@ import AgentKit import Foundation import SwiftData +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentPersistenceTests.swift b/TFMTests/AgentPersistenceTests.swift index 6a8b5d3d..3e1e8849 100644 --- a/TFMTests/AgentPersistenceTests.swift +++ b/TFMTests/AgentPersistenceTests.swift @@ -1,7 +1,12 @@ import AgentKit import Foundation import SwiftData +import TermindApplication +import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentPlanModeTests.swift b/TFMTests/AgentPlanModeTests.swift index ab714d73..0ed5e0e2 100644 --- a/TFMTests/AgentPlanModeTests.swift +++ b/TFMTests/AgentPlanModeTests.swift @@ -1,6 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentRuntimeTests.swift b/TFMTests/AgentRuntimeTests.swift index 5b8921b3..f59b6cda 100644 --- a/TFMTests/AgentRuntimeTests.swift +++ b/TFMTests/AgentRuntimeTests.swift @@ -1,6 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentScratchTests.swift b/TFMTests/AgentScratchTests.swift index a863b864..f260b6c6 100644 --- a/TFMTests/AgentScratchTests.swift +++ b/TFMTests/AgentScratchTests.swift @@ -1,6 +1,12 @@ import AgentKit import AgentKitScrubber import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentSkillTests.swift b/TFMTests/AgentSkillTests.swift index 267250cf..bee00111 100644 --- a/TFMTests/AgentSkillTests.swift +++ b/TFMTests/AgentSkillTests.swift @@ -1,7 +1,12 @@ import AgentKit import Foundation import SwiftData +import TermindApplication +import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentTokenUsageTests.swift b/TFMTests/AgentTokenUsageTests.swift index 8786af3a..590d919a 100644 --- a/TFMTests/AgentTokenUsageTests.swift +++ b/TFMTests/AgentTokenUsageTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AgentToolPresentationTests.swift b/TFMTests/AgentToolPresentationTests.swift index 6a50537f..b0541364 100644 --- a/TFMTests/AgentToolPresentationTests.swift +++ b/TFMTests/AgentToolPresentationTests.swift @@ -1,6 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/AgentToolProgressTests.swift b/TFMTests/AgentToolProgressTests.swift index c477a7f4..3f2d4e69 100644 --- a/TFMTests/AgentToolProgressTests.swift +++ b/TFMTests/AgentToolProgressTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AppAppearanceTests.swift b/TFMTests/AppAppearanceTests.swift index 3f72304b..2b081cf2 100644 --- a/TFMTests/AppAppearanceTests.swift +++ b/TFMTests/AppAppearanceTests.swift @@ -1,4 +1,10 @@ import AppKit +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind @@ -9,9 +15,9 @@ struct AppAppearanceTests { func missingAndInvalidPreferencesFollowSystem() throws { try withDefaults { defaults in #expect(AppAppearance.stored(in: defaults) == .system) - defaults.set("unsupported", forKey: SettingsKey.appAppearance) + defaults.set("unsupported", forKey: DesktopSettingsKey.appAppearance) #expect(AppAppearance.stored(in: defaults) == .system) - defaults.set(42, forKey: SettingsKey.appAppearance) + defaults.set(42, forKey: DesktopSettingsKey.appAppearance) #expect(AppAppearance.stored(in: defaults) == .system) } } @@ -32,7 +38,7 @@ struct AppAppearanceTests { #expect(parent.appearance == nil) for (selection, name) in [("light", NSAppearance.Name.aqua), ("dark", .darkAqua)] { - defaults.set(selection, forKey: SettingsKey.appAppearance) + defaults.set(selection, forKey: DesktopSettingsKey.appAppearance) notifications.post(name: UserDefaults.didChangeNotification, object: defaults) #expect(parent.appearance?.name == name) #expect(child.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == name) @@ -41,7 +47,7 @@ struct AppAppearanceTests { #expect(newChild.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == name) } - defaults.set("system", forKey: SettingsKey.appAppearance) + defaults.set("system", forKey: DesktopSettingsKey.appAppearance) notifications.post(name: UserDefaults.didChangeNotification, object: defaults) #expect(parent.appearance == nil) #expect(child.effectiveAppearance == parent.effectiveAppearance) @@ -57,7 +63,7 @@ struct AppAppearanceTests { defaults: defaults, notifications: notifications, apply: { applied.append($0?.name) } ) - defaults.set("dark", forKey: SettingsKey.appAppearance) + defaults.set("dark", forKey: DesktopSettingsKey.appAppearance) controller.start() controller.start() defaults.set(true, forKey: "unrelated.preference") @@ -65,7 +71,7 @@ struct AppAppearanceTests { #expect(applied == [.darkAqua]) controller.stop() - defaults.set("light", forKey: SettingsKey.appAppearance) + defaults.set("light", forKey: DesktopSettingsKey.appAppearance) notifications.post(name: UserDefaults.didChangeNotification, object: defaults) #expect(applied == [.darkAqua]) @@ -75,7 +81,7 @@ struct AppAppearanceTests { ) relaunched.start() #expect(applied == [.darkAqua, .aqua]) - defaults.set("invalid", forKey: SettingsKey.appAppearance) + defaults.set("invalid", forKey: DesktopSettingsKey.appAppearance) notifications.post(name: UserDefaults.didChangeNotification, object: defaults) #expect(applied == [.darkAqua, .aqua, nil]) relaunched.stop() @@ -92,7 +98,7 @@ struct AppAppearanceTests { controller.start() defer { controller.stop() } - defaults.set("dark", forKey: SettingsKey.appAppearance) + defaults.set("dark", forKey: DesktopSettingsKey.appAppearance) for _ in 0..<100 where applied != .darkAqua { try await Task.sleep(for: .milliseconds(10)) } diff --git a/TFMTests/AppLanguageTests.swift b/TFMTests/AppLanguageTests.swift index a27d8eff..38839a1f 100644 --- a/TFMTests/AppLanguageTests.swift +++ b/TFMTests/AppLanguageTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AppReviewControllerTests.swift b/TFMTests/AppReviewControllerTests.swift index d19f97e1..4a8dfacb 100644 --- a/TFMTests/AppReviewControllerTests.swift +++ b/TFMTests/AppReviewControllerTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind @@ -89,8 +95,8 @@ struct AppReviewControllerTests { #expect(h.requests == 0) h.advance(0.001) h.controller.requestIfReady(windowID: h.windowID) { - #expect(h.defaults.stringArray(forKey: SettingsKey.reviewRequestedVersions) == ["1.0"]) - #expect(h.defaults.object(forKey: SettingsKey.reviewLastAttempt) as? Date == h.date) + #expect(h.defaults.stringArray(forKey: DesktopSettingsKey.reviewRequestedVersions) == ["1.0"]) + #expect(h.defaults.object(forKey: DesktopSettingsKey.reviewLastAttempt) as? Date == h.date) h.requests += 1 } h.request() @@ -220,7 +226,7 @@ struct AppReviewControllerTests { h.advance(3) h.request() #expect(h.requests == 0) - #expect(h.defaults.object(forKey: SettingsKey.reviewRequestedVersions) == nil) + #expect(h.defaults.object(forKey: DesktopSettingsKey.reviewRequestedVersions) == nil) h.work(for: 0) h.finish() #expect(h.requests == 0) @@ -258,7 +264,7 @@ struct AppReviewControllerTests { h.work(for: 300) h.finish() #expect(h.requests == 0) - #expect(h.defaults.object(forKey: SettingsKey.reviewFirstUse) == nil) + #expect(h.defaults.object(forKey: DesktopSettingsKey.reviewFirstUse) == nil) #expect(h.activeDays == 0) #expect(AppModel(hosts: HostLibraryModel()).appReview.isEnabled == false) } @@ -289,7 +295,7 @@ struct AppReviewControllerTests { ) } - var activeDays: Int { defaults.stringArray(forKey: SettingsKey.reviewActiveDays)?.count ?? 0 } + var activeDays: Int { defaults.stringArray(forKey: DesktopSettingsKey.reviewActiveDays)?.count ?? 0 } func clear() { defaults.removePersistentDomain(forName: suite) } diff --git a/TFMTests/ArchitectureBoundaryTests.swift b/TFMTests/ArchitectureBoundaryTests.swift index ac2a68fb..c63741fe 100644 --- a/TFMTests/ArchitectureBoundaryTests.swift +++ b/TFMTests/ArchitectureBoundaryTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @Suite @@ -21,7 +26,7 @@ struct ArchitectureBoundaryTests { #expect(mobile.contains("SyncedSkillLibraryPersistence")) #expect(mobile.contains("skills.applyRemoteSnapshot")) let settings = try String( - contentsOf: root.appending(path: "TFI/Features/MobileAISettingsView.swift"), encoding: .utf8) + contentsOf: root.appending(path: "TFI/Features/Settings/MobileAISettingsView.swift"), encoding: .utf8) #expect(!settings.contains("AgentRuntime(")) } @@ -53,14 +58,15 @@ struct ArchitectureBoundaryTests { @Test func assistantCompositionIsSharedAndMobileInputRemainsSwiftUI() throws { let mobile = try String( - contentsOf: sourceRoot.appending(path: "../TFI/App/MobileAssistantModel.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/Application/Agent/MobileAssistantModel.swift"), + encoding: .utf8) let desktop = try String(contentsOf: sourceRoot.appending(path: "App/AppModel.swift"), encoding: .utf8) for source in [mobile, desktop] { #expect(source.contains("AssistantExecutionController(")) #expect(!source.contains("AgentRuntime(")) } let composer = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileAssistantComposer.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/Features/AI/MobileAssistantComposer.swift"), encoding: .utf8) #expect(composer.contains("axis: .vertical")) #expect(!composer.contains("UITextView")) #expect(!composer.contains("UIViewRepresentable")) @@ -145,6 +151,64 @@ struct ArchitectureBoundaryTests { } } + @Test + func appTargetsDoNotReintroduceFrameworkForwardingAliases() throws { + let forwarding = try Regex( + #"(?m)^typealias\s+[A-Za-z_][A-Za-z0-9_]*\s*=\s*(?:Termind|Shared)[A-Za-z0-9_.]*"# + ) + for directory in ["", "../TFI"] { + for file in try swiftFiles(in: directory) { + let source = try String(contentsOf: file, encoding: .utf8) + #expect( + source.firstMatch(of: forwarding) == nil, + "\(file.path) forwards a framework type instead of importing its owner" + ) + } + } + + for file in try swiftFiles(in: "../Frameworks/TermindPersistence") { + #expect(!file.lastPathComponent.hasPrefix("Shared")) + } + + let sharedDeclaration = try Regex( + #"(?m)^public\s+(?:nonisolated\s+)?(?:final\s+)?(?:struct|class|enum|actor|protocol|typealias)\s+Shared[A-Z]"# + ) + for module in [ + "TermindDomain", "TermindApplication", "TermindInfrastructure", + "TermindRemoteInfrastructure", "TermindPresentation", "TermindPersistence", + ] { + for file in try swiftFiles(in: "../Frameworks/\(module)") { + let source = try String(contentsOf: file, encoding: .utf8) + #expect( + source.firstMatch(of: sharedDeclaration) == nil, + "\(file.path) uses Shared as a framework namespace" + ) + } + } + } + + @Test + func mobileTargetUsesTheDesktopLayerVocabulary() throws { + let mobile = sourceRoot.appending(path: "../TFI", directoryHint: .isDirectory) + for name in [ + "App", "Application", "Design", "DesignSystem", "Features", "Infrastructure", "Platform", "UI", + ] { + let directory = mobile.appending(path: name, directoryHint: .isDirectory) + var isDirectory: ObjCBool = false + #expect(FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory)) + #expect(isDirectory.boolValue) + #expect(!(try swiftFiles(in: "../TFI/\(name)")).isEmpty) + } + + try assertNoImports( + in: "../TFI/Application", + forbidden: ["SwiftUI", "UIKit", "SwiftData", "TermindPersistence"]) + try assertNoImports( + in: "../TFI/UI", + forbidden: ["SwiftData", "TermindPersistence", "TermindRemoteInfrastructure"]) + try assertNoConcreteImplementations(in: "../TFI/Features") + } + @Test func sessionFeaturesDependOnTheConnectionPort() throws { for path in ["Features/Terminal/SSHSessionModel.swift", "Features/SFTP/SFTPSessionModel.swift"] { @@ -300,33 +364,40 @@ struct ArchitectureBoundaryTests { @Test func mobileNavigationAndMetricsShareTheDesktopDesign() throws { let root = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileRootView.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/App/MobileRootView.swift"), encoding: .utf8) #expect(!root.contains("Welcome")) #expect(root.contains("Label(\"Transfers\", systemImage: \"arrow.up.arrow.down\")")) let vault = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileVaultView.swift"), encoding: .utf8) - #expect(vault.contains("Label(\"Metrics\", systemImage:")) + contentsOf: sourceRoot.appending(path: "../TFI/Features/Vault/MobileVaultView.swift"), encoding: .utf8) + let navigation = try String( + contentsOf: sourceRoot.appending(path: "../TFI/Features/Shell/MobileNavigation.swift"), encoding: .utf8) + #expect(navigation.contains("case vault, metrics, workspaces, assistant, settings")) #expect(!vault.contains("Label(\"Port Forwarding\", systemImage:")) #expect(vault.contains("open(host.id, .ssh)")) let cards = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileCards.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/UI/Managed/MobileCards.swift"), encoding: .utf8) #expect(vault.contains("MobileActionCard {")) - #expect(cards.contains(".contextMenu { actions }")) + #expect(cards.contains(".contextMenu {")) + #expect(cards.contains("actions")) #expect(!vault.contains("Import SSH Files")) let keys = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileCredentialsView.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/Features/Keychain/MobileCredentialsView.swift"), + encoding: .utf8) #expect(!keys.contains("draft.notes")) let settings = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileSettingsView.swift"), encoding: .utf8) - #expect(settings.contains("Label(\"App\", systemImage: \"app\")")) + contentsOf: sourceRoot.appending(path: "../TFI/Features/Settings/MobileSettingsView.swift"), encoding: .utf8 + ) + #expect(settings.contains("Label(\"App\", systemImage: \"app.badge\")")) #expect(settings.contains("Label(\"Sync\", systemImage: \"icloud\")")) #expect(!settings.contains("Live Activities")) let mobile = try String( - contentsOf: sourceRoot.appending(path: "../TFI/App/MobileMetricsModel.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/Application/Metrics/MobileMetricsModel.swift"), + encoding: .utf8) #expect(mobile.contains("ServerMetricsModel(")) #expect(!mobile.contains("while !Task.isCancelled")) let workspace = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileWorkspaceView.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/Features/Workspaces/MobileWorkspaceView.swift"), + encoding: .utf8) #expect(workspace.contains(".toolbarTitleMenu")) #expect(!workspace.contains("connectionMenuAction")) #expect(workspace.contains("let closeWorkspace: (UUID) -> Void")) @@ -338,7 +409,8 @@ struct ArchitectureBoundaryTests { #expect(root.contains("private func closeWorkspace(_ id: UUID)")) #expect(!workspace.contains("Circle().fill(workspace.connected")) let keyboard = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileTerminalKeyboard.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/Features/Terminal/MobileTerminalKeyboard.swift"), + encoding: .utf8) #expect(keyboard.contains("ScrollView(.horizontal")) #expect(keyboard.contains("keyboardToggle.padding(.horizontal, 8)")) #expect(keyboard.contains("static let barHeight: CGFloat = 44")) @@ -361,7 +433,8 @@ struct ArchitectureBoundaryTests { #expect(project.contains("TFM/TFM.entitlements")) #expect(!project.contains("Termind.entitlements")) let terminal = try String( - contentsOf: sourceRoot.appending(path: "../TFI/Features/MobileTerminalSettingsView.swift"), encoding: .utf8) + contentsOf: sourceRoot.appending(path: "../TFI/Features/Settings/MobileTerminalSettingsView.swift"), + encoding: .utf8) #expect(!terminal.contains("remoteWindowTitleInMenuBar")) let cloud = try swiftFiles(in: "../Frameworks/TermindInfrastructure/Cloud") #expect(cloud.count == 2) diff --git a/TFMTests/AssistantConversationListDropTests.swift b/TFMTests/AssistantConversationListDropTests.swift index af67613c..2ea2bff8 100644 --- a/TFMTests/AssistantConversationListDropTests.swift +++ b/TFMTests/AssistantConversationListDropTests.swift @@ -1,3 +1,10 @@ +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure + #if canImport(AppKit) import AppKit import ListViewKit diff --git a/TFMTests/AssistantSearchTests.swift b/TFMTests/AssistantSearchTests.swift index 3c8abac3..90653c2a 100644 --- a/TFMTests/AssistantSearchTests.swift +++ b/TFMTests/AssistantSearchTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/AuthorizedKeyExportTests.swift b/TFMTests/AuthorizedKeyExportTests.swift index 75d9fd2c..755c7b20 100644 --- a/TFMTests/AuthorizedKeyExportTests.swift +++ b/TFMTests/AuthorizedKeyExportTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/BroadcastInputTests.swift b/TFMTests/BroadcastInputTests.swift index 5c0215a3..b002ebd7 100644 --- a/TFMTests/BroadcastInputTests.swift +++ b/TFMTests/BroadcastInputTests.swift @@ -1,6 +1,10 @@ import Foundation import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/CardMarqueeSelectionTests.swift b/TFMTests/CardMarqueeSelectionTests.swift index 72d9395b..cd58a0cc 100644 --- a/TFMTests/CardMarqueeSelectionTests.swift +++ b/TFMTests/CardMarqueeSelectionTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ChatComposerStateTests.swift b/TFMTests/ChatComposerStateTests.swift index e2c03a81..bfbc8a7d 100644 --- a/TFMTests/ChatComposerStateTests.swift +++ b/TFMTests/ChatComposerStateTests.swift @@ -1,6 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind @@ -327,7 +332,7 @@ struct ChatComposerStateTests { let suiteName = "AgentPermissionMode-Invalid-\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set("unknown-mode", forKey: SettingsKey.agentPermissionMode) + defaults.set("unknown-mode", forKey: DesktopSettingsKey.agentPermissionMode) let store = AIConfigurationModel(persistsSecrets: false, permissionDefaults: defaults) diff --git a/TFMTests/ChatMessageTimestampTests.swift b/TFMTests/ChatMessageTimestampTests.swift index e6d7f6da..5b49809e 100644 --- a/TFMTests/ChatMessageTimestampTests.swift +++ b/TFMTests/ChatMessageTimestampTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ChatSlashCommandTests.swift b/TFMTests/ChatSlashCommandTests.swift index af0b146c..e383fe0e 100644 --- a/TFMTests/ChatSlashCommandTests.swift +++ b/TFMTests/ChatSlashCommandTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ColorSystemTests.swift b/TFMTests/ColorSystemTests.swift index 1053d3af..af9b1195 100644 --- a/TFMTests/ColorSystemTests.swift +++ b/TFMTests/ColorSystemTests.swift @@ -1,6 +1,11 @@ import AppKit import SwiftUI +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/CommandHistoryTests.swift b/TFMTests/CommandHistoryTests.swift index ecfcd02f..e43348f2 100644 --- a/TFMTests/CommandHistoryTests.swift +++ b/TFMTests/CommandHistoryTests.swift @@ -1,7 +1,11 @@ import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/CommerceConfigurationTests.swift b/TFMTests/CommerceConfigurationTests.swift index 5f8e0677..1d32ded1 100644 --- a/TFMTests/CommerceConfigurationTests.swift +++ b/TFMTests/CommerceConfigurationTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ConnectionHistoryTests.swift b/TFMTests/ConnectionHistoryTests.swift index 350c84b2..c7c9c318 100644 --- a/TFMTests/ConnectionHistoryTests.swift +++ b/TFMTests/ConnectionHistoryTests.swift @@ -1,7 +1,11 @@ import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/ConnectionJourneyTests.swift b/TFMTests/ConnectionJourneyTests.swift index f5dc7337..e84c1ed8 100644 --- a/TFMTests/ConnectionJourneyTests.swift +++ b/TFMTests/ConnectionJourneyTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/ContextAttachmentImporterTests.swift b/TFMTests/ContextAttachmentImporterTests.swift index f7fed68e..949bebdc 100644 --- a/TFMTests/ContextAttachmentImporterTests.swift +++ b/TFMTests/ContextAttachmentImporterTests.swift @@ -2,6 +2,12 @@ import AgentKit import AppKit import Foundation import PDFKit +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import ZIPFoundation diff --git a/TFMTests/DataProtectionKeychainTests.swift b/TFMTests/DataProtectionKeychainTests.swift index aa4a9e61..fdd7e8af 100644 --- a/TFMTests/DataProtectionKeychainTests.swift +++ b/TFMTests/DataProtectionKeychainTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ExternalRequestTests.swift b/TFMTests/ExternalRequestTests.swift index 3e3c4537..bb67688d 100644 --- a/TFMTests/ExternalRequestTests.swift +++ b/TFMTests/ExternalRequestTests.swift @@ -2,7 +2,12 @@ import AgentKit import AppKit import Foundation import Observation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/FileURLDropTargetTests.swift b/TFMTests/FileURLDropTargetTests.swift index d0b2b32c..8c402a5d 100644 --- a/TFMTests/FileURLDropTargetTests.swift +++ b/TFMTests/FileURLDropTargetTests.swift @@ -1,3 +1,10 @@ +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure + #if canImport(AppKit) import AppKit import Testing diff --git a/TFMTests/GetMetricsToolTests.swift b/TFMTests/GetMetricsToolTests.swift index 5dfd90f3..2f6b2f64 100644 --- a/TFMTests/GetMetricsToolTests.swift +++ b/TFMTests/GetMetricsToolTests.swift @@ -1,7 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/HostLibraryMoveTests.swift b/TFMTests/HostLibraryMoveTests.swift index cc0e0def..9d1c6bff 100644 --- a/TFMTests/HostLibraryMoveTests.swift +++ b/TFMTests/HostLibraryMoveTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/IdentityCredentialTests.swift b/TFMTests/IdentityCredentialTests.swift index 2d949b26..a9fdd182 100644 --- a/TFMTests/IdentityCredentialTests.swift +++ b/TFMTests/IdentityCredentialTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/KeychainOrderTests.swift b/TFMTests/KeychainOrderTests.swift index e24eb899..b7714dbf 100644 --- a/TFMTests/KeychainOrderTests.swift +++ b/TFMTests/KeychainOrderTests.swift @@ -1,6 +1,11 @@ import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ListViewMigrationTests.swift b/TFMTests/ListViewMigrationTests.swift index 75cc268a..255dafae 100644 --- a/TFMTests/ListViewMigrationTests.swift +++ b/TFMTests/ListViewMigrationTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/LocalModelConfiguration.swift b/TFMTests/LocalModelConfiguration.swift index cbf09706..1bed70ba 100644 --- a/TFMTests/LocalModelConfiguration.swift +++ b/TFMTests/LocalModelConfiguration.swift @@ -1,5 +1,11 @@ import Foundation import SwiftData +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure /// Tests run inside the signed app host. Once that host has an iCloud /// entitlement SwiftData's `.automatic` default would incorrectly attempt to diff --git a/TFMTests/LoginItemTests.swift b/TFMTests/LoginItemTests.swift index d44ad390..ad320628 100644 --- a/TFMTests/LoginItemTests.swift +++ b/TFMTests/LoginItemTests.swift @@ -1,6 +1,12 @@ import AppKit import Foundation import ServiceManagement +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind @@ -156,13 +162,13 @@ struct LoginItemTests { @Test func openingHiddenIsOffUntilTheUserAsksForIt() { let defaults = UserDefaults.standard - let original = defaults.object(forKey: SettingsKey.openAtLoginQuietly) - defer { defaults.set(original, forKey: SettingsKey.openAtLoginQuietly) } + let original = defaults.object(forKey: DesktopSettingsKey.openAtLoginQuietly) + defer { defaults.set(original, forKey: DesktopSettingsKey.openAtLoginQuietly) } - defaults.removeObject(forKey: SettingsKey.openAtLoginQuietly) - #expect(!SettingsKey.opensAtLoginQuietly) + defaults.removeObject(forKey: DesktopSettingsKey.openAtLoginQuietly) + #expect(!DesktopSettingsKey.opensAtLoginQuietly) - defaults.set(true, forKey: SettingsKey.openAtLoginQuietly) - #expect(SettingsKey.opensAtLoginQuietly) + defaults.set(true, forKey: DesktopSettingsKey.openAtLoginQuietly) + #expect(DesktopSettingsKey.opensAtLoginQuietly) } } diff --git a/TFMTests/LoopbackHTTPServerTests.swift b/TFMTests/LoopbackHTTPServerTests.swift index d4e99e55..333a53ef 100644 --- a/TFMTests/LoopbackHTTPServerTests.swift +++ b/TFMTests/LoopbackHTTPServerTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind @@ -17,8 +23,8 @@ struct LoopbackHTTPServerTests { @Test func mcpServerIsOffByDefault() { - #expect(SettingsKey.defaultMCPServerEnabled == false) - #expect(SettingsKey.defaultMCPServerPort == 7_391) + #expect(DesktopSettingsKey.defaultMCPServerEnabled == false) + #expect(DesktopSettingsKey.defaultMCPServerPort == 7_391) } @Test diff --git a/TFMTests/MCPClientSetupCommandTests.swift b/TFMTests/MCPClientSetupCommandTests.swift index f514e91e..bc32adc0 100644 --- a/TFMTests/MCPClientSetupCommandTests.swift +++ b/TFMTests/MCPClientSetupCommandTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/MCPServerSessionTests.swift b/TFMTests/MCPServerSessionTests.swift index 49e8adc8..a763dc59 100644 --- a/TFMTests/MCPServerSessionTests.swift +++ b/TFMTests/MCPServerSessionTests.swift @@ -1,6 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/MCPServerToolsTests.swift b/TFMTests/MCPServerToolsTests.swift index b7b289f7..84a39955 100644 --- a/TFMTests/MCPServerToolsTests.swift +++ b/TFMTests/MCPServerToolsTests.swift @@ -1,6 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/ManagedChooserTests.swift b/TFMTests/ManagedChooserTests.swift index 5ea87182..dd7b6552 100644 --- a/TFMTests/ManagedChooserTests.swift +++ b/TFMTests/ManagedChooserTests.swift @@ -1,3 +1,9 @@ +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/MarkdownParsingTests.swift b/TFMTests/MarkdownParsingTests.swift index fea416a7..08044286 100644 --- a/TFMTests/MarkdownParsingTests.swift +++ b/TFMTests/MarkdownParsingTests.swift @@ -1,6 +1,12 @@ import AppKit import Foundation import MarkdownParser +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/MenuSymbolTests.swift b/TFMTests/MenuSymbolTests.swift index e25f38ae..711362e9 100644 --- a/TFMTests/MenuSymbolTests.swift +++ b/TFMTests/MenuSymbolTests.swift @@ -1,5 +1,11 @@ import AppKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/MetricsInteractionTests.swift b/TFMTests/MetricsInteractionTests.swift index 4e217da3..08b16507 100644 --- a/TFMTests/MetricsInteractionTests.swift +++ b/TFMTests/MetricsInteractionTests.swift @@ -1,7 +1,12 @@ import AppKit import Foundation import SwiftUI +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host @@ -102,7 +107,7 @@ struct MetricsInteractionTests { app.shutdownForTermination() defaults.removePersistentDomain(forName: suite) } - defaults.set(true, forKey: SettingsKey.showMetricsInMenuBar) + defaults.set(true, forKey: DesktopSettingsKey.showMetricsInMenuBar) menu.start(app.serverMetrics) #expect(menu.isVisible) // Status-item hosting and popover presentation cross an asynchronous AppKit scene boundary. @@ -125,10 +130,10 @@ struct MetricsInteractionTests { try await Task.sleep(for: .milliseconds(10)) } #expect(!menu.isPopoverShown) - defaults.set(false, forKey: SettingsKey.showMetricsInMenuBar) + defaults.set(false, forKey: DesktopSettingsKey.showMetricsInMenuBar) menu.refresh() #expect(!menu.isVisible) - defaults.set(true, forKey: SettingsKey.showMetricsInMenuBar) + defaults.set(true, forKey: DesktopSettingsKey.showMetricsInMenuBar) menu.refresh() #expect(menu.isVisible) } diff --git a/TFMTests/MetricsLimitTests.swift b/TFMTests/MetricsLimitTests.swift index a9f975a0..86511ccf 100644 --- a/TFMTests/MetricsLimitTests.swift +++ b/TFMTests/MetricsLimitTests.swift @@ -1,6 +1,10 @@ import Foundation +import TermindApplication import TermindDomain import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/MetricsPresentationTests.swift b/TFMTests/MetricsPresentationTests.swift index b68e944b..bd0b53b2 100644 --- a/TFMTests/MetricsPresentationTests.swift +++ b/TFMTests/MetricsPresentationTests.swift @@ -1,8 +1,12 @@ import AppKit import Foundation import SwiftUI +import TermindApplication import TermindDomain import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/MetricsSnapshotTests.swift b/TFMTests/MetricsSnapshotTests.swift index e33a1de6..618d7a28 100644 --- a/TFMTests/MetricsSnapshotTests.swift +++ b/TFMTests/MetricsSnapshotTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/OnboardingControllerTests.swift b/TFMTests/OnboardingControllerTests.swift index ba6dfd2d..121e2e27 100644 --- a/TFMTests/OnboardingControllerTests.swift +++ b/TFMTests/OnboardingControllerTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind @@ -42,7 +48,7 @@ struct OnboardingControllerTests { func statusBarPathWinsWhenBothContextualStepsAreEligible() throws { let defaults = try makeDefaults() defer { clear(defaults) } - defaults.set(true, forKey: SettingsKey.onboardingQuickOpenCompleted) + defaults.set(true, forKey: DesktopSettingsKey.onboardingQuickOpenCompleted) let controller = OnboardingController(defaults: defaults) let window = WindowModel() controller.windowBecameActive(window) @@ -64,8 +70,8 @@ struct OnboardingControllerTests { func commandGeneratorWaitsForItsRealContext() throws { let defaults = try makeDefaults() defer { clear(defaults) } - defaults.set(true, forKey: SettingsKey.onboardingQuickOpenCompleted) - defaults.set(true, forKey: SettingsKey.onboardingStatusBarPathCompleted) + defaults.set(true, forKey: DesktopSettingsKey.onboardingQuickOpenCompleted) + defaults.set(true, forKey: DesktopSettingsKey.onboardingStatusBarPathCompleted) let controller = OnboardingController(defaults: defaults) let window = WindowModel() controller.windowBecameActive(window) diff --git a/TFMTests/PersistentWorkspaceDeckLayoutTests.swift b/TFMTests/PersistentWorkspaceDeckLayoutTests.swift index 6c4885c9..40963084 100644 --- a/TFMTests/PersistentWorkspaceDeckLayoutTests.swift +++ b/TFMTests/PersistentWorkspaceDeckLayoutTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/PortForwardingEditorTests.swift b/TFMTests/PortForwardingEditorTests.swift index e7bcfa7e..16108f76 100644 --- a/TFMTests/PortForwardingEditorTests.swift +++ b/TFMTests/PortForwardingEditorTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/PortForwardingFeatureTests.swift b/TFMTests/PortForwardingFeatureTests.swift index b280ee9a..f9ab1d29 100644 --- a/TFMTests/PortForwardingFeatureTests.swift +++ b/TFMTests/PortForwardingFeatureTests.swift @@ -1,6 +1,11 @@ import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/ProAccessTests.swift b/TFMTests/ProAccessTests.swift index 60da9f2f..f3d646e0 100644 --- a/TFMTests/ProAccessTests.swift +++ b/TFMTests/ProAccessTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ProviderEndpointIntegrationTests.swift b/TFMTests/ProviderEndpointIntegrationTests.swift index 99b3f919..0960d778 100644 --- a/TFMTests/ProviderEndpointIntegrationTests.swift +++ b/TFMTests/ProviderEndpointIntegrationTests.swift @@ -1,6 +1,11 @@ import AgentKit import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/QuickOpenTests.swift b/TFMTests/QuickOpenTests.swift index 91f6c76b..c317f752 100644 --- a/TFMTests/QuickOpenTests.swift +++ b/TFMTests/QuickOpenTests.swift @@ -1,5 +1,10 @@ import AgentKit +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/RemoteWindowTitleTests.swift b/TFMTests/RemoteWindowTitleTests.swift index aec5431c..d2668ddd 100644 --- a/TFMTests/RemoteWindowTitleTests.swift +++ b/TFMTests/RemoteWindowTitleTests.swift @@ -1,6 +1,12 @@ import AppKit import Foundation import GhosttyTerminal +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/RepositoryOutboxTests.swift b/TFMTests/RepositoryOutboxTests.swift index 6d5ee21c..f9d848d0 100644 --- a/TFMTests/RepositoryOutboxTests.swift +++ b/TFMTests/RepositoryOutboxTests.swift @@ -1,7 +1,11 @@ import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/RuntimeVersionsTests.swift b/TFMTests/RuntimeVersionsTests.swift index 0d3382fa..c4c10441 100644 --- a/TFMTests/RuntimeVersionsTests.swift +++ b/TFMTests/RuntimeVersionsTests.swift @@ -1,3 +1,9 @@ +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/SFTPFeatureTests.swift b/TFMTests/SFTPFeatureTests.swift index ac726190..46179c3d 100644 --- a/TFMTests/SFTPFeatureTests.swift +++ b/TFMTests/SFTPFeatureTests.swift @@ -1,6 +1,11 @@ import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import UniformTypeIdentifiers diff --git a/TFMTests/SFTPHistoryTests.swift b/TFMTests/SFTPHistoryTests.swift index ed093c4e..0f775a33 100644 --- a/TFMTests/SFTPHistoryTests.swift +++ b/TFMTests/SFTPHistoryTests.swift @@ -1,6 +1,11 @@ import Foundation import SwiftData +import TermindApplication +import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/SFTPSessionTests.swift b/TFMTests/SFTPSessionTests.swift index 189f7081..74978ce4 100644 --- a/TFMTests/SFTPSessionTests.swift +++ b/TFMTests/SFTPSessionTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/SSHImportTests.swift b/TFMTests/SSHImportTests.swift index 8275accc..84bba884 100644 --- a/TFMTests/SSHImportTests.swift +++ b/TFMTests/SSHImportTests.swift @@ -1,6 +1,11 @@ import CryptoKit import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/SSHUserCertificateTests.swift b/TFMTests/SSHUserCertificateTests.swift index 1c2e2223..0216d416 100644 --- a/TFMTests/SSHUserCertificateTests.swift +++ b/TFMTests/SSHUserCertificateTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/ScreenshotDemoTests.swift b/TFMTests/ScreenshotDemoTests.swift index bae246c0..e5bc5e89 100644 --- a/TFMTests/ScreenshotDemoTests.swift +++ b/TFMTests/ScreenshotDemoTests.swift @@ -1,4 +1,9 @@ +import TermindApplication +import TermindDomain import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure #if DEBUG import Testing diff --git a/TFMTests/ScrubberWebClientTests.swift b/TFMTests/ScrubberWebClientTests.swift index 65e6d528..2bc52e6a 100644 --- a/TFMTests/ScrubberWebClientTests.swift +++ b/TFMTests/ScrubberWebClientTests.swift @@ -1,6 +1,12 @@ import AgentKit import AgentKitScrubber import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/SerialFeatureTests.swift b/TFMTests/SerialFeatureTests.swift index 347a8615..1fd6cec4 100644 --- a/TFMTests/SerialFeatureTests.swift +++ b/TFMTests/SerialFeatureTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ServerMetricsTests.swift b/TFMTests/ServerMetricsTests.swift index c97ffcb0..171fc7ea 100644 --- a/TFMTests/ServerMetricsTests.swift +++ b/TFMTests/ServerMetricsTests.swift @@ -1,6 +1,10 @@ import Foundation +import TermindApplication import TermindDomain import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/SessionContextTests.swift b/TFMTests/SessionContextTests.swift index 9ea59a4f..7967d91a 100644 --- a/TFMTests/SessionContextTests.swift +++ b/TFMTests/SessionContextTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/SettingsDetailControlTests.swift b/TFMTests/SettingsDetailControlTests.swift index a17ccb45..bf7d0277 100644 --- a/TFMTests/SettingsDetailControlTests.swift +++ b/TFMTests/SettingsDetailControlTests.swift @@ -1,6 +1,10 @@ import Foundation import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/SettingsNavigationTests.swift b/TFMTests/SettingsNavigationTests.swift index 8633b4cb..eaa9de41 100644 --- a/TFMTests/SettingsNavigationTests.swift +++ b/TFMTests/SettingsNavigationTests.swift @@ -1,5 +1,11 @@ import AgentKit import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/SettingsSyncTests.swift b/TFMTests/SettingsSyncTests.swift index 8418e596..18f97609 100644 --- a/TFMTests/SettingsSyncTests.swift +++ b/TFMTests/SettingsSyncTests.swift @@ -3,8 +3,12 @@ import CloudKit import CoreData import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/ShellIntegrationBootstrapTests.swift b/TFMTests/ShellIntegrationBootstrapTests.swift index ed3a5b64..47da4352 100644 --- a/TFMTests/ShellIntegrationBootstrapTests.swift +++ b/TFMTests/ShellIntegrationBootstrapTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/SnippetFeatureTests.swift b/TFMTests/SnippetFeatureTests.swift index 9fc240a8..f245d806 100644 --- a/TFMTests/SnippetFeatureTests.swift +++ b/TFMTests/SnippetFeatureTests.swift @@ -1,6 +1,11 @@ import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/SoftwareKeyFileImporterTests.swift b/TFMTests/SoftwareKeyFileImporterTests.swift index 42af083f..f3361e19 100644 --- a/TFMTests/SoftwareKeyFileImporterTests.swift +++ b/TFMTests/SoftwareKeyFileImporterTests.swift @@ -1,4 +1,10 @@ import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/StatusBarPresentationTests.swift b/TFMTests/StatusBarPresentationTests.swift index b766419f..35671a8d 100644 --- a/TFMTests/StatusBarPresentationTests.swift +++ b/TFMTests/StatusBarPresentationTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/StoreKitTrialTests.swift b/TFMTests/StoreKitTrialTests.swift index 936c2b10..138491c6 100644 --- a/TFMTests/StoreKitTrialTests.swift +++ b/TFMTests/StoreKitTrialTests.swift @@ -1,7 +1,12 @@ import Foundation import StoreKit import StoreKitTest +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/TerminalFindTests.swift b/TFMTests/TerminalFindTests.swift index afaa8b2a..2f6f6c13 100644 --- a/TFMTests/TerminalFindTests.swift +++ b/TFMTests/TerminalFindTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host @@ -60,8 +65,8 @@ struct TerminalFindTests { /// agree on that; ⌘G used to be wired the other way round. @Test func findNextWalksTowardOlderOutput() { - #expect(TerminalFindDirection.previous.bindingAction == "navigate_search:previous") - #expect(TerminalFindDirection.next.bindingAction == "navigate_search:next") + #expect(TerminalFindBinding.navigate(.previous) == "navigate_search:previous") + #expect(TerminalFindBinding.navigate(.next) == "navigate_search:next") } @MainActor diff --git a/TFMTests/TerminalThemeTests.swift b/TFMTests/TerminalThemeTests.swift index 09cf15d8..56879417 100644 --- a/TFMTests/TerminalThemeTests.swift +++ b/TFMTests/TerminalThemeTests.swift @@ -1,3 +1,9 @@ +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind @@ -5,8 +11,8 @@ import Testing struct TerminalThemeTests { @Test func defaultThemeBackgroundsMatchMacOSWindowBackgrounds() { - #expect(TerminalTheme.termindLight.background.hex == "FFFFFF") - #expect(TerminalTheme.termindDark.background.hex == "1E1E1E") + #expect(TermindTerminalPalette.termindLight.background.hex == "FFFFFF") + #expect(TermindTerminalPalette.termindDark.background.hex == "1E1E1E") } @Test @@ -24,13 +30,13 @@ struct TerminalThemeTests { isDark: true ) - #expect(light == TerminalTheme.termindLight.background) - #expect(dark == TerminalTheme.termindDark.background) + #expect(light == TermindTerminalPalette.termindLight.background) + #expect(dark == TermindTerminalPalette.termindDark.background) } @Test func customConfigurationParsesGhosttyLinesInOrder() { - let configuration = GhosttyCustomConfiguration( + let configuration = TerminalCustomConfiguration( """ # comment @@ -75,7 +81,7 @@ struct TerminalThemeTests { isDark: false ) - #expect(background == TerminalTheme.termindLight.background) + #expect(background == TermindTerminalPalette.termindLight.background) } @Test @@ -87,6 +93,6 @@ struct TerminalThemeTests { isDark: true ) - #expect(background == TerminalTheme.termindDark.background) + #expect(background == TermindTerminalPalette.termindDark.background) } } diff --git a/TFMTests/TranscriptListViewTests.swift b/TFMTests/TranscriptListViewTests.swift index a98c7374..f9679a50 100644 --- a/TFMTests/TranscriptListViewTests.swift +++ b/TFMTests/TranscriptListViewTests.swift @@ -3,6 +3,12 @@ import AppKit import Foundation import ListViewKit import SwiftUI +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/TranscriptPerformanceTests.swift b/TFMTests/TranscriptPerformanceTests.swift index ab334d47..0f23347b 100644 --- a/TFMTests/TranscriptPerformanceTests.swift +++ b/TFMTests/TranscriptPerformanceTests.swift @@ -1,5 +1,11 @@ import Darwin import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/UserInputModalityTests.swift b/TFMTests/UserInputModalityTests.swift index 8b52caca..77492f89 100644 --- a/TFMTests/UserInputModalityTests.swift +++ b/TFMTests/UserInputModalityTests.swift @@ -1,4 +1,10 @@ import AppKit +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/VaultSelectionTests.swift b/TFMTests/VaultSelectionTests.swift index 019c88d2..d7d79382 100644 --- a/TFMTests/VaultSelectionTests.swift +++ b/TFMTests/VaultSelectionTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/VaultSyncTests.swift b/TFMTests/VaultSyncTests.swift index 9e846bcd..c8adc9ee 100644 --- a/TFMTests/VaultSyncTests.swift +++ b/TFMTests/VaultSyncTests.swift @@ -1,8 +1,12 @@ import CloudKit import Foundation import SwiftData +import TermindApplication import TermindDomain +import TermindInfrastructure import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/WebSearchTests.swift b/TFMTests/WebSearchTests.swift index 52178836..217874db 100644 --- a/TFMTests/WebSearchTests.swift +++ b/TFMTests/WebSearchTests.swift @@ -1,6 +1,12 @@ import AgentKit import AgentKitScrubber import Foundation +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/WindowSidebarTests.swift b/TFMTests/WindowSidebarTests.swift index f5989dec..8ecbd262 100644 --- a/TFMTests/WindowSidebarTests.swift +++ b/TFMTests/WindowSidebarTests.swift @@ -1,4 +1,10 @@ import AppKit +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing @testable import Termind diff --git a/TFMTests/WindowStateTests.swift b/TFMTests/WindowStateTests.swift index ddf7a80f..d4a3d296 100644 --- a/TFMTests/WindowStateTests.swift +++ b/TFMTests/WindowStateTests.swift @@ -1,7 +1,11 @@ import AgentKit import Foundation +import TermindApplication import TermindDomain import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/TFMTests/WindowToolbarFindTests.swift b/TFMTests/WindowToolbarFindTests.swift index f41dafb5..b3bab1ba 100644 --- a/TFMTests/WindowToolbarFindTests.swift +++ b/TFMTests/WindowToolbarFindTests.swift @@ -1,5 +1,10 @@ import Foundation +import TermindApplication import TermindDomain +import TermindInfrastructure +import TermindPersistence +import TermindPresentation +import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj index 8df29016..a100fbe9 100644 --- a/Termind.xcodeproj/project.pbxproj +++ b/Termind.xcodeproj/project.pbxproj @@ -494,9 +494,9 @@ path = TermindRemoteInfrastructure; sourceTree = ""; }; - FD8DC40513A4A80A71C41FCA /* TFIActivityShared */ = { + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */ = { isa = PBXFileSystemSynchronizedRootGroup; - path = TFIActivityShared; + path = WorkspaceActivitySupport; sourceTree = ""; }; TRMD00000000000000000010 /* TFM */ = { @@ -697,7 +697,7 @@ isa = PBXGroup; children = ( CB622EAF077B1253532F1E18 /* TFIWidgets */, - FD8DC40513A4A80A71C41FCA /* TFIActivityShared */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, DBE378B130478CFB00B16AA1 /* Termind.storekit */, CDDB75F14F753E46BB14D972 /* Frameworks */, TRMD00000000000000000010 /* TFM */, @@ -756,7 +756,7 @@ fileSystemSynchronizedGroups = ( 0FE0DF713B1C0832D76696AF /* TFI */, 46E0A2AA823D19093256BE94 /* Resources */, - FD8DC40513A4A80A71C41FCA /* TFIActivityShared */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, ); name = TFI; packageProductDependencies = ( @@ -913,7 +913,7 @@ fileSystemSynchronizedGroups = ( 46E0A2AA823D19093256BE94 /* Resources */, CB622EAF077B1253532F1E18 /* TFIWidgets */, - FD8DC40513A4A80A71C41FCA /* TFIActivityShared */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, ); name = TermindWidgets; packageProductDependencies = ( @@ -995,7 +995,7 @@ ); fileSystemSynchronizedGroups = ( A102A7EA2C3532D7FA88B0A4 /* TFITests */, - FD8DC40513A4A80A71C41FCA /* TFIActivityShared */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, ); name = TFITests; packageProductDependencies = ( diff --git a/TFIActivityShared/WorkspaceActivityAttributes.swift b/WorkspaceActivitySupport/WorkspaceActivityAttributes.swift similarity index 100% rename from TFIActivityShared/WorkspaceActivityAttributes.swift rename to WorkspaceActivitySupport/WorkspaceActivityAttributes.swift