From 21b058a5c229f2c4b8536b37c59f4966893996fb Mon Sep 17 00:00:00 2001 From: Baptiste Tessiau Date: Sat, 7 Mar 2026 23:23:19 +0100 Subject: [PATCH 01/11] feat: GitHub Codespace integration with SSH tunnel support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ability to connect PolyPilot to GitHub Codespaces, creating remote Copilot sessions that run inside codespace environments. Core architecture: - CodespaceService (split into 3 partial files <326 lines each): SSH tunnel management, gh CLI lifecycle, dotfiles diagnostics - CopilotService.Codespace.cs (785 lines): group creation, health check loop with 30s interval, auto-reconnect, MaxConsecutiveFailures=5 backoff - Two connection strategies: direct SSH tunnel or SSH+port-forward fallback UI features: - ☁️ Codespace chip in New Session form lists all codespaces with state badges - 'Start & Add' boots stopped codespaces and adds them to sidebar - Codespace groups show connection state (●green/●yellow/●red/⏳starting) - Group ⋯ menu: New Session, Open in Browser, Stop Codespace, Start, Retry - Session ⋯ menu respects VS Code Insiders preference via --insiders flag - Copilot Console hidden for codespace sessions (can't resume remotely) - Sessions named 'Main', 'Main 2', etc. (not timestamps) Safety: - Codespace sessions bound to their group (can't be moved) - Health check backoff: 5 consecutive failures → SetupRequired state - Graceful degradation: no SSH → SetupRequired with browser link - Delete group cleans up sessions, tunnels, and client connections - App restart restores groups in Reconnecting state; health check reconnects SDK upgrade: 0.1.30 → 0.1.31 (resolves protocol v2/v3 mismatch) API change: PermissionRequestResult.Kind string → PermissionRequestResultKind.Approved enum Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Components/Layout/CreateSessionForm.razor | 198 ++++- .../Layout/CreateSessionForm.razor.css | 115 ++- .../Components/Layout/SessionListItem.razor | 50 +- .../Layout/SessionListItem.razor.css | 6 + .../Components/Layout/SessionSidebar.razor | 356 +++++++- .../Layout/SessionSidebar.razor.css | 72 ++ PolyPilot/Components/Pages/Settings.razor.css | 1 + PolyPilot/MauiProgram.cs | 1 + PolyPilot/Models/SessionOrganization.cs | 63 ++ PolyPilot/PolyPilot.csproj | 2 +- .../Services/CodespaceService.Diagnostics.cs | 141 ++++ .../Services/CodespaceService.Lifecycle.cs | 206 +++++ PolyPilot/Services/CodespaceService.cs | 326 ++++++++ PolyPilot/Services/CopilotService.Bridge.cs | 30 +- .../Services/CopilotService.Codespace.cs | 784 ++++++++++++++++++ .../Services/CopilotService.Organization.cs | 21 +- .../Services/CopilotService.Persistence.cs | 37 +- PolyPilot/Services/CopilotService.cs | 152 +++- PolyPilot/relaunch.sh | 2 +- 19 files changed, 2521 insertions(+), 42 deletions(-) create mode 100644 PolyPilot/Services/CodespaceService.Diagnostics.cs create mode 100644 PolyPilot/Services/CodespaceService.Lifecycle.cs create mode 100644 PolyPilot/Services/CodespaceService.cs create mode 100644 PolyPilot/Services/CopilotService.Codespace.cs diff --git a/PolyPilot/Components/Layout/CreateSessionForm.razor b/PolyPilot/Components/Layout/CreateSessionForm.razor index b7c65dd61b..b1dd1100c2 100644 --- a/PolyPilot/Components/Layout/CreateSessionForm.razor +++ b/PolyPilot/Components/Layout/CreateSessionForm.razor @@ -3,6 +3,7 @@ @inject IJSRuntime JS @inject RepoManager RepoManager @inject CopilotService CopilotService +@inject CodespaceService CodespaceService @if (!isExpanded) { @@ -12,12 +13,19 @@ else {
@* Mode chips — shown only when repos exist *@ - @if (RepoManager.Repositories.Count > 0) + @if (RepoManager.Repositories.Count > 0 || PlatformHelper.IsDesktop) {
- + @if (RepoManager.Repositories.Count > 0) + { + + } + @if (PlatformHelper.IsDesktop) + { + + }
} @@ -86,7 +94,70 @@ else
} + else if (mode == FormMode.Codespace) + { + @if (isLoadingCodespaces) + { +
⏳ Discovering codespaces...
+ } + else if (codespaces.Count == 0) + { +
No codespaces found.
+ + } + else + { +
+ @foreach (var cs in codespaces) + { + var csName = cs.Name; + var alreadyAdded = CopilotService.Organization.Groups.Any(g => g.CodespaceName == csName); + var isConnecting = connectingCodespace == csName; + var isStopped = cs.State != "Available"; +
+
+ @cs.Repository + @cs.Name + @{ + var existingGroup = CopilotService.Organization.Groups.FirstOrDefault(g => g.CodespaceName == csName); + var sshKnown = existingGroup?.SshAvailable; + } + ● @cs.State@(sshKnown == false ? " · No SSH" : "") +
+ @if (alreadyAdded) + { + ✓ Added + } + else if (cs.State == "Available") + { + + } + else + { + + } +
+ @if (isConnecting && !string.IsNullOrEmpty(connectingStatus)) + { +
@connectingStatus
+ } + } +
+ + } + @if (!string.IsNullOrEmpty(codespaceError)) + { +
+ ⚠ @codespaceError + +
+ } + } + @if (mode != FormMode.Codespace) + {