Feat: Implement real GitHub Copilot Device Flow authentication UI

- Add IPC handlers for github-copilot:authenticate, isAuthenticated, disconnect
- Add schema definitions for new IPC handlers in shared/ipc.ts
- Implement Device Flow authentication button with real flow
- Show device code and verification URL to user when authenticating
- Automatically open GitHub verification page in browser
- Auto-load models after successful authentication
- Update GitHub Copilot model list (gpt-4o, gpt-4-turbo, gpt-3.5-turbo, claude-3.5-sonnet)
- Display loading state and error messages in UI
- Properly integrate with existing auth service

This fixes the broken authentication flow where the button didn't do anything.
This commit is contained in:
Rowboat Developer 2026-04-17 09:56:32 -05:00
parent 4f53f0af48
commit b7866e3067
5 changed files with 135 additions and 17 deletions

View file

@ -29,6 +29,7 @@ import { isSignedIn } from '@x/core/dist/account/account.js';
import { listGatewayModels } from '@x/core/dist/models/gateway.js';
import type { IModelConfigRepo } from '@x/core/dist/models/repo.js';
import type { IOAuthRepo } from '@x/core/dist/auth/repo.js';
import { startGitHubCopilotAuthentication, getGitHubCopilotAccessToken, isGitHubCopilotAuthenticated, disconnectGitHubCopilot } from '@x/core/dist/auth/github-copilot-auth.js';
import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
import { ISlackConfigRepo } from '@x/core/dist/slack/repo.js';
@ -488,6 +489,46 @@ export function setupIpcHandlers() {
const config = await repo.getClientFacingConfig();
return { config };
},
'github-copilot:authenticate': async () => {
try {
const authInfo = await startGitHubCopilotAuthentication();
// Don't await the token promise - it will complete in the background
// Just return the device code info immediately
return {
success: true,
userCode: authInfo.userCode,
verificationUri: authInfo.verificationUri,
expiresIn: authInfo.expiresIn,
};
} catch (error) {
console.error('[GitHub Copilot] Authentication error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Authentication failed',
};
}
},
'github-copilot:isAuthenticated': async () => {
try {
const authenticated = await isGitHubCopilotAuthenticated();
return { authenticated };
} catch (error) {
console.error('[GitHub Copilot] Error checking authentication:', error);
return { authenticated: false };
}
},
'github-copilot:disconnect': async () => {
try {
await disconnectGitHubCopilot();
return { success: true };
} catch (error) {
console.error('[GitHub Copilot] Disconnect error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Disconnect failed',
};
}
},
'account:getRowboat': async () => {
const signedIn = await isSignedIn();
if (!signedIn) {

View file

@ -691,17 +691,64 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
{isGitHubCopilot && (
<div className="space-y-2">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Authentication</span>
<p className="text-xs text-muted-foreground">GitHub Copilot uses Device Flow OAuth for authentication. Click the button below to authenticate with your GitHub account.</p>
<p className="text-xs text-muted-foreground">GitHub Copilot uses Device Flow OAuth for authentication. Click the button to authenticate with your GitHub account.</p>
<Button
onClick={() => {
// Authentication will be triggered when the user selects a model
// The models will be loaded from the auth service
toast.info("Select a GitHub Copilot model to authenticate")
onClick={async () => {
try {
setTestState({ status: "testing" });
const result = await window.ipc.invoke("github-copilot:authenticate", null);
if (!result.success) {
setTestState({ status: "error", error: result.error });
toast.error(`Authentication failed: ${result.error}`);
return;
}
// Show device code to user
const deviceCode = result.userCode;
const verificationUri = result.verificationUri;
toast.info(
`1. Open: ${verificationUri}\n2. Enter code: ${deviceCode}`,
{ duration: 60000 }
);
// Open verification URI in browser
window.open(verificationUri, '_blank');
// Wait a moment then check if authenticated
setTimeout(async () => {
const authCheck = await window.ipc.invoke("github-copilot:isAuthenticated", null);
if (authCheck.authenticated) {
setTestState({ status: "success" });
toast.success("GitHub Copilot authenticated!");
// Reload models after successful auth
const modelsResult = await window.ipc.invoke("models:list", null);
const catalog: Record<string, any[]> = {};
for (const p of modelsResult.providers || []) {
catalog[p.id] = p.models || [];
}
setModelsCatalog(catalog);
}
}, 3000);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Authentication error";
setTestState({ status: "error", error: errorMsg });
toast.error(`Error: ${errorMsg}`);
}
}}
variant="outline"
className="w-full"
disabled={testState.status === "testing"}
>
Authenticate with GitHub
{testState.status === "testing" ? (
<>
<Loader2 className="size-4 animate-spin mr-2" />
Authenticating...
</>
) : (
"Authenticate with GitHub"
)}
</Button>
</div>
)}

View file

@ -14,14 +14,13 @@ import { LlmProvider } from '@x/shared/dist/models.js';
const GITHUB_COPILOT_API_BASE = 'https://models.github.com/api/openai/';
// List of models available through GitHub Copilot
// Based on GitHub Copilot documentation
// Based on GitHub Copilot API documentation
// https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions
export const GITHUB_COPILOT_MODELS = [
'gpt-4o',
'gpt-4-turbo',
'gpt-4',
'gpt-3.5-turbo',
'claude-3.5-sonnet', // If available in student plan
'claude-3-opus', // If available in student plan
'gpt-4o', // GPT-4 Optimized (recommended)
'gpt-4-turbo', // GPT-4 Turbo
'gpt-3.5-turbo', // GPT-3.5 Turbo (fastest)
'claude-3.5-sonnet', // Claude 3.5 Sonnet (if available in plan)
] as const;
export type GitHubCopilotModel = typeof GITHUB_COPILOT_MODELS[number];

View file

@ -227,12 +227,10 @@ export async function listOnboardingModels(): Promise<{ providers: ProviderSumma
id: "github-copilot",
name: "GitHub Copilot Student",
models: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o", name: "GPT-4o (Recommended)" },
{ id: "gpt-4-turbo", name: "GPT-4 Turbo" },
{ id: "gpt-4", name: "GPT-4" },
{ id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" },
{ id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo (Fastest)" },
{ id: "claude-3.5-sonnet", name: "Claude 3.5 Sonnet" },
{ id: "claude-3-opus", name: "Claude 3 Opus" },
],
});

View file

@ -257,6 +257,39 @@ const ipcSchemas = {
})),
}),
},
'github-copilot:authenticate': {
req: z.null(),
res: z.union([
z.object({
success: z.literal(true),
userCode: z.string(),
verificationUri: z.string(),
expiresIn: z.number(),
}),
z.object({
success: z.literal(false),
error: z.string(),
}),
]),
},
'github-copilot:isAuthenticated': {
req: z.null(),
res: z.object({
authenticated: z.boolean(),
}),
},
'github-copilot:disconnect': {
req: z.null(),
res: z.union([
z.object({
success: z.literal(true),
}),
z.object({
success: z.literal(false),
error: z.string(),
}),
]),
},
'account:getRowboat': {
req: z.null(),
res: z.object({