Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ These guides cover the capabilities you can add to your Copilot SDK application.
| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) |
| [Steering & Queueing](./steering-and-queueing.md) | Control message delivery — immediate steering vs. sequential queueing |
| [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage |
| [Remote Sessions](./remote-sessions.md) | Share sessions to GitHub web and mobile via Mission Control |

## Related

Expand Down
163 changes: 163 additions & 0 deletions docs/features/remote-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Remote Sessions

Remote sessions let users access their Copilot session from GitHub web and mobile via [Mission Control](https://github.com). When enabled, the SDK connects each session to Mission Control, producing a URL that can be shared as a link or QR code.

## Prerequisites

- The user must be authenticated (GitHub token or logged-in user)
- The session's working directory must be a GitHub repository

## Enabling Remote Sessions

### Always-on (client-level)

Set `remote: true` when creating the client. Every session in a GitHub repo automatically gets a remote URL.

<!-- tabs:start -->

#### **TypeScript**

<!-- docs-validate: skip -->
```typescript
import { CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient({ remote: true });
const session = await client.createSession({
workingDirectory: "/path/to/github-repo",
onPermissionRequest: async () => ({ allowed: true }),
});

session.on("session.info", (event) => {
if (event.data.infoType === "remote") {
console.log("Remote URL:", event.data.url);
}
});
```

#### **Python**

<!-- docs-validate: skip -->
```python
from copilot import CopilotClient, SubprocessConfig

client = CopilotClient(SubprocessConfig(remote=True))
session = await client.create_session(
working_directory="/path/to/github-repo",
on_permission_request=lambda req: {"allowed": True},
)

def on_event(event):
if event.type == "session.info" and event.data.info_type == "remote":
print(f"Remote URL: {event.data.url}")

session.on(on_event)
```

#### **Go**

<!-- docs-validate: skip -->
```go
client, _ := copilot.NewClient(&copilot.ClientOptions{Remote: true})
session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
WorkingDirectory: "/path/to/github-repo",
OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
},
})

session.On(func(event copilot.SessionEvent) {
if event.Type == "session.info" {
// Check infoType and extract URL
}
})
```

#### **C#**

<!-- docs-validate: skip -->
```csharp
var client = new CopilotClient(new CopilotClientOptions { Remote = true });
var session = await client.CreateSessionAsync(new SessionConfig
{
WorkingDirectory = "/path/to/github-repo",
OnPermissionRequest = (req, inv) =>
Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
});

session.On((SessionEvent e) =>
{
if (e is SessionInfoEvent info && info.Data.InfoType == "remote")
{
Console.WriteLine($"Remote URL: {info.Data.Url}");
}
});
```

<!-- tabs:end -->

### On-demand (per-session toggle)

Use `session.rpc.remote.enable()` to start remote access mid-session, and `session.rpc.remote.disable()` to stop it. This is equivalent to the CLI's `/remote on` and `/remote off` commands.

<!-- tabs:start -->

#### **TypeScript**

<!-- docs-validate: skip -->
```typescript
const result = await session.rpc.remote.enable();
console.log("Remote URL:", result.url);

// Later: stop sharing
await session.rpc.remote.disable();
```

#### **Python**

<!-- docs-validate: skip -->
```python
result = await session.rpc.remote.enable()
print(f"Remote URL: {result.url}")

# Later: stop sharing
await session.rpc.remote.disable()
```

#### **Go**

<!-- docs-validate: skip -->
```go
result, err := session.RPC.Remote.Enable(ctx)
fmt.Println("Remote URL:", *result.URL)
Comment on lines +129 to +131

// Later: stop sharing
err = session.RPC.Remote.Disable(ctx)
```

#### **C#**

<!-- docs-validate: skip -->
```csharp
var result = await session.Rpc.Remote.EnableAsync();
Console.WriteLine($"Remote URL: {result.Url}");

// Later: stop sharing
await session.Rpc.Remote.DisableAsync();
```

<!-- tabs:end -->

## QR Code Generation

The remote URL can be rendered as a QR code for easy mobile access. The SDK provides the URL — use your preferred QR code library:

- **TypeScript**: [qrcode](https://www.npmjs.com/package/qrcode)
- **Python**: [qrcode](https://pypi.org/project/qrcode/)
- **Go**: [go-qrcode](https://github.com/skip2/go-qrcode)
- **C#**: [QRCoder](https://www.nuget.org/packages/QRCoder)

## Notes

- The `remote` client option only applies when the SDK spawns the CLI process. It is ignored when connecting to an external server via `cliUrl`.
- If the working directory is not a GitHub repository, remote setup is silently skipped (always-on mode) or returns an error (on-demand mode).
- Remote sessions require authentication. Ensure `gitHubToken` or `useLoggedInUser` is configured.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Guides for building with the SDK's capabilities.
- [Streaming Events](./features/streaming-events.md) — real-time event reference
- [Steering & Queueing](./features/steering-and-queueing.md) — message delivery modes
- [Session Persistence](./features/session-persistence.md) — resume sessions across restarts
- [Remote Sessions](./features/remote-sessions.md) — share sessions to GitHub web and mobile

### [Hooks Reference](./hooks/index.md)

Expand Down
6 changes: 6 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,11 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
args.AddRange(["--session-idle-timeout", options.SessionIdleTimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture)]);
}

if (options.Remote)
{
args.Add("--remote");
}
Comment on lines +1228 to +1231

var (fileName, processArgs) = ResolveCliCommand(cliPath, args);

var startInfo = new ProcessStartInfo
Expand Down Expand Up @@ -1261,6 +1266,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
startInfo.Environment["COPILOT_CONNECTION_TOKEN"] = connectionToken;
}

// Set COPILOT_HOME if configured
if (!string.IsNullOrEmpty(options.CopilotHome))
{
startInfo.Environment["COPILOT_HOME"] = options.CopilotHome;
Expand Down
64 changes: 63 additions & 1 deletion dotnet/src/Generated/Rpc.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ protected CopilotClientOptions(CopilotClientOptions? other)
SessionFs = other.SessionFs;
SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds;
TcpConnectionToken = other.TcpConnectionToken;
Remote = other.Remote;
}

/// <summary>
Expand Down Expand Up @@ -195,6 +196,15 @@ public string? GithubToken
/// </summary>
public string? TcpConnectionToken { get; set; }

/// <summary>
/// Enable remote session support (Mission Control integration).
/// When true, sessions in a GitHub repository working directory are
/// accessible from GitHub web and mobile.
/// This option is only used when the SDK spawns the CLI process; it is ignored
/// when connecting to an external server via <see cref="CliUrl"/>.
/// </summary>
public bool Remote { get; set; }

/// <summary>
/// Creates a shallow clone of this <see cref="CopilotClientOptions"/> instance.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ func NewClient(options *ClientOptions) *Client {
opts.CopilotHome = options.CopilotHome
}
opts.SessionIdleTimeoutSeconds = options.SessionIdleTimeoutSeconds
opts.Remote = options.Remote
}

// Default Env to current environment if not set
Expand Down Expand Up @@ -1460,6 +1461,10 @@ func (c *Client) startCLIServer(ctx context.Context) error {
args = append(args, "--session-idle-timeout", strconv.Itoa(c.options.SessionIdleTimeoutSeconds))
}

if c.options.Remote {
args = append(args, "--remote")
Comment on lines +1464 to +1465
}

// If CLIPath is a .js file, run it with node
// Note we can't rely on the shebang as Windows doesn't support it
command := cliPath
Expand Down Expand Up @@ -1491,6 +1496,11 @@ func (c *Client) startCLIServer(ctx context.Context) error {
c.process.Env = setEnvValue(c.process.Env, "COPILOT_HOME", c.options.CopilotHome)
}

// Set COPILOT_HOME if configured
if c.options.CopilotHome != "" {
c.process.Env = append(c.process.Env, "COPILOT_HOME="+c.options.CopilotHome)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: COPILOT_HOME is set twice in the same environment array

This PR adds a second block to set COPILOT_HOME, but the pre-existing code at line 1496 already handles it via setEnvValue:

// Already present (line ~1494-1497):
if c.options.CopilotHome != "" {
    c.process.Env = setEnvValue(c.process.Env, "COPILOT_HOME", c.options.CopilotHome)
}

// This PR adds (redundant + incorrect):
// Set COPILOT_HOME if configured
if c.options.CopilotHome != "" {
    c.process.Env = append(c.process.Env, "COPILOT_HOME="+c.options.CopilotHome)  // ← duplicate!
}

setEnvValue replaces an existing entry if it's already in the slice; append just adds another entry. The result is COPILOT_HOME appearing twice in the spawned process's environment. On most systems the first value wins, so the second assignment has no effect — but it's still incorrect and confusing.

Suggestion: Remove the newly added block entirely. The existing setEnvValue call at line 1496 is sufficient and was already there before this PR.

}

if c.options.Telemetry != nil {
t := c.options.Telemetry
c.process.Env = setEnvValue(c.process.Env, "COPILOT_OTEL_ENABLED", "true")
Expand Down
Loading
Loading