Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
77b2c02
Pre-release 0.34.118
actions-user May 16, 2025
82d3232
Release 0.35.0
actions-user May 19, 2025
f04ddbe
Pre-release 0.35.120
actions-user Jun 3, 2025
041a898
Pre-release 0.35.121
actions-user Jun 4, 2025
2e8e989
Release 0.36.0
actions-user Jun 4, 2025
d3cd006
Pre-release 0.36.123
actions-user Jun 13, 2025
fabc66e
Pre-release 0.36.124
actions-user Jun 17, 2025
64a0691
Release 0.37.0
actions-user Jun 18, 2025
81fc588
Pre-release 0.37.126
actions-user Jun 24, 2025
9788b5c
Pre-release 0.37.127
actions-user Jun 27, 2025
d1f7de3
Release 0.38.0
actions-user Jun 30, 2025
e7fd64d
Pre-release 0.38.129
actions-user Jul 9, 2025
c862f92
Create swift.yml
smoku8282 Jul 22, 2025
afbbdad
Release 0.39.0
actions-user Jul 23, 2025
9d1d42f
Release 0.40.0
actions-user Jul 24, 2025
0517f3b
Pre-release 0.40.132
actions-user Aug 1, 2025
c6e9a07
Pre-release 0.40.133
actions-user Aug 12, 2025
3a67130
Release 0.41.0
actions-user Aug 14, 2025
1339ef7
Pre-release 0.41.135
actions-user Aug 27, 2025
65dc134
Pre-release 0.41.136
actions-user Sep 2, 2025
be64a90
Release 0.42.0
actions-user Sep 3, 2025
b3fe4dd
Release 0.43.0
actions-user Sep 4, 2025
4381034
Pre-release 0.43.139
actions-user Sep 15, 2025
079132f
Create launch.json
smoku8282 Sep 19, 2025
aa450c1
Merge branch 'main' of https://github.com/smoku8282/CopilotForXcode
smoku8282 Sep 19, 2025
dee1fd1
Create copilot-instructions.md
smoku8282 Sep 19, 2025
75aa71a
Pre-release 0.43.140
actions-user Sep 19, 2025
1978c49
Merge branch 'github:main' into main
smoku8282 Sep 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Pre-release 0.36.123
  • Loading branch information
actions-user committed Jun 13, 2025
commit d3cd006e3c7b366fec801e18a9ce5167a4f7da65
3 changes: 2 additions & 1 deletion Core/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ let package = Package(
.product(name: "ConversationServiceProvider", package: "Tool"),
.product(name: "GitHubCopilotService", package: "Tool"),
.product(name: "Workspace", package: "Tool"),
.product(name: "Terminal", package: "Tool")
.product(name: "Terminal", package: "Tool"),
.product(name: "SystemUtils", package: "Tool")
]),
.testTarget(
name: "ChatServiceTests",
Expand Down
177 changes: 122 additions & 55 deletions Core/Sources/ChatService/ChatService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Logger
import Workspace
import XcodeInspector
import OrderedCollections
import SystemUtils

public protocol ChatServiceType {
var memory: ContextAwareAutoManagedChatMemory { get set }
Expand Down Expand Up @@ -330,22 +331,42 @@ public final class ChatService: ChatServiceType, ObservableObject {
let workDoneToken = UUID().uuidString
activeRequestId = workDoneToken

let chatMessage = ChatMessage(
var chatMessage = ChatMessage(
id: id,
chatTabID: self.chatTabInfo.id,
role: .user,
content: content,
references: references.toConversationReferences()
)

let currentEditorSkill = skillSet.first(where: { $0.id == CurrentEditorSkill.ID }) as? CurrentEditorSkill
let currentFileReadability = currentEditorSkill == nil
? nil
: FileUtils.checkFileReadability(at: currentEditorSkill!.currentFilePath)
var errorMessage: ChatMessage?

var currentTurnId: String? = turnId
// If turnId is provided, it is used to update the existing message, no need to append the user message
if turnId == nil {
if let currentFileReadability, !currentFileReadability.isReadable {
// For associating error message with user message
currentTurnId = UUID().uuidString
chatMessage.clsTurnID = currentTurnId
errorMessage = buildErrorMessage(
turnId: currentTurnId!,
errorMessages: [
currentFileReadability.errorMessage(
using: CurrentEditorSkill.readabilityErrorMessageProvider
)
].compactMap { $0 }.filter { !$0.isEmpty }
)
}
await memory.appendMessage(chatMessage)
}

// reset file edits
self.resetFileEdits()

// persist
saveChatMessageToStorage(chatMessage)

Expand All @@ -370,32 +391,68 @@ public final class ChatService: ChatServiceType, ObservableObject {
return
}

let skillCapabilities: [String] = [ CurrentEditorSkill.ID, ProblemsInActiveDocumentSkill.ID ]
if let errorMessage {
Task { await memory.appendMessage(errorMessage) }
}

var activeDoc: Doc?
var validSkillSet: [ConversationSkill] = skillSet
if let currentEditorSkill, currentFileReadability?.isReadable == true {
activeDoc = Doc(uri: currentEditorSkill.currentFile.url.absoluteString)
} else {
validSkillSet.removeAll(where: { $0.id == CurrentEditorSkill.ID || $0.id == ProblemsInActiveDocumentSkill.ID })
}

let request = createConversationRequest(
workDoneToken: workDoneToken,
content: content,
activeDoc: activeDoc,
references: references,
model: model,
agentMode: agentMode,
userLanguage: userLanguage,
turnId: currentTurnId,
skillSet: validSkillSet
)

self.lastUserRequest = request
self.skillSet = validSkillSet
try await send(request)
}

private func createConversationRequest(
workDoneToken: String,
content: String,
activeDoc: Doc?,
references: [FileReference],
model: String? = nil,
agentMode: Bool = false,
userLanguage: String? = nil,
turnId: String? = nil,
skillSet: [ConversationSkill]
) -> ConversationRequest {
let skillCapabilities: [String] = [CurrentEditorSkill.ID, ProblemsInActiveDocumentSkill.ID]
let supportedSkills: [String] = skillSet.map { $0.id }
let ignoredSkills: [String] = skillCapabilities.filter {
!supportedSkills.contains($0)
}
let currentEditorSkill = skillSet.first { $0.id == CurrentEditorSkill.ID }
let activeDoc: Doc? = (currentEditorSkill as? CurrentEditorSkill).map { Doc(uri: $0.currentFile.url.absoluteString) }

/// replace the `@workspace` to `@project`
let newContent = replaceFirstWord(in: content, from: "@workspace", to: "@project")

let request = ConversationRequest(workDoneToken: workDoneToken,
content: newContent,
workspaceFolder: "",
activeDoc: activeDoc,
skills: skillCapabilities,
ignoredSkills: ignoredSkills,
references: references,
model: model,
agentMode: agentMode,
userLanguage: userLanguage,
turnId: turnId
return ConversationRequest(
workDoneToken: workDoneToken,
content: newContent,
workspaceFolder: "",
activeDoc: activeDoc,
skills: skillCapabilities,
ignoredSkills: ignoredSkills,
references: references,
model: model,
agentMode: agentMode,
userLanguage: userLanguage,
turnId: turnId
)
self.lastUserRequest = request
self.skillSet = skillSet
try await send(request)
}

public func sendAndWait(_ id: String, content: String) async throws -> String {
Expand Down Expand Up @@ -444,20 +501,16 @@ public final class ChatService: ChatServiceType, ObservableObject {
{
// TODO: clean up contents for resend message
activeRequestId = nil
do {
try await send(
id,
content: lastUserRequest.content,
skillSet: skillSet,
references: lastUserRequest.references ?? [],
model: model != nil ? model : lastUserRequest.model,
agentMode: lastUserRequest.agentMode,
userLanguage: lastUserRequest.userLanguage,
turnId: id
)
} catch {
print("Failed to resend message")
}
try await send(
id,
content: lastUserRequest.content,
skillSet: skillSet,
references: lastUserRequest.references ?? [],
model: model != nil ? model : lastUserRequest.model,
agentMode: lastUserRequest.agentMode,
userLanguage: lastUserRequest.userLanguage,
turnId: id
)
}
}

Expand Down Expand Up @@ -569,6 +622,19 @@ public final class ChatService: ChatServiceType, ObservableObject {

Task {
if var lastUserMessage = await memory.history.last(where: { $0.role == .user }) {

// Case: New conversation where error message was generated before CLS request
// Using clsTurnId to associate this error message with the corresponding user message
// When merging error messages with bot responses from CLS, these properties need to be updated
await memory.mutateHistory { history in
if let existingBotIndex = history.lastIndex(where: {
$0.role == .assistant && $0.clsTurnID == lastUserMessage.clsTurnID
}) {
history[existingBotIndex].id = turnId
history[existingBotIndex].clsTurnID = turnId
}
}

lastUserMessage.clsTurnID = progress.turnId
saveChatMessageToStorage(lastUserMessage)
}
Expand Down Expand Up @@ -653,14 +719,9 @@ public final class ChatService: ChatServiceType, ObservableObject {
Task {
await Status.shared
.updateCLSStatus(.warning, busy: false, message: CLSError.message)
let errorMessage = ChatMessage(
id: progress.turnId,
chatTabID: self.chatTabInfo.id,
clsTurnID: progress.turnId,
role: .assistant,
content: "",
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)]
)
let errorMessage = buildErrorMessage(
turnId: progress.turnId,
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)])
// will persist in resetongoingRequest()
await memory.appendMessage(errorMessage)

Expand All @@ -683,27 +744,17 @@ public final class ChatService: ChatServiceType, ObservableObject {
}
} else if CLSError.code == 400 && CLSError.message.contains("model is not supported") {
Task {
let errorMessage = ChatMessage(
id: progress.turnId,
chatTabID: self.chatTabInfo.id,
role: .assistant,
content: "",
errorMessage: "Oops, the model is not supported. Please enable it first in [GitHub Copilot settings](https://github.com/settings/copilot)."
let errorMessage = buildErrorMessage(
turnId: progress.turnId,
errorMessages: ["Oops, the model is not supported. Please enable it first in [GitHub Copilot settings](https://github.com/settings/copilot)."]
)
await memory.appendMessage(errorMessage)
resetOngoingRequest()
return
}
} else {
Task {
let errorMessage = ChatMessage(
id: progress.turnId,
chatTabID: self.chatTabInfo.id,
clsTurnID: progress.turnId,
role: .assistant,
content: "",
errorMessage: CLSError.message
)
let errorMessage = buildErrorMessage(turnId: progress.turnId, errorMessages: [CLSError.message])
// will persist in resetOngoingRequest()
await memory.appendMessage(errorMessage)
resetOngoingRequest()
Expand All @@ -728,6 +779,22 @@ public final class ChatService: ChatServiceType, ObservableObject {
}
}

private func buildErrorMessage(
turnId: String,
errorMessages: [String] = [],
panelMessages: [CopilotShowMessageParams] = []
) -> ChatMessage {
return .init(
id: turnId,
chatTabID: chatTabInfo.id,
clsTurnID: turnId,
role: .assistant,
content: "",
errorMessages: errorMessages,
panelMessages: panelMessages
)
}

private func resetOngoingRequest() {
activeRequestId = nil
isReceivingMessage = false
Expand Down
13 changes: 13 additions & 0 deletions Core/Sources/ChatService/Skills/CurrentEditorSkill.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import ConversationServiceProvider
import Foundation
import GitHubCopilotService
import JSONRPC
import SystemUtils

public class CurrentEditorSkill: ConversationSkill {
public static let ID = "current-editor"
public let currentFile: FileReference
public var id: String {
return CurrentEditorSkill.ID
}
public var currentFilePath: String { currentFile.url.path }

public init(
currentFile: FileReference
Expand All @@ -20,6 +22,17 @@ public class CurrentEditorSkill: ConversationSkill {
return params.skillId == self.id
}

public static let readabilityErrorMessageProvider: FileUtils.ReadabilityErrorMessageProvider = { status in
switch status {
case .readable:
return nil
case .notFound:
return "Copilot can’t find the current file, so it's not included."
case .permissionDenied:
return "Copilot can't access the current file. Enable \"Files & Folders\" access in [System Settings](x-apple.systempreferences:com.apple.preference.security?Privacy_FilesAndFolders)."
}
}

public func resolveSkill(request: ConversationContextRequest, completion: JSONRPCResponseHandler){
let uri: String? = self.currentFile.url.absoluteString
completion(
Expand Down
8 changes: 4 additions & 4 deletions Core/Sources/ConversationTab/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public struct DisplayedChatMessage: Equatable {
public var references: [ConversationReference] = []
public var followUp: ConversationFollowUp? = nil
public var suggestedTitle: String? = nil
public var errorMessage: String? = nil
public var errorMessages: [String] = []
public var steps: [ConversationProgressStep] = []
public var editAgentRounds: [AgentRound] = []
public var panelMessages: [CopilotShowMessageParams] = []
Expand All @@ -36,7 +36,7 @@ public struct DisplayedChatMessage: Equatable {
references: [ConversationReference] = [],
followUp: ConversationFollowUp? = nil,
suggestedTitle: String? = nil,
errorMessage: String? = nil,
errorMessages: [String] = [],
steps: [ConversationProgressStep] = [],
editAgentRounds: [AgentRound] = [],
panelMessages: [CopilotShowMessageParams] = []
Expand All @@ -47,7 +47,7 @@ public struct DisplayedChatMessage: Equatable {
self.references = references
self.followUp = followUp
self.suggestedTitle = suggestedTitle
self.errorMessage = errorMessage
self.errorMessages = errorMessages
self.steps = steps
self.editAgentRounds = editAgentRounds
self.panelMessages = panelMessages
Expand Down Expand Up @@ -371,7 +371,7 @@ struct Chat {
},
followUp: message.followUp,
suggestedTitle: message.suggestedTitle,
errorMessage: message.errorMessage,
errorMessages: message.errorMessages,
steps: message.steps,
editAgentRounds: message.editAgentRounds,
panelMessages: message.panelMessages
Expand Down
Loading