Skip to content

Commit cb32356

Browse files
committed
New setting - attach Copilot window only to Xcode workspace and project windows, ignoring standalone files
1 parent 52f4f33 commit cb32356

7 files changed

Lines changed: 80 additions & 16 deletions

File tree

‎Core/Sources/HostApp/AdvancedSettings/ChatSection.swift‎

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import AppKitExtension
22
import Client
33
import ComposableArchitecture
44
import ConversationServiceProvider
5+
import Preferences
56
import SwiftUI
67
import Toast
78
import XcodeInspector
@@ -10,7 +11,6 @@ import Logger
1011
import SystemUtils
1112

1213
struct ChatSection: View {
13-
@AppStorage(\.autoAttachChatToXcode) var autoAttachChatToXcode
1414
@AppStorage(\.enableFixError) var enableFixError
1515
@AppStorage(\.enableSubagent) var enableSubagent
1616
@ObservedObject private var featureFlags = FeatureFlagManager.shared
@@ -62,15 +62,12 @@ struct ChatSection: View {
6262

6363
Divider()
6464
}
65-
66-
// Auto Attach toggle
67-
SettingsToggle(
68-
title: "Auto-attach Chat Window to Xcode",
69-
isOn: $autoAttachChatToXcode
70-
)
65+
66+
AutoAttachChatToXcodeSetting()
67+
.padding(SettingsToggle.defaultPadding)
7168

7269
Divider()
73-
70+
7471
// Fix error toggle
7572
SettingsToggle(
7673
title: "Quick fix for error",
@@ -105,6 +102,32 @@ struct ChatSection: View {
105102
}
106103
}
107104

105+
struct AutoAttachChatToXcodeSetting: View {
106+
@AppStorage(\.autoAttachChatToXcodeMode) private var autoAttachChatToXcodeMode
107+
108+
var body: some View {
109+
WithPerceptionTracking {
110+
HStack {
111+
VStack(alignment: .leading) {
112+
Text("Auto-attach Chat Window to Xcode")
113+
.font(.body)
114+
Text("Choose Projects to attach only to Xcode project and workspace windows.")
115+
.font(.footnote)
116+
}
117+
118+
Spacer()
119+
120+
Picker("", selection: $autoAttachChatToXcodeMode) {
121+
Text("Never").tag(AutoAttachChatToXcodeMode.never)
122+
Text("Always").tag(AutoAttachChatToXcodeMode.always)
123+
Text("Projects").tag(AutoAttachChatToXcodeMode.projects)
124+
}
125+
.frame(maxWidth: 200, alignment: .trailing)
126+
}
127+
}
128+
}
129+
}
130+
108131
struct ResponseLanguageSetting: View {
109132
@AppStorage(\.chatResponseLocale) var chatResponseLocale
110133

‎Core/Sources/SuggestionWidget/ChatWindowView.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ struct ChatLoadingView: View {
142142
struct ChatTitleBar: View {
143143
let store: StoreOf<ChatPanelFeature>
144144
@State var isHovering = false
145-
@AppStorage(\.autoAttachChatToXcode) var autoAttachChatToXcode
145+
@AppStorage(\.autoAttachChatToXcodeMode) var autoAttachChatToXcodeMode
146146

147147
var body: some View {
148148
WithPerceptionTracking {
@@ -169,7 +169,7 @@ struct ChatTitleBar: View {
169169

170170
Spacer()
171171

172-
if !autoAttachChatToXcode {
172+
if autoAttachChatToXcodeMode == .never {
173173
TrafficLightButton(
174174
isHovering: isHovering,
175175
isActive: store.isDetached,

‎Core/Sources/SuggestionWidget/WidgetWindowsController.swift‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -572,10 +572,13 @@ extension WidgetWindowsController {
572572
}
573573
}
574574

575-
let isAttachedToXcodeEnabled = UserDefaults.shared.value(for: \.autoAttachChatToXcode)
576-
guard isAttachedToXcodeEnabled else { return }
575+
let autoAttachMode = UserDefaults.shared.value(for: \.autoAttachChatToXcodeMode)
576+
guard autoAttachMode != .never else { return }
577577

578578
guard notif.element.isXcodeWorkspaceWindow else { return }
579+
guard autoAttachMode != .projects
580+
|| WorkspaceXcodeWindowInspector.isProjectOrWorkspaceWindow(windowElement: notif.element)
581+
else { return }
579582

580583
let state = store.withState { $0 }
581584
if state.chatPanelState.isPanelDisplayed && !windows.chatPanelWindow.isWindowHidden {
@@ -667,8 +670,8 @@ extension WidgetWindowsController {
667670
)
668671
}
669672

670-
let isAttachedToXcodeEnabled = UserDefaults.shared.value(for: \.autoAttachChatToXcode)
671-
if isAttachedToXcodeEnabled {
673+
let autoAttachMode = UserDefaults.shared.value(for: \.autoAttachChatToXcodeMode)
674+
if autoAttachMode != .never {
672675
// update in `updateAttachedChatWindowLocation`
673676
} else if isChatPanelDetached {
674677
// don't update it!

‎Tool/Sources/Preferences/Keys.swift‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,13 @@ public extension UserDefaultPreferenceKeys {
321321
.init(defaultValue: "", key: "GlobalCopilotInstructions")
322322
}
323323

324-
var autoAttachChatToXcode: PreferenceKey<Bool> {
324+
var oldAutoAttachChatToXcode: DeprecatedPreferenceKey<Bool> {
325325
.init(defaultValue: true, key: "AutoAttachChatToXcode")
326326
}
327+
328+
var autoAttachChatToXcodeMode: PreferenceKey<AutoAttachChatToXcodeMode> {
329+
.init(defaultValue: .always, key: "AutoAttachChatToXcodeMode")
330+
}
327331

328332
var enableFixError: PreferenceKey<Bool> {
329333
.init(defaultValue: true, key: "EnableFixError")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public enum AutoAttachChatToXcodeMode: Int, CaseIterable {
2+
case never = 0
3+
case always = 1
4+
case projects = 2
5+
}

‎Tool/Sources/Preferences/UserDefaults.swift‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ public extension UserDefaults {
2222
shared.setupDefaultValue(for: \.realtimeNESToggle)
2323
shared.setupDefaultValue(for: \.realtimeSuggestionDebounce)
2424
shared.setupDefaultValue(for: \.suggestionPresentationMode)
25-
shared.setupDefaultValue(for: \.autoAttachChatToXcode)
25+
shared.setupDefaultValue(
26+
for: \.autoAttachChatToXcodeMode,
27+
defaultValue: shared.deprecatedValue(for: \.oldAutoAttachChatToXcode) ? .always : .never
28+
)
2629
shared.setupDefaultValue(for: \.enableFixError)
2730
shared.setupDefaultValue(for: \.enableSubagent)
2831
shared.setupDefaultValue(for: \.enableAutoApproval)

‎Tool/Sources/XcodeInspector/XcodeWindowInspector.swift‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,32 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector {
8585
}
8686
}
8787

88+
public static func isProjectOrWorkspaceWindow(windowElement: AXUIElement) -> Bool {
89+
guard windowElement.isXcodeWorkspaceWindow else { return false }
90+
return isProjectOrWorkspaceWindow(
91+
title: windowElement.title,
92+
workspaceURL: extractWorkspaceURL(windowElement: windowElement)
93+
)
94+
}
95+
96+
static func isProjectOrWorkspaceWindow(title: String, workspaceURL: URL?) -> Bool {
97+
guard let workspaceURL else { return false }
98+
99+
let projectName: String
100+
if ["xcodeproj", "xcworkspace", "playground"].contains(workspaceURL.pathExtension) {
101+
projectName = workspaceURL.deletingPathExtension().lastPathComponent
102+
} else {
103+
projectName = workspaceURL.lastPathComponent
104+
}
105+
guard !projectName.isEmpty else { return false }
106+
107+
return [" — ", " - "].contains { separator in
108+
let prefix = projectName + separator
109+
guard title.hasPrefix(prefix) else { return false }
110+
return !title.dropFirst(prefix.count).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
111+
}
112+
}
113+
88114
static func extractDocumentURL(
89115
windowElement: AXUIElement
90116
) -> URL? {

0 commit comments

Comments
 (0)