diff --git a/apps/daemon/src/agent-os.ts b/apps/daemon/src/agent-os.ts index 1e1dcfb..6a68f62 100644 --- a/apps/daemon/src/agent-os.ts +++ b/apps/daemon/src/agent-os.ts @@ -43,6 +43,9 @@ export const makeAgentOsRuntime = Effect.gen(function* () { catch: (cause) => new AgentOsCommandError({ method: command.method, cause }), }).pipe( + Effect.map((result) => + result instanceof Uint8Array ? result.slice().buffer : result + ), Effect.tap((result) => controlPlane.recordEvent({ commandId: command._id, diff --git a/apps/native/app/(auth)/_layout.tsx b/apps/native/app/(auth)/_layout.tsx new file mode 100644 index 0000000..5b9eb40 --- /dev/null +++ b/apps/native/app/(auth)/_layout.tsx @@ -0,0 +1,5 @@ +import { Stack } from "expo-router"; + +export default function AuthLayout() { + return ; +} diff --git a/apps/native/app/(auth)/login.tsx b/apps/native/app/(auth)/login.tsx new file mode 100644 index 0000000..917cacb --- /dev/null +++ b/apps/native/app/(auth)/login.tsx @@ -0,0 +1,24 @@ +import { NativeLoginForm } from "@code/auth/native"; +import { router } from "expo-router"; +import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native"; + +export default function LoginScreen() { + return ( + + + + router.push("/(auth)/signup")} + onSuccess={() => router.replace("/")} + /> + + + + ); +} diff --git a/apps/native/app/(auth)/signup.tsx b/apps/native/app/(auth)/signup.tsx new file mode 100644 index 0000000..5122015 --- /dev/null +++ b/apps/native/app/(auth)/signup.tsx @@ -0,0 +1,24 @@ +import { NativeSignupForm } from "@code/auth/native"; +import { router } from "expo-router"; +import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native"; + +export default function SignupScreen() { + return ( + + + + router.push("/(auth)/login")} + onSuccess={() => router.replace("/")} + /> + + + + ); +} diff --git a/apps/native/app/(drawer)/index.tsx b/apps/native/app/(drawer)/index.tsx index 7752be5..3ff886d 100644 --- a/apps/native/app/(drawer)/index.tsx +++ b/apps/native/app/(drawer)/index.tsx @@ -1,72 +1,53 @@ +import { authClient } from "@code/auth/native"; import { api } from "@code/backend/convex/_generated/api"; -import { useConvexAuth, useQuery } from "convex/react"; -import { Button, Chip, Separator, Spinner, Surface, useThemeColor } from "heroui-native"; +import { useQuery } from "convex/react"; +import { Button, Spinner, Surface } from "heroui-native"; import { Text, View } from "react-native"; import { Container } from "@/components/container"; -import { SignIn } from "@/components/sign-in"; -import { SignUp } from "@/components/sign-up"; -import { authClient } from "@/lib/auth-client"; export default function Home() { const healthCheck = useQuery(api.healthCheck.get); - const { isAuthenticated } = useConvexAuth(); - const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip"); - const successColor = useThemeColor("success"); - const dangerColor = useThemeColor("danger"); - - const isConnected = healthCheck === "OK"; - const isLoading = healthCheck === undefined; + const user = useQuery(api.auth.getCurrentUser); return ( - - - Better T Stack - - Full-stack TypeScript starter + + Zopu + Your authenticated workspace - {user ? ( - - + + {user === undefined ? ( + + ) : ( + - {user.name} - {user.email} + {user?.name ?? "Signed in"} + {user?.email} - - - ) : null} - - API Status + )} + + + + API status - + {healthCheck === undefined - ? "Checking..." + ? "Checking…" : healthCheck === "OK" - ? "Connected to API" - : "API Disconnected"} + ? "Connected to Convex" + : "Convex unavailable"} - {!user && ( - - - - - )} ); } diff --git a/apps/native/app/_layout.tsx b/apps/native/app/_layout.tsx index 28f2abd..d383d08 100644 --- a/apps/native/app/_layout.tsx +++ b/apps/native/app/_layout.tsx @@ -1,44 +1,53 @@ import "@/global.css"; -import { env } from "@code/env/native"; -import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; -import { ConvexReactClient } from "convex/react"; +import { NativeAuthProvider } from "@code/auth/native"; +import { useConvexAuth } from "convex/react"; import { Stack } from "expo-router"; -import { HeroUINativeProvider } from "heroui-native"; +import { HeroUINativeProvider, Spinner } from "heroui-native"; +import { View } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { AppThemeProvider } from "@/contexts/app-theme-context"; -import { authClient } from "@/lib/auth-client"; -export const unstable_settings = { - initialRouteName: "(drawer)", -}; +function RootNavigator() { + const { isAuthenticated, isLoading } = useConvexAuth(); -const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, { - unsavedChangesWarning: false, -}); + if (isLoading) { + return ( + + + + ); + } -function StackLayout() { return ( - - - + + + + + + + + ); } export default function Layout() { return ( - + - + - + ); } diff --git a/apps/native/components/sign-in.tsx b/apps/native/components/sign-in.tsx deleted file mode 100644 index 513ac9f..0000000 --- a/apps/native/components/sign-in.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useForm } from "@tanstack/react-form"; -import { - Button, - FieldError, - Input, - Label, - Spinner, - Surface, - TextField, - useToast, -} from "heroui-native"; -import { useRef } from "react"; -import { Text, TextInput, View } from "react-native"; -import z from "zod"; - -import { authClient } from "@/lib/auth-client"; - -const signInSchema = z.object({ - email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"), - password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"), -}); - -function getErrorMessage(error: unknown): string | null { - if (!error) return null; - - if (typeof error === "string") { - return error; - } - - if (Array.isArray(error)) { - for (const issue of error) { - const message = getErrorMessage(issue); - if (message) { - return message; - } - } - return null; - } - - if (typeof error === "object" && error !== null) { - const maybeError = error as { message?: unknown }; - if (typeof maybeError.message === "string") { - return maybeError.message; - } - } - - return null; -} - -export function SignIn() { - const passwordInputRef = useRef(null); - const { toast } = useToast(); - - const form = useForm({ - defaultValues: { - email: "", - password: "", - }, - validators: { - onSubmit: signInSchema, - }, - onSubmit: async ({ value, formApi }) => { - await authClient.signIn.email( - { - email: value.email.trim(), - password: value.password, - }, - { - onError(error) { - toast.show({ - variant: "danger", - label: error.error?.message || "Failed to sign in", - }); - }, - onSuccess() { - formApi.reset(); - toast.show({ - variant: "success", - label: "Signed in successfully", - }); - }, - }, - ); - }, - }); - - return ( - - Sign In - - ({ - isSubmitting: state.isSubmitting, - validationError: getErrorMessage(state.errorMap.onSubmit), - })} - > - {({ isSubmitting, validationError }) => { - const formError = validationError; - - return ( - <> - - {formError} - - - - - {(field) => ( - - - { - passwordInputRef.current?.focus(); - }} - /> - - )} - - - - {(field) => ( - - - - - )} - - - - - - ); - }} - - - ); -} diff --git a/apps/native/components/sign-up.tsx b/apps/native/components/sign-up.tsx deleted file mode 100644 index 22ed862..0000000 --- a/apps/native/components/sign-up.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useForm } from "@tanstack/react-form"; -import { - Button, - FieldError, - Input, - Label, - Spinner, - Surface, - TextField, - useToast, -} from "heroui-native"; -import { useRef } from "react"; -import { Text, TextInput, View } from "react-native"; -import z from "zod"; - -import { authClient } from "@/lib/auth-client"; - -const signUpSchema = z.object({ - name: z.string().trim().min(1, "Name is required").min(2, "Name must be at least 2 characters"), - email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"), - password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"), -}); - -function getErrorMessage(error: unknown): string | null { - if (!error) return null; - - if (typeof error === "string") { - return error; - } - - if (Array.isArray(error)) { - for (const issue of error) { - const message = getErrorMessage(issue); - if (message) { - return message; - } - } - return null; - } - - if (typeof error === "object" && error !== null) { - const maybeError = error as { message?: unknown }; - if (typeof maybeError.message === "string") { - return maybeError.message; - } - } - - return null; -} - -export function SignUp() { - const emailInputRef = useRef(null); - const passwordInputRef = useRef(null); - const { toast } = useToast(); - - const form = useForm({ - defaultValues: { - name: "", - email: "", - password: "", - }, - validators: { - onSubmit: signUpSchema, - }, - onSubmit: async ({ value, formApi }) => { - await authClient.signUp.email( - { - name: value.name.trim(), - email: value.email.trim(), - password: value.password, - }, - { - onError(error) { - toast.show({ - variant: "danger", - label: error.error?.message || "Failed to sign up", - }); - }, - onSuccess() { - formApi.reset(); - toast.show({ - variant: "success", - label: "Account created successfully", - }); - }, - }, - ); - }, - }); - - return ( - - Create Account - - ({ - isSubmitting: state.isSubmitting, - validationError: getErrorMessage(state.errorMap.onSubmit), - })} - > - {({ isSubmitting, validationError }) => { - const formError = validationError; - - return ( - <> - - {formError} - - - - - {(field) => ( - - - { - emailInputRef.current?.focus(); - }} - /> - - )} - - - - {(field) => ( - - - { - passwordInputRef.current?.focus(); - }} - /> - - )} - - - - {(field) => ( - - - - - )} - - - - - - ); - }} - - - ); -} diff --git a/apps/native/package.json b/apps/native/package.json index b3204a3..46dc7d1 100644 --- a/apps/native/package.json +++ b/apps/native/package.json @@ -12,24 +12,18 @@ "web": "bun --env-file=../../.env expo start --web" }, "dependencies": { - "@better-auth/expo": "catalog:", + "@code/auth": "workspace:*", "@code/backend": "workspace:*", - "@code/env": "workspace:*", - "@convex-dev/better-auth": "catalog:", "@expo/metro-runtime": "~57.0.2", "@expo/vector-icons": "^15.1.1", "@gorhom/bottom-sheet": "^5.2.14", - "@tanstack/react-form": "catalog:", - "better-auth": "catalog:", "convex": "catalog:", "expo": "~57.0.1", - "expo-constants": "~57.0.2", "expo-font": "~57.0.0", "expo-haptics": "~57.0.0", "expo-linking": "~57.0.1", "expo-network": "~57.0.0", "expo-router": "~57.0.2", - "expo-secure-store": "~57.0.0", "expo-status-bar": "~57.0.0", "expo-web-browser": "~57.0.0", "heroui-native": "^1.0.5", @@ -47,8 +41,7 @@ "tailwind-merge": "catalog:", "tailwind-variants": "^3.2.2", "tailwindcss": "catalog:", - "uniwind": "^1.10.0", - "zod": "catalog:" + "uniwind": "^1.10.0" }, "devDependencies": { "@code/config": "workspace:*", diff --git a/apps/web/.gitignore b/apps/web/.gitignore index f1cb16d..d12719f 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -40,7 +40,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* -*.log* +*.log +*.log.* # TypeScript *.tsbuildinfo diff --git a/apps/web/package.json b/apps/web/package.json index 8de23af..526e014 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,17 +10,15 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { + "@code/auth": "workspace:*", "@code/backend": "workspace:*", "@code/env": "workspace:*", "@code/ui": "workspace:*", - "@convex-dev/better-auth": "catalog:", "@flue/react": "1.0.0-beta.9", "@flue/sdk": "1.0.0-beta.9", "@react-router/fs-routes": "^8.1.0", "@react-router/node": "^8.1.0", "@react-router/serve": "^8.1.0", - "@tanstack/react-form": "catalog:", - "better-auth": "catalog:", "convex": "catalog:", "isbot": "^5.1.44", "lucide-react": "catalog:", @@ -29,8 +27,7 @@ "react-dom": "^19.2.7", "react-router": "^8.1.0", "sonner": "catalog:", - "streamdown": "2.5.0", - "zod": "catalog:" + "streamdown": "2.5.0" }, "devDependencies": { "@code/config": "workspace:*", diff --git a/apps/web/src/components/projects/project-workspace-page.tsx b/apps/web/src/components/projects/project-workspace-page.tsx new file mode 100644 index 0000000..8aa7ed7 --- /dev/null +++ b/apps/web/src/components/projects/project-workspace-page.tsx @@ -0,0 +1,445 @@ +import type { Doc, Id } from "@code/backend/convex/_generated/dataModel"; +import { Button } from "@code/ui/components/button"; +import { Input } from "@code/ui/components/input"; +import { Textarea } from "@code/ui/components/textarea"; +import { + Bot, + ExternalLink, + FileText, + GitFork, + GitPullRequest, + Play, + Plus, +} from "lucide-react"; + +import UserMenu from "@/components/user-menu"; +import { useProjectWorkspace } from "@/hooks/use-project-workspace"; + +const statusStyle: Record["status"], string> = { + completed: + "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", + failed: "border-destructive/40 bg-destructive/10 text-destructive", + "needs-input": + "border-orange-500/40 bg-orange-500/10 text-orange-700 dark:text-orange-300", + open: "border-border text-muted-foreground", + queued: + "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300", + working: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300", +}; + +interface RepositoryFormProps { + readonly busy: boolean; + readonly onChange: (value: string) => void; + readonly onSubmit: () => Promise; + readonly value: string; +} + +const RepositoryForm = ({ + busy, + onChange, + onSubmit, + value, +}: RepositoryFormProps) => ( +
{ + event.preventDefault(); + void onSubmit(); + }} + > +
+ +

+ Connect with an owner/name slug or repository URL. +

+
+ onChange(event.target.value)} + placeholder="rivet-dev/rivet" + required + value={value} + /> + +
+); + +interface ProjectListProps { + readonly projects: readonly Doc<"projects">[] | undefined; + readonly selectedId: Id<"projects"> | null; + readonly onSelect: (projectId: Id<"projects">) => void; +} + +const ProjectList = ({ projects, selectedId, onSelect }: ProjectListProps) => { + const renderContent = () => { + if (projects === undefined) { + return ( +

Loading projects...

+ ); + } + if (projects.length === 0) { + return ( +

+ Connect a repository to create its project artifacts and issue queue. +

+ ); + } + return ( +
+ {projects.map((project) => ( + + ))} +
+ ); + }; + + return ( + + ); +}; + +interface ArtifactGridProps { + readonly artifacts: readonly Doc<"projectArtifacts">[] | undefined; +} + +const ArtifactGrid = ({ artifacts }: ArtifactGridProps) => ( +
+
+

+ Project artifacts +

+

+ Canonical context staged into every issue workspace. +

+
+ {artifacts === undefined ? ( +

Loading artifacts...

+ ) : ( +
+ {artifacts.map((artifact) => ( +
+
+
+ +

+ {artifact.path} +

+
+ + r{artifact.revision} + +
+

+ {artifact.content} +

+
+ ))} +
+ )} +
+); + +interface IssueComposerProps { + readonly body: string; + readonly busy: boolean; + readonly onBodyChange: (value: string) => void; + readonly onSubmit: () => Promise; + readonly onTitleChange: (value: string) => void; + readonly title: string; +} + +const IssueComposer = ({ + body, + busy, + onBodyChange, + onSubmit, + onTitleChange, + title, +}: IssueComposerProps) => ( +
{ + event.preventDefault(); + void onSubmit(); + }} + > +
+

Raise an issue

+

+ The issue becomes the identity for one Flue agent and AgentOS workspace. +

+
+
+ + onTitleChange(event.target.value)} + required + value={title} + /> +
+
+ +