Skip to content

Commit 5ba1688

Browse files
committed
update
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
1 parent 01174a4 commit 5ba1688

File tree

7 files changed

+147
-7
lines changed

7 files changed

+147
-7
lines changed

ui/desktop/src/components/ProviderGuard.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SetupModal } from './SetupModal';
55
import { startOpenRouterSetup } from '../utils/openRouterSetup';
66
import { startTetrateSetup } from '../utils/tetrateSetup';
77
import { startChatGptCodexSetup } from '../utils/chatgptCodexSetup';
8+
import { cancelTetrateSetup } from '../utils/tetrateSetup';
89
import WelcomeGooseLogo from './WelcomeGooseLogo';
910
import { toastService } from '../toasts';
1011
import { OllamaSetup } from './OllamaSetup';
@@ -37,7 +38,9 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
3738
const [userInActiveSetup, setUserInActiveSetup] = useState(false);
3839
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
3940
const [switchModelProvider, setSwitchModelProvider] = useState<string | null>(null);
41+
const [isTetrateSetupInProgress, setIsTetrateSetupInProgress] = useState(false);
4042
const onboardingTracked = useRef(false);
43+
const tetrateSetupRunId = useRef(0);
4144
const scrollContainerRef = useRef<HTMLDivElement>(null);
4245
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
4346

@@ -66,7 +69,9 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
6669
show: boolean;
6770
title: string;
6871
message: string;
72+
showProgress?: boolean;
6973
showRetry: boolean;
74+
closeLabel?: string;
7075
autoClose?: number;
7176
} | null>(null);
7277

@@ -79,10 +84,31 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
7984
} | null>(null);
8085

8186
const handleTetrateSetup = async () => {
87+
if (isTetrateSetupInProgress) {
88+
return;
89+
}
90+
91+
const runId = ++tetrateSetupRunId.current;
92+
setIsTetrateSetupInProgress(true);
93+
setTetrateSetupState({
94+
show: true,
95+
title: 'Complete setup in your browser',
96+
message: 'After finishing sign-in in the browser, return to Goose. You can cancel setup anytime.',
97+
showProgress: true,
98+
showRetry: false,
99+
closeLabel: 'Cancel Setup',
100+
});
82101
trackOnboardingProviderSelected('tetrate');
83102
try {
84103
const result = await startTetrateSetup();
104+
if (runId !== tetrateSetupRunId.current) {
105+
return;
106+
}
107+
108+
setIsTetrateSetupInProgress(false);
109+
85110
if (result.success) {
111+
setTetrateSetupState(null);
86112
setSwitchModelProvider('tetrate');
87113
setShowSwitchModelModal(true);
88114
} else {
@@ -95,6 +121,11 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
95121
});
96122
}
97123
} catch (error) {
124+
if (runId !== tetrateSetupRunId.current) {
125+
return;
126+
}
127+
128+
setIsTetrateSetupInProgress(false);
98129
console.error('Tetrate setup error:', error);
99130
trackOnboardingSetupFailed('tetrate', 'unexpected_error');
100131
setTetrateSetupState({
@@ -134,6 +165,19 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
134165
}
135166
};
136167

168+
const handleCancelTetrateSetup = async () => {
169+
if (!isTetrateSetupInProgress) {
170+
setTetrateSetupState(null);
171+
return;
172+
}
173+
174+
tetrateSetupRunId.current += 1;
175+
setIsTetrateSetupInProgress(false);
176+
setTetrateSetupState(null);
177+
trackOnboardingAbandoned('tetrate_setup');
178+
await cancelTetrateSetup();
179+
};
180+
137181
const handleApiKeySuccess = async (provider: string, _model: string, apiKey: string) => {
138182
trackOnboardingProviderSelected('api_key');
139183
const keyName = `${provider.toUpperCase()}_API_KEY`;
@@ -217,7 +261,7 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
217261
if (setupType === 'openrouter') {
218262
setOpenRouterSetupState(null);
219263
} else if (setupType === 'tetrate') {
220-
setTetrateSetupState(null);
264+
void handleCancelTetrateSetup();
221265
} else {
222266
setChatgptCodexSetupState(null);
223267
}
@@ -366,8 +410,12 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
366410
</div>
367411

368412
<div
369-
onClick={handleTetrateSetup}
370-
className="w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group"
413+
onClick={isTetrateSetupInProgress ? undefined : handleTetrateSetup}
414+
className={`w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl transition-all duration-200 group ${
415+
isTetrateSetupInProgress
416+
? 'cursor-not-allowed opacity-70'
417+
: 'cursor-pointer hover:border-text-muted'
418+
}`}
371419
>
372420
<div className="flex items-start justify-between mb-3">
373421
<div className="flex items-center gap-2">
@@ -490,9 +538,11 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
490538
<SetupModal
491539
title={tetrateSetupState.title}
492540
message={tetrateSetupState.message}
541+
showProgress={tetrateSetupState.showProgress}
493542
showRetry={tetrateSetupState.showRetry}
494543
onRetry={() => handleRetrySetup('tetrate')}
495544
onClose={() => closeSetupModal('tetrate')}
545+
closeLabel={tetrateSetupState.closeLabel}
496546
autoClose={tetrateSetupState.autoClose}
497547
/>
498548
)}

ui/desktop/src/components/SetupModal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface SetupModalProps {
99
onRetry?: () => void;
1010
autoClose?: number;
1111
onClose?: () => void;
12+
closeLabel?: string;
1213
}
1314

1415
export function SetupModal({
@@ -19,6 +20,7 @@ export function SetupModal({
1920
onRetry,
2021
autoClose,
2122
onClose,
23+
closeLabel,
2224
}: SetupModalProps) {
2325
useEffect(() => {
2426
if (autoClose && onClose) {
@@ -45,7 +47,7 @@ export function SetupModal({
4547
{onClose && (
4648
<div className="mb-4">
4749
<Button onClick={onClose} className="w-full">
48-
Close
50+
{closeLabel || 'Close'}
4951
</Button>
5052
<br />
5153
</div>

ui/desktop/src/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { Client, createClient, createConfig } from './api/client';
4848
import { GooseApp } from './api';
4949
import {
5050
TETRATE_AUTH_CLEANUP_INTERVAL_MS,
51+
cancelTetrateAuthFlow,
5152
cleanupExpiredTetrateAuthFlows,
5253
handleTetrateCallbackUrl,
5354
runTetrateAuthFlow,
@@ -1266,6 +1267,10 @@ ipcMain.handle('tetrate-auth-start', async (event) => {
12661267
return runTetrateAuthFlow(client);
12671268
});
12681269

1270+
ipcMain.handle('tetrate-auth-cancel', () => {
1271+
return cancelTetrateAuthFlow();
1272+
});
1273+
12691274
// Handle menu bar icon visibility
12701275
ipcMain.handle('set-menu-bar-icon', async (_event, show: boolean) => {
12711276
updateSettings((s) => {

ui/desktop/src/preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ type ElectronAPI = {
115115
// Function to serve temp images
116116
getTempImage: (filePath: string) => Promise<string | null>;
117117
startTetrateAuth: () => Promise<{ success: boolean; message: string }>;
118+
cancelTetrateAuth: () => Promise<boolean>;
118119
// Update-related functions
119120
getVersion: () => string;
120121
checkForUpdates: () => Promise<{ updateInfo: unknown; error: string | null }>;
@@ -238,6 +239,9 @@ const electronAPI: ElectronAPI = {
238239
startTetrateAuth: (): Promise<{ success: boolean; message: string }> => {
239240
return ipcRenderer.invoke('tetrate-auth-start');
240241
},
242+
cancelTetrateAuth: (): Promise<boolean> => {
243+
return ipcRenderer.invoke('tetrate-auth-cancel');
244+
},
241245
getVersion: (): string => {
242246
return config.GOOSE_VERSION || ipcRenderer.sendSync('get-app-version') || '';
243247
},

ui/desktop/src/tetrateAuth.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ vi.mock('./api', () => ({
1919
import { shell } from 'electron';
2020
import type { Client } from './api/client';
2121
import { verifyTetrateSetup } from './api';
22-
import { __test, handleTetrateCallbackUrl, runTetrateAuthFlow } from './tetrateAuth';
22+
import {
23+
__test,
24+
cancelTetrateAuthFlow,
25+
handleTetrateCallbackUrl,
26+
runTetrateAuthFlow,
27+
} from './tetrateAuth';
2328

2429
describe('tetrateAuth', () => {
2530
afterEach(() => {
@@ -124,6 +129,21 @@ describe('tetrateAuth', () => {
124129
await expect(waitPromise).rejects.toThrow('access_denied');
125130
});
126131

132+
it('uses error_description when the auth callback includes one', async () => {
133+
const { flowId, authUrl } = __test.createTetrateAuthFlow();
134+
const callbackUrl = new URL(authUrl).searchParams.get('callback');
135+
expect(callbackUrl).toBeTruthy();
136+
137+
const errorUrl = new URL(callbackUrl as string);
138+
errorUrl.searchParams.set('error', 'access_denied');
139+
errorUrl.searchParams.set('error_description', 'User denied authorization');
140+
141+
const waitPromise = __test.waitForTetrateCallback(flowId);
142+
expect(handleTetrateCallbackUrl(errorUrl.toString())).toBe(true);
143+
144+
await expect(waitPromise).rejects.toThrow('User denied authorization');
145+
});
146+
127147
it('rejects immediately when the callback has no code and no error', async () => {
128148
const { flowId, authUrl } = __test.createTetrateAuthFlow();
129149
const callbackUrl = new URL(authUrl).searchParams.get('callback');
@@ -136,6 +156,19 @@ describe('tetrateAuth', () => {
136156
await expect(waitPromise).rejects.toThrow('Authentication failed');
137157
});
138158

159+
it('supports canceling an active auth flow', async () => {
160+
const openExternalMock = vi.mocked(shell.openExternal);
161+
openExternalMock.mockResolvedValue();
162+
163+
const flowPromise = runTetrateAuthFlow({} as Client);
164+
expect(cancelTetrateAuthFlow()).toBe(true);
165+
166+
await expect(flowPromise).resolves.toEqual({
167+
success: false,
168+
message: 'Authentication canceled by user',
169+
});
170+
});
171+
139172
it('runs the full auth flow and verifies the code', async () => {
140173
const verifyMock = vi.mocked(verifyTetrateSetup);
141174
const request = new globalThis.Request('http://localhost/test');

ui/desktop/src/tetrateAuth.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@ type TetrateCallbackMatch = {
3030
state: string;
3131
code?: string;
3232
error?: string;
33+
errorDescription?: string;
3334
};
3435

3536
const TETRATE_AUTH_URL = 'https://router.tetrate.ai/auth';
36-
const TETRATE_AUTH_TTL_MS = 10 * 60 * 1000;
37+
const TETRATE_AUTH_TTL_MS = 2 * 60 * 1000;
3738
const TETRATE_AUTH_CALLBACK_SCHEME = 'goose';
3839

3940
const tetrateAuthFlows = new Map<string, TetrateAuthFlow>();
41+
const completedTetrateAuthFlowErrors = new Map<string, string>();
42+
let activeTetrateFlowId: string | null = null;
4043

4144
function createPkcePair(): { codeVerifier: string; codeChallenge: string } {
4245
const codeVerifier = crypto.randomBytes(96).toString('base64url');
@@ -115,8 +118,10 @@ function matchTetrateCallbackUrl(callbackUrl: string): TetrateCallbackMatch | nu
115118
const result: TetrateCallbackMatch = { flowId, state };
116119
const code = parsedUrl.searchParams.get('code');
117120
const error = parsedUrl.searchParams.get('error');
121+
const errorDescription = parsedUrl.searchParams.get('error_description');
118122
if (code) result.code = code;
119123
if (error) result.error = error;
124+
if (errorDescription) result.errorDescription = errorDescription;
120125

121126
return result;
122127
}
@@ -141,6 +146,9 @@ function expireTetrateAuthFlow(flowId: string, message: string): void {
141146
}
142147

143148
log.warn('Tetrate auth flow expired:', { flowId, reason: message });
149+
if (!flow.reject) {
150+
completedTetrateAuthFlowErrors.set(flowId, message);
151+
}
144152
flow.reject?.(new Error(message));
145153
cleanupTetrateAuthFlow(flowId);
146154
}
@@ -194,7 +202,8 @@ export function handleTetrateCallbackUrl(
194202
}
195203

196204
if (match.error) {
197-
expireTetrateAuthFlow(match.flowId, `Authentication denied: ${match.error}`);
205+
const errorMessage = match.errorDescription || match.error;
206+
expireTetrateAuthFlow(match.flowId, `Authentication denied: ${errorMessage}`);
198207
return true;
199208
}
200209

@@ -211,6 +220,11 @@ export function handleTetrateCallbackUrl(
211220
function waitForTetrateCallback(flowId: string): Promise<string> {
212221
const flow = tetrateAuthFlows.get(flowId);
213222
if (!flow) {
223+
const completedFlowError = completedTetrateAuthFlowErrors.get(flowId);
224+
if (completedFlowError) {
225+
completedTetrateAuthFlowErrors.delete(flowId);
226+
return Promise.reject(new Error(completedFlowError));
227+
}
214228
return Promise.reject(new Error('Authentication expired'));
215229
}
216230

@@ -234,6 +248,16 @@ async function startTetrateAuthSession(flowId: string, authUrl: string): Promise
234248
return waitForTetrateCallback(flowId);
235249
}
236250

251+
export function cancelTetrateAuthFlow(message = 'Authentication canceled by user'): boolean {
252+
if (!activeTetrateFlowId) {
253+
return false;
254+
}
255+
const flowId = activeTetrateFlowId;
256+
activeTetrateFlowId = null;
257+
expireTetrateAuthFlow(flowId, message);
258+
return true;
259+
}
260+
237261
function getTetrateAuthErrorMessage(error: unknown): string {
238262
if (error instanceof Error && error.message) {
239263
return error.message;
@@ -251,7 +275,15 @@ function getTetrateAuthErrorMessage(error: unknown): string {
251275
}
252276

253277
export async function runTetrateAuthFlow(client: Client): Promise<TetrateSetupResponse> {
278+
if (activeTetrateFlowId) {
279+
return {
280+
success: false,
281+
message: 'Authentication already in progress',
282+
};
283+
}
284+
254285
const { flowId, authUrl } = createTetrateAuthFlow();
286+
activeTetrateFlowId = flowId;
255287

256288
try {
257289
const callbackUrl = await startTetrateAuthSession(flowId, authUrl);
@@ -294,6 +326,10 @@ export async function runTetrateAuthFlow(client: Client): Promise<TetrateSetupRe
294326
success: false,
295327
message: getTetrateAuthErrorMessage(error),
296328
};
329+
} finally {
330+
if (activeTetrateFlowId === flowId) {
331+
activeTetrateFlowId = null;
332+
}
297333
}
298334
}
299335

@@ -311,6 +347,8 @@ export const __test = {
311347
}
312348
}
313349
tetrateAuthFlows.clear();
350+
completedTetrateAuthFlowErrors.clear();
351+
activeTetrateFlowId = null;
314352
},
315353
waitForTetrateCallback,
316354
};

ui/desktop/src/utils/tetrateSetup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ export async function startTetrateSetup(): Promise<{
1111
};
1212
}
1313
}
14+
15+
export async function cancelTetrateSetup(): Promise<boolean> {
16+
try {
17+
return await window.electron.cancelTetrateAuth();
18+
} catch {
19+
return false;
20+
}
21+
}

0 commit comments

Comments
 (0)