Skip to content

Commit b1eff5f

Browse files
fix(desktop): start recipe deeplink sessions from the recipe prompt (#8424)
Signed-off-by: sunilkumarvalmiki <g.sunilkumarvalmiki@gmail.com> Co-authored-by: Lifei Zhou <lifei@squareup.com>
1 parent ed4836b commit b1eff5f

File tree

5 files changed

+140
-6
lines changed

5 files changed

+140
-6
lines changed

ui/desktop/src/App.test.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import React from 'react';
77
import { screen, render, waitFor } from '@testing-library/react';
88
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
9-
import { AppInner } from './App';
9+
import { AppInner, resolveSessionInitialMessage } from './App';
1010
import { IntlTestWrapper } from './i18n/test-utils';
1111

1212
// Set up globals for jsdom
@@ -60,6 +60,7 @@ vi.mock('./sessions', () => ({
6060
.fn()
6161
.mockResolvedValue({ sessionId: 'test', messages: [], metadata: { description: '' } }),
6262
generateSessionId: vi.fn(),
63+
createSession: vi.fn(),
6364
}));
6465

6566
// Mock the ConfigContext module
@@ -161,7 +162,7 @@ const mockElectron = {
161162

162163
// Mock appConfig
163164
const mockAppConfig = {
164-
get: vi.fn((key: string) => {
165+
get: vi.fn((key: string): string | null => {
165166
if (key === 'GOOSE_WORKING_DIR') return '/test/dir';
166167
return null;
167168
}),
@@ -191,6 +192,10 @@ describe('App Component - Brand New State', () => {
191192
vi.clearAllMocks();
192193
mockNavigate.mockClear();
193194
mockSetSearchParams.mockClear();
195+
mockAppConfig.get.mockImplementation((key: string): string | null => {
196+
if (key === 'GOOSE_WORKING_DIR') return '/test/dir';
197+
return null;
198+
});
194199

195200
// Reset search params
196201
mockSearchParams.forEach((_, key) => {
@@ -290,4 +295,20 @@ describe('App Component - Brand New State', () => {
290295
// App should still initialize without any navigation calls
291296
expect(mockNavigate).not.toHaveBeenCalled();
292297
});
298+
299+
it('should seed recipe sessions with the recipe prompt when no initial message is provided', () => {
300+
expect(
301+
resolveSessionInitialMessage(
302+
{
303+
recipe: {
304+
prompt: 'Write a release note for the latest change',
305+
},
306+
},
307+
undefined
308+
)
309+
).toEqual({
310+
msg: 'Write a release note for the latest change',
311+
images: [],
312+
});
313+
});
293314
});

ui/desktop/src/App.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ const HubRouteWrapper = () => {
6868
return <Hub setView={setView} />;
6969
};
7070

71+
export function resolveSessionInitialMessage(
72+
session: { recipe?: { prompt?: string | null } | null },
73+
initialMessage?: UserInput
74+
): UserInput | undefined {
75+
return (
76+
initialMessage ??
77+
(session.recipe?.prompt ? { msg: session.recipe.prompt, images: [] } : undefined)
78+
);
79+
}
80+
7181
const PairRouteWrapper = ({
7282
activeSessions,
7383
}: {
@@ -105,12 +115,13 @@ const PairRouteWrapper = ({
105115
recipeId: recipeIdFromConfig,
106116
allExtensions: extensionsList,
107117
});
118+
const sessionInitialMessage = resolveSessionInitialMessage(newSession, initialMessage);
108119

109120
window.dispatchEvent(
110121
new CustomEvent(AppEvents.ADD_ACTIVE_SESSION, {
111122
detail: {
112123
sessionId: newSession.id,
113-
initialMessage,
124+
initialMessage: sessionInitialMessage,
114125
},
115126
})
116127
);

ui/desktop/src/components/BaseChat.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,15 @@ export default function BaseChat({
137137
return initialMessage;
138138
}, [initialMessage, recipe?.prompt, session?.user_recipe_values]);
139139

140+
const canAutoSubmit = !recipe || hasNotAcceptedRecipe === false;
141+
140142
useAutoSubmit({
141143
sessionId,
142144
session,
143145
messages,
144146
chatState,
145147
initialMessage: resolvedInitialMessage,
148+
canAutoSubmit,
146149
handleSubmit,
147150
});
148151

@@ -206,7 +209,7 @@ export default function BaseChat({
206209
const sessionLoaded = session !== undefined;
207210

208211
useEffect(() => {
209-
if (!recipe) return;
212+
if (!recipe || !isActiveSession) return;
210213

211214
(async () => {
212215
const accepted = await window.electron.hasAcceptedRecipeBefore(recipe);
@@ -217,7 +220,7 @@ export default function BaseChat({
217220
setHasRecipeSecurityWarnings(scanResult.has_security_warnings);
218221
}
219222
})();
220-
}, [recipe]);
223+
}, [recipe, isActiveSession]);
221224

222225
const handleRecipeAccept = async (accept: boolean) => {
223226
if (recipe && accept) {
@@ -525,7 +528,7 @@ export default function BaseChat({
525528
</div>
526529
</MainPanelLayout>
527530

528-
{recipe && (
531+
{recipe && isActiveSession && (
529532
<RecipeWarningModal
530533
isOpen={!!hasNotAcceptedRecipe}
531534
onConfirm={() => handleRecipeAccept(true)}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { renderHook } from '@testing-library/react';
3+
import { MemoryRouter } from 'react-router-dom';
4+
import type { PropsWithChildren } from 'react';
5+
import { useAutoSubmit } from './useAutoSubmit';
6+
import { ChatState } from '../types/chatState';
7+
import type { Session } from '../api';
8+
import type { UserInput } from '../types/message';
9+
10+
function makeSession(overrides: Partial<Session> = {}): Session {
11+
return {
12+
id: 'sess-1',
13+
name: 'untitled',
14+
message_count: 0,
15+
created_at: new Date().toISOString(),
16+
updated_at: new Date().toISOString(),
17+
working_dir: '/tmp',
18+
extension_data: { active: [], installed: [] },
19+
...overrides,
20+
} as Session;
21+
}
22+
23+
const initialMessage: UserInput = {
24+
msg: 'Run the recipe',
25+
images: [],
26+
};
27+
28+
describe('useAutoSubmit', () => {
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
});
32+
33+
it('does not auto-submit while recipe acceptance is unresolved', () => {
34+
const handleSubmit = vi.fn();
35+
const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent');
36+
37+
const wrapper = ({ children }: PropsWithChildren) => (
38+
<MemoryRouter initialEntries={['/pair?resumeSessionId=sess-1']}>{children}</MemoryRouter>
39+
);
40+
41+
renderHook(
42+
() =>
43+
useAutoSubmit({
44+
sessionId: 'sess-1',
45+
session: makeSession(),
46+
messages: [],
47+
chatState: ChatState.Idle,
48+
initialMessage,
49+
canAutoSubmit: false,
50+
handleSubmit,
51+
}),
52+
{ wrapper }
53+
);
54+
55+
expect(handleSubmit).not.toHaveBeenCalled();
56+
expect(dispatchEventSpy).not.toHaveBeenCalled();
57+
});
58+
59+
it('auto-submits once recipe acceptance is confirmed', () => {
60+
const handleSubmit = vi.fn();
61+
const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent');
62+
63+
const wrapper = ({ children }: PropsWithChildren) => (
64+
<MemoryRouter initialEntries={['/pair?resumeSessionId=sess-1']}>{children}</MemoryRouter>
65+
);
66+
67+
const { rerender } = renderHook(
68+
({ canAutoSubmit }) =>
69+
useAutoSubmit({
70+
sessionId: 'sess-1',
71+
session: makeSession(),
72+
messages: [],
73+
chatState: ChatState.Idle,
74+
initialMessage,
75+
canAutoSubmit,
76+
handleSubmit,
77+
}),
78+
{
79+
initialProps: { canAutoSubmit: false },
80+
wrapper,
81+
}
82+
);
83+
84+
expect(handleSubmit).not.toHaveBeenCalled();
85+
86+
rerender({ canAutoSubmit: true });
87+
88+
expect(handleSubmit).toHaveBeenCalledTimes(1);
89+
expect(handleSubmit).toHaveBeenCalledWith(initialMessage);
90+
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
91+
});
92+
});

ui/desktop/src/hooks/useAutoSubmit.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface UseAutoSubmitProps {
1919
messages: Message[];
2020
chatState: ChatState;
2121
initialMessage: UserInput | undefined;
22+
canAutoSubmit?: boolean;
2223
handleSubmit: (input: UserInput) => void;
2324
}
2425

@@ -32,6 +33,7 @@ export function useAutoSubmit({
3233
messages,
3334
chatState,
3435
initialMessage,
36+
canAutoSubmit = true,
3537
handleSubmit,
3638
}: UseAutoSubmitProps): UseAutoSubmitReturn {
3739
const [searchParams] = useSearchParams();
@@ -65,6 +67,10 @@ export function useAutoSubmit({
6567
return;
6668
}
6769

70+
if (!canAutoSubmit) {
71+
return;
72+
}
73+
6874
if (chatState !== ChatState.Idle) {
6975
return;
7076
}
@@ -107,6 +113,7 @@ export function useAutoSubmit({
107113
sessionId,
108114
messages.length,
109115
chatState,
116+
canAutoSubmit,
110117
clearInitialMessage,
111118
hasUnfilledParameters,
112119
]);

0 commit comments

Comments
 (0)