Skip to content

Commit 06bfc5d

Browse files
authored
feat: add remote session support across all SDKs (#1192)
1 parent 23b1bb8 commit 06bfc5d

22 files changed

Lines changed: 622 additions & 60 deletions

File tree

docs/features/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ These guides cover the capabilities you can add to your Copilot SDK application.
1717
| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) |
1818
| [Steering & Queueing](./steering-and-queueing.md) | Control message delivery — immediate steering vs. sequential queueing |
1919
| [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage |
20+
| [Remote Sessions](./remote-sessions.md) | Share sessions to GitHub web and mobile via Mission Control |
2021

2122
## Related
2223

docs/features/remote-sessions.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Remote Sessions
2+
3+
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.
4+
5+
## Prerequisites
6+
7+
- The user must be authenticated (GitHub token or logged-in user)
8+
- The session's working directory must be a GitHub repository
9+
10+
## Enabling Remote Sessions
11+
12+
### Always-on (client-level)
13+
14+
Set `remote: true` when creating the client. Every session in a GitHub repo automatically gets a remote URL.
15+
16+
<!-- tabs:start -->
17+
18+
#### **TypeScript**
19+
20+
<!-- docs-validate: skip -->
21+
```typescript
22+
import { CopilotClient } from "@github/copilot-sdk";
23+
24+
const client = new CopilotClient({ remote: true });
25+
const session = await client.createSession({
26+
workingDirectory: "/path/to/github-repo",
27+
onPermissionRequest: async () => ({ allowed: true }),
28+
});
29+
30+
session.on("session.info", (event) => {
31+
if (event.data.infoType === "remote") {
32+
console.log("Remote URL:", event.data.url);
33+
}
34+
});
35+
```
36+
37+
#### **Python**
38+
39+
<!-- docs-validate: skip -->
40+
```python
41+
from copilot import CopilotClient, SubprocessConfig
42+
43+
client = CopilotClient(SubprocessConfig(remote=True))
44+
session = await client.create_session(
45+
working_directory="/path/to/github-repo",
46+
on_permission_request=lambda req: {"allowed": True},
47+
)
48+
49+
def on_event(event):
50+
if event.type == "session.info" and event.data.info_type == "remote":
51+
print(f"Remote URL: {event.data.url}")
52+
53+
session.on(on_event)
54+
```
55+
56+
#### **Go**
57+
58+
<!-- docs-validate: skip -->
59+
```go
60+
client, _ := copilot.NewClient(&copilot.ClientOptions{Remote: true})
61+
session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
62+
WorkingDirectory: "/path/to/github-repo",
63+
OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
64+
return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
65+
},
66+
})
67+
68+
session.On(func(event copilot.SessionEvent) {
69+
if event.Type == "session.info" {
70+
// Check infoType and extract URL
71+
}
72+
})
73+
```
74+
75+
#### **C#**
76+
77+
<!-- docs-validate: skip -->
78+
```csharp
79+
var client = new CopilotClient(new CopilotClientOptions { Remote = true });
80+
var session = await client.CreateSessionAsync(new SessionConfig
81+
{
82+
WorkingDirectory = "/path/to/github-repo",
83+
OnPermissionRequest = (req, inv) =>
84+
Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
85+
});
86+
87+
session.On((SessionEvent e) =>
88+
{
89+
if (e is SessionInfoEvent info && info.Data.InfoType == "remote")
90+
{
91+
Console.WriteLine($"Remote URL: {info.Data.Url}");
92+
}
93+
});
94+
```
95+
96+
#### **Rust**
97+
98+
<!-- docs-validate: skip -->
99+
```rust
100+
use copilot_sdk::{Client, ClientOptions};
101+
102+
let client = Client::start(
103+
ClientOptions::new().with_remote(true)
104+
).await?;
105+
let session = client.create_session(
106+
SessionConfig::new("/path/to/github-repo")
107+
.with_permission_handler(|_req, _inv| async {
108+
Ok(PermissionRequestResult::approved())
109+
}),
110+
).await?;
111+
112+
let mut events = session.subscribe();
113+
while let Ok(event) = events.recv().await {
114+
if event.event_type == "session.info" {
115+
// Check info_type and extract URL
116+
}
117+
}
118+
```
119+
120+
<!-- tabs:end -->
121+
122+
### On-demand (per-session toggle)
123+
124+
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.
125+
126+
<!-- tabs:start -->
127+
128+
#### **TypeScript**
129+
130+
<!-- docs-validate: skip -->
131+
```typescript
132+
const result = await session.rpc.remote.enable();
133+
console.log("Remote URL:", result.url);
134+
135+
// Later: stop sharing
136+
await session.rpc.remote.disable();
137+
```
138+
139+
#### **Python**
140+
141+
<!-- docs-validate: skip -->
142+
```python
143+
result = await session.rpc.remote.enable()
144+
print(f"Remote URL: {result.url}")
145+
146+
# Later: stop sharing
147+
await session.rpc.remote.disable()
148+
```
149+
150+
#### **Go**
151+
152+
<!-- docs-validate: skip -->
153+
```go
154+
result, err := session.RPC.Remote.Enable(ctx)
155+
if result.URL != nil {
156+
fmt.Println("Remote URL:", *result.URL)
157+
}
158+
159+
// Later: stop sharing
160+
err = session.RPC.Remote.Disable(ctx)
161+
```
162+
163+
#### **C#**
164+
165+
<!-- docs-validate: skip -->
166+
```csharp
167+
var result = await session.Rpc.Remote.EnableAsync();
168+
Console.WriteLine($"Remote URL: {result.Url}");
169+
170+
// Later: stop sharing
171+
await session.Rpc.Remote.DisableAsync();
172+
```
173+
174+
#### **Rust**
175+
176+
<!-- docs-validate: skip -->
177+
```rust
178+
let result = session.rpc().remote().enable().await?;
179+
if let Some(url) = &result.url {
180+
println!("Remote URL: {url}");
181+
}
182+
183+
// Later: stop sharing
184+
session.rpc().remote().disable().await?;
185+
```
186+
187+
<!-- tabs:end -->
188+
189+
## QR Code Generation
190+
191+
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:
192+
193+
- **TypeScript**: [qrcode](https://www.npmjs.com/package/qrcode)
194+
- **Python**: [qrcode](https://pypi.org/project/qrcode/)
195+
- **Go**: [go-qrcode](https://github.com/skip2/go-qrcode)
196+
- **C#**: [QRCoder](https://www.nuget.org/packages/QRCoder)
197+
- **Rust**: [qrcode](https://crates.io/crates/qrcode)
198+
199+
## Notes
200+
201+
- The `remote` client option only applies when the SDK spawns the CLI process. It is ignored when connecting to an external server via `cliUrl`.
202+
- If the working directory is not a GitHub repository, remote setup is silently skipped (always-on mode) or returns an error (on-demand mode).
203+
- Remote sessions require authentication. Ensure `gitHubToken` or `useLoggedInUser` is configured.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Guides for building with the SDK's capabilities.
4848
- [Streaming Events](./features/streaming-events.md) — real-time event reference
4949
- [Steering & Queueing](./features/steering-and-queueing.md) — message delivery modes
5050
- [Session Persistence](./features/session-persistence.md) — resume sessions across restarts
51+
- [Remote Sessions](./features/remote-sessions.md) — share sessions to GitHub web and mobile
5152

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

dotnet/src/Client.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,11 @@ private static bool IsUnsupportedConnectMethod(RemoteRpcException ex)
13111311
args.AddRange(["--session-idle-timeout", options.SessionIdleTimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture)]);
13121312
}
13131313

1314+
if (options.Remote)
1315+
{
1316+
args.Add("--remote");
1317+
}
1318+
13141319
var (fileName, processArgs) = ResolveCliCommand(cliPath, args);
13151320

13161321
var startInfo = new ProcessStartInfo

dotnet/src/Generated/Rpc.cs

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Types.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ protected CopilotClientOptions(CopilotClientOptions? other)
7272
SessionFs = other.SessionFs;
7373
SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds;
7474
TcpConnectionToken = other.TcpConnectionToken;
75+
Remote = other.Remote;
7576
}
7677

7778
/// <summary>
@@ -195,6 +196,15 @@ public string? GithubToken
195196
/// </summary>
196197
public string? TcpConnectionToken { get; set; }
197198

199+
/// <summary>
200+
/// Enable remote session support (Mission Control integration).
201+
/// When true, sessions in a GitHub repository working directory are
202+
/// accessible from GitHub web and mobile.
203+
/// This option is only used when the SDK spawns the CLI process; it is ignored
204+
/// when connecting to an external server via <see cref="CliUrl"/>.
205+
/// </summary>
206+
public bool Remote { get; set; }
207+
198208
/// <summary>
199209
/// Creates a shallow clone of this <see cref="CopilotClientOptions"/> instance.
200210
/// </summary>

dotnet/test/Unit/CloneTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
2727
GitHubToken = "ghp_test",
2828
UseLoggedInUser = false,
2929
CopilotHome = "/custom/copilot/home",
30+
Remote = true,
3031
SessionIdleTimeoutSeconds = 600,
3132
};
3233

@@ -45,6 +46,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
4546
Assert.Equal(original.GitHubToken, clone.GitHubToken);
4647
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
4748
Assert.Equal(original.CopilotHome, clone.CopilotHome);
49+
Assert.Equal(original.Remote, clone.Remote);
4850
Assert.Equal(original.SessionIdleTimeoutSeconds, clone.SessionIdleTimeoutSeconds);
4951
}
5052

go/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func NewClient(options *ClientOptions) *Client {
235235
opts.CopilotHome = options.CopilotHome
236236
}
237237
opts.SessionIdleTimeoutSeconds = options.SessionIdleTimeoutSeconds
238+
opts.Remote = options.Remote
238239
}
239240

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

1464+
if c.options.Remote {
1465+
args = append(args, "--remote")
1466+
}
1467+
14631468
// If CLIPath is a .js file, run it with node
14641469
// Note we can't rely on the shebang as Windows doesn't support it
14651470
command := cliPath

0 commit comments

Comments
 (0)