Skip to content

Commit f51663a

Browse files
committed
Pre-release 0.32.112
1 parent c32cc7c commit f51663a

File tree

16 files changed

+170
-83
lines changed

16 files changed

+170
-83
lines changed

Diff for: Core/Sources/ChatService/ChatService.swift

+11-26
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,8 @@ public final class ChatService: ChatServiceType, ObservableObject {
8888

8989
private func subscribeToWatchedFilesHandler() {
9090
self.watchedFilesHandler.onWatchedFiles.sink(receiveValue: { [weak self] (request, completion) in
91-
guard let self,
92-
request.params!.workspaceUri != "/",
93-
!ProjectContextSkill.isWorkspaceResolved(self.chatTabInfo.workspacePath)
94-
else { return }
95-
96-
ProjectContextSkill.resolveSkill(
97-
request: request,
98-
workspacePath: self.chatTabInfo.workspacePath,
99-
completion: completion
100-
)
101-
102-
/// after sync complete files to CLS, start file watcher
91+
guard let self, request.params!.workspaceUri != "/" else { return }
10392
self.startFileChangeWatcher()
104-
10593
}).store(in: &cancellables)
10694
}
10795

@@ -463,20 +451,17 @@ public final class ChatService: ChatServiceType, ObservableObject {
463451

464452
Task {
465453
// mark running steps to cancelled
466-
if var message = await memory.history.last,
467-
message.role == .assistant {
468-
message.steps = message.steps.map { step in
469-
return .init(
470-
id: step.id,
471-
title: step.title,
472-
description: step.description,
473-
status: step.status == .running ? .cancelled : step.status,
474-
error: step.error
475-
)
476-
}
454+
await mutateHistory({ history in
455+
guard !history.isEmpty,
456+
let lastIndex = history.indices.last,
457+
history[lastIndex].role == .assistant else { return }
477458

478-
await memory.appendMessage(message)
479-
}
459+
for i in 0..<history[lastIndex].steps.count {
460+
if history[lastIndex].steps[i].status == .running {
461+
history[lastIndex].steps[i].status = .cancelled
462+
}
463+
}
464+
})
480465

481466
// The message of progress report could change rapidly
482467
// Directly upsert the last chat message of history here

Diff for: Core/Sources/ConversationTab/ModelPicker.swift

+23-9
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@ extension AppState {
3131
extension CopilotModelManager {
3232
static func getAvailableChatLLMs() -> [LLMModel] {
3333
let LLMs = CopilotModelManager.getAvailableLLMs()
34-
let availableModels = LLMs.filter(
34+
return LLMs.filter(
3535
{ $0.scopes.contains(.chatPanel) }
3636
).map {
3737
LLMModel(modelName: $0.modelName, modelFamily: $0.modelFamily)
3838
}
39-
return availableModels.isEmpty ? [defaultModel] : availableModels
4039
}
4140
}
4241

@@ -50,6 +49,7 @@ struct ModelPicker: View {
5049
@State private var selectedModel = defaultModel.modelName
5150
@State private var isHovered = false
5251
@State private var isPressed = false
52+
static var lastRefreshModelsTime: Date = .init(timeIntervalSince1970: 0)
5353

5454
init() {
5555
self.updateCurrentModel()
@@ -66,15 +66,23 @@ struct ModelPicker: View {
6666
var body: some View {
6767
WithPerceptionTracking {
6868
Menu(selectedModel) {
69-
ForEach(models, id: \.self) { option in
69+
if models.isEmpty {
7070
Button {
71-
selectedModel = option.modelName
72-
AppState.shared.setSelectedModel(option)
71+
// No action needed
7372
} label: {
74-
if selectedModel == option.modelName {
75-
Text("\(option.modelName)")
76-
} else {
77-
Text(" \(option.modelName)")
73+
Text("Loading...")
74+
}
75+
} else {
76+
ForEach(models, id: \.self) { option in
77+
Button {
78+
selectedModel = option.modelName
79+
AppState.shared.setSelectedModel(option)
80+
} label: {
81+
if selectedModel == option.modelName {
82+
Text("\(option.modelName)")
83+
} else {
84+
Text(" \(option.modelName)")
85+
}
7886
}
7987
}
8088
}
@@ -108,6 +116,12 @@ struct ModelPicker: View {
108116

109117
@MainActor
110118
func refreshModels() async {
119+
let now = Date()
120+
if now.timeIntervalSince(Self.lastRefreshModelsTime) < 60 {
121+
return
122+
}
123+
124+
Self.lastRefreshModelsTime = now
111125
let copilotModels = await SharedChatService.shared.copilotModels()
112126
if !copilotModels.isEmpty {
113127
CopilotModelManager.updateLLMs(copilotModels)

Diff for: Core/Sources/ConversationTab/Views/ConversationProgressStepView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct StatusItemView: View {
2929
case .running:
3030
ProgressView()
3131
.controlSize(.small)
32+
.frame(width: 16, height: 16)
3233
.scaleEffect(0.7)
3334
case .completed:
3435
Image(systemName: "checkmark")

Diff for: Core/Sources/GitHubCopilotViewModel/GitHubCopilotViewModel.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,12 @@ public class GitHubCopilotViewModel: ObservableObject {
155155
waitingForSignIn = false
156156
self.username = username
157157
self.status = status
158+
await Status.shared.updateAuthStatus(.loggedIn, username: username)
159+
broadcastStatusChange()
158160
let models = try? await service.models()
159161
if let models = models, !models.isEmpty {
160162
CopilotModelManager.updateLLMs(models)
161163
}
162-
await Status.shared.updateAuthStatus(.loggedIn, username: username)
163-
broadcastStatusChange()
164164
} catch let error as GitHubCopilotError {
165165
if case .languageServerError(.timeout) = error {
166166
// TODO figure out how to extend the default timeout on a Chime LSP request

Diff for: Core/Sources/PersistMiddleware/Extensions/ChatMessage+Storage.swift

+23
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ extension ChatMessage {
1414
var suggestedTitle: String?
1515
var errorMessage: String?
1616
var steps: [ConversationProgressStep]
17+
18+
// Custom decoder to provide default value for steps
19+
init(from decoder: Decoder) throws {
20+
let container = try decoder.container(keyedBy: CodingKeys.self)
21+
content = try container.decode(String.self, forKey: .content)
22+
rating = try container.decode(ConversationRating.self, forKey: .rating)
23+
references = try container.decode([ConversationReference].self, forKey: .references)
24+
followUp = try container.decodeIfPresent(ConversationFollowUp.self, forKey: .followUp)
25+
suggestedTitle = try container.decodeIfPresent(String.self, forKey: .suggestedTitle)
26+
errorMessage = try container.decodeIfPresent(String.self, forKey: .errorMessage)
27+
steps = try container.decodeIfPresent([ConversationProgressStep].self, forKey: .steps) ?? []
28+
}
29+
30+
// Default memberwise init for encoding
31+
init(content: String, rating: ConversationRating, references: [ConversationReference], followUp: ConversationFollowUp?, suggestedTitle: String?, errorMessage: String?, steps: [ConversationProgressStep]?) {
32+
self.content = content
33+
self.rating = rating
34+
self.references = references
35+
self.followUp = followUp
36+
self.suggestedTitle = suggestedTitle
37+
self.errorMessage = errorMessage
38+
self.steps = steps ?? []
39+
}
1740
}
1841

1942
func toTurnItem() -> TurnItem {

Diff for: Core/Sources/Service/Service.swift

+29-23
Original file line numberDiff line numberDiff line change
@@ -94,24 +94,32 @@ public final class Service {
9494
keyBindingManager.start()
9595

9696
Task {
97-
await XcodeInspector.shared.safe.$activeDocumentURL
98-
.removeDuplicates()
99-
.filter { $0 != .init(fileURLWithPath: "/") }
100-
.compactMap { $0 }
101-
.sink { [weak self] fileURL in
102-
Task {
103-
do {
104-
let _ = try await self?.workspacePool
105-
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
106-
} catch let error as Workspace.WorkspaceFileError {
107-
Logger.workspacePool
108-
.info(error.localizedDescription)
109-
}
110-
catch {
111-
Logger.workspacePool.error(error)
112-
}
97+
await Publishers.CombineLatest(
98+
XcodeInspector.shared.safe.$activeDocumentURL
99+
.removeDuplicates(),
100+
XcodeInspector.shared.safe.$latestActiveXcode
101+
)
102+
.receive(on: DispatchQueue.main)
103+
.sink { [weak self] documentURL, latestXcode in
104+
Task {
105+
let fileURL = documentURL ?? latestXcode?.realtimeDocumentURL
106+
guard fileURL != nil, fileURL != .init(fileURLWithPath: "/") else {
107+
return
108+
}
109+
do {
110+
let _ = try await self?.workspacePool
111+
.fetchOrCreateWorkspaceAndFilespace(
112+
fileURL: fileURL!
113+
)
114+
} catch let error as Workspace.WorkspaceFileError {
115+
Logger.workspacePool
116+
.info(error.localizedDescription)
117+
}
118+
catch {
119+
Logger.workspacePool.error(error)
113120
}
114-
}.store(in: &cancellable)
121+
}
122+
}.store(in: &cancellable)
115123

116124
// Combine both workspace and auth status changes into a single stream
117125
await Publishers.CombineLatest3(
@@ -202,13 +210,11 @@ extension Service {
202210
let name = self.getDisplayNameOfXcodeWorkspace(url: workspaceURL)
203211
let path = workspaceURL.path
204212

205-
// switch workspace and username
206-
self.guiController.store.send(.switchWorkspace(path: path, name: name, username: username))
207-
213+
// switch workspace and username and wait for it to complete
214+
await self.guiController.store.send(.switchWorkspace(path: path, name: name, username: username)).finish()
208215
// restore if needed
209216
await self.guiController.restore(path: path, name: name, username: username)
210-
211-
// init chat tab if no history tab
212-
self.guiController.store.send(.initWorkspaceChatTabIfNeeded(path: path, username: username))
217+
// init chat tab if no history tab (only after workspace is fully switched and restored)
218+
await self.guiController.store.send(.initWorkspaceChatTabIfNeeded(path: path, username: username)).finish()
213219
}
214220
}

Diff for: Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -513,10 +513,13 @@ public struct ChatPanelFeature {
513513
if var existChatWorkspace = state.chatHistory.workspaces[id: chatWorkspace.id] {
514514

515515
if var selectedChatTabInfo = chatWorkspace.tabInfo.first(where: { $0.id == chatWorkspace.selectedTabId }) {
516-
// cancel selectedChatTabInfo in chat workspace
517-
selectedChatTabInfo.isSelected = false
516+
// Keep the selection state when restoring
517+
selectedChatTabInfo.isSelected = true
518518
chatWorkspace.tabInfo[id: selectedChatTabInfo.id] = selectedChatTabInfo
519519

520+
// Update the existing workspace's selected tab to match
521+
existChatWorkspace.selectedTabId = selectedChatTabInfo.id
522+
520523
// merge tab info
521524
existChatWorkspace.tabInfo.append(contentsOf: chatWorkspace.tabInfo)
522525
state.chatHistory.updateHistory(existChatWorkspace)

Diff for: ExtensionService/AppDelegate.swift

+9-3
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
180180
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
181181
app.isUserOfService
182182
else { continue }
183-
if NSWorkspace.shared.runningApplications.contains(where: \.isUserOfService) {
184-
continue
183+
184+
// Check if Xcode is running
185+
let isXcodeRunning = NSWorkspace.shared.runningApplications.contains {
186+
$0.bundleIdentifier == "com.apple.dt.Xcode"
187+
}
188+
189+
if !isXcodeRunning {
190+
Logger.client.info("No Xcode instances running, preparing to quit")
191+
quit()
185192
}
186-
quit()
187193
}
188194
}
189195
}

Diff for: Tool/Sources/ConversationServiceProvider/ConversationServiceProvider.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public struct ConversationProgressStep: Codable, Equatable, Identifiable {
170170
public let id: String
171171
public let title: String
172172
public let description: String?
173-
public let status: StepStatus
173+
public var status: StepStatus
174174
public let error: StepError?
175175

176176
public init(id: String, title: String, description: String?, status: StepStatus, error: StepError?) {
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,55 @@
11
import JSONRPC
22
import Combine
3+
import Workspace
4+
import XcodeInspector
5+
import Foundation
36

47
public protocol WatchedFilesHandler {
58
var onWatchedFiles: PassthroughSubject<(WatchedFilesRequest, (AnyJSONRPCResponse) -> Void), Never> { get }
6-
func handleWatchedFiles(_ request: WatchedFilesRequest, completion: @escaping (AnyJSONRPCResponse) -> Void)
9+
func handleWatchedFiles(_ request: WatchedFilesRequest, workspaceURL: URL, completion: @escaping (AnyJSONRPCResponse) -> Void, service: GitHubCopilotService?)
710
}
811

912
public final class WatchedFilesHandlerImpl: WatchedFilesHandler {
1013
public static let shared = WatchedFilesHandlerImpl()
1114

1215
public let onWatchedFiles: PassthroughSubject<(WatchedFilesRequest, (AnyJSONRPCResponse) -> Void), Never> = .init()
16+
17+
public func handleWatchedFiles(_ request: WatchedFilesRequest, workspaceURL: URL, completion: @escaping (AnyJSONRPCResponse) -> Void, service: GitHubCopilotService?) {
18+
guard let params = request.params, params.workspaceUri != "/" else { return }
1319

14-
public func handleWatchedFiles(_ request: WatchedFilesRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) {
20+
let projectURL = WorkspaceXcodeWindowInspector.extractProjectURL(workspaceURL: workspaceURL, documentURL: nil) ?? workspaceURL
21+
22+
let files = WorkspaceFile.getWatchedFiles(
23+
workspaceURL: workspaceURL,
24+
projectURL: projectURL,
25+
excludeGitIgnoredFiles: params.excludeGitignoredFiles,
26+
excludeIDEIgnoredFiles: params.excludeIDEIgnoredFiles
27+
)
28+
29+
let batchSize = BatchingFileChangeWatcher.maxEventPublishSize
30+
/// only `batchSize`(100) files to complete this event for setup watching workspace in CLS side
31+
let jsonResult: JSONValue = .array(files.prefix(batchSize).map { .string($0) })
32+
let jsonValue: JSONValue = .hash(["files": jsonResult])
33+
34+
completion(AnyJSONRPCResponse(id: request.id, result: jsonValue))
35+
36+
Task {
37+
if files.count > batchSize {
38+
for startIndex in stride(from: batchSize, to: files.count, by: batchSize) {
39+
let endIndex = min(startIndex + batchSize, files.count)
40+
let batch = Array(files[startIndex..<endIndex])
41+
try? await service?.notifyDidChangeWatchedFiles(.init(
42+
workspaceUri: params.workspaceUri,
43+
changes: batch.map { .init(uri: $0, type: .created)}
44+
))
45+
46+
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
47+
}
48+
}
49+
}
50+
51+
/// publish event for watching workspace file changes
1552
onWatchedFiles.send((request, completion))
1653
}
1754
}
55+

Diff for: Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public final class GitHubCopilotWorkspacePlugin: WorkspacePlugin {
2121
}
2222

2323
func createGitHubCopilotService() throws -> GitHubCopilotService {
24-
let newService = try GitHubCopilotService(projectRootURL: projectRootURL)
24+
let newService = try GitHubCopilotService(projectRootURL: projectRootURL, workspaceURL: workspaceURL)
2525
Task {
2626
try await Task.sleep(nanoseconds: 1_000_000_000)
2727
finishLaunchingService()

Diff for: Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,12 @@ extension CustomJSONRPCLanguageServer {
346346
callback: @escaping (AnyJSONRPCResponse) -> Void
347347
) -> Bool {
348348
serverRequestPublisher.send((request: request, callback: callback))
349-
return false
349+
switch request.method {
350+
case "copilot/watchedFiles":
351+
return true
352+
default:
353+
return false
354+
}
350355
}
351356
}
352357

Diff for: Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public class GitHubCopilotBaseService {
147147
sessionId = UUID().uuidString
148148
}
149149

150-
init(projectRootURL: URL) throws {
150+
init(projectRootURL: URL, workspaceURL: URL = URL(fileURLWithPath: "/")) throws {
151151
self.projectRootURL = projectRootURL
152152
self.sessionId = UUID().uuidString
153153
let (server, localServer) = try {
@@ -348,14 +348,14 @@ public final class GitHubCopilotService:
348348
super.init(designatedServer: designatedServer)
349349
}
350350

351-
override public init(projectRootURL: URL = URL(fileURLWithPath: "/")) throws {
351+
override public init(projectRootURL: URL = URL(fileURLWithPath: "/"), workspaceURL: URL = URL(fileURLWithPath: "/")) throws {
352352
do {
353-
try super.init(projectRootURL: projectRootURL)
353+
try super.init(projectRootURL: projectRootURL, workspaceURL: workspaceURL)
354354
localProcessServer?.notificationPublisher.sink(receiveValue: { [weak self] notification in
355355
self?.serverNotificationHandler.handleNotification(notification)
356356
}).store(in: &cancellables)
357357
localProcessServer?.serverRequestPublisher.sink(receiveValue: { [weak self] (request, callback) in
358-
self?.serverRequestHandler.handleRequest(request, callback: callback)
358+
self?.serverRequestHandler.handleRequest(request, workspaceURL: workspaceURL, callback: callback, service: self)
359359
}).store(in: &cancellables)
360360
updateStatusInBackground()
361361

0 commit comments

Comments
 (0)