Skip to content

Commit f006b29

Browse files
Managed via TartManaged via Tart
authored andcommitted
Fix live runtime settings refresh
1 parent 42f2e78 commit f006b29

10 files changed

Lines changed: 127 additions & 13 deletions

macos/OpenBridge/Agent/AgentSessionManager.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,16 @@ final class AgentSessionManager {
158158
}
159159

160160
func refreshConnectorConfiguration() {
161-
isVMReady = true
161+
Task { [weak self] in
162+
guard let self else { return }
163+
do {
164+
_ = try await ensureRuntimeReady()
165+
await refreshLoadedSessionRuntimeConfiguration()
166+
} catch {
167+
vmLoadError = error
168+
logger.warning("Failed to refresh local connector configuration: \(error.localizedDescription)")
169+
}
170+
}
162171
}
163172

164173
func restartConnector() {
@@ -168,6 +177,25 @@ final class AgentSessionManager {
168177
isVMReady = true
169178
}
170179

180+
func applyLocalEnvironmentPermissionModeChange(_ mode: LocalEnvironmentPermissionMode) {
181+
connector?.applyPermissionModeChange(mode)
182+
Task { [weak self] in
183+
guard let self else { return }
184+
await refreshLoadedSessionRuntimeConfiguration()
185+
}
186+
}
187+
188+
func reloadAIProviderConfiguration() async {
189+
await BridgeAIProviderRegistry.registerProviders()
190+
await refreshLoadedSessionRuntimeConfiguration()
191+
}
192+
193+
private func refreshLoadedSessionRuntimeConfiguration() async {
194+
for session in sessions.values {
195+
await session.refreshRuntimeConfiguration()
196+
}
197+
}
198+
171199
private func disconnectVMConnector(shutdownBridge: Bool = false) {
172200
guard vmConnector != nil || embeddedVMBridge != nil else { return }
173201
vmConnector?.disconnect()

macos/OpenBridge/Agent/BridgeAIProviderRegistry.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,21 @@ enum BridgeAIProviderRegistry {
5151

5252
static func selectedModel() async -> Model {
5353
let settings = await BridgeAIProviderSecretStore.readSettings()
54-
return runtimeModel(
54+
return selectedModel(settings: settings)
55+
}
56+
57+
static func selectedModel(settings: BridgeAIProviderSettings) -> Model {
58+
let selected = runtimeModel(
5559
provider: settings.selectedModelProvider,
5660
id: settings.selectedModelID,
5761
settings: settings
58-
) ?? defaultModel(settings: settings)
62+
)
63+
.flatMap { model in
64+
BridgeAIProvider.provider(for: model).flatMap { provider in
65+
settings[provider].isEnabled ? model : nil
66+
}
67+
}
68+
return selected ?? defaultModel(settings: settings)
5969
}
6070

6171
static func runtimeModel(provider: String, id: String) async -> Model? {

macos/OpenBridge/Agent/LocalRuntime/LocalAgentSession.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ final class LocalAgentSession: Identifiable {
184184
let now = Int64(Date().timeIntervalSince1970)
185185
listCreatedAt = listCreatedAt ?? now
186186
listUpdatedAt = listUpdatedAt ?? now
187-
_ = try await ensureLocalAgent()
188187
}
189188

190189
func teardown() async {
@@ -321,24 +320,36 @@ final class LocalAgentSession: Identifiable {
321320

322321
// MARK: - Local KWWK Agent
323322

324-
private func ensureLocalAgent() async throws -> Agent {
325-
if let localAgent {
326-
return localAgent
327-
}
323+
func refreshRuntimeConfiguration() async {
324+
guard let localAgent else { return }
325+
localAgent.state.model = await BridgeAIProviderRegistry.selectedModel()
326+
localAgent.state.systemPrompt = await makeLocalAgentSystemPrompt()
327+
}
328328

329-
await BridgeAIProviderRegistry.registerProviders()
330-
let backgroundManager = BackgroundTaskManager()
329+
private func makeLocalAgentSystemPrompt() async -> String {
331330
let skillManager = SkillManager.shared
332331
let cwd = skillManager.skillDirs.workspace.path
333332
let memoryPrompt = await MemoryRepository.shared.systemPromptSection()
334333
let environmentInventory = try? await AgentSessionManager.shared.localEnvironmentSystemPromptSection()
335-
let systemPrompt = OpenBridgeSystemPromptBuilder.build(
334+
return OpenBridgeSystemPromptBuilder.build(
336335
cwd: cwd,
337336
skills: skillManager.skills,
338337
memory: memoryPrompt,
339338
computerUsePrompt: OpenBridgeComputerUseAgent.systemPromptWithStartupInventory(clientStore: computerUseClientStore),
340339
environmentInventory: environmentInventory
341340
)
341+
}
342+
343+
private func ensureLocalAgent() async throws -> Agent {
344+
if let localAgent {
345+
return localAgent
346+
}
347+
348+
await BridgeAIProviderRegistry.registerProviders()
349+
let backgroundManager = BackgroundTaskManager()
350+
let skillManager = SkillManager.shared
351+
let cwd = skillManager.skillDirs.workspace.path
352+
let systemPrompt = await makeLocalAgentSystemPrompt()
342353
localBackgroundManager = backgroundManager
343354

344355
let config = await CodingAgentConfig(

macos/OpenBridge/Agent/LocalRuntime/LocalRuntimeConnector.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ private struct PendingConfirmation {
3939
var continuations: [CheckedContinuation<PermissionConfirmationReply, Never>]
4040
let sessionId: String?
4141
let grantKey: PermissionGrantKey?
42+
let respondsToPermissionMode: Bool
4243
}
4344

4445
private enum PermissionReplyReason {
@@ -149,6 +150,17 @@ final class LocalRuntimeConnector {
149150
killAllProcesses()
150151
}
151152

153+
func applyPermissionModeChange(_ mode: LocalEnvironmentPermissionMode) {
154+
guard environmentKind == .localMacOS else { return }
155+
156+
switch mode {
157+
case .default:
158+
clearAllGrantedPermissions()
159+
case .fullAccess:
160+
approvePendingHostAccessPermissions()
161+
}
162+
}
163+
152164
private func currentArch() -> String {
153165
#if arch(arm64)
154166
return "arm64"
@@ -497,6 +509,15 @@ private extension LocalRuntimeConnector {
497509
grantedPermissionKeys = Set(grantedPermissionKeys.filter { $0.sessionKey != sessionKey })
498510
}
499511

512+
func approvePendingHostAccessPermissions() {
513+
let confirmationIDs = pendingConfirmations.compactMap { id, pending in
514+
pending.respondsToPermissionMode ? id : nil
515+
}
516+
for confirmationID in confirmationIDs {
517+
resolveConfirmation(id: confirmationID, approved: true)
518+
}
519+
}
520+
500521
/// Injects a permission_request message into the session that triggered the
501522
/// request (identified by sessionId) and suspends until the user clicks
502523
/// Allow or Deny in the chat UI. Returns false immediately if no session
@@ -599,7 +620,8 @@ private extension LocalRuntimeConnector {
599620
pendingConfirmations[confirmationId] = PendingConfirmation(
600621
continuations: [continuation],
601622
sessionId: sessionId,
602-
grantKey: grantKey
623+
grantKey: grantKey,
624+
respondsToPermissionMode: computerUseStart == nil
603625
)
604626
if let grantKey {
605627
pendingPermissionConfirmationIDsByGrantKey[grantKey] = confirmationId

macos/OpenBridge/Agent/LocalRuntime/OpenBridgeSystemPromptBuilder.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ enum OpenBridgeSystemPromptBuilder {
3939
- Treat environment="sandbox" as the default environment for file reads, writes, commands, and project work.
4040
- Do not switch to or target environment="local" on your own initiative. Use environment="local" only when the user explicitly asks for direct host work or when sandbox cannot complete the task.
4141
- environment="local" is protected because it operates directly on this Mac. Host writes, host commands, and sensitive host paths require explicit user approval.
42+
\(localPermissionModeInstruction())
4243
- Local permission is temporary for the current task execution. Do not assume a past approval applies to a later user request.
43-
- Before using bash, write, or edit in environment="local", call request_permission(environment="local", description="...") with a clear, specific description of what you plan to do so the user can make an informed decision.
44+
\(localPermissionRequestInstruction())
4445
- If local permission is pending and you can still make progress in sandbox, continue there. If blocked on approval, wait and explain what you are waiting for.
4546
- Do not operate on sandbox and local filesystems in the same task unless the user explicitly asks for that handoff. Choose one environment for filesystem work.
4647
- After completing sandbox file operations, call current_changes to review staged sandbox changes before finishing.
@@ -59,6 +60,24 @@ enum OpenBridgeSystemPromptBuilder {
5960
"""
6061
}
6162

63+
private static func localPermissionModeInstruction() -> String {
64+
switch SettingsManager.shared.localEnvironmentPermissionMode {
65+
case .default:
66+
"- Current local permission mode: Default. Host writes, host commands, and sensitive host paths require request_permission before use."
67+
case .fullAccess:
68+
"- Current local permission mode: Full Access. Host writes, host commands, and sensitive host paths are already approved for this app session; do not call request_permission solely for local permission, but still prefer sandbox unless local access is required."
69+
}
70+
}
71+
72+
private static func localPermissionRequestInstruction() -> String {
73+
switch SettingsManager.shared.localEnvironmentPermissionMode {
74+
case .default:
75+
"- Before using bash, write, or edit in environment=\"local\", call request_permission(environment=\"local\", description=\"...\") with a clear, specific description of what you plan to do so the user can make an informed decision."
76+
case .fullAccess:
77+
"- Before using bash, write, or edit in environment=\"local\", confirm local access is truly required; request_permission is not needed while Full Access is active."
78+
}
79+
}
80+
6281
private static func skillSection(skills: [Skill]) -> String {
6382
let entries = skills
6483
.filter { !$0.disabled && $0.visibility != .hidden }

macos/OpenBridge/Interface/Chat/ChatEditorViewModel+ComposerEditing.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ extension ChatEditorViewModel {
205205
SettingsManager.shared.localEnvironmentPermissionMode != mode
206206
else { return }
207207
SettingsManager.shared.localEnvironmentPermissionMode = mode
208+
AgentSessionManager.shared.applyLocalEnvironmentPermissionModeChange(mode)
208209
}
209210

210211
private func openAIProviderSettings() {

macos/OpenBridge/Interface/Chat/ChatEditorViewModel.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ final class ChatEditorViewModel {
6666
var voicePendingWaveformPeak: Double = 0
6767
@ObservationIgnored
6868
private var quoteFocusRequestCounter: Int = 0
69+
@ObservationIgnored
70+
private var aiProviderSettingsCancellable: AnyCancellable?
6971

7072
/// Skill selected for the current conversation (delegated to Chat)
7173
var selectedSkill: Skill? {
@@ -125,6 +127,13 @@ final class ChatEditorViewModel {
125127
text: String = ""
126128
) {
127129
self.text = text
130+
aiProviderSettingsCancellable = NotificationCenter.default
131+
.publisher(for: .aiProviderSettingsDidChange)
132+
.sink { [weak self] _ in
133+
Task { @MainActor [weak self] in
134+
await self?.loadSelectedModel()
135+
}
136+
}
128137
Task { await loadSelectedModel() }
129138
}
130139

macos/OpenBridge/Interface/Settings/AIProviders/AIProviderDetailView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ struct AIProviderDetailView: View {
542542
var settings = await BridgeAIProviderSecretStore.readSettings()
543543
settings[provider] = config
544544
try await BridgeAIProviderSecretStore.saveSettings(settings)
545+
await applyProviderConfigurationChange()
545546
statusMessage = "Saved"
546547
await refreshUsage()
547548
} catch {
@@ -613,6 +614,7 @@ struct AIProviderDetailView: View {
613614
var settings = await BridgeAIProviderSecretStore.readSettings()
614615
settings[provider] = config
615616
try await BridgeAIProviderSecretStore.saveSettings(settings)
617+
await applyProviderConfigurationChange()
616618
statusMessage = "Signed in"
617619
await refreshUsage()
618620
} catch is CancellationError {
@@ -647,12 +649,20 @@ struct AIProviderDetailView: View {
647649
var settings = await BridgeAIProviderSecretStore.readSettings()
648650
settings[provider] = config
649651
try await BridgeAIProviderSecretStore.saveSettings(settings)
652+
await applyProviderConfigurationChange()
650653
statusMessage = "Provider reset"
651654
} catch {
652655
errorMessage = error.localizedDescription
653656
}
654657
}
655658

659+
private func applyProviderConfigurationChange() async {
660+
await AgentSessionManager.shared.reloadAIProviderConfiguration()
661+
await MainActor.run {
662+
NotificationCenter.default.post(name: .aiProviderSettingsDidChange, object: nil)
663+
}
664+
}
665+
656666
private func oauthLogin(callbacks: OAuthLogin.Callbacks) async throws -> OAuthCredentials {
657667
switch provider {
658668
case .openAI:

macos/OpenBridge/Interface/Settings/AIProviders/AIProvidersSettingsView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ struct AIProvidersSettingsView: View {
3737
guard newPath.isEmpty else { return }
3838
Task { await reload() }
3939
}
40+
.onReceiveNotification(name: .aiProviderSettingsDidChange) { _ in
41+
Task { await reload() }
42+
}
4043
}
4144

4245
private var header: some View {

macos/OpenBridge/Notifications/NotificationNames.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ extension Notification.Name {
1414
/// Posted when a skill should be activated in chat. The notification's `object` should be a `SkillInfo`.
1515
static let skillActivationRequested = Notification.Name("skillActivationRequested")
1616
static let skillInventoryDidChange = Notification.Name("skillInventoryDidChange")
17+
static let aiProviderSettingsDidChange = Notification.Name("aiProviderSettingsDidChange")
1718

1819
// MARK: - Shortcuts
1920

0 commit comments

Comments
 (0)