Skip to content

Commit eecf26c

Browse files
committed
update
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
1 parent 8015d73 commit eecf26c

File tree

7 files changed

+147
-9
lines changed

7 files changed

+147
-9
lines changed

ui/desktop/src/components/ProviderGuard.tsx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
33
import { useConfig } from './ConfigContext';
44
import { SetupModal } from './SetupModal';
55
import { startOpenRouterSetup } from '../utils/openRouterSetup';
6-
import { startTetrateSetup } from '../utils/tetrateSetup';
6+
import { cancelTetrateSetup, startTetrateSetup } from '../utils/tetrateSetup';
77
import WelcomeGooseLogo from './WelcomeGooseLogo';
88
import { toastService } from '../toasts';
99
import { OllamaSetup } from './OllamaSetup';
@@ -36,7 +36,9 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
3636
const [userInActiveSetup, setUserInActiveSetup] = useState(false);
3737
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
3838
const [switchModelProvider, setSwitchModelProvider] = useState<string | null>(null);
39+
const [isTetrateSetupInProgress, setIsTetrateSetupInProgress] = useState(false);
3940
const onboardingTracked = useRef(false);
41+
const tetrateSetupRunId = useRef(0);
4042
const scrollContainerRef = useRef<HTMLDivElement>(null);
4143
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
4244

@@ -65,15 +67,38 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
6567
show: boolean;
6668
title: string;
6769
message: string;
70+
showProgress?: boolean;
6871
showRetry: boolean;
72+
closeLabel?: string;
6973
autoClose?: number;
7074
} | null>(null);
7175

7276
const handleTetrateSetup = async () => {
77+
if (isTetrateSetupInProgress) {
78+
return;
79+
}
80+
81+
const runId = ++tetrateSetupRunId.current;
82+
setIsTetrateSetupInProgress(true);
83+
setTetrateSetupState({
84+
show: true,
85+
title: 'Complete setup in your browser',
86+
message: 'After finishing sign-in in the browser, return to Goose. You can cancel setup anytime.',
87+
showProgress: true,
88+
showRetry: false,
89+
closeLabel: 'Cancel Setup',
90+
});
7391
trackOnboardingProviderSelected('tetrate');
7492
try {
7593
const result = await startTetrateSetup();
94+
if (runId !== tetrateSetupRunId.current) {
95+
return;
96+
}
97+
98+
setIsTetrateSetupInProgress(false);
99+
76100
if (result.success) {
101+
setTetrateSetupState(null);
77102
setSwitchModelProvider('tetrate');
78103
setShowSwitchModelModal(true);
79104
} else {
@@ -86,6 +111,11 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
86111
});
87112
}
88113
} catch (error) {
114+
if (runId !== tetrateSetupRunId.current) {
115+
return;
116+
}
117+
118+
setIsTetrateSetupInProgress(false);
89119
console.error('Tetrate setup error:', error);
90120
trackOnboardingSetupFailed('tetrate', 'unexpected_error');
91121
setTetrateSetupState({
@@ -97,6 +127,19 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
97127
}
98128
};
99129

130+
const handleCancelTetrateSetup = async () => {
131+
if (!isTetrateSetupInProgress) {
132+
setTetrateSetupState(null);
133+
return;
134+
}
135+
136+
tetrateSetupRunId.current += 1;
137+
setIsTetrateSetupInProgress(false);
138+
setTetrateSetupState(null);
139+
trackOnboardingAbandoned('tetrate_setup');
140+
await cancelTetrateSetup();
141+
};
142+
100143
const handleApiKeySuccess = async (provider: string, _model: string, apiKey: string) => {
101144
trackOnboardingProviderSelected('api_key');
102145
const keyName = `${provider.toUpperCase()}_API_KEY`;
@@ -177,7 +220,7 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
177220
if (setupType === 'openrouter') {
178221
setOpenRouterSetupState(null);
179222
} else {
180-
setTetrateSetupState(null);
223+
void handleCancelTetrateSetup();
181224
}
182225
};
183226

@@ -284,8 +327,12 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
284327
</div>
285328

286329
<div
287-
onClick={handleTetrateSetup}
288-
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
330+
onClick={isTetrateSetupInProgress ? undefined : handleTetrateSetup}
331+
className={`w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl transition-all duration-200 group ${
332+
isTetrateSetupInProgress
333+
? 'cursor-not-allowed opacity-70'
334+
: 'cursor-pointer hover:border-text-muted'
335+
}`}
289336
>
290337
<div className="flex items-start justify-between mb-3">
291338
<div className="flex items-center gap-2">
@@ -408,9 +455,11 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
408455
<SetupModal
409456
title={tetrateSetupState.title}
410457
message={tetrateSetupState.message}
458+
showProgress={tetrateSetupState.showProgress}
411459
showRetry={tetrateSetupState.showRetry}
412460
onRetry={() => handleRetrySetup('tetrate')}
413461
onClose={() => closeSetupModal('tetrate')}
462+
closeLabel={tetrateSetupState.closeLabel}
414463
autoClose={tetrateSetupState.autoClose}
415464
/>
416465
)}

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
import { UPDATES_ENABLED } from './updates';
5050
import './utils/recipeHash';
5151
import { Client, createClient, createConfig } from './api/client';
52-
import { handleTetrateCallbackUrl, runTetrateAuthFlow } from './tetrateAuth';
52+
import { cancelTetrateAuthFlow, handleTetrateCallbackUrl, runTetrateAuthFlow } from './tetrateAuth';
5353
import { GooseApp } from './api';
5454
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
5555

@@ -1321,6 +1321,10 @@ ipcMain.handle('tetrate-auth-start', async (event) => {
13211321
return runTetrateAuthFlow(client);
13221322
});
13231323

1324+
ipcMain.handle('tetrate-auth-cancel', () => {
1325+
return cancelTetrateAuthFlow();
1326+
});
1327+
13241328
// Handle menu bar icon visibility
13251329
ipcMain.handle('set-menu-bar-icon', async (_event, show: boolean) => {
13261330
try {

ui/desktop/src/preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ type ElectronAPI = {
124124
// Function to serve temp images
125125
getTempImage: (filePath: string) => Promise<string | null>;
126126
startTetrateAuth: () => Promise<{ success: boolean; message: string }>;
127+
cancelTetrateAuth: () => Promise<boolean>;
127128
// Update-related functions
128129
getVersion: () => string;
129130
checkForUpdates: () => Promise<{ updateInfo: unknown; error: string | null }>;
@@ -251,6 +252,9 @@ const electronAPI: ElectronAPI = {
251252
startTetrateAuth: (): Promise<{ success: boolean; message: string }> => {
252253
return ipcRenderer.invoke('tetrate-auth-start');
253254
},
255+
cancelTetrateAuth: (): Promise<boolean> => {
256+
return ipcRenderer.invoke('tetrate-auth-cancel');
257+
},
254258
getVersion: (): string => {
255259
return config.GOOSE_VERSION || ipcRenderer.sendSync('get-app-version') || '';
256260
},

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)