├── packages
├── ai
│ ├── index.ts
│ ├── lib
│ │ ├── react.ts
│ │ └── models.ts
│ ├── tsconfig.json
│ ├── package.json
│ └── keys.ts
├── database
│ ├── .env.example
│ ├── migrations
│ │ ├── 0000_chunky_ozymandias.sql
│ │ └── meta
│ │ │ ├── _journal.json
│ │ │ └── 0000_snapshot.json
│ ├── schema.ts
│ ├── tsconfig.json
│ ├── drizzle.config.ts
│ ├── keys.ts
│ ├── index.ts
│ └── package.json
├── storage
│ ├── index.ts
│ ├── client.ts
│ ├── tsconfig.json
│ ├── keys.ts
│ └── package.json
├── design-system
│ ├── postcss.config.mjs
│ ├── lib
│ │ ├── fonts.ts
│ │ └── utils.ts
│ ├── components
│ │ ├── ui
│ │ │ ├── aspect-ratio.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── input.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── resizable.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── toggle-group.tsx
│ │ │ ├── accordion.tsx
│ │ │ ├── button.tsx
│ │ │ └── card.tsx
│ │ └── ai-elements
│ │ │ ├── response.tsx
│ │ │ ├── image.tsx
│ │ │ ├── mode-toggle.tsx
│ │ │ ├── suggestion.tsx
│ │ │ ├── actions.tsx
│ │ │ ├── message.tsx
│ │ │ ├── conversation.tsx
│ │ │ └── sources.tsx
│ ├── tsconfig.json
│ ├── providers
│ │ └── theme.tsx
│ ├── index.tsx
│ ├── components.json
│ └── hooks
│ │ └── use-mobile.ts
├── testing
│ ├── tsconfig.json
│ ├── package.json
│ └── index.js
├── next-config
│ ├── tsconfig.json
│ ├── package.json
│ ├── keys.ts
│ └── index.ts
└── typescript-config
│ ├── react-library.json
│ ├── package.json
│ ├── nextjs.json
│ └── base.json
├── turbo
└── generators
│ ├── package.json
│ ├── templates
│ ├── package.json.hbs
│ └── tsconfig.json.hbs
│ └── config.ts
├── public
├── favicon.ico
├── obby-dev.png
├── splash-image.png
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── logos
│ ├── ai
│ │ ├── obbylabs.webp
│ │ ├── anthropic.svg
│ │ ├── groq.svg
│ │ ├── xai.svg
│ │ ├── togetherai.svg
│ │ ├── google.svg
│ │ ├── fireworks.svg
│ │ ├── vertex.svg
│ │ ├── fireworksai.svg
│ │ ├── openai.svg
│ │ ├── deepseek.svg
│ │ └── mistral.svg
│ ├── obby
│ │ ├── obby-logo.png
│ │ ├── obby-logo.webp
│ │ ├── obby-logo-dark.png
│ │ ├── obby-logo-min.webp
│ │ ├── obby-logo-dark.webp
│ │ ├── obby-logo-transparent.png
│ │ └── obby-logo-transparent.webp
│ └── vercel_logo.svg
├── android-chrome-192x192.png
├── android-chrome-512x512.png
└── site.webmanifest
├── apps
├── obby
│ ├── postcss.config.mjs
│ ├── app
│ │ ├── icon.png
│ │ ├── favicon.ico
│ │ ├── styles.css
│ │ ├── apple-icon.png
│ │ ├── opengraph-image.png
│ │ ├── (main)
│ │ │ ├── preview.tsx
│ │ │ ├── file-explorer.tsx
│ │ │ ├── logs.tsx
│ │ │ ├── header.tsx
│ │ │ └── page.tsx
│ │ ├── global-error.tsx
│ │ ├── layout.tsx
│ │ └── api
│ │ │ └── sandboxes
│ │ │ └── [sandboxId]
│ │ │ ├── cmds
│ │ │ └── [cmdId]
│ │ │ │ └── route.ts
│ │ │ ├── route.ts
│ │ │ └── files
│ │ │ └── route.ts
│ ├── markdown.d.ts
│ ├── public
│ │ ├── obby-logo-min.webp
│ │ ├── vercel.svg
│ │ ├── window.svg
│ │ ├── file.svg
│ │ ├── providers
│ │ │ ├── v0.svg
│ │ │ ├── groq.svg
│ │ │ ├── vercel.svg
│ │ │ ├── gemini.svg
│ │ │ ├── anthropic.svg
│ │ │ ├── openrouter.svg
│ │ │ └── openai.svg
│ │ ├── globe.svg
│ │ └── next.svg
│ ├── components
│ │ ├── model-selector
│ │ │ ├── model-selector.tsx
│ │ │ ├── model-selector-cmdk.tsx
│ │ │ ├── cmdk
│ │ │ │ ├── api-key-section.tsx
│ │ │ │ ├── use-provider-key.tsx
│ │ │ │ ├── model-section.tsx
│ │ │ │ └── provider-sidebar.tsx
│ │ │ └── use-available-models.tsx
│ │ ├── tabs
│ │ │ ├── index.tsx
│ │ │ ├── use-tab-state.ts
│ │ │ ├── tab-content.tsx
│ │ │ ├── tab-group.tsx
│ │ │ └── tab-item.tsx
│ │ ├── chat
│ │ │ ├── message-spinner.tsx
│ │ │ ├── types.tsx
│ │ │ ├── message-part
│ │ │ │ ├── text.tsx
│ │ │ │ ├── reasoning.tsx
│ │ │ │ ├── get-sandbox-url.tsx
│ │ │ │ ├── create-sandbox.tsx
│ │ │ │ ├── run-command.tsx
│ │ │ │ ├── wait-command.tsx
│ │ │ │ ├── generate-files.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── tool-header.tsx
│ │ │ ├── tool-message.tsx
│ │ │ └── message.tsx
│ │ ├── commands-logs
│ │ │ ├── types.ts
│ │ │ └── commands-logs.tsx
│ │ ├── icons
│ │ │ ├── obby-logo.tsx
│ │ │ └── github.tsx
│ │ ├── panels
│ │ │ └── panels.tsx
│ │ ├── file-explorer
│ │ │ ├── file-content.tsx
│ │ │ └── build-file-tree.tsx
│ │ └── banner.tsx
│ ├── vercel.json
│ ├── ai
│ │ ├── messages
│ │ │ ├── metadata.ts
│ │ │ └── data-parts.ts
│ │ ├── tools
│ │ │ ├── generate-files-prompt.md
│ │ │ ├── get-sandbox-url.ts
│ │ │ ├── index.ts
│ │ │ ├── wait-command.ts
│ │ │ └── wait-command.md
│ │ └── validation.ts
│ ├── instrumentation-client.ts
│ ├── lib
│ │ ├── is-relative-url.ts
│ │ ├── use-local-storage-value.ts
│ │ ├── use-index-db-storage.ts
│ │ ├── logger.ts
│ │ └── indexed-db-adapter.ts
│ ├── tsconfig.json
│ ├── next.config.ts
│ ├── scripts
│ │ └── skip-ci.js
│ ├── env.ts
│ ├── .env.example
│ ├── stores
│ │ ├── use-provider-keys-store.ts
│ │ └── use-model-store.ts
│ └── package.json
└── web
│ ├── app
│ ├── icon.png
│ ├── opengraph-image.png
│ ├── global.css
│ ├── api
│ │ └── search
│ │ │ └── route.ts
│ ├── (home)
│ │ └── layout.tsx
│ ├── docs
│ │ ├── layout.tsx
│ │ └── [[...slug]]
│ │ │ └── page.tsx
│ └── layout.tsx
│ ├── postcss.config.mjs
│ ├── public
│ └── obby-logo-min.webp
│ ├── next.config.mjs
│ ├── lib
│ ├── source.ts
│ └── layout.shared.tsx
│ ├── content
│ └── docs
│ │ ├── test.mdx
│ │ └── meta.json
│ ├── mdx-components.tsx
│ ├── source.config.ts
│ ├── package.json
│ ├── tsconfig.json
│ ├── .source
│ └── index.ts
│ └── README.md
├── SECURITY.md
├── tsup.config.ts
├── .vscode
├── extensions.json
├── settings.json
└── launch.json
├── pnpm-workspace.yaml
├── .github
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── pull_request_template.md
└── copilot-instructions.md.example
├── tsconfig.json
├── scripts
├── index.ts
└── utils.ts
├── turbo.json
├── biome.jsonc
├── README.md
├── LICENSE
├── .zed
└── settings.json
└── package.json
/packages/ai/index.ts:
--------------------------------------------------------------------------------
1 | export * from 'ai';
2 |
--------------------------------------------------------------------------------
/packages/database/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=""
--------------------------------------------------------------------------------
/packages/ai/lib/react.ts:
--------------------------------------------------------------------------------
1 | export * from 'ai/react';
2 |
--------------------------------------------------------------------------------
/packages/storage/index.ts:
--------------------------------------------------------------------------------
1 | export * from '@vercel/blob';
2 |
--------------------------------------------------------------------------------
/packages/storage/client.ts:
--------------------------------------------------------------------------------
1 | export * from '@vercel/blob/client';
2 |
--------------------------------------------------------------------------------
/turbo/generators/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "commonjs"
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/apps/obby/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | export { default } from '@repo/design-system/postcss.config.mjs';
2 |
--------------------------------------------------------------------------------
/apps/web/app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/web/app/icon.png
--------------------------------------------------------------------------------
/public/obby-dev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/obby-dev.png
--------------------------------------------------------------------------------
/apps/obby/app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/obby/app/icon.png
--------------------------------------------------------------------------------
/public/splash-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/splash-image.png
--------------------------------------------------------------------------------
/apps/obby/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/obby/app/favicon.ico
--------------------------------------------------------------------------------
/apps/obby/app/styles.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "@repo/design-system/styles/globals.css";
3 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/apps/obby/app/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/obby/app/apple-icon.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/apps/obby/markdown.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.md' {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/public/logos/ai/obbylabs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/ai/obbylabs.webp
--------------------------------------------------------------------------------
/public/logos/obby/obby-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/obby/obby-logo.png
--------------------------------------------------------------------------------
/apps/obby/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/obby/app/opengraph-image.png
--------------------------------------------------------------------------------
/apps/web/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/web/app/opengraph-image.png
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/logos/obby/obby-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/obby/obby-logo.webp
--------------------------------------------------------------------------------
/apps/obby/public/obby-logo-min.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/obby/public/obby-logo-min.webp
--------------------------------------------------------------------------------
/apps/web/public/obby-logo-min.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/apps/web/public/obby-logo-min.webp
--------------------------------------------------------------------------------
/public/logos/obby/obby-logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/obby/obby-logo-dark.png
--------------------------------------------------------------------------------
/public/logos/obby/obby-logo-min.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/obby/obby-logo-min.webp
--------------------------------------------------------------------------------
/public/logos/obby/obby-logo-dark.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/obby/obby-logo-dark.webp
--------------------------------------------------------------------------------
/apps/obby/components/model-selector/model-selector.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | export { ModelSelector } from './select/model-selector';
4 |
--------------------------------------------------------------------------------
/apps/obby/components/tabs/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './tab-content';
2 | export * from './tab-group';
3 | export * from './tab-item';
4 |
--------------------------------------------------------------------------------
/apps/web/app/global.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @import 'fumadocs-ui/css/neutral.css';
3 | @import 'fumadocs-ui/css/preset.css';
4 |
--------------------------------------------------------------------------------
/apps/obby/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://openapi.vercel.sh/vercel.json",
3 | "ignoreCommand": "node scripts/skip-ci.js"
4 | }
5 |
--------------------------------------------------------------------------------
/public/logos/obby/obby-logo-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/obby/obby-logo-transparent.png
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | ## Reporting a Vulnerability
2 |
3 | Please reach out to security@obby.dev with any security concerns or vulnerability reporting.
4 |
--------------------------------------------------------------------------------
/public/logos/obby/obby-logo-transparent.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eersnington/obby-dev/HEAD/public/logos/obby/obby-logo-transparent.webp
--------------------------------------------------------------------------------
/apps/obby/components/model-selector/model-selector-cmdk.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | export { ModelSelectorModal } from './cmdk/model-selector-modal';
4 |
--------------------------------------------------------------------------------
/packages/database/migrations/0000_chunky_ozymandias.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE "page" (
2 | "id" serial PRIMARY KEY NOT NULL,
3 | "name" varchar(255)
4 | );
5 |
--------------------------------------------------------------------------------
/apps/obby/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/obby/ai/messages/metadata.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod/v3';
2 |
3 | export const metadataSchema = z.object({
4 | model: z.string(),
5 | });
6 |
7 | export type Metadata = z.infer;
8 |
--------------------------------------------------------------------------------
/packages/design-system/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | '@tailwindcss/postcss': {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/apps/obby/instrumentation-client.ts:
--------------------------------------------------------------------------------
1 | import { initBotId } from 'botid/client/core';
2 |
3 | initBotId({
4 | protect: [
5 | {
6 | path: '/api/chat',
7 | method: 'POST',
8 | },
9 | ],
10 | });
11 |
--------------------------------------------------------------------------------
/packages/ai/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": "."
5 | },
6 | "include": ["**/*.ts", "**/*.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/testing/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": "."
5 | },
6 | "include": ["./src/*.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/database/schema.ts:
--------------------------------------------------------------------------------
1 | import { pgTable, serial, varchar } from 'drizzle-orm/pg-core';
2 |
3 | export const page = pgTable('page', {
4 | id: serial('id').primaryKey(),
5 | name: varchar('name', { length: 255 }),
6 | });
7 |
--------------------------------------------------------------------------------
/packages/storage/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": "."
5 | },
6 | "include": ["**/*.ts", "**/*.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/database/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": "."
5 | },
6 | "include": ["**/*.ts", "**/*.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/next-config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": "."
5 | },
6 | "include": ["**/*.ts", "**/*.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/obby/components/tabs/use-tab-state.ts:
--------------------------------------------------------------------------------
1 | import { useQueryState } from 'nuqs';
2 |
3 | export function useTabState() {
4 | const [tabId, setTabId] = useQueryState('tab', { defaultValue: 'chat' });
5 | return [tabId, setTabId] as const;
6 | }
7 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | entry: ['scripts/index.ts'],
5 | outDir: 'dist',
6 | sourcemap: false,
7 | minify: true,
8 | dts: true,
9 | format: ['cjs', 'esm'],
10 | });
11 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "biomejs.biome",
4 | "bradlc.vscode-tailwindcss",
5 | "Prisma.prisma",
6 | "unifiedjs.vscode-mdx",
7 | "mikestead.dotenv",
8 | "christian-kohler.npm-intellisense"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/web/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { createMDX } from 'fumadocs-mdx/next';
2 |
3 | const withMDX = createMDX();
4 |
5 | /** @type {import('next').NextConfig} */
6 | const config = {
7 | typedRoutes: true,
8 | reactStrictMode: true,
9 | };
10 |
11 | export default withMDX(config);
12 |
--------------------------------------------------------------------------------
/apps/web/app/api/search/route.ts:
--------------------------------------------------------------------------------
1 | import { createFromSource } from 'fumadocs-core/search/server';
2 | import { source } from '@/lib/source';
3 |
4 | export const { GET } = createFromSource(source, {
5 | // https://docs.orama.com/docs/orama-js/supported-languages
6 | language: 'english',
7 | });
8 |
--------------------------------------------------------------------------------
/turbo/generators/templates/package.json.hbs:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/{{ name }}",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "clean": "git clean -xdf .cache .turbo dist node_modules",
7 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "clean": "git clean -xdf .cache .turbo dist node_modules",
7 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-spinner.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { PulseLoader } from 'react-spinners';
3 |
4 | export function MessageSpinner({ className }: { className?: string }) {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/types.tsx:
--------------------------------------------------------------------------------
1 | import type { UIMessage } from 'ai';
2 | import type { DataPart } from '@/ai/messages/data-parts';
3 | import type { Metadata } from '@/ai/messages/metadata';
4 | import type { ToolSet } from '@/ai/tools';
5 |
6 | export type ChatUIMessage = UIMessage;
7 |
--------------------------------------------------------------------------------
/packages/database/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'drizzle-kit';
2 | import { keys } from './keys';
3 |
4 | export default defineConfig({
5 | schema: './schema.ts',
6 | out: './migrations',
7 | dialect: 'postgresql',
8 | dbCredentials: {
9 | url: keys().DATABASE_URL,
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/packages/database/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7",
3 | "dialect": "postgresql",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "7",
8 | "when": 1754634627346,
9 | "tag": "0000_chunky_ozymandias",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/public/logos/ai/anthropic.svg:
--------------------------------------------------------------------------------
1 | Anthropic
2 |
--------------------------------------------------------------------------------
/packages/database/keys.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from '@t3-oss/env-nextjs';
2 | import { z } from 'zod';
3 |
4 | export const keys = () =>
5 | createEnv({
6 | server: {
7 | DATABASE_URL: z.string().url(),
8 | },
9 | runtimeEnv: {
10 | DATABASE_URL: process.env.DATABASE_URL,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/packages/design-system/lib/fonts.ts:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { GeistMono } from 'geist/font/mono';
3 | import { GeistSans } from 'geist/font/sans';
4 |
5 | export const fonts = cn(
6 | GeistSans.variable,
7 | GeistMono.variable,
8 | 'touch-manipulation font-sans antialiased'
9 | );
10 |
--------------------------------------------------------------------------------
/apps/web/app/(home)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { HomeLayout } from 'fumadocs-ui/layouts/home';
2 | import type { ReactNode } from 'react';
3 | import { baseOptions } from '@/lib/layout.shared';
4 |
5 | export default function Layout({ children }: { children: ReactNode }) {
6 | return {children} ;
7 | }
8 |
--------------------------------------------------------------------------------
/apps/web/lib/source.ts:
--------------------------------------------------------------------------------
1 | import { docs } from '@/.source';
2 | import { loader } from 'fumadocs-core/source';
3 |
4 | // See https://fumadocs.vercel.app/docs/headless/source-api for more info
5 | export const source = loader({
6 | // it assigns a URL to your pages
7 | baseUrl: '/docs',
8 | source: docs.toFumadocsSource(),
9 | });
10 |
--------------------------------------------------------------------------------
/turbo/generators/templates/tsconfig.json.hbs:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./*"],
7 | "@repo/*": ["../../packages/*"]
8 | }
9 | },
10 | "include": ["**/*.ts", "**/*.tsx"],
11 | "exclude": ["node_modules"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/database/index.ts:
--------------------------------------------------------------------------------
1 | import 'server-only';
2 |
3 | import { neon } from '@neondatabase/serverless';
4 | import { drizzle } from 'drizzle-orm/neon-http';
5 | import { keys } from './keys';
6 |
7 | export { sql } from 'drizzle-orm';
8 |
9 | const client = neon(keys().DATABASE_URL);
10 |
11 | export const database = drizzle({ client });
12 |
--------------------------------------------------------------------------------
/packages/ai/lib/models.ts:
--------------------------------------------------------------------------------
1 | import { createOpenAI } from '@ai-sdk/openai';
2 | import { keys } from '../keys';
3 |
4 | const openai = createOpenAI({
5 | apiKey: keys().OPENAI_API_KEY,
6 | compatibility: 'strict',
7 | });
8 |
9 | export const models = {
10 | chat: openai('gpt-4o-mini'),
11 | embeddings: openai('text-embedding-3-small'),
12 | };
13 |
--------------------------------------------------------------------------------
/packages/storage/keys.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from '@t3-oss/env-nextjs';
2 | import { z } from 'zod';
3 |
4 | export const keys = () =>
5 | createEnv({
6 | server: {
7 | BLOB_READ_WRITE_TOKEN: z.string().optional(),
8 | },
9 | runtimeEnv: {
10 | BLOB_READ_WRITE_TOKEN: process.env.BLOB_READ_WRITE_TOKEN,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/apps/obby/components/commands-logs/types.ts:
--------------------------------------------------------------------------------
1 | export type Command = {
2 | sandboxId: string;
3 | cmdId: string;
4 | startedAt: number;
5 | command: string;
6 | args: string[];
7 | exitCode?: number;
8 | logs?: CommandLog[];
9 | };
10 |
11 | export type CommandLog = {
12 | data: string;
13 | stream: 'stdout' | 'stderr';
14 | timestamp: number;
15 | };
16 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | function AspectRatio({
6 | ...props
7 | }: React.ComponentProps) {
8 | return
9 | }
10 |
11 | export { AspectRatio }
12 |
--------------------------------------------------------------------------------
/apps/obby/components/icons/obby-logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | type Props = {
4 | className?: string;
5 | };
6 |
7 | export function ObbyLogo({ className }: Props) {
8 | return (
9 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/apps/obby/lib/is-relative-url.ts:
--------------------------------------------------------------------------------
1 | import { logger } from './logger';
2 |
3 | export function isRelativeUrl(url: string): boolean {
4 | try {
5 | new URL(url);
6 | return false;
7 | } catch (error: unknown) {
8 | const message = error instanceof Error ? error.message : String(error);
9 | logger.error('Error parsing URL', message);
10 | return true;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/content/docs/test.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Components
3 | description: Components
4 | ---
5 |
6 | ## Code Block
7 |
8 | ```js
9 | console.log('Hello World');
10 | ```
11 |
12 | ## Cards
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/apps/web/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import defaultMdxComponents from 'fumadocs-ui/mdx';
2 | import type { MDXComponents } from 'mdx/types';
3 |
4 | // use this function to get MDX components, you will need it for rendering MDX
5 | export function getMDXComponents(components?: MDXComponents): MDXComponents {
6 | return {
7 | ...defaultMdxComponents,
8 | ...components,
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/apps/obby/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/obby/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./*"],
7 | "@repo/*": ["../../packages/*"]
8 | }
9 | },
10 | "include": [
11 | "next-env.d.ts",
12 | "next.config.ts",
13 | "**/*.ts",
14 | "**/*.tsx",
15 | ".next/types/**/*.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@repo/design-system/lib/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/apps/obby/next.config.ts:
--------------------------------------------------------------------------------
1 | import { config, withAnalyzer } from '@repo/next-config';
2 | import { withBotId } from 'botid/next/config';
3 | import type { NextConfig } from 'next';
4 |
5 | import { env } from '@/env';
6 |
7 | let nextConfig: NextConfig = config;
8 |
9 | if (env.ANALYZE === 'true') {
10 | nextConfig = withAnalyzer(nextConfig);
11 | }
12 |
13 | export default withBotId(nextConfig);
14 |
--------------------------------------------------------------------------------
/apps/obby/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/obby/scripts/skip-ci.js:
--------------------------------------------------------------------------------
1 | const { execSync } = require('child_process');
2 |
3 | const commitMessage = execSync('git log -1 --pretty=%B').toString().trim();
4 |
5 | if (commitMessage.includes('[skip ci]')) {
6 | console.log('Skipping build due to [skip ci] in commit message.');
7 | process.exit(0); // this causes Vercel to skip the build
8 | }
9 |
10 | process.exit(1); // continue with build
11 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - apps/*
3 | - packages/*
4 | overrides:
5 | path-to-regexp@>=4.0.0 <6.3.0: '>=6.3.0'
6 | '@octokit/request-error@>=1.0.0 <5.1.1': '>=5.1.1'
7 | esbuild@<=0.24.2: '>=0.25.0'
8 | '@octokit/request@>=1.0.0 <8.4.1': '>=8.4.1'
9 | '@octokit/plugin-paginate-rest@>=1.0.0 <9.2.2': '>=9.2.2'
10 | cookie@<0.7.0: '>=0.7.0'
11 | prismjs@<1.30.0: '>=1.30.0'
12 | tmp@<=0.2.3: '>=0.2.4'
13 |
--------------------------------------------------------------------------------
/packages/design-system/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "plugins": [
6 | {
7 | "name": "next"
8 | }
9 | ],
10 | "paths": {
11 | "@repo/*": ["../*"],
12 | "@repo/design-system/*": ["./*"]
13 | }
14 | },
15 | "include": ["**/*.ts", "**/*.tsx"],
16 | "exclude": ["node_modules"]
17 | }
18 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |
4 | # Maintain dependencies for GitHub Actions
5 | - package-ecosystem: "github-actions"
6 | directory: "/"
7 | open-pull-requests-limit: 10
8 | schedule:
9 | interval: "monthly"
10 |
11 | # Maintain dependencies for npm
12 | - package-ecosystem: "npm"
13 | directory: "/"
14 | open-pull-requests-limit: 10
15 | schedule:
16 | interval: "monthly"
17 |
--------------------------------------------------------------------------------
/packages/testing/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/testing",
3 | "version": "0.0.0",
4 | "private": true,
5 | "main": "./index.js",
6 | "type": "commonjs",
7 | "scripts": {
8 | "clean": "git clean -xdf .cache .turbo dist node_modules",
9 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
10 | },
11 | "devDependencies": {
12 | "@vitejs/plugin-react": "^4.5.0",
13 | "vitest": "^3.1.4"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/docs/layout.tsx:
--------------------------------------------------------------------------------
1 | import { DocsLayout } from 'fumadocs-ui/layouts/docs';
2 | import type { ReactNode } from 'react';
3 | import { baseOptions } from '@/lib/layout.shared';
4 | import { source } from '@/lib/source';
5 |
6 | export default function Layout({ children }: { children: ReactNode }) {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/text.tsx:
--------------------------------------------------------------------------------
1 | import { Response } from '@repo/design-system/components/ai-elements/response';
2 | import type { TextUIPart } from 'ai';
3 |
4 | export function Text({ part }: { part: TextUIPart }) {
5 | return (
6 |
7 | {part.text}
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/apps/obby/env.ts:
--------------------------------------------------------------------------------
1 | import { keys as ai } from '@repo/ai/keys';
2 | import { keys as database } from '@repo/database/keys';
3 | import { keys as core } from '@repo/next-config/keys';
4 | import { keys as storage } from '@repo/storage/keys';
5 | import { createEnv } from '@t3-oss/env-nextjs';
6 |
7 | export const env = createEnv({
8 | extends: [ai(), core(), database(), storage()],
9 | server: {},
10 | client: {},
11 | runtimeEnv: {},
12 | });
13 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/tool-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import type { ReactNode } from 'react';
3 |
4 | export function ToolHeader(props: { className?: string; children: ReactNode }) {
5 | return (
6 |
12 | {props.children}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/design-system/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from "clsx";
2 | import { clsx } from "clsx";
3 | import { toast } from "sonner";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | export const cn = (...inputs: ClassValue[]): string => twMerge(clsx(inputs));
7 |
8 | export const capitalize = (str: string) =>
9 | str.charAt(0).toUpperCase() + str.slice(1);
10 |
11 | export const handleError = (error: unknown): void => {
12 | toast.error(error as string);
13 | };
14 |
--------------------------------------------------------------------------------
/public/logos/ai/groq.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/storage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/storage",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "clean": "git clean -xdf .cache .turbo dist node_modules",
7 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
8 | },
9 | "dependencies": {
10 | "@t3-oss/env-nextjs": "^0.13.4",
11 | "@vercel/blob": "^1.1.1",
12 | "zod": "^3.25.28"
13 | },
14 | "devDependencies": {
15 | "@repo/typescript-config": "workspace:*"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/tool-message.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import type { ReactNode } from 'react';
3 |
4 | export function ToolMessage(props: {
5 | className?: string;
6 | children: ReactNode;
7 | }) {
8 | return (
9 |
15 | {props.children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/design-system/providers/theme.tsx:
--------------------------------------------------------------------------------
1 | import type { ThemeProviderProps } from 'next-themes';
2 | import { ThemeProvider as NextThemeProvider } from 'next-themes';
3 |
4 | export const ThemeProvider = ({
5 | children,
6 | ...properties
7 | }: ThemeProviderProps) => (
8 |
15 | {children}
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/public/logos/ai/xai.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/packages/testing/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const path = require('node:path');
3 | const react = require('@vitejs/plugin-react');
4 | const { defineConfig } = require('vitest/config');
5 |
6 | const config = defineConfig({
7 | plugins: [react()],
8 | test: {
9 | environment: 'jsdom',
10 | },
11 | resolve: {
12 | alias: {
13 | '@': path.resolve(__dirname, './'),
14 | '@repo': path.resolve(__dirname, '../../packages'),
15 | },
16 | },
17 | });
18 |
19 | module.exports = config;
20 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "allowJs": true,
10 | "jsx": "preserve",
11 | "noEmit": true,
12 | "paths": {
13 | "@/*": ["./*"],
14 | "@repo/*": ["../../packages/*"]
15 | }
16 | },
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/obby/lib/use-local-storage-value.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export function useLocalStorageValue(key: string) {
4 | const [value, setValue] = useState('');
5 |
6 | useEffect(() => {
7 | const storedValue = localStorage.getItem(key);
8 | if (storedValue !== null) {
9 | setValue(storedValue);
10 | }
11 | }, [key]);
12 |
13 | useEffect(() => {
14 | localStorage.setItem(key, value);
15 | }, [key, value]);
16 |
17 | return [value, setValue] as const;
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/content/docs/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Documentation",
3 | "icon": "book",
4 | "items": [
5 | {
6 | "title": "Overview",
7 | "link": "index"
8 | },
9 | {
10 | "title": "Setup",
11 | "link": "setup"
12 | },
13 | {
14 | "title": "Features",
15 | "link": "features"
16 | },
17 | {
18 | "title": "Usage",
19 | "link": "usage"
20 | },
21 | {
22 | "title": "Components (demo)",
23 | "link": "test"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/apps/obby/lib/use-index-db-storage.ts:
--------------------------------------------------------------------------------
1 | // storage/indexedDbStorage.ts
2 | import { idbGet, idbPut } from './indexed-db-adapter';
3 |
4 | export const createIndexedDbStorage = (storeName: string) => ({
5 | getItem: async (name: string) => {
6 | const value = await idbGet(storeName, name);
7 | return value ?? null;
8 | },
9 | setItem: async (name: string, value: string) => {
10 | await idbPut(storeName, name, value);
11 | },
12 | removeItem: async (name: string) => {
13 | await idbPut(storeName, name, null);
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/source.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineConfig,
3 | defineDocs,
4 | frontmatterSchema,
5 | metaSchema,
6 | } from 'fumadocs-mdx/config';
7 |
8 | // You can customise Zod schemas for frontmatter and `meta.json` here
9 | // see https://fumadocs.dev/docs/mdx/collections#define-docs
10 | export const docs = defineDocs({
11 | docs: {
12 | schema: frontmatterSchema,
13 | },
14 | meta: {
15 | schema: metaSchema,
16 | },
17 | });
18 |
19 | export default defineConfig({
20 | mdxOptions: {
21 | // MDX options
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/apps/obby/public/providers/v0.svg:
--------------------------------------------------------------------------------
1 | V0
--------------------------------------------------------------------------------
/apps/obby/app/(main)/preview.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Preview as PreviewComponent } from '@/components/preview/preview';
4 | import { useSandboxStore } from './state';
5 |
6 | type Props = {
7 | className?: string;
8 | };
9 |
10 | export function Preview({ className }: Props) {
11 | const { status, url, sandboxId, paths } = useSandboxStore();
12 | return (
13 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/obby/public/providers/groq.svg:
--------------------------------------------------------------------------------
1 | Groq
--------------------------------------------------------------------------------
/apps/obby/lib/logger.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from 'effect';
2 |
3 | const logger = {
4 | info: (message: string, meta?: unknown) => {
5 | Effect.runFork(Effect.logInfo(message, meta));
6 | },
7 | debug: (message: string, meta?: unknown) => {
8 | Effect.runFork(Effect.logDebug(message, meta));
9 | },
10 | warn: (message: string, meta?: unknown) => {
11 | Effect.runFork(Effect.logWarning(message, meta));
12 | },
13 | error: (message: string, meta?: unknown) => {
14 | Effect.runFork(Effect.logError(message, meta));
15 | },
16 | };
17 |
18 | export { logger };
19 |
--------------------------------------------------------------------------------
/apps/obby/app/(main)/file-explorer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FileExplorer as FileExplorerComponent } from "@/components/file-explorer/file-explorer";
4 | import { useSandboxStore } from "./state";
5 |
6 | type Props = {
7 | className: string;
8 | };
9 |
10 | export function FileExplorer({ className }: Props) {
11 | const { sandboxId, status, paths } = useSandboxStore();
12 | return (
13 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/design-system/index.tsx:
--------------------------------------------------------------------------------
1 | import type { ThemeProviderProps } from 'next-themes';
2 | import { Toaster } from './components/ui/sonner';
3 | import { TooltipProvider } from './components/ui/tooltip';
4 | import { ThemeProvider } from './providers/theme';
5 |
6 | type DesignSystemProviderProperties = ThemeProviderProps;
7 |
8 | export const DesignSystemProvider = ({
9 | children,
10 | ...properties
11 | }: DesignSystemProviderProperties) => (
12 |
13 | {children}
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/apps/obby/public/providers/vercel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
--------------------------------------------------------------------------------
/apps/obby/public/providers/gemini.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logos/ai/togetherai.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/design-system/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "styles/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@repo/design-system/components",
15 | "utils": "@repo/design-system/lib/utils",
16 | "hooks": "@repo/design-system/hooks",
17 | "lib": "@repo/design-system/lib",
18 | "ui": "@repo/design-system/components/ui"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/packages/next-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/next-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "clean": "git clean -xdf .cache .turbo dist node_modules",
7 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
8 | },
9 | "devDependencies": {
10 | "@repo/typescript-config": "workspace:*",
11 | "next": "15.5.9"
12 | },
13 | "dependencies": {
14 | "@next/bundle-analyzer": "15.3.2",
15 | "@prisma/nextjs-monorepo-workaround-plugin": "^6.8.2",
16 | "@t3-oss/env-core": "^0.13.4",
17 | "@t3-oss/env-nextjs": "^0.13.4",
18 | "zod": "^3.25.28"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/obby/.env.example:
--------------------------------------------------------------------------------
1 | # Server
2 | DATABASE_URL=""
3 |
4 | # Client
5 | NEXT_PUBLIC_APP_URL="http://localhost:3000"
6 | NEXT_PUBLIC_DOCS_URL="http://localhost:3001"
7 |
8 |
9 | # AI Provider KEYS (Optional, you can set it in client)
10 | OPENAI_API_KEY=""
11 | AI_GATEWAY_API_KEY=""
12 | ANTHROPIC_API_KEY=""
13 | GOOGLE_GENERATIVE_AI_API_KEY=""
14 | GROQ_API_KEY=""
15 | OPENROUTER_API_KEY=""
16 |
17 | AWS_ACCESS_KEY_ID=""
18 | AWS_SECRET_ACCESS_KEY=""
19 | AWS_REGION="us-east-1"
20 |
21 | FIRECRAWL_API_KEY=""
22 |
23 | # Vercel API keys for Sandbox
24 | VERCEL_TEAM_ID=""
25 | VERCEL_PROJECT_ID=""
26 |
27 | # https://vercel.com/account/settings/tokens
28 | VERCEL_TOKEN=""
--------------------------------------------------------------------------------
/apps/obby/components/tabs/tab-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import type { ReactNode } from 'react';
5 | import { useTabState } from './use-tab-state';
6 |
7 | type Props = {
8 | className?: string;
9 | children: ReactNode;
10 | tabId: string;
11 | }
12 |
13 | export function TabContent({ children, tabId, className }: Props) {
14 | const [activeTabId] = useTabState();
15 | return (
16 |
23 | {children}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "incremental": false,
10 | "isolatedModules": true,
11 | "lib": ["es2022", "DOM", "DOM.Iterable"],
12 | "module": "NodeNext",
13 | "moduleDetection": "force",
14 | "moduleResolution": "NodeNext",
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2022",
19 | "strictNullChecks": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "incremental": false,
10 | "isolatedModules": true,
11 | "lib": [
12 | "es2022",
13 | "DOM",
14 | "DOM.Iterable"
15 | ],
16 | "module": "NodeNext",
17 | "moduleDetection": "force",
18 | "moduleResolution": "NodeNext",
19 | "resolveJsonModule": true,
20 | "skipLibCheck": true,
21 | "strict": true,
22 | "target": "ES2022",
23 | "strictNullChecks": true
24 | }
25 | }
--------------------------------------------------------------------------------
/apps/obby/app/(main)/logs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { CommandsLogs } from '@/components/commands-logs/commands-logs';
4 | import { useSandboxStore } from './state';
5 |
6 | export function Logs(props: { className?: string }) {
7 | const { commands, addLog, upsertCommand } = useSandboxStore();
8 | return (
9 | {
13 | upsertCommand(data);
14 | }}
15 | onLog={(data) => {
16 | addLog({
17 | sandboxId: data.sandboxId,
18 | cmdId: data.cmdId,
19 | log: data.log,
20 | });
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/reasoning.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ReasoningContent,
3 | ReasoningTrigger,
4 | Reasoning as ReasoningWrapper,
5 | } from '@repo/design-system/components/ai-elements/reasoning';
6 | import type { ReasoningUIPart } from 'ai';
7 |
8 | export function Reasoning({ part }: { part: ReasoningUIPart }) {
9 | if (part.state === 'done' && !part.text) {
10 | return null;
11 | }
12 |
13 | return (
14 |
18 |
19 | {part.text || '_Thinking_'}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/response.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import { type ComponentProps, memo } from 'react';
5 | import { Streamdown } from 'streamdown';
6 |
7 | type ResponseProps = ComponentProps;
8 |
9 | export const Response = memo(
10 | ({ className, ...props }: ResponseProps) => (
11 | *:first-child]:mt-0 [&>*:last-child]:mb-0',
14 | className
15 | )}
16 | {...props}
17 | />
18 | ),
19 | (prevProps, nextProps) => prevProps.children === nextProps.children
20 | );
21 |
22 | Response.displayName = 'Response';
23 |
--------------------------------------------------------------------------------
/packages/design-system/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const MOBILE_BREAKPOINT = 768;
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(
7 | undefined
8 | );
9 |
10 | React.useEffect(() => {
11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12 | const onChange = () => {
13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14 | };
15 | mql.addEventListener('change', onChange);
16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17 | return () => mql.removeEventListener('change', onChange);
18 | }, []);
19 |
20 | return !!isMobile;
21 | }
22 |
--------------------------------------------------------------------------------
/apps/obby/components/tabs/tab-group.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import type { ReactNode } from 'react';
5 | import { useTabState } from './use-tab-state';
6 |
7 | type Props = {
8 | className?: string;
9 | children: ReactNode;
10 | tabId: string;
11 | }
12 |
13 | export function TabGroup({ children, tabId, className }: Props) {
14 | const [activeTabId] = useTabState();
15 | return (
16 |
23 | {children}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner, ToasterProps } from "sonner"
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme()
8 |
9 | return (
10 |
22 | )
23 | }
24 |
25 | export { Toaster }
26 |
--------------------------------------------------------------------------------
/public/logos/ai/google.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Label({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **next-forge version**
14 | I am using version ...
15 |
16 | **To Reproduce**
17 | Steps to reproduce the behavior:
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | **Expected behavior**
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Desktop (please complete the following information):**
30 | - OS: [e.g. MacOS]
31 | - Browser [e.g. chrome v130, safari]
32 |
--------------------------------------------------------------------------------
/apps/obby/public/providers/anthropic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
15 |
16 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/image.tsx:
--------------------------------------------------------------------------------
1 | /** biome-ignore-all lint/performance/noImgElement: can't pay bills for vercel image */
2 | /** biome-ignore-all lint/nursery/useImageSize: intended */
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import type { Experimental_GeneratedImage } from 'ai';
5 |
6 | export type ImageProps = Experimental_GeneratedImage & {
7 | className?: string;
8 | alt?: string;
9 | };
10 |
11 | export const Image = ({
12 | base64,
13 | uint8Array,
14 | mediaType,
15 | ...props
16 | }: ImageProps) => (
17 |
26 | );
27 |
--------------------------------------------------------------------------------
/public/logos/ai/fireworks.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/obby/app/(main)/header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@repo/design-system/lib/utils";
2 | import Link from "next/link";
3 | import { ObbyLogo } from "@/components/icons/obby-logo";
4 |
5 | type Props = {
6 | className?: string;
7 | };
8 |
9 | export function Header({ className }: Props) {
10 | return (
11 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/database/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/database",
3 | "version": "0.0.0",
4 | "main": "./index.ts",
5 | "types": "./index.ts",
6 | "scripts": {
7 | "clean": "git clean -xdf .cache .turbo dist node_modules",
8 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
9 | },
10 | "dependencies": {
11 | "@neondatabase/serverless": "^1.0.0",
12 | "@t3-oss/env-nextjs": "^0.13.4",
13 | "drizzle-orm": "^0.44.4",
14 | "server-only": "^0.0.1",
15 | "undici": "^7.10.0",
16 | "ws": "^8.18.2",
17 | "zod": "^3.25.28"
18 | },
19 | "devDependencies": {
20 | "@repo/typescript-config": "workspace:*",
21 | "@types/node": "22.15.21",
22 | "@types/ws": "^8.18.1",
23 | "bufferutil": "^4.0.9",
24 | "drizzle-kit": "^0.31.4",
25 | "typescript": "^5.8.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/scripts/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { program } from 'commander';
4 | import { initialize } from './initialize.js';
5 | import { update } from './update.js';
6 |
7 | program
8 | .command('init')
9 | .description('Initialize a new next-forge project')
10 | .option('--name ', 'Name of the project')
11 | .option(
12 | '--package-manager ',
13 | 'Package manager to use (npm, yarn, bun, pnpm)'
14 | )
15 | .option('--disable-git', 'Disable git initialization')
16 | .action(initialize);
17 |
18 | program
19 | .command('update')
20 | .description('Update the project from one version to another')
21 | .option('--from ', 'Version to update from e.g. 1.0.0')
22 | .option('--to ', 'Version to update to e.g. 2.0.0')
23 | .action(update);
24 |
25 | program.parse(process.argv);
26 |
--------------------------------------------------------------------------------
/apps/web/lib/layout.shared.tsx:
--------------------------------------------------------------------------------
1 | import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
2 | import Image from 'next/image';
3 |
4 | /**
5 | * Shared layout configurations
6 | *
7 | * you can customise layouts individually from:
8 | * Home Layout: app/(home)/layout.tsx
9 | * Docs Layout: app/docs/layout.tsx
10 | */
11 | export function baseOptions(): BaseLayoutProps {
12 | return {
13 | nav: {
14 | title: (
15 | <>
16 |
17 | 0bby
18 | >
19 | ),
20 | },
21 | // see https://fumadocs.dev/docs/ui/navigation/links
22 | links: [
23 | {
24 | text: 'GitHub',
25 | url: 'https://github.com/eersnington/obby-dev',
26 | active: 'nested-url',
27 | },
28 | ],
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obby-web",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "next build",
7 | "dev": "next dev -p 3001 --turbo",
8 | "start": "next start",
9 | "postinstall": "fumadocs-mdx"
10 | },
11 | "dependencies": {
12 | "@databuddy/sdk": "^2.0.0",
13 | "fumadocs-core": "15.7.7",
14 | "fumadocs-mdx": "11.8.2",
15 | "fumadocs-ui": "15.7.7",
16 | "next": "15.5.9",
17 | "react": "^19.1.0",
18 | "react-dom": "^19.1.0"
19 | },
20 | "devDependencies": {
21 | "@tailwindcss/postcss": "^4.1.12",
22 | "@types/mdx": "^2.0.13",
23 | "@types/node": "22.15.21",
24 | "@types/react": "^19.1.5",
25 | "@types/react-dom": "^19.1.5",
26 | "postcss": "^8.5.6",
27 | "tailwindcss": "^4.1.12",
28 | "typescript": "^5.9.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "bundler",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "paths": {
19 | "@/.source": ["./.source/index.ts"],
20 | "@/*": ["./*"]
21 | },
22 | "plugins": [
23 | {
24 | "name": "next"
25 | }
26 | ]
27 | },
28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ai/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/ai",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "clean": "git clean -xdf .cache .turbo dist node_modules",
7 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
8 | },
9 | "dependencies": {
10 | "@ai-sdk/gateway": "^1.0.4",
11 | "@ai-sdk/openai": "^2.0.8",
12 | "@ai-sdk/react": "2.0.0-beta.29",
13 | "@ai-sdk/vercel": "1.0.0-alpha.7",
14 | "@t3-oss/env-nextjs": "^0.13.4",
15 | "ai": "5.0.0-beta.29",
16 | "react": "^19.1.0",
17 | "react-markdown": "^10.1.0",
18 | "tailwind-merge": "^3.3.0",
19 | "zod": "^3.25.28"
20 | },
21 | "devDependencies": {
22 | "@repo/typescript-config": "workspace:*",
23 | "@types/node": "22.15.21",
24 | "@types/react": "19.1.5",
25 | "@types/react-dom": "^19.1.5"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Please provide a brief description of the changes introduced in this pull request.
4 |
5 | ## Related Issues
6 |
7 | Closes #
8 |
9 | ## Checklist
10 |
11 | - [ ] My code follows the code style of this project.
12 | - [ ] I have performed a self-review of my code.
13 | - [ ] I have commented my code, particularly in hard-to-understand areas.
14 | - [ ] I have updated the documentation, if necessary.
15 | - [ ] I have added tests that prove my fix is effective or my feature works.
16 | - [ ] New and existing tests pass locally with my changes.
17 |
18 | ## Screenshots (if applicable)
19 |
20 |
21 |
22 | ## Additional Notes
23 |
24 |
25 |
--------------------------------------------------------------------------------
/apps/obby/components/panels/panels.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import type { ReactNode } from 'react';
3 |
4 | type Props = {
5 | className?: string;
6 | children: ReactNode;
7 | };
8 |
9 | export function Panel({ className, children }: Props) {
10 | return (
11 |
17 | {children}
18 |
19 | );
20 | }
21 |
22 | export function PanelHeader({ className, children }: Props) {
23 | return (
24 |
30 | {children}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | )
26 | }
27 |
28 | export { Separator }
29 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@repo/design-system/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/apps/obby/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@repo/design-system/components/ui/button';
4 | import { fonts } from '@repo/design-system/lib/fonts';
5 | import { captureException } from '@sentry/nextjs';
6 | import type NextError from 'next/error';
7 | import { useEffect } from 'react';
8 |
9 | type GlobalErrorProperties = {
10 | readonly error: NextError & { digest?: string };
11 | readonly reset: () => void;
12 | };
13 |
14 | const GlobalError = (props: GlobalErrorProperties) => {
15 | useEffect(() => {
16 | captureException(props.error);
17 | }, [props.error]);
18 |
19 | return (
20 |
21 |
22 | Oops, something went wrong
23 | props.reset()}>Try again
24 |
25 |
26 | );
27 | };
28 |
29 | export default GlobalError;
30 |
--------------------------------------------------------------------------------
/apps/obby/components/tabs/tab-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import type { ReactNode } from 'react';
5 | import { useTabState } from './use-tab-state';
6 |
7 | type Props = {
8 | children: ReactNode;
9 | tabId: string;
10 | };
11 |
12 | export function TabItem({ children, tabId }: Props) {
13 | const [activeTabId, setTabId] = useTabState();
14 | return (
15 |
16 | setTabId(tabId)}
19 | onKeyDown={(e) => {
20 | if (e.key === 'Enter' || e.key === ' ') {
21 | e.preventDefault();
22 | setTabId(tabId);
23 | }
24 | }}
25 | type="button"
26 | >
27 | {children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/app/global.css";
2 | import { Databuddy } from "@databuddy/sdk/react";
3 | import { RootProvider } from "fumadocs-ui/provider";
4 | import { Geist_Mono } from "next/font/google";
5 | import type { ReactNode } from "react";
6 |
7 | const geistMono = Geist_Mono({
8 | subsets: ["latin"],
9 | });
10 |
11 | export default function Layout({ children }: { children: ReactNode }) {
12 | return (
13 |
14 |
15 |
16 |
22 | {children}
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.com/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "ui": "tui",
5 | "envMode": "loose",
6 | "tasks": {
7 | "build": {
8 | "dependsOn": ["^build", "test"],
9 | "outputs": [
10 | ".next/**",
11 | "!.next/cache/**",
12 | ".basehub/**",
13 | "**/generated/**",
14 | "storybook-static/**",
15 | ".react-email/**"
16 | ]
17 | },
18 | "test": {
19 | "dependsOn": ["^test"]
20 | },
21 | "analyze": {
22 | "dependsOn": ["^analyze"]
23 | },
24 | "dev": {
25 | "cache": false,
26 | "persistent": true
27 | },
28 | "translate": {
29 | "dependsOn": ["^translate"],
30 | "cache": false
31 | },
32 | "clean": {
33 | "cache": false
34 | },
35 | "//#clean": {
36 | "cache": false
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/apps/obby/public/providers/openrouter.svg:
--------------------------------------------------------------------------------
1 | OpenRouter
--------------------------------------------------------------------------------
/packages/design-system/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Progress({
9 | className,
10 | value,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
22 |
27 |
28 | )
29 | }
30 |
31 | export { Progress }
32 |
--------------------------------------------------------------------------------
/packages/next-config/keys.ts:
--------------------------------------------------------------------------------
1 | import { vercel } from '@t3-oss/env-core/presets-zod';
2 | import { createEnv } from '@t3-oss/env-nextjs';
3 | import { z } from 'zod';
4 |
5 | export const keys = () =>
6 | createEnv({
7 | extends: [vercel()],
8 | server: {
9 | ANALYZE: z.string().optional(),
10 |
11 | // Added by Vercel
12 | NEXT_RUNTIME: z.enum(['nodejs', 'edge']).optional(),
13 | },
14 | client: {
15 | NEXT_PUBLIC_APP_URL: z.string().url(),
16 | NEXT_PUBLIC_API_URL: z.string().url().optional(),
17 | NEXT_PUBLIC_DOCS_URL: z.string().url().optional(),
18 | },
19 | runtimeEnv: {
20 | ANALYZE: process.env.ANALYZE,
21 | NEXT_RUNTIME: process.env.NEXT_RUNTIME,
22 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
23 | NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
24 | NEXT_PUBLIC_DOCS_URL: process.env.NEXT_PUBLIC_DOCS_URL,
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/apps/obby/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { DesignSystemProvider } from "@repo/design-system";
2 | import { VercelToolbar } from "@vercel/toolbar/next";
3 | import type { Metadata } from "next";
4 | import { NuqsAdapter } from "nuqs/adapters/next/app";
5 | import type { ReactNode } from "react";
6 |
7 | import "./styles.css";
8 |
9 | export const metadata: Metadata = {
10 | metadataBase: new URL("https://obby.dev"),
11 | title: "Obby - OSS v0",
12 | description: "Open Source v0 alternative but it can run almost anything.",
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | }: Readonly<{ children: ReactNode }>) {
18 | return (
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | function Collapsible({
6 | ...props
7 | }: React.ComponentProps) {
8 | return
9 | }
10 |
11 | function CollapsibleTrigger({
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
19 | )
20 | }
21 |
22 | function CollapsibleContent({
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
30 | )
31 | }
32 |
33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
34 |
--------------------------------------------------------------------------------
/public/logos/ai/vertex.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/obby/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/biome.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
3 | "extends": ["ultracite"],
4 | "linter": {
5 | "enabled": true,
6 | "domains": {
7 | "solid": "none"
8 | },
9 | "rules": {
10 | "complexity": {
11 | "noExcessiveLinesPerFunction": "off"
12 | },
13 | "nursery": {
14 | //Enforce type definitions to consistently use either interface or type.
15 | "useConsistentTypeDefinitions": {
16 | "level": "warn",
17 | "options": {
18 | "style": "type"
19 | }
20 | }
21 | }
22 | }
23 | },
24 |
25 | "files": {
26 | "includes": [
27 | "**",
28 | "!**/packages/design-system/components/ui",
29 | "!**/packages/design-system/lib",
30 | "!**/packages/design-system/hooks",
31 | "!**/apps/docs/**/*.json",
32 | "!**/apps/email/.react-email",
33 | "!**/apps/api/scripts",
34 | "!**/apps/app/scripts",
35 | "!**/*.css"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Obby – OSS v0
4 |
5 |
6 |
7 | Obby is Open Source implementation of v0 agent by Vercel. It helps you design, build, and ship real apps from scratch.
8 |
9 |
10 | Link: obby.dev
11 |
12 |
13 | ## Getting started
14 |
15 | ### Clone the repo
16 |
17 | ```bash
18 | git clone https://github.com/eersnington/obby-dev.git
19 | ```
20 |
21 | ### Navigate to the project directory
22 |
23 | ```bash
24 | cd obby-dev
25 | ```
26 |
27 | ### Install dependencies
28 |
29 | ```bash
30 | pnpm install
31 | ```
32 |
33 | ### Start the development server
34 |
35 | ```bash
36 | pnpm run dev
37 | ```
38 |
39 | ### Acknowledgment
40 |
41 | This project is based on OSS Vibe Coding Project in [Vercel Examples](https://github.com/vercel/examples).
42 | Big thanks to the Vercel team for providing such great starter template on using Vercel Sanbox
--------------------------------------------------------------------------------
/apps/obby/components/model-selector/cmdk/api-key-section.tsx:
--------------------------------------------------------------------------------
1 | import { Label } from '@repo/design-system/components/ui/label';
2 | import { KeyIcon } from 'lucide-react';
3 | import { type ModelProvider, PROVIDER_KEY_SCHEMAS } from '@/ai/constants';
4 | import { AwsKeyForm } from './aws-key-form';
5 | import { TokenKeyForm } from './token-key-form';
6 |
7 | type Props = {
8 | provider: ModelProvider;
9 | };
10 |
11 | export function ApiKeySection({ provider }: Props) {
12 | if (!provider) {
13 | return null;
14 | }
15 |
16 | const schema = PROVIDER_KEY_SCHEMAS[provider];
17 |
18 | return (
19 |
20 |
21 |
22 | API Configuration
23 |
24 |
25 | {schema.type === 'token' ? (
26 |
27 | ) : (
28 |
29 | )}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@repo/design-system/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/public/logos/vercel_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Created by potrace 1.11, written by Peter Selinger 2001-2013
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/get-sandbox-url.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { LinkIcon } from 'lucide-react';
3 | import type { DataPart } from '@/ai/messages/data-parts';
4 | import { MessageSpinner } from '../message-spinner';
5 | import { ToolHeader } from '../tool-header';
6 | import { ToolMessage } from '../tool-message';
7 |
8 | export function GetSandboxURL(props: {
9 | className?: string;
10 | message: DataPart['get-sandbox-url'];
11 | }) {
12 | return (
13 |
14 |
15 |
16 | {props.message.url ? (
17 | Got Sandbox URL
18 | ) : (
19 | Getting Sandbox URL
20 | )}
21 |
22 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/apps/obby/app/api/sandboxes/[sandboxId]/cmds/[cmdId]/route.ts:
--------------------------------------------------------------------------------
1 | import { Sandbox } from '@vercel/sandbox';
2 | import { type NextRequest, NextResponse } from 'next/server';
3 | import { env } from '@/env';
4 |
5 | type Params = {
6 | sandboxId: string;
7 | cmdId: string;
8 | };
9 |
10 | export async function GET(
11 | _request: NextRequest,
12 | { params }: { params: Promise }
13 | ) {
14 | const cmdParams = await params;
15 | const sandbox = await Sandbox.get({
16 | ...cmdParams,
17 | teamId: env.VERCEL_TEAM_ID ?? '',
18 | projectId: env.VERCEL_PROJECT_ID ?? '',
19 | token: env.VERCEL_TOKEN ?? '',
20 | });
21 | const command = await sandbox.getCommand(cmdParams.cmdId);
22 |
23 | /**
24 | * The wait can get to fail when the Sandbox is stopped but the command
25 | * was still running. In such case we return empty for finish data.
26 | */
27 | const done = await command.wait().catch(() => null);
28 | return NextResponse.json({
29 | sandboxId: sandbox.sandboxId,
30 | cmdId: command.cmdId,
31 | startedAt: command.startedAt,
32 | exitCode: done?.exitCode,
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 eersnington
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/obby/components/file-explorer/file-content.tsx:
--------------------------------------------------------------------------------
1 | import { PulseLoader } from 'react-spinners';
2 | import useSWR from 'swr';
3 | import { SyntaxHighlighter } from './syntax-highlighter';
4 |
5 | type Props = {
6 | sandboxId: string;
7 | path: string;
8 | };
9 |
10 | export function FileContent({ sandboxId, path }: Props) {
11 | const searchParams = new URLSearchParams({ path });
12 | const content = useSWR(
13 | `/api/sandboxes/${sandboxId}/files?${searchParams.toString()}`,
14 | async (pathname: string, init: RequestInit) => {
15 | const response = await fetch(pathname, init);
16 | const text = await response.text();
17 | return text;
18 | },
19 | { refreshInterval: 500 }
20 | );
21 |
22 | return (
23 |
24 | {content.isLoading || !content.data ? (
25 |
28 | ) : (
29 |
30 | )}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/database/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "2c6ae3d2-48ec-4c97-8082-cba33274c856",
3 | "prevId": "00000000-0000-0000-0000-000000000000",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.page": {
8 | "name": "page",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "serial",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "name": {
18 | "name": "name",
19 | "type": "varchar(255)",
20 | "primaryKey": false,
21 | "notNull": false
22 | }
23 | },
24 | "indexes": {},
25 | "foreignKeys": {},
26 | "compositePrimaryKeys": {},
27 | "uniqueConstraints": {},
28 | "policies": {},
29 | "checkConstraints": {},
30 | "isRLSEnabled": false
31 | }
32 | },
33 | "enums": {},
34 | "schemas": {},
35 | "sequences": {},
36 | "roles": {},
37 | "policies": {},
38 | "views": {},
39 | "_meta": {
40 | "columns": {},
41 | "schemas": {},
42 | "tables": {}
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/apps/obby/ai/tools/generate-files-prompt.md:
--------------------------------------------------------------------------------
1 | Create a set of files based on the current state of the project and conversation. Your output will be uploaded directly into a Vercel Sandbox environment, so it must be immediately usable and correct on first iteration. Do not include explanations or markdown. Your output will be parsed programmatically and uploaded to a live environment.
2 |
3 | ## Instructions
4 |
5 | 1. Generate only the files that are relevant to the user's request.
6 | 2. All file paths must be relative to the sandbox root (e.g., \`src/app.tsx\`, \`package.json\`, \`routes/api.ts\`).
7 | 3. Ensure every file is syntactically valid, consistent with the chosen tech stack, and complete.
8 | 4. Do not include placeholder comments like “TODO” unless explicitly instructed.
9 | 5. Assume any previously generated files already exist in the sandbox — write with compatibility in mind.
10 | 6. Favor minimal, functional implementations that demonstrate correctness and are ready to be run, built, or extended.
11 | 7. Include configuration, setup, or support files (e.g., \`.env\`, \`tsconfig.json\`, \`vite.config.ts\`) if the task depends on them working.
12 |
--------------------------------------------------------------------------------
/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import type { PlopTypes } from '@turbo/gen';
2 |
3 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
4 | plop.setGenerator('init', {
5 | description: 'Generate a new package for the Monorepo',
6 | prompts: [
7 | {
8 | type: 'input',
9 | name: 'name',
10 | message:
11 | 'What is the name of the package? (You can skip the `@repo/` prefix)',
12 | },
13 | ],
14 | actions: [
15 | (answers) => {
16 | if (
17 | 'name' in answers &&
18 | typeof answers.name === 'string' &&
19 | answers.name.startsWith('@repo/')
20 | ) {
21 | answers.name = answers.name.replace('@repo/', '');
22 | }
23 | return 'Config sanitized';
24 | },
25 | {
26 | type: 'add',
27 | path: 'packages/{{ name }}/package.json',
28 | templateFile: 'templates/package.json.hbs',
29 | },
30 | {
31 | type: 'add',
32 | path: 'packages/{{ name }}/tsconfig.json',
33 | templateFile: 'templates/tsconfig.json.hbs',
34 | },
35 | ],
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/apps/obby/components/model-selector/use-available-models.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import {
3 | ANTHROPIC_MODELS,
4 | BEDROCK_MODELS,
5 | GATEWAY_MODELS,
6 | GOOGLE_MODELS,
7 | GROQ_MODELS,
8 | type Model,
9 | type ModelProvider,
10 | OPENAI_MODELS,
11 | OPENROUTER_MODELS,
12 | VERCEL_MODELS,
13 | } from '@/ai/constants';
14 |
15 | type DisplayModel = {
16 | id: string;
17 | label: string;
18 | provider?: ModelProvider;
19 | byokOnly?: boolean;
20 | };
21 |
22 | const ALL_MODELS: readonly Model[] = [
23 | ...OPENAI_MODELS,
24 | ...ANTHROPIC_MODELS,
25 | ...GOOGLE_MODELS,
26 | ...GROQ_MODELS,
27 | ...BEDROCK_MODELS,
28 | ...VERCEL_MODELS,
29 | ...GATEWAY_MODELS,
30 | ...OPENROUTER_MODELS,
31 | ] as const;
32 |
33 | export function useAvailableModels() {
34 | const models = useMemo((): DisplayModel[] => {
35 | return ALL_MODELS.map((model) => ({
36 | id: model.id,
37 | label: model.name,
38 | provider: model.provider,
39 | byokOnly: model.byokOnly,
40 | }));
41 | }, []);
42 |
43 | return {
44 | models,
45 | isLoading: false,
46 | error: null,
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/create-sandbox.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { BoxIcon } from 'lucide-react';
3 | import { Streamdown } from 'streamdown';
4 | import type { DataPart } from '@/ai/messages/data-parts';
5 | import { MessageSpinner } from '../message-spinner';
6 | import { ToolHeader } from '../tool-header';
7 | import { ToolMessage } from '../tool-message';
8 |
9 | export function CreateSandbox(props: {
10 | className?: string;
11 | message: DataPart['create-sandbox'];
12 | }) {
13 | return (
14 |
15 |
16 |
17 | {props.message.status === 'loading' ? (
18 | Creating Sandbox
19 | ) : (
20 | Created Sandbox
21 | )}
22 |
23 |
24 | {props.message.status === 'loading' && }
25 | {props.message.sandboxId && (
26 | {`Sandbox created with id \`${props.message.sandboxId}\``}
27 | )}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[javascript]": {
3 | "editor.defaultFormatter": "biomejs.biome"
4 | },
5 | "[json]": {
6 | "editor.defaultFormatter": "biomejs.biome"
7 | },
8 | "[jsonc]": {
9 | "editor.defaultFormatter": "biomejs.biome"
10 | },
11 | "[typescript]": {
12 | "editor.defaultFormatter": "biomejs.biome"
13 | },
14 | "[typescriptreact]": {
15 | "editor.defaultFormatter": "biomejs.biome"
16 | },
17 | "editor.codeActionsOnSave": {
18 | "quickfix.biome": "explicit",
19 | "source.organizeImports.biome": "explicit",
20 | "source.fixAll.biome": "explicit"
21 | },
22 | "editor.defaultFormatter": "esbenp.prettier-vscode",
23 | "editor.formatOnPaste": true,
24 | "editor.formatOnSave": true,
25 | "emmet.showExpandedAbbreviation": "never",
26 | "prettier.enable": false,
27 | "typescript.tsdk": "node_modules/typescript/lib",
28 | "tailwindCSS.experimental.configFile": "packages/design-system/styles/globals.css",
29 | "[javascriptreact]": {
30 | "editor.defaultFormatter": "biomejs.biome"
31 | },
32 | "[css]": {
33 | "editor.defaultFormatter": "biomejs.biome"
34 | },
35 | "[graphql]": {
36 | "editor.defaultFormatter": "biomejs.biome"
37 | }
38 | }
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/run-command.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { SquareChevronRightIcon } from 'lucide-react';
3 | import { Streamdown } from 'streamdown';
4 | import type { DataPart } from '@/ai/messages/data-parts';
5 | import { MessageSpinner } from '../message-spinner';
6 | import { ToolHeader } from '../tool-header';
7 | import { ToolMessage } from '../tool-message';
8 |
9 | export function RunCommand(props: {
10 | className?: string;
11 | message: DataPart['run-command'];
12 | }) {
13 | return (
14 |
15 |
16 |
17 | {props.message.status === 'loading' ? (
18 | Dispatching Command
19 | ) : (
20 | Command Dispatched
21 | )}
22 |
23 |
24 |
25 | {`\`${props.message.command} ${props.message.args.join(
26 | ' '
27 | )}\``}
28 | {props.message.status === 'loading' && }
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/apps/obby/app/api/sandboxes/[sandboxId]/route.ts:
--------------------------------------------------------------------------------
1 | import { Sandbox } from '@vercel/sandbox';
2 | import { APIError } from '@vercel/sandbox/dist/api-client/api-error';
3 | import { type NextRequest, NextResponse } from 'next/server';
4 | import { env } from '@/env';
5 |
6 | /**
7 | * We must change the SDK to add data to the instance and then
8 | * use it to retrieve the status of the Sandbox.
9 | */
10 | export async function GET(
11 | _request: NextRequest,
12 | { params }: { params: Promise<{ sandboxId: string }> }
13 | ) {
14 | const { sandboxId } = await params;
15 | try {
16 | const sandbox = await Sandbox.get({
17 | sandboxId,
18 | teamId: env.VERCEL_TEAM_ID ?? '',
19 | projectId: env.VERCEL_PROJECT_ID ?? '',
20 | token: env.VERCEL_TOKEN ?? '',
21 | });
22 |
23 | await sandbox.runCommand({
24 | cmd: 'echo',
25 | args: ['Sandbox status check'],
26 | });
27 |
28 | return NextResponse.json({ status: 'running' });
29 | } catch (error) {
30 | if (
31 | error instanceof APIError &&
32 | error.json.error.code === 'sandbox_stopped'
33 | ) {
34 | return NextResponse.json({ status: 'stopped' });
35 | }
36 | throw error;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/obby/components/model-selector/cmdk/use-provider-key.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { toast } from "sonner";
3 | import type { ModelProvider } from "@/ai/constants";
4 | import type { ProviderKeyValue } from "@/stores/use-provider-keys-store";
5 | import { useProviderKeysStore } from "@/stores/use-provider-keys-store";
6 |
7 | export function useProviderKey(provider: ModelProvider) {
8 | const getKey = useProviderKeysStore((s) => s.getKey);
9 | const setKey = useProviderKeysStore((s) => s.setKey);
10 | const [saving, setSaving] = useState(false);
11 |
12 | const storedKey = getKey(provider);
13 |
14 | const saveKey = (value: ProviderKeyValue | null) => {
15 | try {
16 | setSaving(true);
17 | setKey(provider, value);
18 |
19 | if (value === null) {
20 | toast.success("Cleared", {
21 | description: `Your API key for ${provider} has been removed.`,
22 | });
23 | } else {
24 | toast.success("Saved", {
25 | description: `Your API key for ${provider} has been saved.`,
26 | });
27 | }
28 | } finally {
29 | setSaving(false);
30 | }
31 | };
32 |
33 | return {
34 | key: storedKey,
35 | setKey: saveKey,
36 | saving,
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/public/logos/ai/fireworksai.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/obby/ai/validation.ts:
--------------------------------------------------------------------------------
1 | import type { UIMessage } from 'ai';
2 | import { z } from 'zod';
3 | import { type ModelProvider, PROVIDERS } from './constants';
4 |
5 | export const ModelProviderSchema = z.enum(
6 | PROVIDERS as [ModelProvider, ...ModelProvider[]]
7 | );
8 |
9 | export const ProviderKeyValueSchema = z.union([
10 | z.string(),
11 | z.object({
12 | region: z.string(),
13 | accessKeyId: z.string(),
14 | secretAccessKey: z.string(),
15 | sessionToken: z.string().optional(),
16 | }),
17 | ]);
18 |
19 | export const UIMessageSchema = z.custom();
20 |
21 | // keeping them optional avoids forcing clients to send them every time.
22 | export const ToolOptionsSchema = z
23 | .object({
24 | webScrape: z.boolean().optional(),
25 | webSearch: z.boolean().optional(),
26 | context7: z.boolean().optional(),
27 | })
28 | .partial()
29 | .default({});
30 |
31 | export const ChatBodySchema = z.object({
32 | messages: z.array(UIMessageSchema),
33 | modelId: z.string().optional(),
34 | provider: ModelProviderSchema.optional(),
35 | providerApiKey: ProviderKeyValueSchema.optional(),
36 | tools: ToolOptionsSchema.optional(),
37 | });
38 |
39 | export type ToolOptions = z.infer;
40 | export type ValidatedChatBody = z.infer;
41 |
--------------------------------------------------------------------------------
/apps/obby/stores/use-provider-keys-store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { createJSONStorage, persist } from 'zustand/middleware';
3 | import { createIndexedDbStorage } from '@/lib/use-index-db-storage';
4 |
5 | const storeName = 'provider-keys';
6 |
7 | export type ProviderKeyValue =
8 | | string
9 | | {
10 | region: string;
11 | accessKeyId: string;
12 | secretAccessKey: string;
13 | sessionToken?: string;
14 | };
15 |
16 | type ProviderKeysState = {
17 | keys: Record;
18 | setKey: (provider: string, value: ProviderKeyValue | null) => void;
19 | getKey: (provider: string) => ProviderKeyValue | undefined;
20 | clearKeys: () => void;
21 | };
22 |
23 | export const useProviderKeysStore = create()(
24 | persist(
25 | (set, get) => ({
26 | keys: {},
27 | setKey: (provider, value) =>
28 | set((state) => ({
29 | keys: {
30 | ...state.keys,
31 | [provider]: value ?? undefined,
32 | },
33 | })),
34 | getKey: (provider) => get().keys[provider],
35 | clearKeys: () => set({ keys: {} }),
36 | }),
37 | {
38 | name: storeName,
39 | storage: createJSONStorage(() => createIndexedDbStorage(storeName)),
40 | }
41 | )
42 | );
43 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitive from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Switch({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 |
27 |
28 | )
29 | }
30 |
31 | export { Switch }
32 |
--------------------------------------------------------------------------------
/apps/obby/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/wait-command.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { SquareChevronRightIcon } from 'lucide-react';
3 | import { Streamdown } from 'streamdown';
4 | import type { DataPart } from '@/ai/messages/data-parts';
5 | import { MessageSpinner } from '../message-spinner';
6 | import { ToolHeader } from '../tool-header';
7 | import { ToolMessage } from '../tool-message';
8 |
9 | export function WaitCommand(props: {
10 | className?: string;
11 | message: DataPart['wait-command'];
12 | }) {
13 | return (
14 |
15 |
16 |
17 | {props.message.status === 'loading' && Waiting for Command }
18 | {props.message.status === 'done' && (
19 |
20 | Command Finished
21 | {typeof props.message.exitCode === 'number'
22 | ? ` (${props.message.exitCode})`
23 | : ''}
24 |
25 | )}
26 |
27 |
28 | {`\`${props.message.command} ${props.message.args.join(
29 | ' '
30 | )}\``}
31 | {props.message.status === 'loading' && }
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/apps/obby/components/banner.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { XIcon } from 'lucide-react';
4 | import { useState } from 'react';
5 |
6 | type Props = {
7 | defaultOpen: boolean;
8 | onDismiss: () => void;
9 | };
10 |
11 | export function Banner({ defaultOpen, onDismiss }: Props) {
12 | const [open, setOpen] = useState(defaultOpen);
13 | if (!open) {
14 | return null;
15 | }
16 |
17 | return (
18 |
19 | Vercel Coding Agent demo This demo showcases a full-stack
20 | coding agent built with Vercel's AI Cloud, AI SDK, and Next.js This
21 | example gives you full flexibility of the underlying model via Vercel AI
22 | Gateway and code execution via Vercel Sandbox. For a drop-in, higher-level
23 | solution for adding vibe coding capabilities to your applications, check
24 | out the v0 Platform API.
25 | {
29 | onDismiss();
30 | setOpen(false);
31 | }}
32 | type="button"
33 | >
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/apps/obby/components/model-selector/cmdk/model-section.tsx:
--------------------------------------------------------------------------------
1 | import { CommandInput } from '@repo/design-system/components/ui/command';
2 | import type { ModelProvider } from '@/ai/constants';
3 | import { ApiKeySection } from './api-key-section';
4 | import { ModelList } from './model-list';
5 |
6 | type Props = {
7 | provider?: ModelProvider;
8 | onModelSelect: (modelId: string, provider: ModelProvider) => void;
9 | isLoading: boolean;
10 | };
11 |
12 | export function ModelSection({ provider, onModelSelect, isLoading }: Props) {
13 | if (!provider) {
14 | return (
15 |
16 |
17 |
Loading providers...
18 |
19 |
20 | );
21 | }
22 |
23 | return (
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { CheckIcon } from "lucide-react"
6 |
7 | import { cn } from "@repo/design-system/lib/utils"
8 |
9 | function Checkbox({
10 | className,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
22 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export { Checkbox }
33 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { BotIcon, UserIcon } from 'lucide-react';
3 | import { MessagePart } from './message-part';
4 | import type { ChatUIMessage } from './types';
5 |
6 | type Props = {
7 | message: ChatUIMessage;
8 | };
9 |
10 | export function Message({ message }: Props) {
11 | return (
12 |
18 | {/* Message Header */}
19 |
20 | {message.role === 'user' ? (
21 | <>
22 |
23 | You
24 | >
25 | ) : (
26 | <>
27 |
28 | Assistant ({message.metadata?.model})
29 | >
30 | )}
31 |
32 |
33 | {/* Message Content */}
34 |
35 | {message.parts.map((part, index) => (
36 | // biome-ignore lint/suspicious/noArrayIndexKey: TODO: i need to add a unique id for each part
37 |
38 | ))}
39 |
40 |
41 | {/* Message Actions: Coming Soon */}
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/.github/copilot-instructions.md.example:
--------------------------------------------------------------------------------
1 | # Copilot Guidelines
2 |
3 | This project uses .
4 |
5 | ## Project Structure
6 | Structure of how project files are setup. Making changes to files should be in their respected file.
7 | ```
8 | | App | Description |
9 | |-----------|-----------------------------------------------------------------------------|
10 | | api | Contains serverless functions designed to run separately from the main app e.g. webhooks and cron jobs. |
11 | | app | The main application, featuring a shadcn/ui template. |
12 | | docs | The documentation, which contains the documentation for the app e.g. guides and tutorials. |
13 | | email | The email preview server from react.email. |
14 | | storybook | The storybook, which contains the storybook for the app. |
15 | | studio | Prisma Studio, which is a graphical editor for the database. |
16 | | web | The website, featuring a twblocks template. |
17 | ```
18 |
19 | ## Nesting
20 | - Avoid deeply nested code. Break down logic into smaller functions.
21 | - Opening curly braces should be on the same line as the statement.
22 |
23 | ## Error Handling
24 | - Always catch a specific error instead of a generic one.
25 | - Log the error message and stack trace.
26 |
27 |
--------------------------------------------------------------------------------
/apps/web/.source/index.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck -- skip type checking
2 | import * as docs_4 from "../content/docs/usage.mdx?collection=docs&hash=1756750757746"
3 | import * as docs_3 from "../content/docs/test.mdx?collection=docs&hash=1756750757746"
4 | import * as docs_2 from "../content/docs/setup.mdx?collection=docs&hash=1756750757746"
5 | import * as docs_1 from "../content/docs/index.mdx?collection=docs&hash=1756750757746"
6 | import * as docs_0 from "../content/docs/features.mdx?collection=docs&hash=1756750757746"
7 | import { _runtime } from "fumadocs-mdx"
8 | import * as _source from "../source.config"
9 | export const docs = _runtime.docs([{ info: {"path":"features.mdx","absolutePath":"/Users/eers/Documents/GitHub/obby-dev/apps/web/content/docs/features.mdx"}, data: docs_0 }, { info: {"path":"index.mdx","absolutePath":"/Users/eers/Documents/GitHub/obby-dev/apps/web/content/docs/index.mdx"}, data: docs_1 }, { info: {"path":"setup.mdx","absolutePath":"/Users/eers/Documents/GitHub/obby-dev/apps/web/content/docs/setup.mdx"}, data: docs_2 }, { info: {"path":"test.mdx","absolutePath":"/Users/eers/Documents/GitHub/obby-dev/apps/web/content/docs/test.mdx"}, data: docs_3 }, { info: {"path":"usage.mdx","absolutePath":"/Users/eers/Documents/GitHub/obby-dev/apps/web/content/docs/usage.mdx"}, data: docs_4 }], [{"info":{"path":"meta.json","absolutePath":"/Users/eers/Documents/GitHub/obby-dev/apps/web/content/docs/meta.json"},"data":{"title":"Documentation","icon":"book"}}])
--------------------------------------------------------------------------------
/.zed/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatter": "language_server",
3 | "format_on_save": "on",
4 | "languages": {
5 | "JavaScript": {
6 | "formatter": {
7 | "language_server": {
8 | "name": "biome"
9 | }
10 | },
11 | "code_actions_on_format": {
12 | "source.fixAll.biome": true,
13 | "source.organizeImports.biome": true
14 | }
15 | },
16 | "TypeScript": {
17 | "formatter": {
18 | "language_server": {
19 | "name": "biome"
20 | }
21 | },
22 | "code_actions_on_format": {
23 | "source.fixAll.biome": true,
24 | "source.organizeImports.biome": true
25 | }
26 | },
27 | "JSX": {
28 | "formatter": {
29 | "language_server": {
30 | "name": "biome"
31 | }
32 | },
33 | "code_actions_on_format": {
34 | "source.fixAll.biome": true,
35 | "source.organizeImports.biome": true
36 | }
37 | },
38 | "TSX": {
39 | "formatter": {
40 | "language_server": {
41 | "name": "biome"
42 | }
43 | },
44 | "code_actions_on_format": {
45 | "source.fixAll.biome": true,
46 | "source.organizeImports.biome": true
47 | }
48 | }
49 | },
50 | "lsp": {
51 | "typescript-language-server": {
52 | "settings": {
53 | "typescript": {
54 | "preferences": {
55 | "includePackageJsonAutoImports": "on"
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
4 | import { useTheme } from 'next-themes';
5 | import { Button } from '../components/ui/button';
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from '../components/ui/dropdown-menu';
12 |
13 | const themes = [
14 | { label: 'Light', value: 'light' },
15 | { label: 'Dark', value: 'dark' },
16 | { label: 'System', value: 'system' },
17 | ];
18 |
19 | export const ModeToggle = () => {
20 | const { setTheme } = useTheme();
21 |
22 | return (
23 |
24 |
25 |
30 |
31 |
32 | Toggle theme
33 |
34 |
35 |
36 | {themes.map(({ label, value }) => (
37 | setTheme(value)}>
38 | {label}
39 |
40 | ))}
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/suggestion.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import type { ComponentProps } from 'react';
5 | import { Button } from '../ui/button';
6 | import { ScrollArea, ScrollBar } from '../ui/scroll-area';
7 |
8 | export type SuggestionsProps = ComponentProps;
9 |
10 | export const Suggestions = ({
11 | className,
12 | children,
13 | ...props
14 | }: SuggestionsProps) => (
15 |
16 |
17 | {children}
18 |
19 |
20 |
21 | );
22 |
23 | export type SuggestionProps = Omit, 'onClick'> & {
24 | suggestion: string;
25 | onClick?: (suggestion: string) => void;
26 | };
27 |
28 | export const Suggestion = ({
29 | suggestion,
30 | onClick,
31 | className,
32 | variant = 'outline',
33 | size = 'sm',
34 | children,
35 | ...props
36 | }: SuggestionProps) => {
37 | const handleClick = () => {
38 | onClick?.(suggestion);
39 | };
40 |
41 | return (
42 |
50 | {children || suggestion}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/apps/obby/app/api/sandboxes/[sandboxId]/files/route.ts:
--------------------------------------------------------------------------------
1 | import { Sandbox } from '@vercel/sandbox';
2 | import { type NextRequest, NextResponse } from 'next/server';
3 | import z from 'zod/v3';
4 | import { env } from '@/env';
5 |
6 | const FileParamsSchema = z.object({
7 | sandboxId: z.string(),
8 | path: z.string(),
9 | });
10 |
11 | export async function GET(
12 | request: NextRequest,
13 | { params }: { params: Promise<{ sandboxId: string }> }
14 | ) {
15 | const { sandboxId } = await params;
16 | const fileParams = FileParamsSchema.safeParse({
17 | path: request.nextUrl.searchParams.get('path'),
18 | sandboxId,
19 | });
20 |
21 | if (fileParams.success === false) {
22 | return NextResponse.json(
23 | { error: 'Invalid parameters. You must pass a `path` as query' },
24 | { status: 400 }
25 | );
26 | }
27 |
28 | const sandbox = await Sandbox.get({
29 | ...fileParams.data,
30 | teamId: env.VERCEL_TEAM_ID ?? '',
31 | projectId: env.VERCEL_PROJECT_ID ?? '',
32 | token: env.VERCEL_TOKEN ?? '',
33 | });
34 | const stream = await sandbox.readFile(fileParams.data);
35 | if (!stream) {
36 | return NextResponse.json(
37 | { error: 'File not found in the Sandbox' },
38 | { status: 404 }
39 | );
40 | }
41 |
42 | return new NextResponse(
43 | new ReadableStream({
44 | async pull(controller) {
45 | for await (const chunk of stream) {
46 | controller.enqueue(chunk);
47 | }
48 | controller.close();
49 | },
50 | })
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/public/logos/ai/openai.svg:
--------------------------------------------------------------------------------
1 | OpenAI
2 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/generate-files.tsx:
--------------------------------------------------------------------------------
1 | import { CloudUploadIcon } from 'lucide-react';
2 | import type { DataPart } from '@/ai/messages/data-parts';
3 | import { MessageSpinner } from '../message-spinner';
4 | import { ToolHeader } from '../tool-header';
5 | import { ToolMessage } from '../tool-message';
6 |
7 | export function GenerateFiles(props: {
8 | className?: string;
9 | message: DataPart['generating-files'];
10 | }) {
11 | const generated =
12 | props.message.status === 'generating'
13 | ? props.message.paths.slice(0, props.message.paths.length - 1)
14 | : props.message.paths;
15 |
16 | const generating =
17 | props.message.status === 'generating' ? props.message.paths.at(-1) : null;
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | {props.message.status === 'done'
25 | ? 'Uploaded files'
26 | : 'Generating files'}
27 |
28 |
29 |
30 |
31 | {generated.map((path) => (
32 |
33 | ✔︎ {path}
34 |
35 | ))}
36 | {generating && (
37 |
38 | {` ${generating}`}
39 |
40 | )}
41 | {props.message.status !== 'done' &&
}
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/obby/stores/use-model-store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { createJSONStorage, persist } from 'zustand/middleware';
3 | import type { ModelProvider } from '@/ai/constants';
4 | import { createIndexedDbStorage } from '@/lib/use-index-db-storage';
5 |
6 | type ModelState = {
7 | selectedProvider?: ModelProvider;
8 | selectedModelId?: string;
9 | _hasHydrated: boolean;
10 |
11 | setProvider: (provider: ModelProvider) => void;
12 | setModel: (modelId: string) => void;
13 | selectModel: (provider: ModelProvider, modelId: string) => void;
14 | _setHasHydrated: (hasHydrated: boolean) => void;
15 | };
16 |
17 | export const useModelStore = create()(
18 | persist(
19 | (set) => ({
20 | selectedProvider: undefined,
21 | selectedModelId: undefined,
22 | _hasHydrated: false,
23 |
24 | setProvider: (provider) => {
25 | set({ selectedProvider: provider });
26 | },
27 |
28 | setModel: (modelId) => {
29 | set({ selectedModelId: modelId });
30 | },
31 |
32 | selectModel: (provider, modelId) => {
33 | set({
34 | selectedProvider: provider,
35 | selectedModelId: modelId,
36 | });
37 | },
38 |
39 | _setHasHydrated: (hasHydrated) => {
40 | set({ _hasHydrated: hasHydrated });
41 | },
42 | }),
43 | {
44 | name: 'model-selection',
45 | storage: createJSONStorage(() =>
46 | createIndexedDbStorage('model-selection')
47 | ),
48 | onRehydrateStorage: () => (state) => {
49 | state?._setHasHydrated(true);
50 | },
51 | }
52 | )
53 | );
54 |
--------------------------------------------------------------------------------
/apps/obby/components/icons/github.tsx:
--------------------------------------------------------------------------------
1 | export function GithubIcon({ className }: { className?: string } = {}) {
2 | return (
3 |
9 | GitHub
10 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/app/docs/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { createRelativeLink } from 'fumadocs-ui/mdx';
2 | import {
3 | DocsBody,
4 | DocsDescription,
5 | DocsPage,
6 | DocsTitle,
7 | } from 'fumadocs-ui/page';
8 | import type { Metadata } from 'next';
9 | import { notFound } from 'next/navigation';
10 | import { source } from '@/lib/source';
11 | import { getMDXComponents } from '@/mdx-components';
12 |
13 | export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
14 | const params = await props.params;
15 | const page = source.getPage(params.slug);
16 | if (!page) {
17 | notFound();
18 | }
19 |
20 | const MDXContent = page.data.body;
21 |
22 | return (
23 |
24 | {page.data.title}
25 | {page.data.description}
26 |
27 |
33 |
34 |
35 | );
36 | }
37 |
38 | export async function generateStaticParams() {
39 | return await source.generateParams();
40 | }
41 |
42 | export async function generateMetadata(
43 | props: PageProps<'/docs/[[...slug]]'>
44 | ): Promise {
45 | const params = await props.params;
46 | const page = source.getPage(params.slug);
47 | if (!page) {
48 | notFound();
49 | }
50 |
51 | return {
52 | title: page.data.title,
53 | description: page.data.description,
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obby-dev",
3 | "version": "0.5.0",
4 | "bin": {
5 | "next-forge": "dist/index.js"
6 | },
7 | "files": [
8 | "dist/index.js"
9 | ],
10 | "scripts": {
11 | "build": "turbo build",
12 | "dev": "turbo dev",
13 | "lint:init": "ultracite init",
14 | "lint": "ultracite lint",
15 | "format": "ultracite format",
16 | "test": "turbo test",
17 | "analyze": "turbo analyze",
18 | "translate": "turbo translate",
19 | "boundaries": "turbo boundaries",
20 | "db:migrate": "cd packages/database && npx drizzle-kit migrate",
21 | "db:generate": "cd packages/database && npx drizzle-kit generate",
22 | "db:pull": "cd packages/database && npx drizzle-kit pull",
23 | "bump-deps": "npx npm-check-updates --deep -u -x react-day-picker",
24 | "bump-ui": "npx shadcn@latest add --all --overwrite -c packages/design-system",
25 | "migrate": "cd packages/database && npx prisma format && npx prisma generate && npx prisma db push",
26 | "clean": "git clean -xdf node_modules"
27 | },
28 | "devDependencies": {
29 | "@auto-it/first-time-contributor": "^11.3.0",
30 | "@biomejs/biome": "2.2.0",
31 | "@repo/typescript-config": "workspace:*",
32 | "@turbo/gen": "^2.5.3",
33 | "@types/node": "^22.15.21",
34 | "tsup": "^8.5.0",
35 | "turbo": "^2.5.3",
36 | "typescript": "^5.8.3",
37 | "ultracite": "^5.1.3",
38 | "vitest": "^3.1.4"
39 | },
40 | "engines": {
41 | "node": ">=18"
42 | },
43 | "packageManager": "pnpm@10.11.0",
44 | "dependencies": {
45 | "@clack/prompts": "^0.11.0",
46 | "commander": "^14.0.0"
47 | },
48 | "type": "module"
49 | }
50 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import type { ComponentProps } from 'react';
5 | import { Button } from '../ui/button';
6 | import {
7 | Tooltip,
8 | TooltipContent,
9 | TooltipProvider,
10 | TooltipTrigger,
11 | } from '../ui/tooltip';
12 |
13 | export type ActionsProps = ComponentProps<'div'>;
14 |
15 | export const Actions = ({ className, children, ...props }: ActionsProps) => (
16 |
17 | {children}
18 |
19 | );
20 |
21 | export type ActionProps = ComponentProps & {
22 | tooltip?: string;
23 | label?: string;
24 | };
25 |
26 | export const Action = ({
27 | tooltip,
28 | children,
29 | label,
30 | className,
31 | variant = 'ghost',
32 | size = 'sm',
33 | ...props
34 | }: ActionProps) => {
35 | const button = (
36 |
46 | {children}
47 | {label || tooltip}
48 |
49 | );
50 |
51 | if (tooltip) {
52 | return (
53 |
54 |
55 | {button}
56 |
57 | {tooltip}
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | return button;
65 | };
66 |
--------------------------------------------------------------------------------
/apps/obby/lib/indexed-db-adapter.ts:
--------------------------------------------------------------------------------
1 | const DB_NAME = 'obby-ai';
2 | const DB_VERSION = 1;
3 |
4 | export async function openDb(): Promise {
5 | return await new Promise((resolve, reject) => {
6 | const req = indexedDB.open(DB_NAME, DB_VERSION);
7 | req.onupgradeneeded = () => {
8 | const db = req.result;
9 | if (!db.objectStoreNames.contains('provider-keys')) {
10 | db.createObjectStore('provider-keys');
11 | }
12 | if (!db.objectStoreNames.contains('model-selection')) {
13 | db.createObjectStore('model-selection');
14 | }
15 | };
16 | req.onsuccess = () => resolve(req.result);
17 | req.onerror = () => reject(req.error);
18 | });
19 | }
20 |
21 | export async function idbPut(
22 | storeName: string,
23 | key: IDBValidKey,
24 | value: T | null
25 | ) {
26 | const db = await openDb();
27 | return new Promise((resolve, reject) => {
28 | const tx = db.transaction(storeName, 'readwrite');
29 | const store = tx.objectStore(storeName);
30 | const req = value == null ? store.delete(key) : store.put(value, key);
31 | req.onsuccess = () => resolve();
32 | req.onerror = () => reject(req.error);
33 | });
34 | }
35 |
36 | export async function idbGet(
37 | storeName: string,
38 | key: IDBValidKey
39 | ): Promise {
40 | const db = await openDb();
41 | return new Promise((resolve, reject) => {
42 | const tx = db.transaction(storeName, 'readonly');
43 | const store = tx.objectStore(storeName);
44 | const req = store.get(key);
45 | req.onsuccess = () => resolve(req.result as T | undefined);
46 | req.onerror = () => reject(req.error);
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/apps/obby/public/providers/openai.svg:
--------------------------------------------------------------------------------
1 | OpenAI
--------------------------------------------------------------------------------
/packages/design-system/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { CircleIcon } from "lucide-react"
6 |
7 | import { cn } from "@repo/design-system/lib/utils"
8 |
9 | function RadioGroup({
10 | className,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 | )
20 | }
21 |
22 | function RadioGroupItem({
23 | className,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
35 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export { RadioGroup, RadioGroupItem }
46 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | # web
2 |
3 | This is a Next.js application generated with
4 | [Create Fumadocs](https://github.com/fuma-nama/fumadocs).
5 |
6 | Run development server:
7 |
8 | ```bash
9 | npm run dev
10 | # or
11 | pnpm dev
12 | # or
13 | yarn dev
14 | ```
15 |
16 | Open http://localhost:3000 with your browser to see the result.
17 |
18 | ## Explore
19 |
20 | In the project, you can see:
21 |
22 | - `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
23 | - `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
24 |
25 | | Route | Description |
26 | | ------------------------- | ------------------------------------------------------ |
27 | | `app/(home)` | The route group for your landing page and other pages. |
28 | | `app/docs` | The documentation layout and pages. |
29 | | `app/api/search/route.ts` | The Route Handler for search. |
30 |
31 | ### Fumadocs MDX
32 |
33 | A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
34 |
35 | Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
36 |
37 | ## Learn More
38 |
39 | To learn more about Next.js and Fumadocs, take a look at the following
40 | resources:
41 |
42 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
43 | features and API.
44 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
45 | - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
46 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function HoverCard({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function HoverCardTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return (
18 |
19 | )
20 | }
21 |
22 | function HoverCardContent({
23 | className,
24 | align = "center",
25 | sideOffset = 4,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
30 |
40 |
41 | )
42 | }
43 |
44 | export { HoverCard, HoverCardTrigger, HoverCardContent }
45 |
--------------------------------------------------------------------------------
/apps/obby/components/commands-logs/commands-logs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ScrollArea } from '@repo/design-system/components/ui/scroll-area';
4 | import { SquareChevronRight } from 'lucide-react';
5 | import { useEffect, useRef } from 'react';
6 | import { Panel, PanelHeader } from '@/components/panels/panels';
7 | import { CommandLogs } from './command-logs';
8 | import type { Command, CommandLog } from './types';
9 |
10 | type Props = {
11 | className?: string;
12 | commands: Command[];
13 | onLog: (data: { sandboxId: string; cmdId: string; log: CommandLog }) => void;
14 | onCompleted: (data: Command) => void;
15 | };
16 |
17 | export function CommandsLogs(props: Props) {
18 | const bottomRef = useRef(null);
19 |
20 | // biome-ignore lint/correctness/useExhaustiveDependencies: wrong suggestion
21 | useEffect(() => {
22 | bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
23 | }, [props.commands]);
24 |
25 | return (
26 |
27 |
28 |
29 |
30 | Sandbox Remote Output
31 |
32 |
33 |
34 |
35 |
36 | {props.commands.map((command) => (
37 |
43 | ))}
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@repo/design-system/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-9 px-2 min-w-9",
20 | sm: "h-8 px-1.5 min-w-8",
21 | lg: "h-10 px-2.5 min-w-10",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | function Toggle({
32 | className,
33 | variant,
34 | size,
35 | ...props
36 | }: React.ComponentProps &
37 | VariantProps) {
38 | return (
39 |
44 | )
45 | }
46 |
47 | export { Toggle, toggleVariants }
48 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Next.js: debug server-side",
6 | "type": "node-terminal",
7 | "request": "launch",
8 | "command": "pnpm dev"
9 | },
10 | {
11 | "name": "Next.js: debug client-side (app)",
12 | "type": "chrome",
13 | "request": "launch",
14 | "url": "http://localhost:3000"
15 | },
16 | {
17 | "name": "Next.js: debug client-side (web)",
18 | "type": "chrome",
19 | "request": "launch",
20 | "url": "http://localhost:3001"
21 | },
22 | {
23 | "name": "Next.js: debug client-side (api)",
24 | "type": "chrome",
25 | "request": "launch",
26 | "url": "http://localhost:3002"
27 | },
28 | {
29 | "name": "Next.js: debug client-side (email)",
30 | "type": "chrome",
31 | "request": "launch",
32 | "url": "http://localhost:3003"
33 | },
34 | {
35 | "name": "Next.js: debug client-side (app)",
36 | "type": "chrome",
37 | "request": "launch",
38 | "url": "http://localhost:3004"
39 | },
40 | {
41 | "name": "Next.js: debug client-side (studio)",
42 | "type": "chrome",
43 | "request": "launch",
44 | "url": "http://localhost:3005"
45 | },
46 | {
47 | "name": "Next.js: debug full stack",
48 | "type": "node",
49 | "request": "launch",
50 | "program": "${workspaceFolder}/node_modules/.bin/next",
51 | "runtimeArgs": ["--inspect"],
52 | "skipFiles": ["/**"],
53 | "serverReadyAction": {
54 | "action": "debugWithEdge",
55 | "killOnServerStop": true,
56 | "pattern": "- Local:.+(https?://.+)",
57 | "uriFormat": "%s",
58 | "webRoot": "${workspaceFolder}"
59 | }
60 | }
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/scripts/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ExecSyncOptions, exec as execRaw } from 'node:child_process';
2 | import { readFile } from 'node:fs/promises';
3 | import { join } from 'node:path';
4 | import { promisify } from 'node:util';
5 |
6 | export const url = 'https://github.com/vercel/next-forge';
7 |
8 | export const cleanFileName = (file: string) =>
9 | file.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\\/g, '/');
10 |
11 | export const execSyncOpts: ExecSyncOptions = { stdio: 'ignore' };
12 |
13 | export const internalContentDirs = [join('.github', 'workflows'), 'docs'];
14 |
15 | export const internalContentFiles = [
16 | join('.github', 'CONTRIBUTING.md'),
17 | join('.github', 'FUNDING.yml'),
18 | join('.github', 'SECURITY.md'),
19 | '.autorc',
20 | 'CHANGELOG.md',
21 | 'license.md',
22 | ];
23 |
24 | export const allInternalContent = [
25 | ...internalContentDirs,
26 | ...internalContentFiles,
27 | ];
28 |
29 | export const semver = /^\d+\.\d+\.\d+$/;
30 |
31 | export const tempDirName = 'next-forge-update';
32 |
33 | export const exec = promisify(execRaw);
34 |
35 | export const supportedPackageManagers = ['npm', 'yarn', 'bun', 'pnpm'];
36 |
37 | export const getAvailableVersions = async (): Promise => {
38 | const changelog = await readFile('CHANGELOG.md', 'utf-8');
39 | const versionRegex = /# v(\d+\.\d+\.\d+)/g;
40 | const matches = [...changelog.matchAll(versionRegex)];
41 |
42 | return matches
43 | .map((match) => match[1])
44 | .sort((a, b) => {
45 | const [aMajor, aMinor, aPatch] = a.split('.').map(Number);
46 | const [bMajor, bMinor, bPatch] = b.split('.').map(Number);
47 | if (aMajor !== bMajor) {
48 | return bMajor - aMajor;
49 | }
50 | if (aMinor !== bMinor) {
51 | return bMinor - aMinor;
52 | }
53 | return bPatch - aPatch;
54 | });
55 | };
56 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/message.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import type { UIMessage } from 'ai';
3 | import type { ComponentProps, HTMLAttributes } from 'react';
4 | import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
5 |
6 | export type MessageProps = HTMLAttributes & {
7 | from: UIMessage['role'];
8 | };
9 |
10 | export const Message = ({ className, from, ...props }: MessageProps) => (
11 | div]:max-w-[80%]',
16 | className
17 | )}
18 | {...props}
19 | />
20 | );
21 |
22 | export type MessageContentProps = HTMLAttributes
;
23 |
24 | export const MessageContent = ({
25 | children,
26 | className,
27 | ...props
28 | }: MessageContentProps) => (
29 |
39 | {children}
40 |
41 | );
42 |
43 | export type MessageAvatarProps = ComponentProps & {
44 | src: string;
45 | name?: string;
46 | };
47 |
48 | export const MessageAvatar = ({
49 | src,
50 | name,
51 | className,
52 | ...props
53 | }: MessageAvatarProps) => (
54 |
55 |
56 | {name?.slice(0, 2) || 'ME'}
57 |
58 | );
59 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@repo/design-system/lib/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/apps/obby/ai/tools/get-sandbox-url.ts:
--------------------------------------------------------------------------------
1 | import { Sandbox } from '@vercel/sandbox';
2 | import type { UIMessage, UIMessageStreamWriter } from 'ai';
3 | import { tool } from 'ai';
4 | import z from 'zod/v3';
5 | import { env } from '@/env';
6 | import type { DataPart } from '../messages/data-parts';
7 | import description from './get-sandbox-url.md';
8 |
9 | type Params = {
10 | writer: UIMessageStreamWriter>;
11 | };
12 |
13 | export const getSandboxURL = ({ writer }: Params) =>
14 | tool({
15 | description,
16 | inputSchema: z.object({
17 | sandboxId: z
18 | .string()
19 | .describe(
20 | "The unique identifier of the Vercel Sandbox (e.g., 'sbx_abc123xyz'). This ID is returned when creating a Vercel Sandbox and is used to reference the specific sandbox instance."
21 | ),
22 | port: z
23 | .number()
24 | .describe(
25 | 'The port number where a service is running inside the Vercel Sandbox (e.g., 3000 for Next.js dev server, 8000 for Python apps, 5000 for Flask). The port must have been exposed when the sandbox was created or when running commands.'
26 | ),
27 | }),
28 | execute: async ({ sandboxId, port }, { toolCallId }) => {
29 | writer.write({
30 | id: toolCallId,
31 | type: 'data-get-sandbox-url',
32 | data: { status: 'loading' },
33 | });
34 |
35 | const sandbox = await Sandbox.get({
36 | sandboxId,
37 | teamId: env.VERCEL_TEAM_ID ?? '',
38 | projectId: env.VERCEL_PROJECT_ID ?? '',
39 | token: env.VERCEL_TOKEN ?? '',
40 | });
41 | const url = sandbox.domain(port);
42 |
43 | writer.write({
44 | id: toolCallId,
45 | type: 'data-get-sandbox-url',
46 | data: { url, status: 'done' },
47 | });
48 |
49 | return { url };
50 | },
51 | });
52 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Popover({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function PopoverTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function PopoverContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
42 | function PopoverAnchor({
43 | ...props
44 | }: React.ComponentProps) {
45 | return
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
49 |
--------------------------------------------------------------------------------
/packages/ai/keys.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from '@t3-oss/env-nextjs';
2 | import { z } from 'zod';
3 |
4 | export const keys = () =>
5 | createEnv({
6 | server: {
7 | OPENAI_API_KEY: z.string().startsWith('sk-').optional(),
8 | AI_GATEWAY_API_KEY: z.string().optional(),
9 | ANTHROPIC_API_KEY: z.string().optional(),
10 | GOOGLE_GENERATIVE_AI_API_KEY: z.string().optional(),
11 | GROQ_API_KEY: z.string().optional(),
12 | OPENROUTER_API_KEY: z.string().optional(),
13 | VERCEL_API_KEY: z.string().optional(),
14 | AWS_ACCESS_KEY_ID: z.string().optional(),
15 | AWS_SECRET_ACCESS_KEY: z.string().optional(),
16 | AWS_REGION: z.string().optional(),
17 | FIRECRAWL_API_KEY: z.string().optional(),
18 | // teamId: env.VERCEL_TEAM_ID,
19 | // projectId: env.VERCEL_PROJECT_ID,
20 | // token: env.VERCEL_TOKEN,
21 | VERCEL_TEAM_ID: z.string().optional(),
22 | VERCEL_PROJECT_ID: z.string().optional(),
23 | VERCEL_TOKEN: z.string().optional(),
24 | },
25 | runtimeEnv: {
26 | OPENAI_API_KEY: process.env.OPENAI_API_KEY,
27 | AI_GATEWAY_API_KEY: process.env.AI_GATEWAY_API_KEY,
28 | ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
29 | GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
30 | GROQ_API_KEY: process.env.GROQ_API_KEY,
31 | OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
32 | AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
33 | AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
34 | AWS_REGION: process.env.AWS_REGION,
35 | FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY,
36 | VERCEL_API_KEY: process.env.VERCEL_API_KEY,
37 | VERCEL_TEAM_ID: process.env.VERCEL_TEAM_ID,
38 | VERCEL_PROJECT_ID: process.env.VERCEL_PROJECT_ID,
39 | VERCEL_TOKEN: process.env.VERCEL_TOKEN,
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/conversation.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import { ArrowDownIcon } from 'lucide-react';
5 | import type { ComponentProps } from 'react';
6 | import { useCallback } from 'react';
7 | import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
8 | import { Button } from '../ui/button';
9 |
10 | export type ConversationProps = ComponentProps;
11 |
12 | export const Conversation = ({ className, ...props }: ConversationProps) => (
13 |
20 | );
21 |
22 | export type ConversationContentProps = ComponentProps<
23 | typeof StickToBottom.Content
24 | >;
25 |
26 | export const ConversationContent = ({
27 | className,
28 | ...props
29 | }: ConversationContentProps) => (
30 |
31 | );
32 |
33 | export type ConversationScrollButtonProps = ComponentProps;
34 |
35 | export const ConversationScrollButton = ({
36 | className,
37 | ...props
38 | }: ConversationScrollButtonProps) => {
39 | const { isAtBottom, scrollToBottom } = useStickToBottomContext();
40 |
41 | const handleScrollToBottom = useCallback(() => {
42 | scrollToBottom();
43 | }, [scrollToBottom]);
44 |
45 | return (
46 | !isAtBottom && (
47 |
58 |
59 |
60 | )
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function ScrollArea({
9 | className,
10 | children,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 |
23 | {children}
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function ScrollBar({
32 | className,
33 | orientation = "vertical",
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
50 |
54 |
55 | )
56 | }
57 |
58 | export { ScrollArea, ScrollBar }
59 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@repo/design-system/lib/utils';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 | import type * as React from 'react';
4 |
5 | const alertVariants = cva(
6 | 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
7 | {
8 | variants: {
9 | variant: {
10 | default: 'bg-card text-card-foreground',
11 | destructive:
12 | 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current',
13 | },
14 | },
15 | defaultVariants: {
16 | variant: 'default',
17 | },
18 | }
19 | );
20 |
21 | function Alert({
22 | className,
23 | variant,
24 | ...props
25 | }: React.ComponentProps<'div'> & VariantProps) {
26 | return (
27 |
33 | );
34 | }
35 |
36 | function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
37 | return (
38 |
46 | );
47 | }
48 |
49 | function AlertDescription({
50 | className,
51 | ...props
52 | }: React.ComponentProps<'div'>) {
53 | return (
54 |
62 | );
63 | }
64 |
65 | export { Alert, AlertTitle, AlertDescription };
66 |
--------------------------------------------------------------------------------
/apps/obby/ai/messages/data-parts.ts:
--------------------------------------------------------------------------------
1 | import type { Document, SearchResultWeb } from '@mendable/firecrawl-js';
2 | import z from 'zod/v3';
3 |
4 | export const dataPartSchema = z.object({
5 | 'create-sandbox': z.object({
6 | sandboxId: z.string().optional(),
7 | status: z.enum(['loading', 'done']),
8 | }),
9 | 'generating-files': z.object({
10 | paths: z.array(z.string()),
11 | status: z.enum(['generating', 'uploading', 'uploaded', 'done']),
12 | }),
13 | 'run-command': z.object({
14 | command: z.string(),
15 | args: z.array(z.string()),
16 | status: z.enum(['loading', 'done']),
17 | commandId: z.string().optional(),
18 | sandboxId: z.string(),
19 | }),
20 | 'wait-command': z.object({
21 | sandboxId: z.string(),
22 | commandId: z.string(),
23 | command: z.string(),
24 | args: z.array(z.string()),
25 | exitCode: z.number().optional(),
26 | status: z.enum(['loading', 'done']),
27 | }),
28 | 'get-sandbox-url': z.object({
29 | url: z.string().optional(),
30 | status: z.enum(['loading', 'done']),
31 | }),
32 | 'web-scrape': z.object({
33 | url: z.string().optional(),
34 | status: z.enum(['loading', 'done', 'error']),
35 | result: z.custom().optional(),
36 | error: z.string().optional(),
37 | }),
38 | 'web-search': z.object({
39 | query: z.string().optional(),
40 | status: z.enum(['loading', 'done', 'error']),
41 | results: z.custom().optional(),
42 | resultsCount: z.number().optional(),
43 | error: z.string().optional(),
44 | }),
45 | context7: z.object({
46 | query: z.string().optional(),
47 | status: z.enum(['loading', 'done', 'error']),
48 | libraryId: z.string().optional(),
49 | topic: z.string().optional(),
50 | tokens: z.number().optional(),
51 | result: z.string().optional(),
52 | error: z.string().optional(),
53 | }),
54 | });
55 |
56 | export type DataPart = z.infer;
57 |
--------------------------------------------------------------------------------
/apps/obby/components/chat/message-part/index.tsx:
--------------------------------------------------------------------------------
1 | import type { UIMessage } from 'ai';
2 | import type { DataPart } from '@/ai/messages/data-parts';
3 | import type { Metadata } from '@/ai/messages/metadata';
4 | import type { ToolSet } from '@/ai/tools';
5 | import { Context7 } from './context7';
6 | import { CreateSandbox } from './create-sandbox';
7 | import { GenerateFiles } from './generate-files';
8 | import { GetSandboxURL } from './get-sandbox-url';
9 | import { Reasoning } from './reasoning';
10 | import { RunCommand } from './run-command';
11 | import { Text } from './text';
12 | import { WaitCommand } from './wait-command';
13 | import { WebScrape } from './web-scrape';
14 | import { WebSearch } from './web-search';
15 |
16 | type Props = {
17 | part: UIMessage['parts'][number];
18 | };
19 |
20 | export function MessagePart({ part }: Props) {
21 | if (part.type === 'data-generating-files') {
22 | return ;
23 | }
24 | if (part.type === 'data-create-sandbox') {
25 | return ;
26 | }
27 | if (part.type === 'data-get-sandbox-url') {
28 | return ;
29 | }
30 | if (part.type === 'data-run-command') {
31 | return ;
32 | }
33 | if (part.type === 'data-wait-command') {
34 | return ;
35 | }
36 | if (part.type === 'data-web-scrape') {
37 | return ;
38 | }
39 | if (part.type === 'data-web-search') {
40 | return ;
41 | }
42 | if (part.type === 'data-context7') {
43 | return ;
44 | }
45 | if (part.type === 'reasoning') {
46 | return ;
47 | }
48 | if (part.type === 'text') {
49 | return ;
50 | }
51 | if (part.type) {
52 | // console.log(JSON.stringify(part, undefined, 4));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/apps/obby/ai/tools/index.ts:
--------------------------------------------------------------------------------
1 | import type { InferUITools, UIMessage, UIMessageStreamWriter } from 'ai';
2 | import type { ModelProvider } from '../constants';
3 | import type { DataPart } from '../messages/data-parts';
4 | import type { ToolOptions } from '../validation';
5 | import { context7 } from './context7';
6 | import { createSandbox } from './create-sandbox';
7 | import { generateFiles } from './generate-files';
8 | import { getSandboxURL } from './get-sandbox-url';
9 | import { runCommand } from './run-command';
10 | import { waitCommand } from './wait-command';
11 | import { webScrape } from './web-scrape';
12 | import { webSearch } from './web-search';
13 |
14 | type BaseParams = {
15 | provider: ModelProvider | undefined;
16 | modelId: string;
17 | writer: UIMessageStreamWriter>;
18 | };
19 |
20 | export type ToolsBuilderParams = BaseParams & {
21 | options?: {
22 | tools?: ToolOptions;
23 | };
24 | };
25 |
26 | export function tools(params: ToolsBuilderParams) {
27 | const { provider, modelId, writer, options } = params;
28 | const enabled = options?.tools ?? {};
29 |
30 | const base = {
31 | createSandbox: createSandbox({ writer }),
32 | generateFiles: generateFiles({ writer, modelId, provider }),
33 | getSandboxURL: getSandboxURL({ writer }),
34 | runCommand: runCommand({ writer }),
35 | waitCommand: waitCommand({ writer }),
36 | };
37 |
38 | const optional = {
39 | ...(enabled.webScrape ? { webScrape: webScrape({ writer }) } : {}),
40 | ...(enabled.webSearch ? { webSearch: webSearch({ writer }) } : {}),
41 | ...(enabled.context7 ? { context7: context7({ writer }) } : {}),
42 | };
43 |
44 | return {
45 | ...base,
46 | ...optional,
47 | };
48 | }
49 |
50 | export type ToolSet = InferUITools>;
51 |
52 | export const AVAILABLE_OPTIONAL_TOOLS = [
53 | 'webScrape',
54 | 'webSearch',
55 | 'context7',
56 | ] as const;
57 |
58 | export type OptionalToolName = (typeof AVAILABLE_OPTIONAL_TOOLS)[number];
59 |
--------------------------------------------------------------------------------
/apps/obby/app/(main)/page.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from "lucide-react";
2 | import { Suspense } from "react";
3 | import { TabContent, TabGroup, TabItem } from "@/components/tabs";
4 | import { Chat } from "./chat";
5 | import { FileExplorer } from "./file-explorer";
6 | import { Header } from "./header";
7 | import { Logs } from "./logs";
8 | import { Preview } from "./preview";
9 |
10 | export default function Page() {
11 | return (
12 |
13 |
}>
14 |
15 |
16 | Chat
17 | Preview
18 | File Explorer
19 | Logs
20 |
21 |
22 |
26 |
27 |
28 |
29 | {/* Preview - now contains both web preview and code view */}
30 |
31 |
32 |
33 |
34 | {/* File Explorer - ONLY visible on mobile */}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/public/logos/ai/deepseek.svg:
--------------------------------------------------------------------------------
1 | DeepSeek
--------------------------------------------------------------------------------
/packages/design-system/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 0,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
62 |
--------------------------------------------------------------------------------
/packages/next-config/index.ts:
--------------------------------------------------------------------------------
1 | import withBundleAnalyzer from '@next/bundle-analyzer';
2 |
3 | // @ts-expect-error No declaration file
4 | import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin';
5 | import type { NextConfig } from 'next';
6 |
7 | const otelRegex = /@opentelemetry\/instrumentation/;
8 | const mdRegex = /\.md/;
9 |
10 | export const config: NextConfig = {
11 | transpilePackages: ['shiki'],
12 |
13 | turbopack: {
14 | rules: {
15 | '*.md': {
16 | loaders: ['raw-loader'],
17 | as: '*.js',
18 | },
19 | },
20 | },
21 |
22 | images: {
23 | formats: ['image/avif', 'image/webp'],
24 | remotePatterns: [
25 | {
26 | protocol: 'https',
27 | hostname: 'img.clerk.com',
28 | },
29 | ],
30 | dangerouslyAllowSVG: true,
31 | contentDispositionType: 'attachment',
32 | contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
33 | },
34 |
35 | // biome-ignore lint/suspicious/useAwait: rewrites is async
36 | async rewrites() {
37 | return [
38 | {
39 | source: '/ingest/static/:path*',
40 | destination: 'https://us-assets.i.posthog.com/static/:path*',
41 | },
42 | {
43 | source: '/ingest/:path*',
44 | destination: 'https://us.i.posthog.com/:path*',
45 | },
46 | {
47 | source: '/ingest/decide',
48 | destination: 'https://us.i.posthog.com/decide',
49 | },
50 | ];
51 | },
52 |
53 | // biome-ignore lint/nursery/noShadow: config is a valid parameter name
54 | webpack(config, { isServer }) {
55 | if (isServer) {
56 | config.plugins = config.plugins || [];
57 | config.plugins.push(new PrismaPlugin());
58 | }
59 |
60 | config.module.rules.push({
61 | test: mdRegex,
62 | type: 'asset/source',
63 | });
64 |
65 | config.ignoreWarnings = [{ module: otelRegex }];
66 |
67 | return config;
68 | },
69 |
70 | // This is required to support PostHog trailing slash API requests
71 | skipTrailingSlashRedirect: true,
72 | };
73 |
74 | export const withAnalyzer = (sourceConfig: NextConfig): NextConfig =>
75 | withBundleAnalyzer()(sourceConfig);
76 |
--------------------------------------------------------------------------------
/packages/design-system/components/ai-elements/sources.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@repo/design-system/lib/utils';
4 | import { BookIcon, ChevronDownIcon } from 'lucide-react';
5 | import type { ComponentProps } from 'react';
6 | import {
7 | Collapsible,
8 | CollapsibleContent,
9 | CollapsibleTrigger,
10 | } from '../ui/collapsible';
11 |
12 | export type SourcesProps = ComponentProps<'div'>;
13 |
14 | export const Sources = ({ className, ...props }: SourcesProps) => (
15 |
19 | );
20 |
21 | export type SourcesTriggerProps = ComponentProps & {
22 | count: number;
23 | };
24 |
25 | export const SourcesTrigger = ({
26 | className,
27 | count,
28 | children,
29 | ...props
30 | }: SourcesTriggerProps) => (
31 |
35 | {children ?? (
36 | <>
37 | Used {count} sources
38 |
39 | >
40 | )}
41 |
42 | );
43 |
44 | export type SourcesContentProps = ComponentProps;
45 |
46 | export const SourcesContent = ({
47 | className,
48 | ...props
49 | }: SourcesContentProps) => (
50 |
58 | );
59 |
60 | export type SourceProps = ComponentProps<'a'>;
61 |
62 | export const Source = ({ href, title, children, ...props }: SourceProps) => (
63 |
70 | {children ?? (
71 | <>
72 |
73 | {title}
74 | >
75 | )}
76 |
77 | );
78 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Tabs({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function TabsList({
22 | className,
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
34 | )
35 | }
36 |
37 | function TabsTrigger({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | function TabsContent({
54 | className,
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
63 | )
64 | }
65 |
66 | export { Tabs, TabsList, TabsTrigger, TabsContent }
67 |
--------------------------------------------------------------------------------
/apps/obby/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obby-app",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev -p 3000 --turbopack",
6 | "build": "next build --turbopack",
7 | "start": "next start",
8 | "analyze": "ANALYZE=true npm run build",
9 | "clean": "git clean -xdf .cache .turbo dist node_modules",
10 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
11 | },
12 | "dependencies": {
13 | "@ai-sdk/amazon-bedrock": "^3.0.4",
14 | "@ai-sdk/anthropic": "^2.0.1",
15 | "@ai-sdk/gateway": "1.0.0-beta.15",
16 | "@ai-sdk/google": "^2.0.4",
17 | "@ai-sdk/groq": "^2.0.5",
18 | "@ai-sdk/openai": "^2.0.8",
19 | "@ai-sdk/react": "2.0.0-beta.29",
20 | "@ai-sdk/vercel": "1.0.0-alpha.7",
21 | "@mendable/firecrawl-js": "^4.3.1",
22 | "@openrouter/ai-sdk-provider": "^1.1.2",
23 | "@repo/database": "workspace:*",
24 | "@repo/design-system": "workspace:*",
25 | "@repo/next-config": "workspace:*",
26 | "@sentry/nextjs": "^9.22.0",
27 | "@t3-oss/env-nextjs": "^0.13.4",
28 | "@tanstack/react-query": "^5.85.5",
29 | "@types/react-syntax-highlighter": "15.5.13",
30 | "@vercel/sandbox": "0.0.17",
31 | "@vercel/toolbar": "^0.1.38",
32 | "ai": "5.0.0-beta.29",
33 | "botid": "1.4.5",
34 | "effect": "^3.17.10",
35 | "fuse.js": "^7.1.0",
36 | "import-in-the-middle": "^1.13.2",
37 | "lucide-react": "^0.511.0",
38 | "ms": "^2.1.3",
39 | "next": "15.5.9",
40 | "next-themes": "^0.4.6",
41 | "nuqs": "2.4.3",
42 | "react": "19.1.0",
43 | "react-dom": "19.1.0",
44 | "react-markdown": "^10.1.0",
45 | "react-spinners": "0.17.0",
46 | "react-syntax-highlighter": "15.6.1",
47 | "rehype-raw": "7.0.0",
48 | "remark-gfm": "4.0.1",
49 | "require-in-the-middle": "^7.5.2",
50 | "sonner": "^2.0.7",
51 | "streamdown": "^1.2.0",
52 | "swr": "2.3.4",
53 | "zod": "^3.25.28",
54 | "zustand": "^4.5.6"
55 | },
56 | "devDependencies": {
57 | "@repo/typescript-config": "workspace:*",
58 | "@types/ms": "^2.1.0",
59 | "@types/node": "22.15.21",
60 | "@types/react": "19.1.5",
61 | "@types/react-dom": "19.1.5",
62 | "jsdom": "^26.1.0",
63 | "raw-loader": "4.0.2",
64 | "tailwindcss": "^4.1.7",
65 | "typescript": "^5.8.3"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { GripVerticalIcon } from "lucide-react"
5 | import * as ResizablePrimitive from "react-resizable-panels"
6 |
7 | import { cn } from "@repo/design-system/lib/utils"
8 |
9 | function ResizablePanelGroup({
10 | className,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
22 | )
23 | }
24 |
25 | function ResizablePanel({
26 | ...props
27 | }: React.ComponentProps) {
28 | return
29 | }
30 |
31 | function ResizableHandle({
32 | withHandle,
33 | className,
34 | ...props
35 | }: React.ComponentProps & {
36 | withHandle?: boolean
37 | }) {
38 | return (
39 | div]:rotate-90",
43 | className
44 | )}
45 | {...props}
46 | >
47 | {withHandle && (
48 |
49 |
50 |
51 | )}
52 |
53 | )
54 | }
55 |
56 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
57 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@repo/design-system/lib/utils"
7 |
8 | function Slider({
9 | className,
10 | defaultValue,
11 | value,
12 | min = 0,
13 | max = 100,
14 | ...props
15 | }: React.ComponentProps) {
16 | const _values = React.useMemo(
17 | () =>
18 | Array.isArray(value)
19 | ? value
20 | : Array.isArray(defaultValue)
21 | ? defaultValue
22 | : [min, max],
23 | [value, defaultValue, min, max]
24 | )
25 |
26 | return (
27 |
39 |
45 |
51 |
52 | {Array.from({ length: _values.length }, (_, index) => (
53 |
58 | ))}
59 |
60 | )
61 | }
62 |
63 | export { Slider }
64 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@repo/design-system/lib/utils"
8 | import { toggleVariants } from "@repo/design-system/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | function ToggleGroup({
18 | className,
19 | variant,
20 | size,
21 | children,
22 | ...props
23 | }: React.ComponentProps &
24 | VariantProps) {
25 | return (
26 |
36 |
37 | {children}
38 |
39 |
40 | )
41 | }
42 |
43 | function ToggleGroupItem({
44 | className,
45 | children,
46 | variant,
47 | size,
48 | ...props
49 | }: React.ComponentProps &
50 | VariantProps) {
51 | const context = React.useContext(ToggleGroupContext)
52 |
53 | return (
54 |
68 | {children}
69 |
70 | )
71 | }
72 |
73 | export { ToggleGroup, ToggleGroupItem }
74 |
--------------------------------------------------------------------------------
/public/logos/ai/mistral.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as AccordionPrimitive from '@radix-ui/react-accordion';
4 | import { cn } from '@repo/design-system/lib/utils';
5 | import { ChevronDownIcon } from 'lucide-react';
6 | import type * as React from 'react';
7 |
8 | function Accordion({
9 | ...props
10 | }: React.ComponentProps) {
11 | return ;
12 | }
13 |
14 | function AccordionItem({
15 | className,
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
24 | );
25 | }
26 |
27 | function AccordionTrigger({
28 | className,
29 | children,
30 | ...props
31 | }: React.ComponentProps) {
32 | return (
33 |
34 | svg]:rotate-180',
37 | className
38 | )}
39 | data-slot="accordion-trigger"
40 | {...props}
41 | >
42 | {children}
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | function AccordionContent({
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps) {
54 | return (
55 |
60 | {children}
61 |
62 | );
63 | }
64 |
65 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
66 |
--------------------------------------------------------------------------------
/apps/obby/components/model-selector/cmdk/provider-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@repo/design-system/components/ui/button';
2 | import { cn } from '@repo/design-system/lib/utils';
3 | import { LayersIcon } from 'lucide-react';
4 | import { type ModelProvider, PROVIDER_LOGOS, PROVIDERS } from '@/ai/constants';
5 |
6 | type Props = {
7 | selected?: ModelProvider;
8 | onHover: (provider: ModelProvider) => void;
9 | onCommit: (provider: ModelProvider) => void;
10 | };
11 |
12 | export function ProviderSidebar({ selected, onHover, onCommit }: Props) {
13 | const iconFor = (provider: ModelProvider) =>
14 | PROVIDER_LOGOS[provider] ?? '/providers/openai.svg';
15 |
16 | const handleCommit = (p: ModelProvider) => {
17 | onCommit(p);
18 | };
19 |
20 | const handleHover = (p: ModelProvider) => {
21 | onHover(p);
22 | };
23 |
24 | return (
25 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/apps/obby/ai/tools/wait-command.ts:
--------------------------------------------------------------------------------
1 | import { Sandbox } from '@vercel/sandbox';
2 | import type { UIMessage, UIMessageStreamWriter } from 'ai';
3 | import { tool } from 'ai';
4 | import z from 'zod/v3';
5 | import { env } from '@/env';
6 | import type { DataPart } from '../messages/data-parts';
7 | import description from './wait-command.md';
8 |
9 | type Params = {
10 | writer: UIMessageStreamWriter>;
11 | };
12 |
13 | export const waitCommand = ({ writer }: Params) =>
14 | tool({
15 | description,
16 | inputSchema: z.object({
17 | sandboxId: z
18 | .string()
19 | .describe('The ID of the Sandbox where the command was run'),
20 | commandId: z.string().describe('The ID of the command to wait for.'),
21 | command: z
22 | .string()
23 | .describe('The command field of the command to wait for'),
24 | args: z
25 | .array(z.string())
26 | .describe('The arguments field of the command to wait for'),
27 | }),
28 | execute: async (
29 | { sandboxId, commandId, command, args },
30 | { toolCallId }
31 | ) => {
32 | writer.write({
33 | id: toolCallId,
34 | type: 'data-wait-command',
35 | data: { command, args, commandId, sandboxId, status: 'loading' },
36 | });
37 |
38 | const sandbox = await Sandbox.get({
39 | sandboxId,
40 | teamId: env.VERCEL_TEAM_ID ?? '',
41 | projectId: env.VERCEL_PROJECT_ID ?? '',
42 | token: env.VERCEL_TOKEN ?? '',
43 | });
44 | const cmd = await sandbox.getCommand(commandId);
45 | const done = await cmd.wait();
46 | const [stdout, stderr] = await Promise.all([
47 | done.stdout(),
48 | done.stderr(),
49 | ]);
50 |
51 | writer.write({
52 | id: toolCallId,
53 | type: 'data-wait-command',
54 | data: {
55 | command,
56 | args,
57 | commandId,
58 | sandboxId,
59 | exitCode: done.exitCode,
60 | status: 'done',
61 | },
62 | });
63 |
64 | return (
65 | `The command with ID ${commandId} has finished with exit code ${done.exitCode}.\n` +
66 | 'Stdout of the command was: \n' +
67 | `\`\`\`\n${stdout}\n\`\`\`\n` +
68 | 'Stderr of the command was: \n' +
69 | `\`\`\`\n${stderr}\n\`\`\``
70 | );
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@repo/design-system/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/apps/obby/components/file-explorer/build-file-tree.tsx:
--------------------------------------------------------------------------------
1 | export type FileNode = {
2 | children?: FileNode[];
3 | content?: string;
4 | expanded?: boolean;
5 | name: string;
6 | path: string;
7 | type: 'file' | 'folder';
8 | };
9 |
10 | type FileNodeBuilder = {
11 | children?: { [key: string]: FileNodeBuilder };
12 | content?: string;
13 | expanded?: boolean;
14 | name: string;
15 | path: string;
16 | type: 'file' | 'folder';
17 | };
18 |
19 | export function buildFileTree(paths: string[]): FileNode[] {
20 | type NodeMap = Record;
21 | const root: NodeMap = {};
22 |
23 | const createNode = (
24 | part: string,
25 | isFile: boolean,
26 | currentPath: string
27 | ): FileNodeBuilder => ({
28 | name: part,
29 | type: isFile ? 'file' : 'folder',
30 | path: currentPath,
31 | content: isFile
32 | ? `// Content for ${currentPath}\n// This will be loaded when the file is selected`
33 | : undefined,
34 | children: isFile ? undefined : {},
35 | expanded: false,
36 | });
37 |
38 | const insertPath = (nodeMap: NodeMap, path: string): void => {
39 | const parts = path.split('/').filter(Boolean);
40 | let current: NodeMap = nodeMap;
41 | let currentPath = '';
42 |
43 | for (const [index, part] of parts.entries()) {
44 | currentPath = `${currentPath}/${part}`;
45 | const isFile = index === parts.length - 1;
46 |
47 | if (!current[part]) {
48 | current[part] = createNode(part, isFile, currentPath);
49 | }
50 |
51 | if (!isFile) {
52 | if (!current[part].children) {
53 | current[part].children = {};
54 | }
55 | current = current[part].children as NodeMap;
56 | }
57 | }
58 | };
59 |
60 | for (const path of paths) {
61 | insertPath(root, path);
62 | }
63 |
64 | const convertToArray = (obj: {
65 | [key: string]: FileNodeBuilder;
66 | }): FileNode[] => {
67 | return Object.values(obj)
68 | .map(
69 | (node): FileNode => ({
70 | ...node,
71 | children: node.children ? convertToArray(node.children) : undefined,
72 | })
73 | )
74 | .sort((a, b) => {
75 | if (a.type !== b.type) {
76 | return a.type === 'folder' ? -1 : 1;
77 | }
78 | return a.name.localeCompare(b.name);
79 | });
80 | };
81 |
82 | return convertToArray(root);
83 | }
84 |
--------------------------------------------------------------------------------
/packages/design-system/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@repo/design-system/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/apps/obby/ai/tools/wait-command.md:
--------------------------------------------------------------------------------
1 | Use this tool to wait for a previously started command (triggered via `Run Command`) in a Vercel Sandbox to finish. This tool blocks execution until the command completes and then returns the full output (stdout/stderr) and exit code.
2 |
3 | It is essential for managing sequential workflows where one command depends on the successful completion of another.
4 |
5 | ## When to Use This Tool
6 |
7 | Use Wait Command when:
8 |
9 | 1. You’ve started a command using `Run Command` and need to know when it completes
10 | 2. You must run a **second command that depends on the results of the first**
11 | 3. You want to retrieve the result (logs, errors, exit status) of a previously started command
12 | 4. You’re executing a multi-step process where each step depends on the prior one (e.g., install → build → run)
13 |
14 | ## Usage Guidelines
15 |
16 | - Always use the `commandId` returned by `Run Command` to identify which command to wait for
17 | - Only wait for one command at a time
18 | - You MUST NOT run a dependent command until you’ve waited for and confirmed the prior command completed successfully
19 | - Check the exit code:
20 | - Exit code `0` means success
21 | - Non-zero codes indicate failure; handle accordingly
22 |
23 | ## Sequencing Best Practices
24 |
25 | - Wait immediately after starting any important command unless it is meant to run in the background
26 | - ✅ Run `npm install` → Wait → Run `npm run dev`
27 | - ❌ Run both without waiting
28 | - After the wait completes:
29 | - If successful, proceed
30 | - If failed, report the failure or take corrective action
31 |
32 | ## When NOT to Use This Tool
33 |
34 | Avoid using this tool when:
35 |
36 | 1. No `Run Command` has been issued yet
37 | 2. The command you started is meant to run in the background (e.g., a dev server or watcher)
38 | 3. You don’t need the result or output of the command
39 |
40 | ## Example
41 |
42 |
43 | User: Install dependencies and then start the dev server
44 | Assistant:
45 | 1. Run Command: `npm install` → receives `commandId`
46 | 2. Wait Command: wait for `commandId` to finish
47 | 3. Run Command: `npm run dev` (no wait needed, this runs in background)
48 |
49 |
50 | ## Summary
51 |
52 | Use Wait Command to block and monitor a previously run command until it finishes. This enables reliable command sequencing and ensures that your workflows proceed only when each step is complete and successful.
53 |
--------------------------------------------------------------------------------