From f1796d879a35d925038f1edaa3607d2fe4e2e293 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Sat, 19 Sep 2026 12:30:16 +0800 Subject: [PATCH] Remove useless code --- .../PortForwardingEditorPolicyTests.swift | 29 +- .../Agent/TermindAgentPolicy.swift | 1 - .../SSHKeyboardInteractiveChallenge.swift | 1 - .../ForwardingLibraryModel.swift | 1 - .../FeatureModels/MemoryModel.swift | 1 - .../PortForwardingEditorPolicy.swift | 23 +- .../Shell/CommandRiskRules.swift | 4 +- .../Cloud/MetricsSyncCoordinator.swift | 12 +- .../Cloud/SettingsSyncCoordinator.swift | 12 +- .../Cloud/VaultSyncCoordinator.swift | 12 +- .../PortForwardingFields.swift | 7 +- .../SSH/SSHForwardingSession.swift | 4 - Memory/REFACTOR.md | 18 ++ .../Agent/MobileAssistantModel.swift | 2 - TFI/Features/Ports/MobileForwardingView.swift | 7 +- TFI/Platform/DeviceCensorship.swift | 39 --- TFITests/DeviceCensorshipTests.swift | 12 - TFM/App/DesktopSettingsKey.swift | 2 - .../MCPServer/MCPUploadCoordinator.swift | 1 - TFM/Design/DesignTokens.swift | 1 - .../AI/ConversationTranscriptView.swift | 15 +- TFM/Features/AI/ConversationViews.swift | 24 +- TFM/Features/AI/MarkdownBlockListView.swift | 281 +----------------- TFM/Features/AI/MarkdownCodeBlockView.swift | 125 -------- TFM/Features/AI/SlashCommandMenuView.swift | 3 +- TFM/Features/AI/TranscriptNativeRows.swift | 17 +- TFM/Features/Assistant/AssistantView.swift | 13 +- .../KnownHosts/KnownHostsPageView.swift | 38 +-- .../Metrics/MetricsConnectionNotice.swift | 1 - .../Metrics/MetricsServerDetails.swift | 3 +- TFM/Features/Metrics/MetricsView.swift | 2 +- TFM/Features/Ports/PortForwardingView.swift | 40 +-- TFM/Features/SFTP/SFTPBrowserView.swift | 2 +- TFM/Features/SFTP/SFTPSessionModel.swift | 4 +- TFM/Features/Snippets/SnippetsPageView.swift | 8 - TFM/Features/Terminal/SSHSessionModel.swift | 4 +- .../Terminal/TerminalPlaceholderView.swift | 2 +- TFM/Features/Workspaces/WindowModel.swift | 6 +- TFM/Platform/AppShortcut.swift | 23 -- TFM/Platform/MainWindowCoordinator.swift | 1 - TFM/Platform/MetricsMenuBarController.swift | 7 - TFM/UI/Managed/ManagedCardPage.swift | 33 ++ TFMTests/PortForwardingEditorTests.swift | 83 +----- TFMTests/PortForwardingFeatureTests.swift | 1 - 44 files changed, 139 insertions(+), 786 deletions(-) delete mode 100644 TFI/Platform/DeviceCensorship.swift delete mode 100644 TFITests/DeviceCensorshipTests.swift diff --git a/FrameworkTests/PortForwardingEditorPolicyTests.swift b/FrameworkTests/PortForwardingEditorPolicyTests.swift index 99a9944b..2b167b09 100644 --- a/FrameworkTests/PortForwardingEditorPolicyTests.swift +++ b/FrameworkTests/PortForwardingEditorPolicyTests.swift @@ -11,11 +11,10 @@ struct PortForwardingEditorPolicyTests { func onlySavedStoppedRulesCanStart(hasSavedConfiguration: Bool, isActive: Bool) { let host = Host(name: "Server", host: "server.invalid") let rule = PortForwardingRule(hostID: host.id) - let policy = PortForwardingEditorPolicy( - rule: rule, savedRule: hasSavedConfiguration ? rule : nil, savedHost: host, - isRuntimeActive: isActive) - #expect(policy.canStart == (hasSavedConfiguration && !isActive)) - #expect(policy.canEdit == !isActive) + #expect( + PortForwardingEditorPolicy.canStart( + rule.id, savedRule: hasSavedConfiguration ? rule : nil, savedHost: host, isRuntimeActive: isActive) + == (hasSavedConfiguration && !isActive)) } enum UnavailableConfiguration: CaseIterable { @@ -34,13 +33,9 @@ struct PortForwardingEditorPolicyTests { case .mismatchedHost: savedHost = Host(name: "Other", host: "other.invalid") case .mismatchedRuleID: savedRule?.id = UUID() } - let policy = PortForwardingEditorPolicy( - rule: rule, savedRule: savedRule, savedHost: savedHost, - isRuntimeActive: false) - - #expect(!policy.hasSavedConfiguration) - #expect(!policy.canStart) - #expect(policy.canEdit) + #expect( + !PortForwardingEditorPolicy.canStart( + rule.id, savedRule: savedRule, savedHost: savedHost, isRuntimeActive: false)) } @Test(arguments: [0, 65_536]) @@ -49,12 +44,8 @@ struct PortForwardingEditorPolicyTests { let draft = PortForwardingRule(hostID: host.id) var savedRule = draft savedRule.bindPort = invalidPort - let policy = PortForwardingEditorPolicy( - rule: PortForwardingEditorPolicy.normalized(draft), savedRule: savedRule, savedHost: host, - isRuntimeActive: false) - - #expect(!policy.hasSavedConfiguration) - #expect(!policy.canStart) - #expect(policy.canEdit) + #expect( + !PortForwardingEditorPolicy.canStart( + draft.id, savedRule: savedRule, savedHost: host, isRuntimeActive: false)) } } diff --git a/Frameworks/TermindApplication/Agent/TermindAgentPolicy.swift b/Frameworks/TermindApplication/Agent/TermindAgentPolicy.swift index 9f286b16..892d3911 100644 --- a/Frameworks/TermindApplication/Agent/TermindAgentPolicy.swift +++ b/Frameworks/TermindApplication/Agent/TermindAgentPolicy.swift @@ -1,6 +1,5 @@ import AgentKit import Foundation -import TermindDomain /// What Termind, specifically, tells its agent. /// diff --git a/Frameworks/TermindApplication/Connections/SSHKeyboardInteractiveChallenge.swift b/Frameworks/TermindApplication/Connections/SSHKeyboardInteractiveChallenge.swift index 38dace7d..c63bad6b 100644 --- a/Frameworks/TermindApplication/Connections/SSHKeyboardInteractiveChallenge.swift +++ b/Frameworks/TermindApplication/Connections/SSHKeyboardInteractiveChallenge.swift @@ -1,5 +1,4 @@ import Foundation -import TermindDomain /// Server-authored authentication data. Neither challenges nor answers belong /// in an Agent transcript, connection log, or persistent credential store. diff --git a/Frameworks/TermindApplication/FeatureModels/ForwardingLibraryModel.swift b/Frameworks/TermindApplication/FeatureModels/ForwardingLibraryModel.swift index 9e060400..0a2333a3 100644 --- a/Frameworks/TermindApplication/FeatureModels/ForwardingLibraryModel.swift +++ b/Frameworks/TermindApplication/FeatureModels/ForwardingLibraryModel.swift @@ -46,7 +46,6 @@ public final class ForwardingLibraryModel { public func portForwarding(_ id: PortForwardingRule.ID) -> PortForwardingRule? { library.portForwarding(id) } - public func routeSummary(for host: Host) -> String { library.routeSummary(for: host) } public func sshCommand(for rule: PortForwardingRule, host: Host? = nil) -> String? { library.sshCommand(for: rule, host: host) } diff --git a/Frameworks/TermindApplication/FeatureModels/MemoryModel.swift b/Frameworks/TermindApplication/FeatureModels/MemoryModel.swift index 67d313c2..c884a2a0 100644 --- a/Frameworks/TermindApplication/FeatureModels/MemoryModel.swift +++ b/Frameworks/TermindApplication/FeatureModels/MemoryModel.swift @@ -1,6 +1,5 @@ import AgentKit import Foundation -import TermindDomain @Observable @MainActor public final class MemoryModel { diff --git a/Frameworks/TermindApplication/FeatureModels/PortForwardingEditorPolicy.swift b/Frameworks/TermindApplication/FeatureModels/PortForwardingEditorPolicy.swift index 6116e97a..b214273f 100644 --- a/Frameworks/TermindApplication/FeatureModels/PortForwardingEditorPolicy.swift +++ b/Frameworks/TermindApplication/FeatureModels/PortForwardingEditorPolicy.swift @@ -3,23 +3,12 @@ import TermindDomain import struct TermindDomain.Host -/// Shared edit and start policy for the platform-specific forwarding editors. -public nonisolated struct PortForwardingEditorPolicy { - public let hasSavedConfiguration: Bool - public let isRuntimeActive: Bool - - public var canStart: Bool { hasSavedConfiguration && !isRuntimeActive } - public var canEdit: Bool { !isRuntimeActive } - - public init( - rule: PortForwardingRule?, - savedRule: PortForwardingRule?, savedHost: Host?, - isRuntimeActive: Bool - ) { - hasSavedConfiguration = - savedRule?.isValid == true && savedRule?.hostID == savedHost?.id - && (rule == nil || rule?.id == savedRule?.id) - self.isRuntimeActive = isRuntimeActive +/// Shared validation and start policy for the platform-specific forwarding editors. +public nonisolated enum PortForwardingEditorPolicy { + public static func canStart( + _ id: PortForwardingRule.ID, savedRule: PortForwardingRule?, savedHost: Host?, isRuntimeActive: Bool + ) -> Bool { + savedRule?.id == id && savedRule?.isValid == true && savedRule?.hostID == savedHost?.id && !isRuntimeActive } public static func normalized(_ rule: PortForwardingRule) -> PortForwardingRule? { diff --git a/Frameworks/TermindDomain/Shell/CommandRiskRules.swift b/Frameworks/TermindDomain/Shell/CommandRiskRules.swift index 0f828b15..c6a0b667 100644 --- a/Frameworks/TermindDomain/Shell/CommandRiskRules.swift +++ b/Frameworks/TermindDomain/Shell/CommandRiskRules.swift @@ -25,7 +25,7 @@ nonisolated enum CommandRiskRules { switch name { case "rm": return classifyRemove(args) case "find": return classifyFind(args) - case "chmod", "chown", "chgrp": return classifyPermissions(name, args) + case "chmod", "chown", "chgrp": return classifyPermissions(args) case "awk": return caution(name) case "sort": return hasFlag(args, ["-o", "--output", "--compress-program"]) ? caution(name) : .safe @@ -157,7 +157,7 @@ nonisolated enum CommandRiskRules { return .safe } - private static func classifyPermissions(_ name: String, _ args: [String]) -> RiskAssessment { + private static func classifyPermissions(_ args: [String]) -> RiskAssessment { if shortFlags(args).contains("R") { return RiskAssessment( risk: .destructive, diff --git a/Frameworks/TermindInfrastructure/Cloud/MetricsSyncCoordinator.swift b/Frameworks/TermindInfrastructure/Cloud/MetricsSyncCoordinator.swift index f039bdb0..a73c4af5 100644 --- a/Frameworks/TermindInfrastructure/Cloud/MetricsSyncCoordinator.swift +++ b/Frameworks/TermindInfrastructure/Cloud/MetricsSyncCoordinator.swift @@ -159,13 +159,13 @@ public final class MetricsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unch case .accountChange(let change): try lifecycle.handleAccountChange(change.changeType) case .fetchedRecordZoneChanges(let changes): - try handleFetchedChanges(changes, engine: engine) + try handleFetchedChanges(changes) case .fetchedDatabaseChanges(let changes): if changes.deletions.contains(where: { $0.zoneID == zoneID }) { try lifecycle.handleZoneDeletion() } case .sentRecordZoneChanges(let changes): - try handleSentChanges(changes, engine: engine) + try handleSentChanges(changes) case .didFetchChanges, .didSendChanges: try lifecycle.updateUpToDateStatus() default: @@ -177,9 +177,7 @@ public final class MetricsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unch } } - private func handleFetchedChanges( - _ changes: CKSyncEngine.Event.FetchedRecordZoneChanges, engine: CKSyncEngine - ) throws { + private func handleFetchedChanges(_ changes: CKSyncEngine.Event.FetchedRecordZoneChanges) throws { var recordsByKey: [MetricsRecordKey: CKRecord] = [:] var envelopes: [MetricsEnvelope] = [] var quarantinedError: String? @@ -217,9 +215,7 @@ public final class MetricsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unch if let quarantinedError { status = .failed(quarantinedError) } } - private func handleSentChanges( - _ changes: CKSyncEngine.Event.SentRecordZoneChanges, engine: CKSyncEngine - ) throws { + private func handleSentChanges(_ changes: CKSyncEngine.Event.SentRecordZoneChanges) throws { for record in changes.savedRecords where record.recordID.zoneID == zoneID { guard let envelope = try? Self.envelope(from: record) else { continue } try journal.markUploaded(envelope.key, version: envelope.version) diff --git a/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift b/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift index d6792f1d..f16344c2 100644 --- a/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift +++ b/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift @@ -209,7 +209,7 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc try lifecycle.handleAccountChange(change.changeType) case .fetchedRecordZoneChanges(let changes): - try handleFetchedChanges(changes, engine: engine) + try handleFetchedChanges(changes) case .fetchedDatabaseChanges(let changes): if changes.deletions.contains(where: { $0.zoneID == zoneID }) { @@ -217,7 +217,7 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc } case .sentRecordZoneChanges(let changes): - try handleSentChanges(changes, engine: engine) + try handleSentChanges(changes) case .didFetchChanges, .didSendChanges: try lifecycle.updateUpToDateStatus() @@ -231,9 +231,7 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc } } - private func handleFetchedChanges( - _ changes: CKSyncEngine.Event.FetchedRecordZoneChanges, engine: CKSyncEngine - ) throws { + private func handleFetchedChanges(_ changes: CKSyncEngine.Event.FetchedRecordZoneChanges) throws { var recordsByKey: [SettingsRecordKey: CKRecord] = [:] var legacyGuardianKeys = Set() var envelopes: [SettingsEnvelope] = [] @@ -294,9 +292,7 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc if let quarantinedError { status = .failed(quarantinedError) } } - private func handleSentChanges( - _ changes: CKSyncEngine.Event.SentRecordZoneChanges, engine: CKSyncEngine - ) throws { + private func handleSentChanges(_ changes: CKSyncEngine.Event.SentRecordZoneChanges) throws { for record in changes.savedRecords where record.recordID.zoneID == zoneID { guard let envelope = try? Self.envelope(from: record) else { continue } try journal.markUploaded(envelope.key, version: envelope.version) diff --git a/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift b/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift index 14ece353..e281ef28 100644 --- a/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift +++ b/Frameworks/TermindInfrastructure/Cloud/VaultSyncCoordinator.swift @@ -169,7 +169,7 @@ public final class VaultSyncCoordinator: NSObject, CKSyncEngineDelegate, @unchec try lifecycle.handleAccountChange(change.changeType) case .fetchedRecordZoneChanges(let changes): - try handleFetchedChanges(changes, engine: engine) + try handleFetchedChanges(changes) case .fetchedDatabaseChanges(let changes): if changes.deletions.contains(where: { $0.zoneID == zoneID }) { @@ -177,7 +177,7 @@ public final class VaultSyncCoordinator: NSObject, CKSyncEngineDelegate, @unchec } case .sentRecordZoneChanges(let changes): - try handleSentChanges(changes, engine: engine) + try handleSentChanges(changes) case .didFetchChanges, .didSendChanges: try lifecycle.updateUpToDateStatus() @@ -191,9 +191,7 @@ public final class VaultSyncCoordinator: NSObject, CKSyncEngineDelegate, @unchec } } - private func handleFetchedChanges( - _ changes: CKSyncEngine.Event.FetchedRecordZoneChanges, engine: CKSyncEngine - ) throws { + private func handleFetchedChanges(_ changes: CKSyncEngine.Event.FetchedRecordZoneChanges) throws { var recordsByKey: [VaultRecordKey: CKRecord] = [:] var envelopes: [VaultEnvelope] = [] var quarantinedError: String? @@ -239,9 +237,7 @@ public final class VaultSyncCoordinator: NSObject, CKSyncEngineDelegate, @unchec if let quarantinedError { status = .failed(quarantinedError) } } - private func handleSentChanges( - _ changes: CKSyncEngine.Event.SentRecordZoneChanges, engine: CKSyncEngine - ) throws { + private func handleSentChanges(_ changes: CKSyncEngine.Event.SentRecordZoneChanges) throws { for record in changes.savedRecords where record.recordID.zoneID == zoneID { guard let envelope = try? Self.envelope(from: record) else { continue } try journal.markUploaded(envelope.key, version: envelope.version) diff --git a/Frameworks/TermindPresentation/PortForwardingFields.swift b/Frameworks/TermindPresentation/PortForwardingFields.swift index 186bf97c..dfde58f0 100644 --- a/Frameworks/TermindPresentation/PortForwardingFields.swift +++ b/Frameworks/TermindPresentation/PortForwardingFields.swift @@ -84,10 +84,9 @@ public struct PortForwardingFields: View { } -public struct PortForwardingDirectionPicker: View { - @Binding private var kind: PortForwardingRule.Kind - public init(kind: Binding) { _kind = kind } - public var body: some View { +private struct PortForwardingDirectionPicker: View { + @Binding var kind: PortForwardingRule.Kind + var body: some View { Picker("Direction", selection: $kind) { Text("Local").tag(PortForwardingRule.Kind.local).accessibilityLabel("Local Forward") Text("Remote").tag(PortForwardingRule.Kind.remote).accessibilityLabel("Remote Forward") diff --git a/Frameworks/TermindRemoteInfrastructure/SSH/SSHForwardingSession.swift b/Frameworks/TermindRemoteInfrastructure/SSH/SSHForwardingSession.swift index c9501d41..52e4e485 100644 --- a/Frameworks/TermindRemoteInfrastructure/SSH/SSHForwardingSession.swift +++ b/Frameworks/TermindRemoteInfrastructure/SSH/SSHForwardingSession.swift @@ -35,10 +35,6 @@ public actor SSHForwardingSession: SSHForwardingSessionProtocol { ) } - public func connect(_ configuration: SSHConnectionConfiguration) async throws { - try await connect(SSHConnectionRequest(configuration: configuration)) - } - public func connect(_ request: SSHConnectionRequest, progress: SSHRouteProgressHandler? = nil) async throws { try await worker.connect(request, progress: progress) } diff --git a/Memory/REFACTOR.md b/Memory/REFACTOR.md index 03c0af4a..f4232484 100644 --- a/Memory/REFACTOR.md +++ b/Memory/REFACTOR.md @@ -494,3 +494,21 @@ secondary action and layout parameter. Keep Connect/Return as the single confirm Workspace for one or more selected SSH/SFTP Hosts. Remove the redundant button/policy/help and three unused localized entries; retain shared BatchLayout for Host context menus. Estimate −45–60 chooser lines; actual −53 including stable accessibility identifiers for behavioral UI coverage. Focus/menu changes are tracked by the same regression run. + +## Port Forwarding refactor, 2026-09-19 — completed + +`rg` plus Periphery on TFM/TFI confirmed the removable surface. Removed the unused forwarding-library route wrapper +and configuration-only session overload, made the direction picker private, and replaced the stateful editor policy +with static normalization/start validation that retains rule-ID, Host-ID, saved validity and runtime checks. Shared +the otherwise identical Port Forwarding/Known Hosts deletion, pruning and error presentation through one typed +managed-card modifier; feature state and transactions remain local. Actual change: −35 production and −73 test lines. + +Controller bookkeeping consolidation was rejected: one record would have to encode active generation, stopped-but- +serializing work and failed-but-presented Journey state, plus an identity token to safely retire completed tasks after +rapid stop/start. The concrete shape adds lifecycle state without reducing code, so the existing dictionaries remain. + +Focused 53 tests passed: Framework forwarding/editor/Known Host presentation, desktop forwarding/editor/Journey/ +selection, and mobile forwarding/Vault lifecycle. TFM and TFI pinned builds passed. The ordinary TFM test build is +currently blocked by unrelated concurrent `ColorSystemTests` and `BroadcastInputTests` API edits; the 29 desktop tests +passed when only those two files were excluded. Formatting, localization and diff checks passed. Retained boundaries: +forwarding/jump pumps, frozen schemas, forwarding authentication/Journey ownership and platform-native card renderers. diff --git a/TFI/Application/Agent/MobileAssistantModel.swift b/TFI/Application/Agent/MobileAssistantModel.swift index 0502df53..3aca6afd 100644 --- a/TFI/Application/Agent/MobileAssistantModel.swift +++ b/TFI/Application/Agent/MobileAssistantModel.swift @@ -259,8 +259,6 @@ final class MobileAssistantSceneModel { var speech: SpeechTranscriptionCoordinator { runtime.speech } var configuration: AIConfigurationModel { runtime.configuration } var skills: SkillLibraryModel { runtime.skills } - var isRunning: Bool { runtime.isRunning } - var isSuspending: Bool { runtime.isSuspending } var error: String? { get { runtime.error } set { runtime.error = newValue } diff --git a/TFI/Features/Ports/MobileForwardingView.swift b/TFI/Features/Ports/MobileForwardingView.swift index dccff6a1..a1ca1785 100644 --- a/TFI/Features/Ports/MobileForwardingView.swift +++ b/TFI/Features/Ports/MobileForwardingView.swift @@ -94,10 +94,9 @@ struct MobileForwardingView: View { } } private func canStart(_ rule: PortForwardingRule) -> Bool { - PortForwardingEditorPolicy( - rule: nil, savedRule: rule, savedHost: app.library.host(rule.hostID), - isRuntimeActive: app.forwardings.state(for: rule.id).isActive - ).canStart + PortForwardingEditorPolicy.canStart( + rule.id, savedRule: rule, savedHost: app.library.host(rule.hostID), + isRuntimeActive: app.forwardings.state(for: rule.id).isActive) } private func start(_ rule: PortForwardingRule) { showing = rule.id diff --git a/TFI/Platform/DeviceCensorship.swift b/TFI/Platform/DeviceCensorship.swift deleted file mode 100644 index 5963858a..00000000 --- a/TFI/Platform/DeviceCensorship.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import UIKit - -enum DeviceCensorship { - static let canDisplayTaiwanFlag: Bool = detectTaiwanFlagColor() - - static func containsColoredPixel(_ bytes: [UInt8], bytesPerPixel: Int = 4) -> Bool { - guard bytesPerPixel >= 3, bytes.count >= bytesPerPixel else { return false } - return stride(from: 0, through: bytes.count - bytesPerPixel, by: bytesPerPixel).contains { offset in - let red = bytes[offset] - let green = bytes[offset + 1] - let blue = bytes[offset + 2] - return red != green || green != blue - } - } - - private static func detectTaiwanFlagColor() -> Bool { - let text = "🇹🇼" as NSString - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 16) - ] - let size = text.size(withAttributes: attributes) - let format = UIGraphicsImageRendererFormat() - format.opaque = false - format.scale = 1 - let renderer = UIGraphicsImageRenderer( - size: CGSize(width: max(1, ceil(size.width)), height: max(1, ceil(size.height))), - format: format - ) - let image = renderer.image { _ in - text.draw(at: .zero, withAttributes: attributes) - } - guard - let data = image.cgImage?.dataProvider?.data, - let bytes = CFDataGetBytePtr(data) - else { return false } - return containsColoredPixel(Array(UnsafeBufferPointer(start: bytes, count: CFDataGetLength(data)))) - } -} diff --git a/TFITests/DeviceCensorshipTests.swift b/TFITests/DeviceCensorshipTests.swift deleted file mode 100644 index 7bd551ae..00000000 --- a/TFITests/DeviceCensorshipTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Testing - -@testable import TermindIOS - -struct DeviceCensorshipTests { - @Test - func colorDetectionHandlesColorGrayscaleAndMissingPixels() { - #expect(DeviceCensorship.containsColoredPixel([0, 0, 255, 255])) - #expect(!DeviceCensorship.containsColoredPixel([64, 64, 64, 255])) - #expect(!DeviceCensorship.containsColoredPixel([])) - } -} diff --git a/TFM/App/DesktopSettingsKey.swift b/TFM/App/DesktopSettingsKey.swift index 6384fd00..94b6daae 100644 --- a/TFM/App/DesktopSettingsKey.swift +++ b/TFM/App/DesktopSettingsKey.swift @@ -54,6 +54,4 @@ nonisolated enum DesktopSettingsKey { UserDefaults.standard.object(forKey: openAtLoginQuietly) as? Bool ?? defaultOpenAtLoginQuietly } - /// The last approval posture selected in the Assistant composer. - static let agentPermissionMode = "assistant.permissionMode" } diff --git a/TFM/Application/MCPServer/MCPUploadCoordinator.swift b/TFM/Application/MCPServer/MCPUploadCoordinator.swift index 15e7444b..fdc222ab 100644 --- a/TFM/Application/MCPServer/MCPUploadCoordinator.swift +++ b/TFM/Application/MCPServer/MCPUploadCoordinator.swift @@ -1,7 +1,6 @@ import AgentKit import Foundation import TermindApplication -import TermindDomain import TermindRemoteInfrastructure nonisolated struct MCPUploadRequest: Sendable { diff --git a/TFM/Design/DesignTokens.swift b/TFM/Design/DesignTokens.swift index 1e2f2edd..6ad7ef10 100644 --- a/TFM/Design/DesignTokens.swift +++ b/TFM/Design/DesignTokens.swift @@ -273,7 +273,6 @@ nonisolated enum Metrics { enum VaultContent { static let min: CGFloat = 280 - static let ideal: CGFloat = 680 static let splitMinWidth: CGFloat = min + 1 + Metrics.VaultDetail.width } diff --git a/TFM/Features/AI/ConversationTranscriptView.swift b/TFM/Features/AI/ConversationTranscriptView.swift index 10da760d..a6bd0f58 100644 --- a/TFM/Features/AI/ConversationTranscriptView.swift +++ b/TFM/Features/AI/ConversationTranscriptView.swift @@ -163,14 +163,12 @@ struct ConversationView: View { SteeringQueueView( conversation: conversation, isCompact: isCompact, - isEmbedded: true, - surfaceStyle: interactionSurfaceStyle + isEmbedded: true ) } } ComposerView( conversation: conversation, - surfaceStyle: interactionSurfaceStyle, maxContentWidth: isCompact ? nil : Metrics.Chat.contentMaxWidth, sessionContext: sessionContext, includesTerminalSuggestion: showsTerminalActions, @@ -185,7 +183,7 @@ struct ConversationView: View { private var archivedSurface: some View { if isCompact { archivedSurfaceContent - .composerBlock(style: interactionSurfaceStyle) + .composerBlock() .padding(.horizontal, Metrics.Space.l) .padding(.top, Metrics.Space.s) .padding(.bottom, Metrics.Space.s) @@ -209,10 +207,6 @@ struct ConversationView: View { } } - private var interactionSurfaceStyle: AssistantInteractionSurfaceStyle { - isCompact ? .sidebar : .standard - } - private func chatColumn( @ViewBuilder content: () -> Content ) -> some View { @@ -253,7 +247,7 @@ struct ConversationView: View { ), alignment: .top ) - .composerBlock(style: interactionSurfaceStyle) + .composerBlock() .padding(.horizontal, Metrics.Space.l) .frame(maxWidth: isCompact ? .infinity : Metrics.Chat.contentMaxWidth) .frame(maxWidth: .infinity) @@ -928,7 +922,6 @@ struct SteeringQueueView: View { let conversation: ConversationModel var isCompact = false var isEmbedded = false - var surfaceStyle: AssistantInteractionSurfaceStyle = .standard /// The rows' own height, because a `ScrollView` will not report one — left /// alone it takes every point it is offered, and the strip would stand at @@ -964,7 +957,7 @@ struct SteeringQueueView: View { // as one object with a queue on top rather than as two panels — see // `assistantInteractionSurface`. No hand-drawn stroke: system glass // draws its own edge, and a second one on top of it reads as a seam. - .assistantInteractionSurface(style: surfaceStyle) + .assistantInteractionSurface() .padding(.horizontal, isEmbedded ? 0 : Metrics.Space.l) .padding(.top, Metrics.Space.s) .accessibilityElement(children: .contain) diff --git a/TFM/Features/AI/ConversationViews.swift b/TFM/Features/AI/ConversationViews.swift index abad3de5..a80c451c 100644 --- a/TFM/Features/AI/ConversationViews.swift +++ b/TFM/Features/AI/ConversationViews.swift @@ -340,13 +340,6 @@ struct FlowLayout: Layout { /// meant to read as the same object. Two hand-written copies of the same six /// lines drift the first time one radius changes, and the drift shows up as two /// blocks in one column that are almost — but not quite — the same shape. -/// Which column the interaction surface is in. It no longer changes what the -/// surface is *made of* — both are system glass — only how much room it has. -enum AssistantInteractionSurfaceStyle { - case standard - case sidebar -} - /// The one interactive surface in the Assistant column. /// /// Composer, Steering and the archived-conversation notice are the same object @@ -355,7 +348,6 @@ enum AssistantInteractionSurfaceStyle { /// hand-drawn stroke directly above a Composer made of system glass, which read /// as two unrelated panels stacked by accident. private struct AssistantInteractionSurface: ViewModifier { - let style: AssistantInteractionSurfaceStyle let isEmphasized: Bool func body(content: Content) -> some View { @@ -387,12 +379,10 @@ private struct AssistantInteractionGlass: View, Equatable { extension View { func assistantInteractionSurface( - style: AssistantInteractionSurfaceStyle = .standard, isEmphasized: Bool = false ) -> some View { modifier( AssistantInteractionSurface( - style: style, isEmphasized: isEmphasized ) ) @@ -401,10 +391,9 @@ extension View { /// The composer's own name for the shared surface, kept so the call sites /// that mean "this is the composer block" still read that way. func composerBlock( - style: AssistantInteractionSurfaceStyle = .standard, isEmphasized: Bool = false ) -> some View { - assistantInteractionSurface(style: style, isEmphasized: isEmphasized) + assistantInteractionSurface(isEmphasized: isEmphasized) } } @@ -510,7 +499,6 @@ struct ComposerView: View { let conversation: ConversationModel var prompt: LocalizedStringKey = "Ask anything…" - var surfaceStyle: AssistantInteractionSurfaceStyle = .standard /// The full Assistant destination shares one centered column with its transcript. /// `nil` keeps compact/sidebar composers fluid. var maxContentWidth: CGFloat? = nil @@ -615,10 +603,7 @@ struct ComposerView: View { .frame(maxWidth: .infinity, minHeight: 32, maxHeight: 32) .animation(Motion.panel(reduceMotion: reduceMotion), value: showsTranscriptionControls) } - .composerBlock( - style: surfaceStyle, - isEmphasized: isFocused - ) + .composerBlock(isEmphasized: isFocused) // Anchored to the whole surface rather than the TextEditor. The editor // begins inside the surface padding (and can have Context chips above // it), which let the palette overlap the Chat frame. @@ -1052,10 +1037,6 @@ struct ComposerView: View { ) } - private var slashCommands: [ChatSlashCommand] { - slashRegistry.commands(in: slashContext) - } - private var slashOptions: (ChatSlashCommand) -> [ChatSlashCommand.Option] { { slashRegistry.options(for: $0, in: slashContext) } } @@ -1095,7 +1076,6 @@ struct ComposerView: View { completion: completion, selection: min(slashSelection, completion.rows.count - 1), scrollRequest: slashScrollRequest, - surfaceStyle: surfaceStyle, contextShare: contextShare, trailingText: slashTrailingText, onHover: { slashSelection = $0 }, diff --git a/TFM/Features/AI/MarkdownBlockListView.swift b/TFM/Features/AI/MarkdownBlockListView.swift index 6108d109..d45267a9 100644 --- a/TFM/Features/AI/MarkdownBlockListView.swift +++ b/TFM/Features/AI/MarkdownBlockListView.swift @@ -1,172 +1,12 @@ import AppKit -import Combine import MarkdownParser import MarkdownView import SwiftUI import TermindPresentation -/// The Markdown entry point used by the transcript. -/// -/// MarkdownView owns parsing, block layout, tables, syntax highlighting and -/// text selection. Termind keeps a thin policy layer around it for the two -/// things a general renderer must not decide for this app: which links are safe -/// to open and which assistant-authored shell fences may reach a terminal. -struct TermindMarkdownView: View { - enum Tone: Hashable { - case primary - case secondary - } - - let text: String - var isStreaming = false - var isAssistant = true - var tone: Tone = .primary - - @Environment(\.markdownFillsWidth) private var fillsWidth - @Environment(\.colorScheme) private var colorScheme - @StateObject private var model: RenderingModel - - init( - text: String, - isStreaming: Bool = false, - isAssistant: Bool = true, - isCompact _: Bool = false, - tone: Tone = .primary - ) { - self.text = text - self.isStreaming = isStreaming - self.isAssistant = isAssistant - self.tone = tone - - let theme = TermindMarkdownTheme.make(tone: tone) - _model = StateObject( - wrappedValue: RenderingModel( - source: text, - allowsShellActions: isAssistant, - theme: theme - )) - } - - var body: some View { - VStack(alignment: .leading, spacing: Metrics.Chat.blockSpacing) { - ForEach(model.rendered.segments) { segment in - switch segment.content { - case .markdown(let content): - TermindMarkdownNativeView( - content: content, - theme: model.rendered.theme, - fillsWidth: fillsWidth - ) - case .shell(let language, let command): - AssistantShellCodeBlockView( - language: language, - command: command - ) - } - } - } - .markdownWidth(fills: fillsWidth) - .onChange( - of: UpdateID( - text: text, - isStreaming: isStreaming, - isAssistant: isAssistant, - tone: tone, - colorScheme: colorScheme - ), - initial: true - ) { _, _ in - refreshIfNeeded() - } - } - - /// MarkdownView is designed for streaming, but splitting actionable shell - /// blocks still requires a root parse. Upstream's balanced emitter already - /// controls the presentation cadence, so every emitted delta is rendered - /// immediately instead of being throttled again at the view layer. - private func refreshIfNeeded() { - guard - model.source != text - || model.rendered.allowsShellActions != isAssistant - || model.rendered.themeTone != tone - || model.rendered.themeColorScheme != colorScheme - else { return } - refresh() - } - - private func refresh() { - model.refresh( - source: text, - allowsShellActions: isAssistant, - theme: TermindMarkdownTheme.make(tone: tone, colorScheme: colorScheme) - ) - } - - private struct UpdateID: Hashable { - let text: String - let isStreaming: Bool - let isAssistant: Bool - let tone: Tone - let colorScheme: ColorScheme - } - - @MainActor - private final class RenderingModel: ObservableObject { - @Published private(set) var rendered: TermindMarkdownRenderedDocument - private(set) var source: String - - init( - source: String, - allowsShellActions: Bool, - theme: TermindMarkdownTheme.Value - ) { - self.source = source - rendered = TermindMarkdownRenderedDocument( - source: source, - allowsShellActions: allowsShellActions, - theme: theme - ) - } - - func refresh( - source: String, - allowsShellActions: Bool, - theme: TermindMarkdownTheme.Value - ) { - rendered = TermindMarkdownRenderedDocument( - source: source, - allowsShellActions: allowsShellActions, - theme: theme - ) - self.source = source - } - } -} - -private struct MarkdownFillsWidthKey: EnvironmentKey { - nonisolated static let defaultValue = true -} - -extension EnvironmentValues { - /// Assistant prose and plans fill their reading column. User messages opt - /// out so a short question keeps a short bubble. - var markdownFillsWidth: Bool { - get { self[MarkdownFillsWidthKey.self] } - set { self[MarkdownFillsWidthKey.self] = newValue } - } -} - -extension View { - func markdownWidth(fills: Bool, alignment: Alignment = .leading) -> some View { - frame(maxWidth: fills ? .infinity : nil, alignment: alignment) - } -} - -/// Kept as the transcript-wide type scale for non-Markdown labels and the -/// Termind-owned shell card. MarkdownView receives matching AppKit fonts below. -enum MarkdownTypography { - static let body = Font.body - static let monospaced = Font.system(.body, design: .monospaced) +enum TermindMarkdownTone: Hashable { + case primary + case secondary } // MARK: - Parsing policy @@ -324,9 +164,6 @@ struct TermindMarkdownRenderedDocument { let segments: [Segment] let theme: MarkdownTheme - let allowsShellActions: Bool - let themeTone: TermindMarkdownView.Tone - let themeColorScheme: ColorScheme? init(source: String, allowsShellActions: Bool, theme: TermindMarkdownTheme.Value) { let parserResult = MarkdownParser().parse(MarkdownSoftBreaks.hardened(source)) @@ -355,9 +192,6 @@ struct TermindMarkdownRenderedDocument { } } self.theme = theme.theme - self.allowsShellActions = allowsShellActions - themeTone = theme.tone - themeColorScheme = theme.colorScheme } } @@ -365,12 +199,11 @@ struct TermindMarkdownRenderedDocument { enum TermindMarkdownTheme { struct Value { let theme: MarkdownTheme - let tone: TermindMarkdownView.Tone let colorScheme: ColorScheme? } static func make( - tone: TermindMarkdownView.Tone, + tone: TermindMarkdownTone, colorScheme: ColorScheme? = nil ) -> Value { var theme = MarkdownTheme() @@ -412,7 +245,7 @@ enum TermindMarkdownTheme { theme.table.stripeCellBackgroundColor = resolved( .quaternaryLabelColor, colorScheme: colorScheme ).withAlphaComponent(0.06) - return Value(theme: theme, tone: tone, colorScheme: colorScheme) + return Value(theme: theme, colorScheme: colorScheme) } private static func resolved(_ color: NSColor, colorScheme: ColorScheme?) -> NSColor { @@ -429,107 +262,3 @@ enum TermindMarkdownTheme { return resolved } } - -// MARK: - AppKit bridge - -private struct TermindMarkdownNativeView: NSViewRepresentable { - let content: MarkdownContent - let theme: MarkdownTheme - let fillsWidth: Bool - - func makeNSView(context: Context) -> MarkdownTextView { - let view = MarkdownTextView() - view.setContentHuggingPriority(.required, for: .vertical) - view.setContentCompressionResistancePriority(.required, for: .vertical) - view.setContentHuggingPriority(fillsWidth ? .defaultLow : .required, for: .horizontal) - view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - view.linkHandler = { payload, _, _ in - TermindMarkdownLinkOpener.open(payload) - } - context.coordinator.apply(content: content, theme: theme, to: view) - return view - } - - func updateNSView(_ view: MarkdownTextView, context: Context) { - view.setContentHuggingPriority(fillsWidth ? .defaultLow : .required, for: .horizontal) - context.coordinator.apply(content: content, theme: theme, to: view) - } - - func sizeThatFits( - _ proposal: ProposedViewSize, - nsView view: MarkdownTextView, - context: Context - ) -> CGSize? { - context.coordinator.sizeThatFits( - proposal, - view: view, - fillsWidth: fillsWidth - ) - } - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - @MainActor - final class Coordinator { - private var lastContent: MarkdownContent? - private var lastTheme: MarkdownTheme? - private var concreteWidth: CGFloat = 0 - - func apply(content: MarkdownContent, theme: MarkdownTheme, to view: MarkdownTextView) { - guard lastContent !== content || lastTheme != theme else { return } - view.setContentImmediately(content, theme: theme) - view.invalidateIntrinsicContentSize() - lastContent = content - lastTheme = theme - } - - func sizeThatFits( - _ proposal: ProposedViewSize, - view: MarkdownTextView, - fillsWidth: Bool - ) -> CGSize? { - if proposal.width == 0 { return .zero } - - let availableWidth: CGFloat - if let width = proposal.width, width.isFinite, width > 0 { - availableWidth = width - concreteWidth = width - } else if concreteWidth > 0 { - availableWidth = concreteWidth - } else if view.bounds.width > 0 { - availableWidth = view.bounds.width - } else { - return nil - } - - let bounded = view.boundingSize(for: availableWidth) - let width = - fillsWidth - ? availableWidth - : min(availableWidth, max(1, ceil(bounded.width))) - let height = ceil(view.boundingSize(for: width).height) - return CGSize(width: width, height: height) - } - } -} - -@MainActor -private enum TermindMarkdownLinkOpener { - static func open(_ payload: LinkPayload) { - let url: URL? - switch payload { - case .url(let value): - url = value - case .string(let value): - url = URL(string: value) - } - guard let url, - TermindMarkdownParser.allowedLinkSchemes.contains( - url.scheme?.lowercased() ?? "" - ) - else { return } - NSWorkspace.shared.open(url) - } -} diff --git a/TFM/Features/AI/MarkdownCodeBlockView.swift b/TFM/Features/AI/MarkdownCodeBlockView.swift index c9409dde..7fd4d78a 100644 --- a/TFM/Features/AI/MarkdownCodeBlockView.swift +++ b/TFM/Features/AI/MarkdownCodeBlockView.swift @@ -1,129 +1,4 @@ import SwiftUI -import TermindDomain - -/// The one fenced block Termind owns instead of MarkdownView: a shell command -/// authored by the assistant and therefore eligible for local risk assessment. -struct AssistantShellCodeBlockView: View { - let language: String - let command: String - - var body: some View { - ShellCodeBlockView(language: language, command: command) - } -} - -// MARK: - Shell - -private struct ShellCodeBlockView: View { - let language: String - let command: String - - @Environment(WindowModel.self) private var window - @State private var isConfirming = false - - private var assessment: RiskAssessment { CommandRiskClassifier.assess(command) } - - /// Whether this fence may be delivered to a prompt at all. - /// - /// `SSHSessionModel.insert` writes raw bytes with no bracketed-paste - /// wrapper, so an embedded newline reaches the PTY as Return. "Insert" on a - /// three-line script would execute the first two lines while presenting - /// itself as a preview — so multi-line fences get the badge and a copy - /// button and nothing that touches the session. - private var isSingleLine: Bool { !command.contains("\n") } - - /// The shell this card would reach. - /// - /// `selectedWorkspace` is `nil` whenever the pinned Assistant destination is showing, and - /// that is exactly where the full-width transcript lives — so the buttons - /// disable there instead of looking live and doing nothing. - private var target: SSHSessionModel? { - guard let workspace = window.selectedWorkspace else { return nil } - return workspace.focusedPane.sshSession ?? workspace.broadcastTargets.first - } - - var body: some View { - VStack(alignment: .leading, spacing: Metrics.Space.m) { - HStack(spacing: Metrics.Space.xs) { - Text(language) - .font(.caption2) - .foregroundStyle(.secondary) - Spacer(minLength: Metrics.Space.s) - RiskBadge(assessment: assessment) - } - - Text(command) - .font(MarkdownTypography.monospaced) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - - actions - } - .padding(Metrics.Chat.codeInset) - .background( - Color(nsColor: .controlBackgroundColor), - in: .rect(cornerRadius: Metrics.Radius.large) - ) - .overlay { - RoundedRectangle(cornerRadius: Metrics.Radius.large) - .strokeBorder( - assessment.risk == .destructive - ? TermindColor.danger.opacity(0.4) : Color.secondary.opacity(0.2) - ) - } - .confirmationDialog( - Text("Run a destructive command?"), - isPresented: $isConfirming, - titleVisibility: .visible - ) { - Button("Run \(command)", role: .destructive) { deliver(submit: true) } - Button("Cancel", role: .cancel) {} - } message: { - Text(assessment.reasons.joined(separator: "\n")) - } - } - - @ViewBuilder - private var actions: some View { - HStack(spacing: Metrics.Space.s) { - if isSingleLine { - Button("Insert") { deliver(submit: false) } - .disabled(target == nil) - Button("Run") { run() } - .buttonStyle(.borderedProminent) - .tint( - assessment.risk == .destructive - ? TermindColor.danger : TermindColor.accent - ) - .disabled(target == nil) - } else { - Text("Several commands — copy and review before running.") - .font(.caption2) - .foregroundStyle(.secondary) - } - Spacer(minLength: Metrics.Space.s) - CopyButton(text: command) - } - .controlSize(.small) - .help(Text(target == nil ? String(localized: "No shell in the foreground") : "")) - } - - private func run() { - if assessment.requiresSecondConfirmation { - isConfirming = true - } else { - deliver(submit: true) - } - } - - /// Through `Workspace.deliver`, so a card obeys the same broadcast rule as - /// every other programmatic route to a prompt. `submit: false` writes - /// without the newline; the user's own Return is what runs it. - private func deliver(submit: Bool) { - guard let workspace = window.selectedWorkspace, let origin = target else { return } - workspace.deliver(command, from: origin, submit: submit) - } -} // MARK: - Copy diff --git a/TFM/Features/AI/SlashCommandMenuView.swift b/TFM/Features/AI/SlashCommandMenuView.swift index 6436d138..61a2012e 100644 --- a/TFM/Features/AI/SlashCommandMenuView.swift +++ b/TFM/Features/AI/SlashCommandMenuView.swift @@ -18,7 +18,6 @@ struct SlashCommandMenuView: View { /// Advanced only by keyboard navigation. Hover still changes the highlight, /// but must not recenter a list underneath a stationary pointer. let scrollRequest: Int - var surfaceStyle: AssistantInteractionSurfaceStyle = .standard var contextShare: Double? /// The value a row currently holds — the resolved effort beside /// `/reasoning`. Returning `nil` leaves the trailing column empty. @@ -48,7 +47,7 @@ struct SlashCommandMenuView: View { // what stops a short list from rubber-banding when the pointer crosses // it while longer command and option lists remain scrollable. .frame(height: listHeight) - .composerBlock(style: surfaceStyle) + .composerBlock() .accessibilityElement(children: .contain) .accessibilityLabel(Text("Commands")) } diff --git a/TFM/Features/AI/TranscriptNativeRows.swift b/TFM/Features/AI/TranscriptNativeRows.swift index 43788945..bf32b75f 100644 --- a/TFM/Features/AI/TranscriptNativeRows.swift +++ b/TFM/Features/AI/TranscriptNativeRows.swift @@ -17,7 +17,7 @@ final class TranscriptMarkdownCache { init( source: String, allowsShellActions: Bool, - tone: TermindMarkdownView.Tone, + tone: TermindMarkdownTone, colorScheme: ColorScheme ) { self.source = source @@ -34,7 +34,7 @@ final class TranscriptMarkdownCache { let slot: String let source: String let shell: Bool - let tone: TermindMarkdownView.Tone + let tone: TermindMarkdownTone let colorScheme: ColorScheme } @@ -49,7 +49,7 @@ final class TranscriptMarkdownCache { slot: String, source: String, shell: Bool, - tone: TermindMarkdownView.Tone = .primary, + tone: TermindMarkdownTone = .primary, colorScheme: ColorScheme ) -> Package { let key = Key( @@ -1234,7 +1234,7 @@ private enum NativeTranscriptFactory { source: String, slot: String, shell: Bool, - tone: TermindMarkdownView.Tone = .primary, + tone: TermindMarkdownTone = .primary, fillsWidth: Bool = true, horizontalInset: CGFloat = 0, context: NativeTranscriptContext @@ -1632,15 +1632,6 @@ private enum NativeTranscriptFactory { } -@MainActor -private func nativeRiskColor(_ risk: CommandRisk) -> NSColor { - switch risk { - case .safe: .systemGreen - case .caution: .systemOrange - case .destructive: .systemRed - } -} - @MainActor private func nativeHierarchicalColor(_ color: NSColor, opacity: CGFloat) -> NSColor { NSColor(name: nil) { appearance in diff --git a/TFM/Features/Assistant/AssistantView.swift b/TFM/Features/Assistant/AssistantView.swift index 224cb39d..3042207f 100644 --- a/TFM/Features/Assistant/AssistantView.swift +++ b/TFM/Features/Assistant/AssistantView.swift @@ -212,7 +212,6 @@ struct AssistantPanelView: View { ComposerView( conversation: window.sidebar.assistantDraft, prompt: "Ask anything…", - surfaceStyle: .sidebar, sessionContext: window.sidebar.sessionContext, includesTerminalSuggestion: true, onSend: { @@ -695,7 +694,7 @@ nonisolated enum AssistantConversationDropResolver { } return .init( section: section, - beforeID: nextConversation(after: index, in: section, items: items) + beforeID: nextConversation(after: index, items: items) ) } @@ -712,12 +711,11 @@ nonisolated enum AssistantConversationDropResolver { guard let sectionIndex = items.firstIndex(where: { $0.section == section }) else { return nil } - return nextConversation(after: sectionIndex, in: section, items: items) + return nextConversation(after: sectionIndex, items: items) } private static func nextConversation( after index: Int, - in section: AssistantConversationListItem.Section, items: [AssistantConversationListItem] ) -> UUID? { guard index + 1 < items.count else { return nil } @@ -878,7 +876,7 @@ private struct AssistantConversationNativeList: NSViewRepresentable { .environment(window) .environment(\.locale, locale) .environment(\.colorScheme, colorScheme) - row.configure(id: item.id, rootView: AnyView(content)) + row.configure(rootView: AnyView(content)) } } } @@ -1064,7 +1062,6 @@ final class AssistantConversationListContainer: NSView { @MainActor private final class AssistantConversationHostingRow: ListRowView { private let hostingView = NSHostingView(rootView: AnyView(EmptyView())) - private var representedID: AssistantConversationListItem.ID? override init(frame: CGRect) { super.init(frame: frame) @@ -1094,13 +1091,11 @@ private final class AssistantConversationHostingRow: ListRowView { empty() } - func configure(id: AssistantConversationListItem.ID, rootView: AnyView) { - representedID = id + func configure(rootView: AnyView) { hostingView.rootView = rootView } private func empty() { - representedID = nil hostingView.rootView = AnyView(EmptyView()) } } diff --git a/TFM/Features/KnownHosts/KnownHostsPageView.swift b/TFM/Features/KnownHosts/KnownHostsPageView.swift index 6578875c..78588749 100644 --- a/TFM/Features/KnownHosts/KnownHostsPageView.swift +++ b/TFM/Features/KnownHosts/KnownHostsPageView.swift @@ -16,7 +16,7 @@ struct KnownHostsPageView: ManagedCardPage { typealias Item = KnownHostEntry @Environment(AppModel.self) private var app @Environment(WindowModel.self) private var window - let state: KnownHostsPageState + @Bindable var state: KnownHostsPageState var managedSections: [ManagedCardSection] { [ @@ -74,9 +74,11 @@ struct KnownHostsPageView: ManagedCardPage { } } func managedNoResultsState() -> some View { ContentUnavailableView.search(text: state.query) } - var managedPresentation: KnownHostsPresentation { - KnownHostsPresentation( - state: state, ids: app.knownHosts.entries.map(\.id), + var managedPresentation: ManagedCardDeletionPresentation { + ManagedCardDeletionPresentation( + deleting: $state.deleting, error: $state.error, ids: app.knownHosts.entries.map(\.id), + confirmationTitle: "Delete Host Keys?", errorTitle: "Known Hosts Operation Failed", + explanation: Text("The next connection will ask you to verify the server key again."), prune: { window.pruneVaultSelections(in: app.hostLibrary) }, delete: { if !app.knownHosts.delete(state.deleting) { state.error = app.knownHosts.storageError } @@ -121,34 +123,6 @@ private struct KnownHostCard: View { } } -struct KnownHostsPresentation: ManagedCardPresentationModifier { - @Bindable var state: KnownHostsPageState - let ids: [UUID] - let prune: () -> Void - let delete: () -> Void - func body(content: Content) -> some View { - content - .onAppear(perform: prune) - .onChange(of: ids) { prune() } - .confirmationDialog( - "Delete Host Keys?", - isPresented: Binding(get: { !state.deleting.isEmpty }, set: { if !$0 { state.deleting = [] } }) - ) { - Button("Delete", role: .destructive, action: delete) - } message: { - Text("The next connection will ask you to verify the server key again.") - } - .alert( - "Known Hosts Operation Failed", - isPresented: Binding(get: { state.error != nil }, set: { if !$0 { state.error = nil } }) - ) { - Button("OK") { state.error = nil } - } message: { - Text(state.error ?? "") - } - } -} - struct KnownHostEditorView: View { @Environment(AppModel.self) private var app @Environment(WindowModel.self) private var window diff --git a/TFM/Features/Metrics/MetricsConnectionNotice.swift b/TFM/Features/Metrics/MetricsConnectionNotice.swift index 5cea3be7..88a89634 100644 --- a/TFM/Features/Metrics/MetricsConnectionNotice.swift +++ b/TFM/Features/Metrics/MetricsConnectionNotice.swift @@ -4,7 +4,6 @@ import TermindApplication /// Connection failures stay actionable even though healthy metrics need no status footer. struct MetricsConnectionNotice: View { let server: ServerMetric - let model: ServerMetricsModel var body: some View { if !server.status.isEmpty, server.status != String(localized: "Live") { diff --git a/TFM/Features/Metrics/MetricsServerDetails.swift b/TFM/Features/Metrics/MetricsServerDetails.swift index d0770f44..77fc5a1b 100644 --- a/TFM/Features/Metrics/MetricsServerDetails.swift +++ b/TFM/Features/Metrics/MetricsServerDetails.swift @@ -4,10 +4,9 @@ import TermindPresentation struct MetricsServerDetails: View { let server: ServerMetric - let model: ServerMetricsModel var body: some View { VStack(spacing: 12) { - MetricsConnectionNotice(server: server, model: model) + MetricsConnectionNotice(server: server) MetricsSystemPanels(server: server) } } diff --git a/TFM/Features/Metrics/MetricsView.swift b/TFM/Features/Metrics/MetricsView.swift index 10ce288d..e68ba346 100644 --- a/TFM/Features/Metrics/MetricsView.swift +++ b/TFM/Features/Metrics/MetricsView.swift @@ -72,7 +72,7 @@ struct MetricsDetailView: View { MetricsDetailLayout(split: split, page: window.metricsDetailPage) { ScrollView { - MetricsServerDetails(server: server, model: model) + MetricsServerDetails(server: server) .padding(1) } .metricsPlainScrollEdges() diff --git a/TFM/Features/Ports/PortForwardingView.swift b/TFM/Features/Ports/PortForwardingView.swift index d9c2e1ae..830392cd 100644 --- a/TFM/Features/Ports/PortForwardingView.swift +++ b/TFM/Features/Ports/PortForwardingView.swift @@ -16,7 +16,7 @@ struct PortForwardingPageView: ManagedCardPage { typealias Item = PortForwardingRule @Environment(AppModel.self) private var app @Environment(WindowModel.self) private var window - let state: PortForwardingPageState + @Bindable var state: PortForwardingPageState var managedSections: [ManagedCardSection] { [ @@ -87,9 +87,10 @@ struct PortForwardingPageView: ManagedCardPage { } } func managedNoResultsState() -> some View { ContentUnavailableView.search(text: state.query) } - var managedPresentation: PortForwardingPagePresentation { - PortForwardingPagePresentation( - state: state, ids: app.forwardings.rules.map(\.id), + var managedPresentation: ManagedCardDeletionPresentation { + ManagedCardDeletionPresentation( + deleting: $state.deleting, error: $state.error, ids: app.forwardings.rules.map(\.id), + confirmationTitle: "Delete Forwarding Rules?", errorTitle: "Operation Failed", prune: { window.pruneVaultSelections(in: app.hostLibrary) }, delete: { do { try app.forwardings.delete(state.deleting) } catch { state.error = error.localizedDescription } @@ -132,10 +133,9 @@ struct PortForwardingPageView: ManagedCardPage { } } private func canStart(_ rule: Item) -> Bool { - PortForwardingEditorPolicy( - rule: nil, savedRule: rule, savedHost: app.hostLibrary.host(rule.hostID), - isRuntimeActive: app.forwarding.state(for: rule.id).isActive - ).canStart + PortForwardingEditorPolicy.canStart( + rule.id, savedRule: rule, savedHost: app.hostLibrary.host(rule.hostID), + isRuntimeActive: app.forwarding.state(for: rule.id).isActive) } private func open(_ rule: Item) { window.openVaultWork(.forwardingEditor(rule.id), on: .portForwarding) } private func start(_ rule: Item) { @@ -153,30 +153,6 @@ struct PortForwardingPageView: ManagedCardPage { } } -struct PortForwardingPagePresentation: ManagedCardPresentationModifier { - @Bindable var state: PortForwardingPageState - let ids: [UUID] - let prune: () -> Void - let delete: () -> Void - func body(content: Content) -> some View { - content.onAppear(perform: prune).onChange(of: ids) { prune() } - .confirmationDialog( - "Delete Forwarding Rules?", - isPresented: Binding(get: { !state.deleting.isEmpty }, set: { if !$0 { state.deleting = [] } }) - ) { - Button("Delete", role: .destructive, action: delete) - } - .alert( - "Operation Failed", - isPresented: Binding(get: { state.error != nil }, set: { if !$0 { state.error = nil } }) - ) { - Button("OK") { state.error = nil } - } message: { - Text(state.error ?? "") - } - } -} - struct PortForwardingDetailView: View { let ruleID: UUID? @Environment(AppModel.self) private var app diff --git a/TFM/Features/SFTP/SFTPBrowserView.swift b/TFM/Features/SFTP/SFTPBrowserView.swift index 1e459df1..debfa3c1 100644 --- a/TFM/Features/SFTP/SFTPBrowserView.swift +++ b/TFM/Features/SFTP/SFTPBrowserView.swift @@ -313,7 +313,7 @@ struct SFTPBrowserView: View { ) }, continueHostKey: { - sftpSession.continueWithTemporaryHostKey($0, using: app.transfers) + sftpSession.continueWithTemporaryHostKey($0) }, trustHostKey: { sftpSession.trustAndConnect($0, using: app.transfers) }, denyHostKey: { diff --git a/TFM/Features/SFTP/SFTPSessionModel.swift b/TFM/Features/SFTP/SFTPSessionModel.swift index 40fdca3d..3c24d3b3 100644 --- a/TFM/Features/SFTP/SFTPSessionModel.swift +++ b/TFM/Features/SFTP/SFTPSessionModel.swift @@ -568,9 +568,7 @@ final class SFTPSessionModel: Identifiable { connection.trustHostKey(observation, using: store) } - func continueWithTemporaryHostKey( - _ observation: SSHHostKeyObservation, using store: any RemoteSessionHostStore - ) { + func continueWithTemporaryHostKey(_ observation: SSHHostKeyObservation) { _ = connection.useTemporaryHostKey(observation) } diff --git a/TFM/Features/Snippets/SnippetsPageView.swift b/TFM/Features/Snippets/SnippetsPageView.swift index 19e560ea..5fc79206 100644 --- a/TFM/Features/Snippets/SnippetsPageView.swift +++ b/TFM/Features/Snippets/SnippetsPageView.swift @@ -218,8 +218,6 @@ struct SnippetsPageView: ManagedCardPage { state: state, actions: SnippetsPagePresentation.Actions( store: app.snippets, - saveSnippet: saveSnippet, - savePackage: savePackage, deletePackage: deletePackage, deleteSnippets: deleteSnippets, performExecution: performExecution, @@ -254,10 +252,6 @@ struct SnippetsPageView: ManagedCardPage { saveSnippet(snippet.duplicated()) } - private func savePackage(_ package: SnippetPackage) { - app.snippets.upsert(package) - } - private func deletePackage(_ package: SnippetPackage, keepSnippets: Bool) { let deletedPackageIDs = Set( [package.id] + app.snippets.descendantSnippetPackages(of: package.id).map(\.id) @@ -346,8 +340,6 @@ struct SnippetsPageView: ManagedCardPage { struct SnippetsPagePresentation: ManagedCardPresentationModifier { struct Actions { let store: SnippetLibraryModel - let saveSnippet: (Snippet) -> Void - let savePackage: (SnippetPackage) -> Void let deletePackage: (SnippetPackage, Bool) -> Void let deleteSnippets: ([Snippet]) -> Void let performExecution: ([Snippet]) -> Void diff --git a/TFM/Features/Terminal/SSHSessionModel.swift b/TFM/Features/Terminal/SSHSessionModel.swift index fd6ca666..796ec00f 100644 --- a/TFM/Features/Terminal/SSHSessionModel.swift +++ b/TFM/Features/Terminal/SSHSessionModel.swift @@ -738,9 +738,7 @@ final class SSHSessionModel: Identifiable { connection.trustHostKey(observation, using: store) } - func continueWithTemporaryHostKey( - _ observation: SSHHostKeyObservation, using store: any RemoteSessionHostStore - ) { + func continueWithTemporaryHostKey(_ observation: SSHHostKeyObservation) { _ = connection.useTemporaryHostKey(observation) } diff --git a/TFM/Features/Terminal/TerminalPlaceholderView.swift b/TFM/Features/Terminal/TerminalPlaceholderView.swift index 91fe2a1e..6a1e06ef 100644 --- a/TFM/Features/Terminal/TerminalPlaceholderView.swift +++ b/TFM/Features/Terminal/TerminalPlaceholderView.swift @@ -121,7 +121,7 @@ struct TerminalPlaceholderView: View { ) }, continueHostKey: { - sshSession.continueWithTemporaryHostKey($0, using: app.hostLibrary) + sshSession.continueWithTemporaryHostKey($0) }, trustHostKey: { sshSession.trustAndConnect($0, using: app.hostLibrary) }, denyHostKey: { diff --git a/TFM/Features/Workspaces/WindowModel.swift b/TFM/Features/Workspaces/WindowModel.swift index 3e20f01e..3eb34737 100644 --- a/TFM/Features/Workspaces/WindowModel.swift +++ b/TFM/Features/Workspaces/WindowModel.swift @@ -1,7 +1,6 @@ import Foundation import TermindApplication import TermindDomain -import TermindInfrastructure import TermindRemoteInfrastructure import struct TermindDomain.Host @@ -511,7 +510,7 @@ final class WindowModel { workspaces.flatMap { workspace in workspace.panes .filter { $0.kind == kind } - .map { PaneRef(workspace: workspace, pane: $0) } + .map(PaneRef.init(pane:)) } } @@ -536,7 +535,7 @@ final class WindowModel { pane.kind == .sshSession || pane.kind == .sftpSession || pane.kind == .serialSession else { return nil } - return PaneRef(workspace: workspace, pane: pane) + return PaneRef(pane: pane) } } } @@ -1483,7 +1482,6 @@ nonisolated enum DragPayload: Hashable, Sendable { /// to know which workspace owns it, and a reference cycle for the sake of the sidebar /// would be paying in the model for something the view can compute. struct PaneRef: Identifiable { - let workspace: Workspace let pane: Pane var id: Pane.ID { pane.id } diff --git a/TFM/Platform/AppShortcut.swift b/TFM/Platform/AppShortcut.swift index 4851308c..2ea7b321 100644 --- a/TFM/Platform/AppShortcut.swift +++ b/TFM/Platform/AppShortcut.swift @@ -22,16 +22,6 @@ enum AppShortcutKey: Hashable, Sendable, ExpressibleByExtendedGraphemeClusterLit } } - var appKitKeyEquivalent: String { - switch self { - case .character(let character): String(character) - case .tab: "\t" - case .escape: "\u{1B}" - case .f3: String(Character(UnicodeScalar(NSF3FunctionKey)!)) - case .rightArrow: String(Character(UnicodeScalar(NSRightArrowFunctionKey)!)) - case .downArrow: String(Character(UnicodeScalar(NSDownArrowFunctionKey)!)) - } - } } struct AppShortcutRegistration: Hashable, Sendable { @@ -121,16 +111,3 @@ extension View { return keyboardShortcut(key.keyEquivalent, modifiers: modifiers) } } - -extension NSMenuItem { - func setTermindShortcut( - _ key: AppShortcutKey, - modifiers: EventModifiers = .command, - terminalRouting: AppShortcutRegistry.TerminalRouting = .application - ) { - let shortcut = AppShortcutRegistration(key, modifiers: modifiers) - AppShortcutRegistry.shared.register(shortcut, terminalRouting: terminalRouting) - keyEquivalent = key.appKitKeyEquivalent - keyEquivalentModifierMask = shortcut.appKitModifiers - } -} diff --git a/TFM/Platform/MainWindowCoordinator.swift b/TFM/Platform/MainWindowCoordinator.swift index 13ac8999..d4827d0b 100644 --- a/TFM/Platform/MainWindowCoordinator.swift +++ b/TFM/Platform/MainWindowCoordinator.swift @@ -1,6 +1,5 @@ import AppKit import SwiftUI -import TermindApplication /// Process-owned scene requests; live workspaces stay in their source until the /// destination has a native window. Scene restoration never replays a move. diff --git a/TFM/Platform/MetricsMenuBarController.swift b/TFM/Platform/MetricsMenuBarController.swift index e95b2fd5..8f70b484 100644 --- a/TFM/Platform/MetricsMenuBarController.swift +++ b/TFM/Platform/MetricsMenuBarController.swift @@ -15,13 +15,6 @@ final class MetricsMenuBarController: NSObject, NSPopoverDelegate { init(defaults: UserDefaults = .standard) { self.defaults = defaults } - var isVisible: Bool { item != nil } - var isPopoverShown: Bool { popover.isShown } - var hasNeutralInitialFocus: Bool { - guard let window = popover.contentViewController?.view.window else { return false } - return window.firstResponder == nil || window.firstResponder === window - } - func start(_ model: ServerMetricsModel) { self.model = model if observer == nil { diff --git a/TFM/UI/Managed/ManagedCardPage.swift b/TFM/UI/Managed/ManagedCardPage.swift index cda0e796..27915875 100644 --- a/TFM/UI/Managed/ManagedCardPage.swift +++ b/TFM/UI/Managed/ManagedCardPage.swift @@ -67,6 +67,39 @@ struct ManagedCardItemContext { /// the inherited body after the shared page has been rendered. protocol ManagedCardPresentationModifier: ViewModifier {} +struct ManagedCardDeletionPresentation: ManagedCardPresentationModifier { + @Binding var deleting: Set + @Binding var error: String? + let ids: [ID] + let confirmationTitle: LocalizedStringKey + let errorTitle: LocalizedStringKey + var explanation: Text? = nil + let prune: () -> Void + let delete: () -> Void + + func body(content: Content) -> some View { + content + .onAppear(perform: prune) + .onChange(of: ids) { prune() } + .confirmationDialog( + confirmationTitle, + isPresented: Binding(get: { !deleting.isEmpty }, set: { if !$0 { deleting = [] } }) + ) { + Button("Delete", role: .destructive, action: delete) + } message: { + if let explanation { explanation } + } + .alert( + errorTitle, + isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } }) + ) { + Button("OK") { error = nil } + } message: { + Text(error ?? "") + } + } +} + /// SwiftUI's protocol-oriented equivalent of a page base class. Conformers /// provide data and business views; this protocol extension owns the body. protocol ManagedCardPage: View { diff --git a/TFMTests/PortForwardingEditorTests.swift b/TFMTests/PortForwardingEditorTests.swift index e4562ed5..743ffc66 100644 --- a/TFMTests/PortForwardingEditorTests.swift +++ b/TFMTests/PortForwardingEditorTests.swift @@ -2,7 +2,6 @@ import Foundation import TermindApplication import TermindDomain import TermindInfrastructure -import TermindPersistence import TermindPresentation import TermindRemoteInfrastructure import Testing @@ -13,66 +12,6 @@ import struct TermindDomain.Host @Suite @MainActor struct PortForwardingEditorTests { - @Test func savedConfigurationIsIndependentFromTheCurrentDraft() { - let host = Host(name: "Server", host: "server.invalid") - let rule = PortForwardingRule(hostID: host.id) - let library = HostLibraryModel(hosts: [host]) - func state(_ candidate: PortForwardingRule?, _ candidateHost: Host, saved: Bool = true) - -> PortForwardingEditorPolicy - { - PortForwardingEditorPolicy( - rule: candidate, savedRule: saved ? rule : nil, - savedHost: saved ? library.host(candidateHost.id) : nil, - isRuntimeActive: false) - } - #expect(state(rule, host).canStart) - #expect(state(rule, host).canEdit) - // A nil normalized draft can still use the matching saved configuration. - #expect(state(nil, host).canStart) - var changedRule = rule - changedRule.bindPort += 1 - #expect(state(changedRule, host).canStart) - var changedHost = host - changedHost.host = "changed.invalid" - #expect(state(rule, changedHost).canStart) - changedHost.id = UUID() - #expect(!state(rule, changedHost).canStart) - let unsavedRule = PortForwardingRule(hostID: host.id) - #expect(!state(unsavedRule, host).canStart) - let deletedHost = PortForwardingEditorPolicy( - rule: rule, savedRule: rule, savedHost: nil, - isRuntimeActive: PortForwardingRuntimeState.stopped.isActive) - #expect(!deletedHost.canStart) - } - - @Test - func allActivePhasesLockEditing() { - let phases: [PortForwardingRuntimeState] = [ - .preparing, .awaitingTrust, .awaitingAuthentication, .connecting, .starting, - .running(activeConnections: 2), - ] - for phase in phases { - let state = PortForwardingEditorPolicy( - rule: nil, savedRule: nil, savedHost: nil, isRuntimeActive: phase.isActive) - #expect(!state.canStart) - #expect(!state.canEdit) - } - let stopped = PortForwardingEditorPolicy( - rule: nil, savedRule: nil, savedHost: nil, - isRuntimeActive: PortForwardingRuntimeState.stopped.isActive) - #expect(stopped.canEdit) - } - - @Test func failedSavedRulesCanBeEditedAndRetried() { - let host = Host(name: "Server", host: "server.invalid") - let rule = PortForwardingRule(hostID: host.id) - let state = PortForwardingEditorPolicy( - rule: rule, savedRule: rule, savedHost: host, - isRuntimeActive: PortForwardingRuntimeState.failed("Connection refused").isActive) - #expect(state.canEdit) - #expect(state.canStart) - } - @Test(arguments: [PortForwardingRule.Kind.local, .remote]) func normalizationMatchesSavedValuesAndRejectsInvalidFields(kind: PortForwardingRule.Kind) throws { let host = Host(name: "Server", host: "server.invalid") @@ -136,21 +75,19 @@ struct PortForwardingEditorTests { #expect(library.upsert(rule)) // Reopening the host editor reloads persisted metadata, including its assigned sort index. let savedHost = try #require(library.host(host.id)) - let state = PortForwardingEditorPolicy( - rule: PortForwardingEditorPolicy.normalized(rule), - savedRule: app.forwardings.portForwarding(rule.id), - savedHost: app.hostLibrary.host(savedHost.id), - isRuntimeActive: app.forwarding.state(for: rule.id).isActive) - #expect(state.canStart) + #expect( + PortForwardingEditorPolicy.canStart( + rule.id, savedRule: app.forwardings.portForwarding(rule.id), + savedHost: app.hostLibrary.host(savedHost.id), + isRuntimeActive: app.forwarding.state(for: rule.id).isActive)) #expect(app.forwarding.state(for: rule.id) == .stopped) var remotelyChanged = rule remotelyChanged.bindPort = 9000 library.upsert(remotelyChanged) - let reopened = PortForwardingEditorPolicy( - rule: PortForwardingEditorPolicy.normalized(rule), - savedRule: app.forwardings.portForwarding(rule.id), - savedHost: app.hostLibrary.host(savedHost.id), - isRuntimeActive: app.forwarding.state(for: rule.id).isActive) - #expect(reopened.canStart) + #expect( + PortForwardingEditorPolicy.canStart( + rule.id, savedRule: app.forwardings.portForwarding(rule.id), + savedHost: app.hostLibrary.host(savedHost.id), + isRuntimeActive: app.forwarding.state(for: rule.id).isActive)) } } diff --git a/TFMTests/PortForwardingFeatureTests.swift b/TFMTests/PortForwardingFeatureTests.swift index 4f5e119d..ceea80ce 100644 --- a/TFMTests/PortForwardingFeatureTests.swift +++ b/TFMTests/PortForwardingFeatureTests.swift @@ -2,7 +2,6 @@ import Foundation import SwiftData import TermindApplication import TermindDomain -import TermindInfrastructure import TermindPersistence import TermindPresentation import TermindRemoteInfrastructure