Skip to content

Commit e445012

Browse files
committed
Pre-release 0.44.147
1 parent 2a6a8d6 commit e445012

File tree

10 files changed

+238
-48
lines changed

10 files changed

+238
-48
lines changed

Core/Sources/HostApp/SharedComponents/Badge.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,30 @@ struct BadgeItem {
2222

2323
struct Badge: View {
2424
let text: String
25+
let attributedText: AttributedString?
2526
let level: BadgeItem.Level
2627
let icon: String?
2728
let isSelected: Bool
2829

2930
init(badgeItem: BadgeItem) {
3031
text = badgeItem.text
32+
attributedText = nil
3133
level = badgeItem.level
3234
icon = badgeItem.icon
3335
isSelected = badgeItem.isSelected
3436
}
3537

3638
init(text: String, level: BadgeItem.Level, icon: String? = nil, isSelected: Bool = false) {
3739
self.text = text
40+
self.attributedText = nil
41+
self.level = level
42+
self.icon = icon
43+
self.isSelected = isSelected
44+
}
45+
46+
init(attributedText: AttributedString, level: BadgeItem.Level, icon: String? = nil, isSelected: Bool = false) {
47+
self.text = String(attributedText.characters)
48+
self.attributedText = attributedText
3849
self.level = level
3950
self.icon = icon
4051
self.isSelected = isSelected
@@ -47,10 +58,18 @@ struct Badge: View {
4758
.font(.caption2)
4859
.padding(.vertical, 1)
4960
}
50-
Text(text)
51-
.fontWeight(.semibold)
52-
.font(.caption2)
53-
.lineLimit(1)
61+
if let attributedText = attributedText {
62+
Text(attributedText)
63+
.fontWeight(.semibold)
64+
.font(.caption2)
65+
.lineLimit(1)
66+
.truncationMode(.middle)
67+
} else {
68+
Text(text)
69+
.fontWeight(.semibold)
70+
.font(.caption2)
71+
.lineLimit(1)
72+
}
5473
}
5574
.padding(.vertical, 1)
5675
.padding(.horizontal, 3)
@@ -77,5 +96,6 @@ struct Badge: View {
7796
lineWidth: 1
7897
)
7998
)
99+
.help(text)
80100
}
81101
}

Core/Sources/HostApp/ToolsConfigView.swift

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ struct MCPConfigView: View {
2222
@Environment(\.colorScheme) var colorScheme
2323

2424
private static var lastSyncTimestamp: Date? = nil
25+
@State private var debounceTimer: Timer?
26+
private static let refreshDebounceInterval: TimeInterval = 1.0 // 1.0 second debounce
2527

2628
enum ToolType: String, CaseIterable, Identifiable {
2729
case MCP, BuiltIn
@@ -241,16 +243,24 @@ struct MCPConfigView: View {
241243
UserDefaults.shared.set(jsonString, for: \.gitHubCopilotMCPConfig)
242244
}
243245

244-
Task {
245-
do {
246-
let service = try getService()
247-
try await service.postNotification(
248-
name: Notification.Name
249-
.gitHubCopilotShouldRefreshEditorInformation.rawValue
250-
)
251-
toast("MCP configuration updated", .info)
252-
} catch {
253-
toast(error.localizedDescription, .error)
246+
// Debounce the refresh notification to avoid sending too frequently
247+
debounceTimer?.invalidate()
248+
debounceTimer = Timer.scheduledTimer(withTimeInterval: MCPConfigView.refreshDebounceInterval, repeats: false) { _ in
249+
Task {
250+
do {
251+
let service = try getService()
252+
try await service.postNotification(
253+
name: Notification.Name
254+
.gitHubCopilotShouldRefreshEditorInformation.rawValue
255+
)
256+
await MainActor.run {
257+
toast("Fetching MCP tools...", .info)
258+
}
259+
} catch {
260+
await MainActor.run {
261+
toast(error.localizedDescription, .error)
262+
}
263+
}
254264
}
255265
}
256266
}

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryInstallation.swift

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -263,25 +263,8 @@ public class MCPRegistryService: ObservableObject {
263263
let jsonData = try JSONSerialization.data(withJSONObject: config, options: [.prettyPrinted])
264264
try jsonData.write(to: configFileURL)
265265

266-
// Update UserDefaults and trigger refresh
267-
// Extract only the "servers" object to save to UserDefaults (consistent with ToolsConfigView)
268-
if let serversDict = config["servers"] as? [String: Any] {
269-
let serversData = try JSONSerialization.data(withJSONObject: serversDict, options: [.prettyPrinted])
270-
if let jsonString = String(data: serversData, encoding: .utf8) {
271-
UserDefaults.shared.set(jsonString, for: \.gitHubCopilotMCPConfig)
272-
}
273-
}
274-
275-
Task {
276-
do {
277-
let service = try getService()
278-
try await service.postNotification(
279-
name: Notification.Name.gitHubCopilotShouldRefreshEditorInformation.rawValue
280-
)
281-
} catch {
282-
Logger.client.error("Failed to post refresh notification: \(error)")
283-
}
284-
}
266+
// Note: UserDefaults update and notification will be handled by ToolsConfigView's file monitor
267+
// with debouncing to prevent duplicate notifications
285268
}
286269

287270
// MARK: - Server Installation Status

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLInputField.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct MCPRegistryURLInputField: View {
1313
let isSheet: Bool
1414
let mcpRegistryEntry: MCPRegistryEntry?
1515
let onValidationChange: ((Bool) -> Void)?
16+
let onCommit: (() -> Void)?
1617

1718
private var isRegistryOnly: Bool {
1819
mcpRegistryEntry?.registryAccess == .registryOnly
@@ -23,13 +24,15 @@ struct MCPRegistryURLInputField: View {
2324
maxURLLength: Int = 2048,
2425
isSheet: Bool = false,
2526
mcpRegistryEntry: MCPRegistryEntry? = nil,
26-
onValidationChange: ((Bool) -> Void)? = nil
27+
onValidationChange: ((Bool) -> Void)? = nil,
28+
onCommit: (() -> Void)? = nil
2729
) {
2830
self._urlText = urlText
2931
self.maxURLLength = maxURLLength
3032
self.isSheet = isSheet
3133
self.mcpRegistryEntry = mcpRegistryEntry
3234
self.onValidationChange = onValidationChange
35+
self.onCommit = onCommit
3336
}
3437

3538
var body: some View {
@@ -43,6 +46,9 @@ struct MCPRegistryURLInputField: View {
4346
.onChange(of: urlText) { newValue in
4447
handleURLChange(newValue)
4548
}
49+
.onSubmit {
50+
onCommit?()
51+
}
4652
}
4753
} else {
4854
TextField("MCP Registry URL:", text: $urlText)
@@ -52,20 +58,25 @@ struct MCPRegistryURLInputField: View {
5258
.onChange(of: urlText) { newValue in
5359
handleURLChange(newValue)
5460
}
61+
.onSubmit {
62+
onCommit?()
63+
}
5564
}
5665

5766
Menu {
5867
ForEach(urlHistory, id: \.self) { url in
5968
Button(url) {
6069
urlText = url
6170
isFocused = false
71+
onCommit?()
6272
}
6373
}
6474

6575
Divider()
6676

6777
Button("Reset to Default") {
6878
urlText = defaultMCPRegistryURL
79+
onCommit?()
6980
}
7081

7182
if !urlHistory.isEmpty {

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLSheet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ struct MCPRegistryURLSheet: View {
6666
}
6767

6868
private func openHelpLink() {
69-
NSWorkspace.shared.open(URL(string: "https://registry.mcpservers.org")!)
69+
NSWorkspace.shared.open(URL(string: "https://docs.github.com/en/copilot/how-tos/provide-context/use-mcp/select-an-mcp-registry")!)
7070
}
7171
}

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLView.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,12 @@ struct MCPRegistryURLView: View {
7575
maxURLLength: maxURLLength,
7676
isSheet: false,
7777
mcpRegistryEntry: mcpRegistry?.first,
78-
onValidationChange: { isValid in
79-
if isValid && (!tempURLText.isEmpty || tempURLText.isEmpty) {
78+
onValidationChange: { _ in
79+
// Only validate, don't update mcpRegistryURL here
80+
},
81+
onCommit: {
82+
// Update mcpRegistryURL when user presses Enter
83+
if tempURLText != mcpRegistryURL {
8084
mcpRegistryURL = tempURLText
8185
}
8286
}
@@ -105,6 +109,7 @@ struct MCPRegistryURLView: View {
105109
)
106110
.animation(.easeInOut(duration: 0.3), value: isExpanded)
107111
.onAppear {
112+
tempURLText = mcpRegistryURL
108113
Task { await getMCPRegistryAllowlist() }
109114
}
110115
.onReceive(DistributedNotificationCenter.default().publisher(for: .authStatusDidChange)) { _ in
@@ -122,6 +127,11 @@ struct MCPRegistryURLView: View {
122127
}
123128

124129
private func loadMCPServers() async {
130+
// Update mcpRegistryURL with current tempURLText before loading
131+
if tempURLText != mcpRegistryURL {
132+
mcpRegistryURL = tempURLText
133+
}
134+
125135
isLoading = true
126136
defer { isLoading = false }
127137
do {
@@ -208,7 +218,17 @@ struct MCPRegistryURLView: View {
208218
defer { isLoading = false }
209219

210220
// Let the view model handle the entire update flow including clearing and fetching
211-
await MCPServerGalleryWindow.refreshFromURL(mcpRegistryEntry: mcpRegistry?.first)
221+
if let error = await MCPServerGalleryWindow.refreshFromURL(mcpRegistryEntry: mcpRegistry?.first) {
222+
// Display error in the URL view
223+
if let serviceError = error as? XPCExtensionServiceError {
224+
errorMessage = serviceError.underlyingError?.localizedDescription ?? serviceError.localizedDescription
225+
} else {
226+
errorMessage = error.localizedDescription
227+
}
228+
isExpanded = true
229+
} else {
230+
errorMessage = ""
231+
}
212232
}
213233
}
214234

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPServerGalleryView.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import GitHubCopilotService
55
import Logger
66
import SharedUIComponents
77
import SwiftUI
8+
import XPCShared
89

910
enum MCPServerGalleryWindow {
1011
static let identifier = "MCPServerGalleryWindow"
@@ -53,8 +54,8 @@ enum MCPServerGalleryWindow {
5354
currentViewModel?.updateData(serverList: serverList, mcpRegistryEntry: mcpRegistryEntry)
5455
}
5556

56-
@MainActor static func refreshFromURL(mcpRegistryEntry: MCPRegistryEntry? = nil) async {
57-
await currentViewModel?.refreshFromURL(mcpRegistryEntry: mcpRegistryEntry)
57+
@MainActor static func refreshFromURL(mcpRegistryEntry: MCPRegistryEntry? = nil) async -> Error? {
58+
return await currentViewModel?.refreshFromURL(mcpRegistryEntry: mcpRegistryEntry)
5859
}
5960

6061
static func isOpen() -> Bool {
@@ -87,6 +88,14 @@ struct MCPServerGalleryView: View {
8788

8889
var body: some View {
8990
VStack(spacing: 0) {
91+
if let error = viewModel.lastError {
92+
if let serviceError = error as? XPCExtensionServiceError {
93+
Badge(text: serviceError.underlyingError?.localizedDescription ?? serviceError.localizedDescription, level: .danger, icon: "xmark.circle.fill")
94+
} else {
95+
Badge(text: error.localizedDescription, level: .danger, icon: "xmark.circle.fill")
96+
}
97+
}
98+
9099
tableHeaderView
91100
serverListView
92101
}

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPServerGalleryViewModel.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ final class MCPServerGalleryViewModel: ObservableObject {
2828
@Published var pendingServer: MCPRegistryServerDetail?
2929
@Published var infoSheetServer: MCPRegistryServerDetail?
3030
@Published var mcpRegistryEntry: MCPRegistryEntry?
31+
@Published private(set) var lastError: Error?
3132

3233
@AppStorage(\.mcpRegistryURL) var mcpRegistryURL
3334

@@ -154,11 +155,12 @@ final class MCPServerGalleryViewModel: ObservableObject {
154155
searchText = ""
155156

156157
// Load servers from the base URL
157-
await loadServerList(resetToFirstPage: true)
158+
_ = await loadServerList(resetToFirstPage: true)
158159
}
159160
}
160161

161-
func refreshFromURL(mcpRegistryEntry: MCPRegistryEntry? = nil) async {
162+
// Called from Settings view to refresh with optional new registry entry
163+
func refreshFromURL(mcpRegistryEntry: MCPRegistryEntry? = nil) async -> Error? {
162164
isRefreshing = true
163165
defer { isRefreshing = false }
164166

@@ -170,10 +172,12 @@ final class MCPServerGalleryViewModel: ObservableObject {
170172
Logger.client.info("Cleared gallery view model data for refresh")
171173

172174
// Load servers from the base URL
173-
await loadServerList(resetToFirstPage: true)
175+
let error = await loadServerList(resetToFirstPage: true)
174176

175177
// Reload installed servers after fetching new data
176178
loadInstalledServers()
179+
180+
return error
177181
}
178182

179183
func updateData(serverList: MCPRegistryServerList, mcpRegistryEntry: MCPRegistryEntry? = nil) {
@@ -214,7 +218,7 @@ final class MCPServerGalleryViewModel: ObservableObject {
214218
}
215219
}
216220

217-
private func loadServerList(resetToFirstPage: Bool) async {
221+
private func loadServerList(resetToFirstPage: Bool) async -> Error? {
218222
if resetToFirstPage {
219223
isInitialLoading = true
220224
} else {
@@ -225,6 +229,8 @@ final class MCPServerGalleryViewModel: ObservableObject {
225229
isInitialLoading = false
226230
isLoadingMore = false
227231
}
232+
233+
lastError = nil
228234

229235
do {
230236
let service = try getService()
@@ -247,8 +253,12 @@ final class MCPServerGalleryViewModel: ObservableObject {
247253
servers.append(contentsOf: serverList?.servers ?? [])
248254
registryMetadata = serverList?.metadata
249255
}
256+
257+
return nil
250258
} catch {
251259
Logger.client.error("Failed to load MCP servers: \(error)")
260+
lastError = error
261+
return error
252262
}
253263
}
254264

0 commit comments

Comments
 (0)