From a9c060c53278183c28fee0a1711a40120a905031 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Fri, 28 Aug 2026 11:12:07 -0300 Subject: [PATCH 1/8] Gate first sign-in behind a per-user onboarding wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two columns on users say where somebody is in first-run onboarding and when they finished; /api/me carries the status and one POST moves it. The app redirects an unfinished user to /onboarding — an animated three-step wizard — and an address the build does not know now goes home through the same gate instead of a bare 404. --- app/package.json | 2 + .../components/channels/composer/composer.tsx | 23 +- .../computer/desktop-illustration.tsx | 275 ++ app/src/lib/auth/queries.ts | 18 + app/src/lib/onboarding/mutations.ts | 48 + app/src/routeTree.gen.ts | 21 + app/src/routes/__root.tsx | 12 +- app/src/routes/_authed.tsx | 12 +- app/src/routes/_authed/onboarding.tsx | 220 ++ bun.lock | 8 + server/drizzle/0015_onboarding.sql | 2 + ...backfill_existing_users_have_onboarded.sql | 10 + server/drizzle/meta/0015_snapshot.json | 2530 +++++++++++++++++ server/drizzle/meta/_journal.json | 16 +- server/src/app.ts | 60 +- server/src/db/schema/core.ts | 11 + server/src/index.ts | 3 + server/tests/guards.test.ts | 2 + server/tests/onboarding-routes.test.ts | 157 + 19 files changed, 3419 insertions(+), 11 deletions(-) create mode 100644 app/src/components/computer/desktop-illustration.tsx create mode 100644 app/src/lib/onboarding/mutations.ts create mode 100644 app/src/routes/_authed/onboarding.tsx create mode 100644 server/drizzle/0015_onboarding.sql create mode 100644 server/drizzle/0016_backfill_existing_users_have_onboarded.sql create mode 100644 server/drizzle/meta/0015_snapshot.json create mode 100644 server/tests/onboarding-routes.test.ts diff --git a/app/package.json b/app/package.json index d9cf9783..8c81b04e 100644 --- a/app/package.json +++ b/app/package.json @@ -21,6 +21,7 @@ "@shadcn/react": "^0.3.0", "@tabler/icons-react": "^3.36.1", "@tanstack/react-form": "^1.33.5", + "@tanstack/react-hotkeys": "^0.10.0", "@tanstack/react-query": "^5.101.4", "@tanstack/react-router": "^1.170.27", "better-auth": "^1.6.27", @@ -31,6 +32,7 @@ "prompt-area": "^0.6.3", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-use-measure": "^2.1.7", "shadcn": "^4.17.0", "streamdown": "^2.5.0", "tailwind-merge": "^3.6.0", diff --git a/app/src/components/channels/composer/composer.tsx b/app/src/components/channels/composer/composer.tsx index 0c4781b2..1d36f139 100644 --- a/app/src/components/channels/composer/composer.tsx +++ b/app/src/components/channels/composer/composer.tsx @@ -34,6 +34,12 @@ const COMPACT_MAX_HEIGHT_PX = 96; export type ComposerProps = { className?: string; + /** + * Classes for the editor itself rather than the frame. `className` styles the box — border, + * background, width; the type inside it is PromptArea's, so changing it (a hero composer's + * `text-lg`) goes through here, where tailwind-merge lets it beat the built-in `text-sm`. + */ + editorClassName?: string; compact?: boolean; /** Agents that `@` can address. Empty means the mention menu reports an empty channel. */ agents?: readonly AgentOption[]; @@ -83,10 +89,12 @@ export type ComposerProps = { * Defaults to `pending`, which is the right answer for a caller with no gap between the two. */ stoppable?: boolean; + initialValue?: string; }; export function Composer({ className, + editorClassName, compact = false, agents = [], commands = PLACEHOLDER_COMMANDS, @@ -96,8 +104,11 @@ export function Composer({ disabled = false, pending = false, stoppable, + initialValue, }: ComposerProps) { - const [value, setValue] = useState([]); + const [value, setValue] = useState( + initialValue ? [{ type: "text", text: initialValue }] : [], + ); const [isSubmitting, setIsSubmitting] = useState(false); const submitInFlight = useRef(false); const promptAreaRef = useRef(null); @@ -268,7 +279,10 @@ export function Composer({