Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 50 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Contributing

Thanks for helping improve **GitHub Copilot for Xcode**.

## Before you start

- Read [README.md](./README.md) for product-level setup.
- Read [DEVELOPMENT.md](./DEVELOPMENT.md) for architecture notes and local build details.
- Use the existing documentation and test patterns already present in the repository.

## Development workflow

1. Install the dependencies required by the Xcode workspace, including Node and `npm` as described in [DEVELOPMENT.md](./DEVELOPMENT.md).
2. Open `Copilot for Xcode.xcworkspace` in Xcode.
3. Build the `Copilot for Xcode` scheme or use `Script/localbuild-app.sh` for a local archive.
4. When changing extension behavior, test the app, `ExtensionService`, `CommunicationBridge`, and `EditorExtension` targets together.

## Testing

- Run the relevant Xcode tests before sending a change.
- Add new tests to `TestPlan.xctestplan` when you create a new unit test target entry.
- Keep changes small and validate only the areas you touched when possible.

## Documentation expectations

Update documentation when you change:

- installation or permission flows
- authentication or BYOK setup
- Xcode menu locations or feature behavior
- development or testing procedures

## Code style

- Follow the existing Swift style in the repository.
- Use SwiftFormat settings from `.swiftformat`.
- Prefer focused doc comments for public or cross-module types when behavior is not obvious.

## Security and secrets

- Never commit API keys, provider tokens, or other credentials.
- Keep BYOK secrets in the app configuration flow only.
- Review changes that touch permissions, auth, or file access carefully.

## Pull request checklist

- Build or test the affected targets.
- Update docs if the user or developer workflow changed.
- Keep the scope minimal and avoid unrelated refactors.
- Include troubleshooting details when fixing Xcode-specific behavior.
8 changes: 7 additions & 1 deletion Core/Sources/ChatService/ChatService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import GitHelper
import LanguageServerProtocol
import SuggestionBasic

/// Common chat operations shared by workspace tabs and standalone chat surfaces.
public protocol ChatServiceType {
var memory: ContextAwareAutoManagedChatMemory { get set }
func send(
Expand All @@ -41,6 +42,7 @@ public protocol ChatServiceType {
func copyCode(_ id: String) async
}

/// Tracks a pending client-side tool call so the service can route the eventual response back correctly.
struct ToolCallRequest {
let requestId: JSONId
let turnId: String
Expand All @@ -49,6 +51,7 @@ struct ToolCallRequest {
let completion: (AnyJSONRPCResponse) -> Void
}

/// Stores parent-child relationships between turns created by subagents or tool confirmation flows.
struct ConversationTurnTrackingState {
var turnParentMap: [String: String] = [:] // Maps subturn ID to parent turn ID
var validConversationIds: Set<String> = [] // Tracks all valid conversation IDs including subagents
Expand All @@ -59,8 +62,9 @@ struct ConversationTurnTrackingState {
}
}

/// Owns chat history, Copilot conversation state, and tool-call coordination for a single chat tab.
public final class ChatService: ChatServiceType, ObservableObject {

public var memory: ContextAwareAutoManagedChatMemory
@Published public internal(set) var chatHistory: [ChatMessage] = []
@Published public internal(set) var isReceivingMessage = false
Expand Down Expand Up @@ -115,6 +119,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
chatTabInfo.isSelected = tabInfo.isSelected
}

/// Keeps published chat history in sync with the backing memory store and progress events.
private func subscribeToNotifications() {
memory.observeHistoryChange { [weak self] in
Task { [weak self] in
Expand All @@ -136,6 +141,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
}.store(in: &cancellables)
}

/// Listens for context lookups so Copilot requests can pull in editor and workspace state on demand.
private func subscribeToConversationContextRequest() {
self.conversationContextHandler.onConversationContext.sink(receiveValue: { [weak self] (request, completion) in
guard let skills = self?.skillSet, !skills.isEmpty, request.params!.conversationId == self?.conversationId else { return }
Expand Down
12 changes: 8 additions & 4 deletions Core/Sources/Service/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import ChatService
import Persist
import PersistMiddleware

/// Serializes work that must stay coordinated with the long-lived extension service.
@globalActor public enum ServiceActor {
public actor TheActor {}
public static let shared = TheActor()
Expand Down Expand Up @@ -96,6 +97,7 @@ public final class Service {
Logger.telemetryLogger = TelemetryLogger()
}

/// Starts the host-side controllers that power the menu bar UI, suggestions, and workspace sync.
@MainActor
public func start() {
scheduledCleaner.start()
Expand Down Expand Up @@ -161,6 +163,7 @@ public final class Service {
}
}

/// Stops long-running integrations so the app can exit without leaving helper processes behind.
@MainActor
public func prepareForExit() async {
Logger.service.info("Prepare for exit.")
Expand Down Expand Up @@ -193,7 +196,7 @@ public extension Service {

// internal extension
extension Service {

/// Switches the active chat workspace after both the workspace URL and auth state are known.
func onNewActiveWorkspaceURLOrAuthStatus(newURL: URL?, newStatus: AuthStatus) {
Task { @MainActor in
// check path
Expand All @@ -212,10 +215,11 @@ extension Service {
}
}

/// Updates chat state to the selected workspace and restores any persisted conversation data.
///
/// - Parameters:
/// - workspaceURL: The active workspace URL that need switch to
/// - path: Path of the workspace URL
/// - username: Curent github username
/// - workspaceURL: The active workspace URL to switch to.
/// - username: The current signed-in GitHub username.
@MainActor
func doSwitchWorkspace(workspaceURL: URL, username: String) async {
// get workspace display name
Expand Down
2 changes: 2 additions & 0 deletions Core/Sources/Service/XPCService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import GitHubCopilotViewModel
import Workspace
import ConversationServiceProvider

/// Bridges Xcode extension requests into the long-running service process.
public class XPCService: NSObject, XPCServiceProtocol {
// MARK: - Service

Expand Down Expand Up @@ -51,6 +52,7 @@ public class XPCService: NSObject, XPCServiceProtocol {

// MARK: - Suggestion

/// Decodes editor state, executes a suggestion command, and replies with the updated buffer content.
@discardableResult
private func replyWithUpdatedContent(
editorContent: Data,
Expand Down
11 changes: 11 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ To build the application locally, follow these steps:

2. After successful build, the application will be available in the build directory. Copy `GitHub Copilot for Xcode.app` to your Applications folder to test it locally.

## Debugging the Local Language Server

To debug against a local checkout of the Visual Studio Code Copilot language server, create `/home/runner/work/CopilotForXcode/CopilotForXcode/Config.local.xcconfig` and set:

```xcconfig
LANGUAGE_SERVER_PATH = /absolute/path/to/your/copilot-language-server/repo
NODE_PATH = /absolute/path/to/node
```

`NODE_PATH` is optional. When it is omitted, debug builds now fall back to `node` from your shell `PATH`.

## SwiftUI Previews

Looks like SwiftUI Previews are not very happy with Objective-C packages when running with app targets. To use previews, please switch schemes to the package product targets.
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
# <img align="center" height="70" src="./Docs/Images/AppIcon.png"/> GitHub Copilot for Xcode

[![Build status](https://github.com/Pjrich1313/CopilotForXcode/actions/workflows/codeql.yml/badge.svg)](https://github.com/Pjrich1313/CopilotForXcode/actions/workflows/codeql.yml)
[![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-Repository-blue?logo=githubactions&logoColor=white)](https://github.com/Pjrich1313/CopilotForXcode/actions)
[![Version](https://img.shields.io/github/v/release/Pjrich1313/CopilotForXcode?display_name=tag)](https://github.com/Pjrich1313/CopilotForXcode/releases)

[GitHub Copilot](https://github.com/features/copilot) for Xcode is the leading AI coding assistant for Swift, Objective-C and iOS/macOS development. It delivers intelligent Completions, Chat, and Code Review—plus advanced features like Agent Mode, Next Edit Suggestions, MCP Registry, and Copilot Vision to make Xcode development faster and smarter.

## Quick Start

1. Install the app with [Homebrew](https://brew.sh/) or from the latest DMG by following the [installation guide](./docs/INSTALLATION.md).
2. Launch `GitHub Copilot for Xcode`, then grant the required `Background`, `Accessibility`, and `Xcode Source Editor Extension` permissions.
3. Sign in with the GitHub account that has your GitHub Copilot access, then complete any optional BYOK provider setup in [configuration](./docs/CONFIGURATION.md).
4. Open Xcode and use `Editor > GitHub Copilot` to start Chat, Agent Mode, Code Review, or inline completions.
5. Keep the [feature guide](./docs/FEATURES.md), [examples](./docs/EXAMPLES.md), and [troubleshooting guide](./TROUBLESHOOTING.md) nearby while you get set up.

## Chat

GitHub Copilot Chat provides suggestions to your specific coding tasks via chat.
Expand All @@ -27,8 +39,20 @@ You can receive auto-complete type suggestions from GitHub Copilot either by sta
## Requirements

- macOS 12+
- Xcode 8+
- A GitHub account
- A version of Xcode that supports Source Editor Extensions
- A GitHub account with GitHub Copilot access, or provider credentials for BYOK model usage

## Documentation

- [Installation guide](./docs/INSTALLATION.md)
- [Configuration and authentication](./docs/CONFIGURATION.md)
- [Feature guide](./docs/FEATURES.md)
- [Common Xcode scenarios and examples](./docs/EXAMPLES.md)
- [Bring Your Own Key (BYOK)](./Docs/BYOK.md)
- [Custom instructions](./Docs/CustomInstructions.md)
- [Prompt files](./Docs/PromptFiles.md)
- [Troubleshooting](./TROUBLESHOOTING.md)
- [Contributing](./CONTRIBUTING.md)

## Getting Started

Expand Down Expand Up @@ -87,7 +111,7 @@ You can receive auto-complete type suggestions from GitHub Copilot either by sta
Keyboard shortcuts can be set for all menu items in the `Key Bindings`
section of Xcode preferences.

1. To sign into GitHub Copilot, click the `Sign in` button in the settings application. This will open a browser window and copy a code to the clipboard. Paste the code into the GitHub login page and authorize the application.
1. To sign into GitHub Copilot, click the `Sign in` button in the settings application. This will open a browser window and copy a code to the clipboard. Paste the code into the GitHub login page, authorize the application, and complete the flow with the GitHub account that has your Copilot access.
<p align="center">
<img alt="Screenshot of sign-in popup" src="./Docs/Images/device-code.png" width="372" />
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ public extension Notification.Name {
.Name("com.github.CopilotForXcode.GithubCopilotAgentTrustToolAnnotationsDidChange")
}

private func resolvedInfoDictionaryString(_ key: String) -> String? {
guard let value = Bundle.main.infoDictionary?[key] as? String else {
return nil
}

let trimmedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedValue.isEmpty, !(trimmedValue.hasPrefix("$(") && trimmedValue.hasSuffix(")")) else {
return nil
}

return trimmedValue
}

public class GitHubCopilotBaseService {
let projectRootURL: URL
var server: GitHubCopilotLSP
Expand Down Expand Up @@ -219,11 +232,11 @@ public class GitHubCopilotBaseService {

#if DEBUG
// Use local language server if set and available
if let languageServerPath = Bundle.main.infoDictionary?["LANGUAGE_SERVER_PATH"] as? String {
if let languageServerPath = resolvedInfoDictionaryString("LANGUAGE_SERVER_PATH") {
let jsPath = URL(fileURLWithPath: NSString(string: languageServerPath).expandingTildeInPath)
.appendingPathComponent("dist")
.appendingPathComponent("language-server.js")
let nodePath = Bundle.main.infoDictionary?["NODE_PATH"] as? String ?? "node"
let nodePath = resolvedInfoDictionaryString("NODE_PATH") ?? "node"
if FileManager.default.fileExists(atPath: jsPath.path) {
path = "/usr/bin/env"
if projectRootURL.path == "/" {
Expand Down
6 changes: 5 additions & 1 deletion Tool/Sources/Workspace/WorkspacePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ public extension DependencyValues {
}
}

/// Serializes workspace creation and mutation so file-backed state stays consistent across Xcode events.
@globalActor public enum WorkspaceActor {
public actor TheActor {}
public static let shared = TheActor()
}

/// Tracks open workspaces and attaches per-workspace plugins used by suggestions and chat features.
public class WorkspacePool {
public enum Error: Swift.Error, LocalizedError {
case invalidWorkspaceURL(URL)
Expand All @@ -42,6 +44,7 @@ public class WorkspacePool {
self.plugins = plugins
}

/// Registers a workspace plugin and attaches it to both current and future workspaces.
public func registerPlugin<Plugin: WorkspacePlugin>(_ plugin: @escaping (Workspace) -> Plugin) {
let id = ObjectIdentifier(Plugin.self)
let erasedPlugin: (Workspace) -> WorkspacePlugin = { plugin($0) }
Expand Down Expand Up @@ -87,6 +90,7 @@ public class WorkspacePool {
return workspace.flatMap { ws in filespace.map { fs in (ws, fs) } }
}

/// Returns the existing workspace for a URL or creates a new one when Xcode opens a project for the first time.
@WorkspaceActor
public func fetchOrCreateWorkspace(workspaceURL: URL) async throws -> Workspace {
guard workspaceURL != URL(fileURLWithPath: "/") else {
Expand All @@ -102,6 +106,7 @@ public class WorkspacePool {
return new
}

/// Resolves the active workspace/filespace pair for an editor file, creating missing state as needed.
@WorkspaceActor
public func fetchOrCreateWorkspaceAndFilespace(fileURL: URL) async throws
-> (workspace: Workspace, filespace: Filespace)
Expand Down Expand Up @@ -187,4 +192,3 @@ extension WorkspacePool {
return new
}
}

1 change: 1 addition & 0 deletions Tool/Sources/XPCShared/XPCServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Status
import SuggestionBasic

@objc(XPCServiceProtocol)
/// Public XPC surface used by the editor extension and host app to communicate with the service process.
public protocol XPCServiceProtocol {
func getSuggestedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void)
func getNextSuggestedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void)
Expand Down
Loading