Skip to content
Merged
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
203 changes: 203 additions & 0 deletions docs/features/remote-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# 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}");
}
});
```

#### **Rust**

<!-- docs-validate: skip -->
```rust
use copilot_sdk::{Client, ClientOptions};

let client = Client::start(
ClientOptions::new().with_remote(true)
).await?;
let session = client.create_session(
SessionConfig::new("/path/to/github-repo")
.with_permission_handler(|_req, _inv| async {
Ok(PermissionRequestResult::approved())
}),
).await?;

let mut events = session.subscribe();
while let Ok(event) = events.recv().await {
if event.event_type == "session.info" {
// Check info_type and extract 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)
if result.URL != nil {
fmt.Println("Remote URL:", *result.URL)
}

// 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();
```

#### **Rust**

<!-- docs-validate: skip -->
```rust
let result = session.rpc().remote().enable().await?;
if let Some(url) = &result.url {
println!("Remote URL: {url}");
}

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

<!-- 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)
- **Rust**: [qrcode](https://crates.io/crates/qrcode)

## 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
5 changes: 5 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,11 @@ private static bool IsUnsupportedConnectMethod(RemoteRpcException ex)
args.AddRange(["--session-idle-timeout", options.SessionIdleTimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture)]);
}

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

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

var startInfo = new ProcessStartInfo
Expand Down
66 changes: 66 additions & 0 deletions 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
2 changes: 2 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
GitHubToken = "ghp_test",
UseLoggedInUser = false,
CopilotHome = "/custom/copilot/home",
Remote = true,
SessionIdleTimeoutSeconds = 600,
};

Expand All @@ -45,6 +46,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
Assert.Equal(original.GitHubToken, clone.GitHubToken);
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
Assert.Equal(original.CopilotHome, clone.CopilotHome);
Assert.Equal(original.Remote, clone.Remote);
Assert.Equal(original.SessionIdleTimeoutSeconds, clone.SessionIdleTimeoutSeconds);
}

Expand Down
5 changes: 5 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
Loading
Loading