mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
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:
parent
4f53f0af48
commit
b7866e3067
5 changed files with 135 additions and 17 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue