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.35.120
  • Loading branch information
actions-user committed Jun 3, 2025
commit f04ddbedf93a26fb4ae7347026b2c82354eb28aa
100 changes: 82 additions & 18 deletions Core/Sources/ChatService/ChatService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import OrderedCollections

public protocol ChatServiceType {
var memory: ContextAwareAutoManagedChatMemory { get set }
func send(_ id: String, content: String, skillSet: [ConversationSkill], references: [FileReference], model: String?, agentMode: Bool) async throws
func send(_ id: String, content: String, skillSet: [ConversationSkill], references: [FileReference], model: String?, agentMode: Bool, userLanguage: String?, turnId: String?) async throws
func stopReceivingMessage() async
func upvote(_ id: String, _ rating: ConversationRating) async
func downvote(_ id: String, _ rating: ConversationRating) async
Expand Down Expand Up @@ -79,6 +79,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
private var activeRequestId: String?
private(set) public var conversationId: String?
private var skillSet: [ConversationSkill] = []
private var lastUserRequest: ConversationRequest?
private var isRestored: Bool = false
private var pendingToolCallRequests: [String: ToolCallRequest] = [:]
init(provider: any ConversationServiceProvider,
Expand All @@ -98,6 +99,18 @@ public final class ChatService: ChatServiceType, ObservableObject {
subscribeToClientToolConfirmationEvent()
}

deinit {
Task { [weak self] in
await self?.stopReceivingMessage()
}

// Clear all subscriptions
cancellables.forEach { $0.cancel() }
cancellables.removeAll()

// Memory will be deallocated automatically
}

private func subscribeToNotifications() {
memory.observeHistoryChange { [weak self] in
Task { [weak self] in
Expand Down Expand Up @@ -303,7 +316,16 @@ public final class ChatService: ChatServiceType, ObservableObject {
}
}

public func send(_ id: String, content: String, skillSet: Array<ConversationSkill>, references: Array<FileReference>, model: String? = nil, agentMode: Bool = false) async throws {
public func send(
_ id: String,
content: String,
skillSet: Array<ConversationSkill>,
references: Array<FileReference>,
model: String? = nil,
agentMode: Bool = false,
userLanguage: String? = nil,
turnId: String? = nil
) async throws {
guard activeRequestId == nil else { return }
let workDoneToken = UUID().uuidString
activeRequestId = workDoneToken
Expand All @@ -315,11 +337,15 @@ public final class ChatService: ChatServiceType, ObservableObject {
content: content,
references: references.toConversationReferences()
)
await memory.appendMessage(chatMessage)

// If turnId is provided, it is used to update the existing message, no need to append the user message
if turnId == nil {
await memory.appendMessage(chatMessage)
}

// reset file edits
self.resetFileEdits()

// persist
saveChatMessageToStorage(chatMessage)

Expand Down Expand Up @@ -363,7 +389,11 @@ public final class ChatService: ChatServiceType, ObservableObject {
ignoredSkills: ignoredSkills,
references: references,
model: model,
agentMode: agentMode)
agentMode: agentMode,
userLanguage: userLanguage,
turnId: turnId
)
self.lastUserRequest = request
self.skillSet = skillSet
try await send(request)
}
Expand Down Expand Up @@ -408,12 +438,23 @@ public final class ChatService: ChatServiceType, ObservableObject {
deleteChatMessageFromStorage(id)
}

// Not used for now
public func resendMessage(id: String) async throws {
if let message = (await memory.history).first(where: { $0.id == id })
public func resendMessage(id: String, model: String? = nil) async throws {
if let _ = (await memory.history).first(where: { $0.id == id }),
let lastUserRequest
{
// TODO: clean up contents for resend message
activeRequestId = nil
do {
try await send(id, content: message.content, skillSet: [], references: [])
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")
}
Expand Down Expand Up @@ -611,17 +652,34 @@ public final class ChatService: ChatServiceType, ObservableObject {
if CLSError.code == 402 {
Task {
await Status.shared
.updateCLSStatus(.error, busy: false, message: CLSError.message)
.updateCLSStatus(.warning, busy: false, message: CLSError.message)
let errorMessage = ChatMessage(
id: progress.turnId,
chatTabID: self.chatTabInfo.id,
clsTurnID: progress.turnId,
role: .system,
content: CLSError.message
role: .assistant,
content: "",
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)]
)
// will persist in resetongoingRequest()
await memory.removeMessage(progress.turnId)
await memory.appendMessage(errorMessage)

if let lastUserRequest {
guard let fallbackModel = CopilotModelManager.getFallbackLLM(
scope: lastUserRequest.agentMode ? .agentPanel : .chatPanel
) else {
resetOngoingRequest()
return
}
do {
CopilotModelManager.switchToFallbackModel()
try await resendMessage(id: progress.turnId, model: fallbackModel.id)
} catch {
Logger.gitHubCopilot.error(error)
resetOngoingRequest()
}
return
}
}
} else if CLSError.code == 400 && CLSError.message.contains("model is not supported") {
Task {
Expand All @@ -633,6 +691,8 @@ public final class ChatService: ChatServiceType, ObservableObject {
errorMessage: "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 {
Expand All @@ -646,10 +706,10 @@ public final class ChatService: ChatServiceType, ObservableObject {
)
// will persist in resetOngoingRequest()
await memory.appendMessage(errorMessage)
resetOngoingRequest()
return
}
}
resetOngoingRequest()
return
}

Task {
Expand All @@ -664,9 +724,8 @@ public final class ChatService: ChatServiceType, ObservableObject {
)
// will persist in resetOngoingRequest()
await memory.appendMessage(message)
resetOngoingRequest()
}

resetOngoingRequest()
}

private func resetOngoingRequest() {
Expand Down Expand Up @@ -732,7 +791,12 @@ public final class ChatService: ChatServiceType, ObservableObject {

do {
if let conversationId = conversationId {
try await conversationProvider?.createTurn(with: conversationId, request: request, workspaceURL: getWorkspaceURL())
try await conversationProvider?
.createTurn(
with: conversationId,
request: request,
workspaceURL: getWorkspaceURL()
)
} else {
var requestWithTurns = request

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public final class ContextAwareAutoManagedChatMemory: ChatMemory {
systemPrompt: ""
)
}

deinit { }

public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) async {
await memory.mutateHistory(update)
Expand Down
28 changes: 21 additions & 7 deletions Core/Sources/ConversationTab/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public struct DisplayedChatMessage: Equatable {
public enum Role: Equatable {
case user
case assistant
case system
case ignored
}

Expand All @@ -28,8 +27,20 @@ public struct DisplayedChatMessage: Equatable {
public var errorMessage: String? = nil
public var steps: [ConversationProgressStep] = []
public var editAgentRounds: [AgentRound] = []

public init(id: String, role: Role, text: String, references: [ConversationReference] = [], followUp: ConversationFollowUp? = nil, suggestedTitle: String? = nil, errorMessage: String? = nil, steps: [ConversationProgressStep] = [], editAgentRounds: [AgentRound] = []) {
public var panelMessages: [CopilotShowMessageParams] = []

public init(
id: String,
role: Role,
text: String,
references: [ConversationReference] = [],
followUp: ConversationFollowUp? = nil,
suggestedTitle: String? = nil,
errorMessage: String? = nil,
steps: [ConversationProgressStep] = [],
editAgentRounds: [AgentRound] = [],
panelMessages: [CopilotShowMessageParams] = []
) {
self.id = id
self.role = role
self.text = text
Expand All @@ -39,6 +50,7 @@ public struct DisplayedChatMessage: Equatable {
self.errorMessage = errorMessage
self.steps = steps
self.editAgentRounds = editAgentRounds
self.panelMessages = panelMessages
}
}

Expand Down Expand Up @@ -137,6 +149,7 @@ struct Chat {

@Dependency(\.openURL) var openURL
@AppStorage(\.enableCurrentEditorContext) var enableCurrentEditorContext: Bool
@AppStorage(\.chatResponseLocale) var chatResponseLocale

var body: some ReducerOf<Self> {
BindingReducer()
Expand Down Expand Up @@ -180,7 +193,7 @@ struct Chat {
let selectedModelFamily = AppState.shared.getSelectedModelFamily() ?? CopilotModelManager.getDefaultChatModel(scope: AppState.shared.modelScope())?.modelFamily
let agentMode = AppState.shared.isAgentModeEnabled()
return .run { _ in
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles, model: selectedModelFamily, agentMode: agentMode)
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles, model: selectedModelFamily, agentMode: agentMode, userLanguage: chatResponseLocale)
}.cancellable(id: CancelID.sendMessage(self.id))

case let .toolCallAccepted(toolCallId):
Expand Down Expand Up @@ -209,7 +222,7 @@ struct Chat {
let selectedModelFamily = AppState.shared.getSelectedModelFamily() ?? CopilotModelManager.getDefaultChatModel(scope: AppState.shared.modelScope())?.modelFamily

return .run { _ in
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles, model: selectedModelFamily)
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles, model: selectedModelFamily, userLanguage: chatResponseLocale)
}.cancellable(id: CancelID.sendMessage(self.id))

case .returnButtonTapped:
Expand Down Expand Up @@ -343,9 +356,9 @@ struct Chat {
id: message.id,
role: {
switch message.role {
case .system: return .system
case .user: return .user
case .assistant: return .assistant
case .system: return .ignored
}
}(),
text: message.content,
Expand All @@ -360,7 +373,8 @@ struct Chat {
suggestedTitle: message.suggestedTitle,
errorMessage: message.errorMessage,
steps: message.steps,
editAgentRounds: message.editAgentRounds
editAgentRounds: message.editAgentRounds,
panelMessages: message.panelMessages
))

return all
Expand Down
41 changes: 5 additions & 36 deletions Core/Sources/ConversationTab/ChatPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ public struct ChatPanel: View {
.accessibilityElement(children: .combine)
.accessibilityLabel("Chat Messages Group")

if chat.history.last?.role == .system {
ChatCLSError(chat: chat).padding(.trailing, 16)
} else if (chat.history.last?.followUp) != nil {
if let _ = chat.history.last?.followUp {
ChatFollowUp(chat: chat)
.padding(.trailing, 16)
.padding(.vertical, 8)
Expand Down Expand Up @@ -344,10 +342,9 @@ struct ChatHistoryItem: View {
errorMessage: message.errorMessage,
chat: chat,
steps: message.steps,
editAgentRounds: message.editAgentRounds
editAgentRounds: message.editAgentRounds,
panelMessages: message.panelMessages
)
case .system:
FunctionMessage(chat: chat, id: message.id, text: text)
case .ignored:
EmptyView()
}
Expand Down Expand Up @@ -516,8 +513,7 @@ struct ChatPanelInputArea: View {
submitChatMessage()
}
dropDownShowingType = nil
},
completions: chatAutoCompletion
}
)
.focused(focusedField, equals: .textField)
.bind($chat.focusedField, to: focusedField)
Expand Down Expand Up @@ -800,11 +796,7 @@ struct ChatPanelInputArea: View {
if !chat.isAgentMode {
promptTemplates = await SharedChatService.shared.loadChatTemplates() ?? []
}

guard !promptTemplates.isEmpty else {
return [releaseNotesTemplate]
}


let templates = promptTemplates + [releaseNotesTemplate]
let skippedTemplates = [ "feedback", "help" ]

Expand All @@ -831,29 +823,6 @@ struct ChatPanelInputArea: View {
return chatAgents.filter { $0.slug.hasPrefix(prefix) && includedAgents.contains($0.slug) }
}

func chatAutoCompletion(text: String, proposed: [String], range: NSRange) -> [String] {
guard text.count == 1 else { return [] }
let plugins = [String]() // chat.pluginIdentifiers.map { "/\($0)" }
let availableFeatures = plugins + [
// "/exit",
"@code",
"@sense",
"@project",
"@web",
]

let result: [String] = availableFeatures
.filter { $0.hasPrefix(text) && $0 != text }
.compactMap {
guard let index = $0.index(
$0.startIndex,
offsetBy: range.location,
limitedBy: $0.endIndex
) else { return nil }
return String($0[index...])
}
return result
}
func subscribeToActiveDocumentChangeEvent() {
Publishers.CombineLatest(
XcodeInspector.shared.$latestActiveXcode,
Expand Down
Loading