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 .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "24"

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install
run: bun install --frozen-lockfile --ignore-scripts

- name: Install desktop type dependencies
run: bun install --cwd desktop --frozen-lockfile --ignore-scripts

- name: Run linter
run: bun run lint:all
Expand All @@ -26,7 +33,7 @@ jobs:
run: bun run typecheck

- name: Run tests
run: bun test
run: bun run test

- name: Build
run: bun run build
run: bun run build
174 changes: 174 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: Desktop

on:
workflow_dispatch:
pull_request:
paths:
- .github/workflows/desktop.yml
- desktop/**
- src/**
- tests/**
- package.json
- bun.lock
- tsconfig.json
- eslint.config.js
push:
branches: [main, master]
paths:
- .github/workflows/desktop.yml
- desktop/**
- src/**
- tests/**
- package.json
- bun.lock
- tsconfig.json
- eslint.config.js

permissions:
contents: read

concurrency:
group: desktop-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
native:
name: Native smoke on ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-15, windows-2022, ubuntu-24.04]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
defaults:
run:
shell: bash
env:
CI: "true"
CSC_IDENTITY_AUTO_DISCOVERY: "false"
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "24"

- name: Put caches and build output in runner temporary storage
run: |
node --input-type=module <<'NODE'
import fs from "node:fs"
import path from "node:path"
const runnerTemp = process.env.RUNNER_TEMP
const root = path.join(runnerTemp, "copilot-bridge")
const environment = {
BUN_INSTALL_CACHE_DIR: path.join(root, "cache", "bun"),
ELECTRON_CACHE: path.join(root, "cache", "electron"),
ELECTRON_BUILDER_CACHE: path.join(root, "cache", "electron-builder"),
XDG_CACHE_HOME: path.join(root, "cache", "xdg"),
npm_config_cache: path.join(root, "cache", "npm"),
TMPDIR: runnerTemp,
TMP: runnerTemp,
TEMP: runnerTemp,
COPILOT_BRIDGE_PACKAGE_OUTPUT: path.join(root, "package"),
}
const envLines = Object.entries(environment).map(([name, value]) => `${name}=${value}`)
fs.appendFileSync(process.env.GITHUB_ENV, envLines.join("\n") + "\n")
const output = path.join(root, "dist")
fs.mkdirSync(output, { recursive: true })
fs.symlinkSync(output, "desktop/dist", process.platform === "win32" ? "junction" : "dir")
NODE

- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.12"

- name: Install root dependencies without Git hooks
run: bun install --frozen-lockfile --ignore-scripts

- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile --ignore-scripts

- name: Install Electron binary
run: node desktop/node_modules/electron/install.js

- name: Install Linux native dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
dbus-x11 libasound2t64 libatk1.0-0t64 libatk-bridge2.0-0t64 \
libcups2t64 libdrm2 libgbm1 libgtk-3-0t64 libnss3 \
libx11-xcb1 libxcomposite1 libxdamage1 libxkbcommon0 libxrandr2 \
libxss1 xauth xvfb

- name: Lint desktop files
run: bun run lint --no-cache desktop

- name: Typecheck proxy
run: bun run typecheck

- name: Test proxy
run: bun test ./tests

- name: Typecheck desktop
run: bun run desktop:typecheck

- name: Test desktop
run: bun run desktop:test

- name: Build native code and renderer
run: bun run desktop:build

- name: Smoke test native app
if: runner.os != 'Linux'
env:
COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/native
run: bun run desktop:smoke -- --default-fixture-profile

- name: Smoke test native app on Linux
if: runner.os == 'Linux'
env:
COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/native
run: dbus-run-session -- xvfb-run -a bun run desktop:smoke -- --default-fixture-profile

- name: Package host architecture and smoke test installed executable
if: runner.os != 'Linux'
env:
COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/packaged
run: node desktop/scripts/package-smoke.ts

- name: Package host architecture and smoke test installed executable on Linux
if: runner.os == 'Linux'
env:
COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/packaged
run: dbus-run-session -- xvfb-run -a node desktop/scripts/package-smoke.ts

- name: Archive unsigned self-contained app
run: |
node --input-type=module <<'NODE'
import { execFileSync } from "node:child_process"
import path from "node:path"
const archive = path.join(process.env.RUNNER_TEMP, "copilot-bridge", "Copilot-Bridge-unsigned.tar.gz")
const tar = process.platform === "win32"
? path.join(process.env.SystemRoot, "System32", "tar.exe")
: "tar"
execFileSync(tar, ["-czf", archive, "-C", process.env.COPILOT_BRIDGE_PACKAGE_OUTPUT, "."], { stdio: "inherit" })
NODE

- name: Upload native evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: copilot-bridge-${{ runner.os }}-${{ runner.arch }}-evidence
path: ${{ runner.temp }}/copilot-bridge/evidence
if-no-files-found: warn
retention-days: 14

- name: Upload unsigned app
uses: actions/upload-artifact@v4
with:
name: copilot-bridge-${{ runner.os }}-${{ runner.arch }}-unsigned
path: ${{ runner.temp }}/copilot-bridge/Copilot-Bridge-unsigned.tar.gz
if-no-files-found: error
retention-days: 14
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ node_modules/

# build output
dist/
/node_modules
/dist
/desktop/node_modules
/desktop/dist
/desktop/release
.gstack/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,7 @@ bun run start
- `--rate-limit <seconds>`: Enforces a minimum time interval between requests. For example, `copilot-proxy-api start --rate-limit 30` will ensure there's at least a 30-second gap between requests.
- `--wait`: Use this with `--rate-limit`. It makes the server wait for the cooldown period to end instead of rejecting the request with an error. This is useful for clients that don't automatically retry on rate limit errors.
- If you have a GitHub business or enterprise plan account with Copilot, use the `--account-type` flag (e.g., `--account-type business`). See the [official documentation](https://docs.github.com/en/enterprise-cloud@latest/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-access-to-github-copilot-in-your-organization/managing-github-copilot-access-to-your-organizations-network#configuring-copilot-subscription-based-network-routing-for-your-enterprise-or-organization) for more details.

## Desktop app

[Copilot Bridge](desktop/README.md) is the separately versioned desktop companion for macOS, Windows, and Linux. It provides GitHub sign-in, a keyed local gateway, models, usage, activity, and setup commands for coding agents. The desktop app starts at version 0.0.1.
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
root = "./tests"
40 changes: 40 additions & 0 deletions desktop/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Desktop architecture

Copilot Bridge gives a Copilot subscriber a local gateway and agent setup in one desktop app. The sandboxed renderer presents safe snapshots. Electron main owns settings, GitHub device login, encrypted persistence, clipboard actions, and the lifetime of one proxy child. That child owns the existing process-wide proxy state and protocol handlers.

The app bundles Electron's Node runtime. Users do not install Bun or Node. The desktop package starts at 0.0.1 and does not change the CLI's release history.

## Runtime choice

Two independent designs compared running Hono inside Electron main with running it in a dedicated child. The child design preserves the current route and service signatures, contains CPU work and failures, and makes a stopped process a reliable boundary for account state. The embedded design required a broad state refactor through the existing CLI and handlers. An independent reviewer selected the child design.

The child runs a desktop-specific HTTP composition around the existing inference routes. It binds to 127.0.0.1, requires a generated local key, rejects browser origins, and never mounts the CLI token or debug routes. It uses the Hono Node adapter because a direct Node experiment found the current srvx adapter can treat normal POST upload completion as cancellation.

## Ownership

| Area | Owner |
| --- | --- |
| Safe commands and snapshots | `shared/contracts.ts` |
| Agent setup text | `shared/connections.ts` |
| Account and desired gateway state | `main/controller.ts` |
| GitHub device flow | Native account code |
| Persistent secrets | Native vault using Electron safeStorage |
| Proxy singleton and response streams | Dedicated child |
| Native window, IPC, tray, and quit | Electron bootstrap |
| Presentation and local filters | React renderer |

No renderer API accepts an arbitrary URL, file path, executable, or IPC channel. Upstream credentials do not enter snapshots. Local-key copies and complete agent configurations go directly from native code to the clipboard. IPC checks the originating window and frame, and validates commands at the controller boundary.

## Lifecycle

Start and stop converge to one desired state. Login cancellation and sign-out invalidate old asynchronous work before cleanup. Stop closes the listener and drains active work for a bounded period, then terminates the child if needed. A replacement starts only after the previous child exits. Parent disconnection also ends the child. Quit waits for cleanup.

Settings and credentials have one writer. Credentials persist only when OS-backed encryption is available. Linux basic-text storage is treated as unavailable. Otherwise the app explicitly runs in session-only mode. Existing CLI token files and agent configuration files remain under their current owners.

Activity is a bounded session history of completed-request metadata. The existing stream observer supplies final timing and token counts. It does not record prompts, generated text, tool arguments, headers, or raw upstream errors. This release adds no history database or telemetry.

## Verification

The [feature map](FEATURES.md) defines observable workflows. Local fixtures replace only upstream services; the renderer, native bridge, child, Hono routes, and loopback requests still run. Browser checks use gstack browse. Native smoke checks launch Electron and verify the actual window and listener. Packaged checks cover dependency assets and shutdown. Tests on this Mac establish macOS behavior. Windows and Linux require their native CI jobs before a cross-platform release claim.

The decision trail and architecture candidates for this development run are under the project's mac-ssd `desktop-app` directory.
20 changes: 20 additions & 0 deletions desktop/FEATURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Desktop feature map

The desktop app has its own version, starting at 0.0.1. The existing CLI keeps its release history.

| Feature | Expected behavior | Verification |
| --- | --- | --- |
| GitHub sign-in | Show a device code, open GitHub, obey polling limits, and handle approval, denial, expiry, cancellation, and network errors. | Local upstream fixtures and the rendered sign-in flow. Live sign-in is recorded separately. |
| Account storage | Keep credentials in the native process. Persist only encrypted credentials. Use a visible session-only mode when secure storage is unavailable. | Native storage tests and inspection of test data and renderer snapshots. |
| Local gateway | Start one authenticated listener at 127.0.0.1. Stop releases its port. Repeated actions, failed binds, sign-out, and quit leave no orphan listener. | Real local HTTP, a controlled child process, and the native UI. |
| Protocols | Preserve OpenAI Chat Completions, OpenAI Responses, Anthropic Messages, and their supported streaming behavior. | Existing proxy tests plus real HTTP through the desktop-owned Node runtime. |
| Models | Show the actual Copilot catalog, supported features, and model limits. Search and select a model for agent setup and test requests. | Fixture catalog in the UI and live discovery when credentials are available. |
| Usage | Display account quotas from GitHub. Distinguish unlimited, unavailable, and zero remaining. Keep fetch timestamps visible. | Controlled quota responses and UI refresh. |
| Agent setup | Preview and copy Claude Code, Codex, OpenCode, and HTTP configurations with POSIX or PowerShell setup. Copy the local key without exposing upstream credentials. | UI copy actions and requests using the resulting endpoint and key. Existing agent files are never overwritten. |
| Test request | Send the selected model a real request through the local gateway and show its response or an actionable error. | Controlled upstream response, completed activity, and live Copilot execution recorded separately. |
| Activity | Show bounded session metadata after streams finish. Distinguish failures and cancellations. Store no prompt, generated text, headers, or upstream error bodies in history. | JSON and streaming requests, cancellation, clearing, and redaction checks. |
| Settings | Validate ports and persist appearance, preferred model, start behavior, account type, and shell choice. Apply lifecycle changes through the native owner. | UI save, invalid-input checks, and app restart. |
| Desktop lifecycle | Offer native tray actions, a single app instance, safe close behavior, and complete quit cleanup. | Actual Electron launches. Platform-specific behavior requires the matching OS. |
| Packaging | Bundle the runtime for macOS, Windows, and Linux. Users need neither Node nor Bun. Include PDF dependencies and worker assets. | Local macOS launch and package checks. Native Windows/Linux CI must be distinguished from checks run on this Mac. |

The first release supports one Copilot account. Other providers, account rotation, automatic edits to agent files, cloud sync, signing, publishing, and deployment are outside this development scope.
Loading
Loading