Skip to content

Commit 38859e9

Browse files
committed
fix(web): composer double-submit guard + onboarding session refresh + drop dotted bg
- Composer: an in-flight submit guard (ref + disabled state) so rapid double/triple clicks no longer fire multiple createChat calls → no duplicate threads in the sidebar. - Onboarding: force a fresh session token (getToken skipCache) after completion so the middleware's metadata.onboarding_complete claim is current (belt-and-suspenders with the Clerk session-token 'metadata' claim added to the prod instance). - Onboarding page: remove the paper-dot-field dotted background (bud-style clean bg).
1 parent 0589bab commit 38859e9

3 files changed

Lines changed: 24 additions & 4 deletions

File tree

apps/web/src/app/onboarding/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
44

55
export default function OnboardingPage() {
66
return (
7-
<main className="paper-dot-field flex min-h-screen items-center justify-center bg-white px-6 py-12 text-[#1b1b1b]">
7+
<main className="flex min-h-screen items-center justify-center bg-white px-6 py-12 text-[#1b1b1b]">
88
<Suspense
99
fallback={
1010
<div className="h-96 w-full max-w-[392px] rounded-[28px] border border-[#f1f1f1] bg-white" />

apps/web/src/components/home/home-composer.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,20 @@ export function HomeComposer({
186186
const [value, setValue] = useState(initialPrompt ?? "");
187187
const [skillCreatorMode, setSkillCreatorMode] = useState(skillCreator);
188188
const [authRedirectTo, setAuthRedirectTo] = useState<string | null>(null);
189+
// In-flight submit guard: without it, rapid double/triple clicks fire multiple createChat
190+
// calls → duplicate threads in the sidebar. The ref blocks re-entry synchronously (setState
191+
// is async); the state also disables the button. Only the non-navigating paths reset it —
192+
// a successful submit navigates away and unmounts this component.
193+
const [isSubmitting, setIsSubmitting] = useState(false);
194+
const submittingRef = useRef(false);
195+
const endSubmitting = useCallback(() => {
196+
submittingRef.current = false;
197+
setIsSubmitting(false);
198+
}, []);
189199
const typewriterPlaceholder = useTypewriterPlaceholder();
190200
const intent = INTENTS.find((candidate) => candidate.id === intentId) ?? null;
191201
const placeholder = intent ? intent.placeholder : typewriterPlaceholder;
192-
const canSubmit = value.trim().length > 0;
202+
const canSubmit = value.trim().length > 0 && !isSubmitting;
193203
const { data: userSkills } = useQuery({
194204
queryFn: () => listUserSkills(getToken),
195205
queryKey: USER_SKILLS_QUERY,
@@ -218,9 +228,11 @@ export function HomeComposer({
218228

219229
function submit(event?: FormEvent<HTMLFormElement>) {
220230
event?.preventDefault();
221-
if (!canSubmit) {
231+
if (!canSubmit || submittingRef.current) {
222232
return;
223233
}
234+
submittingRef.current = true;
235+
setIsSubmitting(true);
224236
const trimmed = value.trim();
225237
const skill = resolveSubmitSkill(repoUrl, intent, skillChip);
226238
const prompt = composePromptWithComposerContext({
@@ -242,12 +254,14 @@ export function HomeComposer({
242254
toast.error(
243255
"That project's latest thread is busy - wait for the run to finish or pick another project.",
244256
);
257+
endSubmitting();
245258
return;
246259
}
247260
const handoff = buildExistingProjectParams(prompt).toString();
248261
router.push(`/chats/${encodeURIComponent(result.threadId)}?${handoff}`);
249262
} catch (error) {
250263
toast.error(error instanceof Error ? error.message : "Could not open that project.");
264+
endSubmitting();
251265
}
252266
}
253267

@@ -314,6 +328,7 @@ export function HomeComposer({
314328
// authenticated chat create.
315329
const params = buildLaunchParams({ model, prompt, repo: repoUrl, surface });
316330
setAuthRedirectTo(`/?${params.toString()}`);
331+
endSubmitting();
317332
return;
318333
}
319334
try {
@@ -328,6 +343,7 @@ export function HomeComposer({
328343
router.push(`/chats/${encodeURIComponent(thread.id)}?${handoff}`);
329344
} catch (error) {
330345
toast.error(error instanceof Error ? error.message : "Could not start that chat.");
346+
endSubmitting();
331347
}
332348
}
333349

apps/web/src/components/onboarding/onboarding-flow.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export function OnboardingFlow() {
8989
onboardingStep: { status: planStatus, step: "plan" },
9090
});
9191
await user?.reload();
92+
// Force a fresh session token so the middleware's `metadata.onboarding_complete` claim
93+
// reflects the just-set public metadata — otherwise navigating bounces back to /onboarding
94+
// on the stale JWT.
95+
await getToken({ skipCache: true });
9296
} catch {
9397
setPhase("retry");
9498
return;
@@ -99,7 +103,7 @@ export function OnboardingFlow() {
99103
setPhase("retry");
100104
}
101105
},
102-
[mutateAsync, router, user],
106+
[getToken, mutateAsync, router, user],
103107
);
104108

105109
useEffect(() => {

0 commit comments

Comments
 (0)