Persist agent permission mode

This commit is contained in:
Kaede Akino committed 2026-08-26 11:37:07 +08:00
1 parent aa136347da
commit 09f5d027bf
3 files changed
+67 -8

No files matched your search

+2
View File
@@ -30,6 +30,8 @@ nonisolated enum SettingsKey {
}
/// Restore the previous window's tabs on launch.
static let restoreTabs = "window.restoreTabs"
/// The last approval posture selected in the Assistant composer.
static let agentPermissionMode = "assistant.permissionMode"
static let sftpGlobalTransferLimit = "sftp.transfers.globalLimit"
static let sftpPerHostTransferLimit = "sftp.transfers.perHostLimit"
static let defaultSFTPGlobalTransferLimit = 3
+20 -8
View File
@@ -27,8 +27,8 @@ final class AIStore {
private(set) var lastSelectedModel: AIModelRef?
/// The approval posture new and otherwise-unset conversations follow.
/// Session-only. Unlike the model selection, approval posture is not part
/// of provider configuration and is deliberately not persisted here.
/// Stored in local preferences rather than provider configuration so it
/// survives relaunches without becoming part of the synced SwiftData model.
private(set) var lastSelectedPermissionMode: ChatPermissionMode
/// Live MCP connection state, keyed by server.
@@ -44,6 +44,7 @@ final class AIStore {
private var secrets: [String: String] = [:]
private let keychain: KeychainSecretStore
private let persistsSecrets: Bool
private let permissionDefaults: UserDefaults?
private(set) var secretStorageError: String?
private var persistence: AIPersistence?
private(set) var storageError: String?
@@ -55,23 +56,33 @@ final class AIStore {
lastSelectedModel: AIModelRef? = nil,
lastSelectedPermissionMode: ChatPermissionMode = .askForApproval,
keychain: KeychainSecretStore = KeychainSecretStore(),
persistsSecrets: Bool = true
persistsSecrets: Bool = true,
permissionDefaults: UserDefaults? = nil
) {
self.modelProviders = modelProviders
self.mcpServers = mcpServers
self.roles = roles
self.lastSelectedModel = lastSelectedModel
self.lastSelectedPermissionMode = lastSelectedPermissionMode
self.lastSelectedPermissionMode = permissionDefaults
.flatMap { $0.string(forKey: SettingsKey.agentPermissionMode) }
.flatMap(ChatPermissionMode.init(rawValue:))
?? lastSelectedPermissionMode
self.keychain = keychain
self.persistsSecrets = persistsSecrets
self.permissionDefaults = permissionDefaults
}
convenience init(
modelContext: ModelContext,
keychain: KeychainSecretStore = KeychainSecretStore(),
persistsSecrets: Bool = true
persistsSecrets: Bool = true,
permissionDefaults: UserDefaults = .standard
) {
self.init(keychain: keychain, persistsSecrets: persistsSecrets)
self.init(
keychain: keychain,
persistsSecrets: persistsSecrets,
permissionDefaults: permissionDefaults
)
let persistence = AIPersistence(context: modelContext)
self.persistence = persistence
do {
@@ -378,10 +389,11 @@ final class AIStore {
mutatePersistedState { lastSelectedModel = ref }
}
/// Records a composer permission choice for subsequent unset conversations.
/// It does not alter command or MCP execution policy.
/// Records a composer permission choice for subsequent unset conversations
/// and future app launches. It does not alter command or MCP execution policy.
func select(_ permissionMode: ChatPermissionMode) {
lastSelectedPermissionMode = permissionMode
permissionDefaults?.set(permissionMode.rawValue, forKey: SettingsKey.agentPermissionMode)
}
// MARK: - Provider persistence
+45
View File
@@ -101,6 +101,51 @@ struct ChatComposerStateTests {
#expect(store.resolve(conversation: nil as ChatPermissionMode?) == .approveForMe)
}
@Test
func permissionDefaultPersistsAcrossStoreInstancesIncludingFullAccess() throws {
let suiteName = "ChatPermissionMode-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suiteName))
defer { defaults.removePersistentDomain(forName: suiteName) }
let first = AIStore(persistsSecrets: false, permissionDefaults: defaults)
first.select(ChatPermissionMode.approveForMe)
let second = AIStore(persistsSecrets: false, permissionDefaults: defaults)
#expect(second.lastSelectedPermissionMode == .approveForMe)
second.select(ChatPermissionMode.fullAccess)
let third = AIStore(persistsSecrets: false, permissionDefaults: defaults)
#expect(third.lastSelectedPermissionMode == .fullAccess)
let explicit = ConversationModel()
explicit.permissionMode = .askForApproval
#expect(third.resolve(conversation: explicit.permissionMode) == .askForApproval)
}
@Test
func missingPersistedPermissionDefaultsToAskForApproval() throws {
let suiteName = "ChatPermissionMode-Missing-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suiteName))
defer { defaults.removePersistentDomain(forName: suiteName) }
let store = AIStore(persistsSecrets: false, permissionDefaults: defaults)
#expect(store.lastSelectedPermissionMode == .askForApproval)
}
@Test
func invalidPersistedPermissionDefaultsToAskForApproval() throws {
let suiteName = "ChatPermissionMode-Invalid-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suiteName))
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set("unknown-mode", forKey: SettingsKey.agentPermissionMode)
let store = AIStore(persistsSecrets: false, permissionDefaults: defaults)
#expect(store.lastSelectedPermissionMode == .askForApproval)
}
@Test
func modelSelectionStillFeedsUnsetConversationsWithoutOverridingExplicitOnes() {
let firstModel = AIModel(id: "first")