Skip to content

Commit 1d98c6e

Browse files
committed
Pre-release 0.44.145
1 parent 27b2225 commit 1d98c6e

28 files changed

+2795
-245
lines changed

Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct GeneralSettingsView: View {
1919
return ""
2020
}
2121
}
22-
22+
2323
var extensionPermissionSubtitle: any View {
2424
switch store.isExtensionPermissionGranted {
2525
case .notGranted:
@@ -40,8 +40,7 @@ struct GeneralSettingsView: View {
4040
return Text("")
4141
}
4242
}
43-
44-
43+
4544
var extensionPermissionBadge: BadgeItem? {
4645
switch store.isExtensionPermissionGranted {
4746
case .notGranted:
@@ -52,8 +51,8 @@ struct GeneralSettingsView: View {
5251
return nil
5352
}
5453
}
55-
56-
var extensionPermissionAction: ()->Void {
54+
55+
var extensionPermissionAction: () -> Void {
5756
switch store.isExtensionPermissionGranted {
5857
case .disabled:
5958
return { shouldShowRestartXcodeAlert = true }
@@ -89,23 +88,20 @@ struct GeneralSettingsView: View {
8988
} footer: {
9089
HStack {
9190
Spacer()
92-
Button("?") {
93-
NSWorkspace.shared.open(
94-
URL(string: "https://github.com/github/CopilotForXcode/blob/main/TROUBLESHOOTING.md")!
95-
)
96-
}
97-
.clipShape(Circle())
91+
AdaptiveHelpLink(action: { NSWorkspace.shared.open(
92+
URL(string: "https://github.com/github/CopilotForXcode/blob/main/TROUBLESHOOTING.md")!
93+
)})
9894
}
9995
}
10096
.alert(
10197
"Enable Extension Permission",
10298
isPresented: $shouldPresentExtensionPermissionAlert
10399
) {
104100
Button(
105-
"Open System Preferences",
106-
action: {
107-
NSWorkspace.openXcodeExtensionsPreferences()
108-
}).keyboardShortcut(.defaultAction)
101+
"Open System Preferences",
102+
action: {
103+
NSWorkspace.openXcodeExtensionsPreferences()
104+
}).keyboardShortcut(.defaultAction)
109105
Button("View How-to Guide", action: {
110106
let url = "https://github.com/github/CopilotForXcode/blob/main/TROUBLESHOOTING.md#extension-permission"
111107
NSWorkspace.shared.open(URL(string: url)!)
@@ -126,7 +122,7 @@ struct GeneralSettingsView: View {
126122
Button("Restart Now") {
127123
NSWorkspace.restartXcode()
128124
}.keyboardShortcut(.defaultAction)
129-
125+
130126
Button("Cancel", role: .cancel) {}
131127
} message: {
132128
Text("Quit and restart Xcode to enable Github Copilot for Xcode extension.")
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import SwiftUI
2+
import AppKit
3+
4+
// MARK: - SplitButton Menu Item
5+
6+
public struct SplitButtonMenuItem: Identifiable {
7+
public let id = UUID()
8+
public let title: String
9+
public let action: () -> Void
10+
11+
public init(title: String, action: @escaping () -> Void) {
12+
self.title = title
13+
self.action = action
14+
}
15+
}
16+
17+
// MARK: - SplitButton using NSComboButton
18+
19+
@available(macOS 13.0, *)
20+
public struct SplitButton: NSViewRepresentable {
21+
let title: String
22+
let primaryAction: () -> Void
23+
let isDisabled: Bool
24+
let menuItems: [SplitButtonMenuItem]
25+
26+
public init(
27+
title: String,
28+
isDisabled: Bool = false,
29+
primaryAction: @escaping () -> Void,
30+
menuItems: [SplitButtonMenuItem] = []
31+
) {
32+
self.title = title
33+
self.isDisabled = isDisabled
34+
self.primaryAction = primaryAction
35+
self.menuItems = menuItems
36+
}
37+
38+
public func makeNSView(context: Context) -> NSComboButton {
39+
let button = NSComboButton()
40+
41+
button.title = title
42+
button.target = context.coordinator
43+
button.action = #selector(Coordinator.handlePrimaryAction)
44+
button.isEnabled = !isDisabled
45+
46+
47+
context.coordinator.button = button
48+
context.coordinator.updateMenu(with: menuItems)
49+
50+
return button
51+
}
52+
53+
public func updateNSView(_ nsView: NSComboButton, context: Context) {
54+
nsView.title = title
55+
nsView.isEnabled = !isDisabled
56+
context.coordinator.updateMenu(with: menuItems)
57+
}
58+
59+
public func makeCoordinator() -> Coordinator {
60+
Coordinator(primaryAction: primaryAction)
61+
}
62+
63+
public class Coordinator: NSObject {
64+
let primaryAction: () -> Void
65+
weak var button: NSComboButton?
66+
private var menuItemActions: [UUID: () -> Void] = [:]
67+
68+
init(primaryAction: @escaping () -> Void) {
69+
self.primaryAction = primaryAction
70+
}
71+
72+
@objc func handlePrimaryAction() {
73+
primaryAction()
74+
}
75+
76+
@objc func handleMenuItemAction(_ sender: NSMenuItem) {
77+
if let itemId = sender.representedObject as? UUID,
78+
let action = menuItemActions[itemId] {
79+
action()
80+
}
81+
}
82+
83+
func updateMenu(with items: [SplitButtonMenuItem]) {
84+
let menu = NSMenu()
85+
menuItemActions.removeAll()
86+
87+
// Add fixed menu title if there are items
88+
if !items.isEmpty {
89+
if #available(macOS 14.0, *) {
90+
let headerItem = NSMenuItem.sectionHeader(title: "Install Server With")
91+
menu.addItem(headerItem)
92+
} else {
93+
let headerItem = NSMenuItem()
94+
headerItem.title = "Install Server With"
95+
headerItem.isEnabled = false
96+
menu.addItem(headerItem)
97+
}
98+
99+
// Add menu items
100+
for item in items {
101+
let menuItem = NSMenuItem(
102+
title: item.title,
103+
action: #selector(handleMenuItemAction(_:)),
104+
keyEquivalent: ""
105+
)
106+
menuItem.target = self
107+
menuItem.representedObject = item.id
108+
menuItemActions[item.id] = item.action
109+
menu.addItem(menuItem)
110+
}
111+
}
112+
113+
button?.menu = menu
114+
}
115+
}
116+
}

Core/Sources/HostApp/ToolsConfigView.swift

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import Client
2+
import ComposableArchitecture
3+
import ConversationServiceProvider
24
import Foundation
5+
import GitHubCopilotService
36
import Logger
47
import SharedUIComponents
58
import SwiftUI
9+
import SystemUtils
610
import Toast
7-
import ConversationServiceProvider
8-
import GitHubCopilotService
9-
import ComposableArchitecture
1011

1112
struct MCPConfigView: View {
1213
@State private var mcpConfig: String = ""
@@ -16,11 +17,12 @@ struct MCPConfigView: View {
1617
@State private var lastModificationDate: Date? = nil
1718
@State private var fileMonitorTask: Task<Void, Error>? = nil
1819
@State private var isMCPFFEnabled = false
20+
@State private var isEditorPreviewEnabled = false
1921
@State private var selectedOption = ToolType.MCP
2022
@Environment(\.colorScheme) var colorScheme
2123

2224
private static var lastSyncTimestamp: Date? = nil
23-
25+
2426
enum ToolType: String, CaseIterable, Identifiable {
2527
case MCP, BuiltIn
2628
var id: Self { self }
@@ -30,25 +32,45 @@ struct MCPConfigView: View {
3032
WithPerceptionTracking {
3133
ScrollView {
3234
Picker("", selection: $selectedOption) {
33-
Text("MCP").tag(ToolType.MCP)
34-
Text("Built-In").tag(ToolType.BuiltIn)
35+
if #available(macOS 26.0, *) {
36+
Text("MCP".padded(centerTo: 24, with: "\u{2002}")).tag(ToolType.MCP)
37+
Text("Built-In".padded(centerTo: 24, with: "\u{2002}")).tag(ToolType.BuiltIn)
38+
} else {
39+
Text("MCP").tag(ToolType.MCP)
40+
Text("Built-In").tag(ToolType.BuiltIn)
41+
}
3542
}
36-
.pickerStyle(.segmented)
3743
.frame(width: 400)
38-
44+
.labelsHidden()
45+
.pickerStyle(.segmented)
46+
.padding(.top, 12)
47+
.padding(.bottom, 4)
48+
3949
Group {
4050
if selectedOption == .MCP {
4151
VStack(alignment: .leading, spacing: 8) {
4252
MCPIntroView(isMCPFFEnabled: $isMCPFFEnabled)
4353
if isMCPFFEnabled {
4454
MCPManualInstallView()
55+
56+
if isEditorPreviewEnabled && ( SystemUtils.isPrereleaseBuild || SystemUtils.isDeveloperMode ) {
57+
MCPRegistryURLView()
58+
}
59+
4560
MCPToolsListView()
61+
62+
HStack {
63+
Spacer()
64+
AdaptiveHelpLink(action: { NSWorkspace.shared.open(
65+
URL(string: "https://modelcontextprotocol.io/introduction")!
66+
) })
67+
}
4668
}
4769
}
4870
.onAppear {
4971
setupConfigFilePath()
5072
Task {
51-
await updateMCPFeatureFlag()
73+
await updateFeatureFlag()
5274
}
5375
}
5476
.onDisappear {
@@ -57,31 +79,32 @@ struct MCPConfigView: View {
5779
.onChange(of: isMCPFFEnabled) { newMCPFFEnabled in
5880
if newMCPFFEnabled {
5981
startMonitoringConfigFile()
60-
refreshConfiguration(())
82+
refreshConfiguration()
6183
} else {
6284
stopMonitoringConfigFile()
6385
}
6486
}
6587
.onReceive(DistributedNotificationCenter.default()
6688
.publisher(for: .gitHubCopilotFeatureFlagsDidChange)) { _ in
6789
Task {
68-
await updateMCPFeatureFlag()
90+
await updateFeatureFlag()
6991
}
70-
}
92+
}
7193
} else {
7294
BuiltInToolsListView()
7395
}
7496
}
75-
.padding(20)
97+
.padding(.horizontal, 20)
7698
}
7799
}
78100
}
79-
80-
private func updateMCPFeatureFlag() async {
101+
102+
private func updateFeatureFlag() async {
81103
do {
82104
let service = try getService()
83105
if let featureFlags = try await service.getCopilotFeatureFlags() {
84106
isMCPFFEnabled = featureFlags.mcp
107+
isEditorPreviewEnabled = featureFlags.editorPreviewFeatures
85108
}
86109
} catch {
87110
Logger.client.error("Failed to get copilot feature flags: \(error)")
@@ -101,7 +124,7 @@ struct MCPConfigView: View {
101124
try? """
102125
{
103126
"servers": {
104-
127+
105128
}
106129
}
107130
""".write(to: configFileURL, atomically: true, encoding: .utf8)
@@ -174,7 +197,7 @@ struct MCPConfigView: View {
174197
if let validJson = readAndValidateJSON(from: configFileURL) {
175198
await MainActor.run {
176199
mcpConfig = validJson
177-
refreshConfiguration(validJson)
200+
refreshConfiguration()
178201
toast("MCP configuration file updated", .info)
179202
}
180203
} else {
@@ -194,7 +217,7 @@ struct MCPConfigView: View {
194217
fileMonitorTask = nil
195218
}
196219

197-
func refreshConfiguration(_: Any) {
220+
func refreshConfiguration() {
198221
if MCPConfigView.lastSyncTimestamp == lastModificationDate {
199222
return
200223
}
@@ -221,6 +244,16 @@ struct MCPConfigView: View {
221244
}
222245
}
223246

247+
extension String {
248+
func padded(centerTo total: Int, with pad: Character = " ") -> String {
249+
guard count < total else { return self }
250+
let deficit = total - count
251+
let left = deficit / 2
252+
let right = deficit - left
253+
return String(repeating: pad, count: left) + self + String(repeating: pad, count: right)
254+
}
255+
}
256+
224257
#Preview {
225258
MCPConfigView()
226259
.frame(width: 800, height: 600)

Core/Sources/HostApp/ToolsSettings/MCPManualInstallView.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ struct MCPManualInstallView: View {
5555
VStack(alignment: .leading, spacing: 8) {
5656
HStack(spacing: 8) {
5757
Text("Example Configuration").foregroundColor(.primary.opacity(0.85))
58-
5958
CopyButton(
6059
copy: {
6160
NSPasteboard.general.clearContents()
@@ -152,9 +151,3 @@ struct MCPManualInstallView: View {
152151
NSWorkspace.shared.open(url)
153152
}
154153
}
155-
156-
#Preview {
157-
MCPManualInstallView()
158-
.padding()
159-
.frame(width: 900)
160-
}

0 commit comments

Comments
 (0)